tests/testthat/test-share.R

# Tests for share() - zero-copy shared objects

test_that("share returns an ALTREP-backed shared vector for atomic inputs", {
    x <- 1:100
    shared <- share(x)

    expect_true(is_shared_vector(shared))
    expect_s3_class(shared, "shard_shared_vector")

    info <- shared_info(shared)
    expect_true(info$size > 0)
    expect_true(!is.null(info$path))
})

test_that("share returns a shard_shared handle for non-shareable types", {
    x <- letters
    shared <- share(x)
    expect_s3_class(shared, "shard_shared")
    expect_true(shared$size > 0)
    expect_true(!is.null(shared$path))
    close(shared)
})

test_that("share and local round-trip preserves data", {
    # Integer vector
    x <- 1:100
    shared <- share(x)
    recovered <- materialize(shared)
    expect_identical(x, recovered)

    # Numeric vector
    x <- rnorm(50)
    shared <- share(x)
    recovered <- materialize(shared)
    expect_equal(x, recovered)

    # Character vector
    x <- letters
    shared <- share(x)
    recovered <- fetch(shared)
    expect_identical(x, recovered)
    close(shared)

    # Logical vector
    x <- c(TRUE, FALSE, TRUE, NA)
    shared <- share(x)
    recovered <- materialize(shared)
    expect_identical(x, recovered)
})

test_that("share preserves matrix structure", {
    mat <- matrix(1:20, nrow = 4, ncol = 5)
    shared <- share(mat)

    info <- shared_info(shared)
    expect_equal(info$class_info$type, "matrix")
    expect_equal(info$class_info$dim, c(4L, 5L))

    recovered <- materialize(shared)
    expect_identical(mat, recovered)
})

test_that("share preserves array structure", {
    arr <- array(1:24, dim = c(2, 3, 4))
    shared <- share(arr)

    info <- shared_info(shared)
    expect_equal(info$class_info$type, "array")
    expect_equal(info$class_info$dim, c(2L, 3L, 4L))

    recovered <- materialize(shared)
    expect_identical(arr, recovered)
})

test_that("share preserves data.frame structure", {
    df <- data.frame(
        a = 1:5,
        b = letters[1:5],
        c = c(TRUE, FALSE, TRUE, FALSE, TRUE),
        stringsAsFactors = FALSE
    )
    shared <- share(df)

    expect_equal(shared$class_info$type, "data.frame")
    expect_equal(shared$class_info$nrow, 5)
    expect_equal(shared$class_info$ncol, 3)

    recovered <- fetch(shared)
    expect_identical(df, recovered)

    close(shared)
})

test_that("share preserves list structure", {
    lst <- list(
        a = 1:5,
        b = "hello",
        c = list(nested = TRUE)
    )
    shared <- share(lst)

    expect_equal(shared$class_info$type, "list")
    expect_equal(shared$class_info$length, 3)

    recovered <- fetch(shared)
    expect_identical(lst, recovered)

    close(shared)
})

test_that("share creates read-only segment by default", {
    x <- 1:10
    shared <- share(x)

    info <- shared_info(shared)
    expect_true(info$readonly)
})

test_that("share with readonly=FALSE creates writable segment", {
    x <- 1:10
    shared <- share(x, readonly = FALSE)

    info <- shared_info(shared)
    expect_false(info$readonly)
})

test_that("materialize is equivalent to fetch", {
    x <- letters
    shared <- share(x)

    via_fetch <- fetch(shared)
    via_materialize <- materialize(shared)

    expect_identical(via_fetch, via_materialize)

    close(shared)
})

test_that("materialize.default returns input unchanged", {
    x <- 1:10
    expect_identical(materialize(x), x)

    y <- list(a = 1, b = 2)
    expect_identical(materialize(y), y)
})

test_that("is_shared correctly identifies shared objects", {
    x <- 1:10
    shared <- share(x)

    expect_true(is_shared(shared))
    expect_false(is_shared(x))
    expect_false(is_shared(NULL))
    expect_false(is_shared(list()))
})

test_that("shared_info returns complete information", {
    x <- matrix(1:20, nrow = 4)
    shared <- share(x)

    info <- shared_info(shared)

    expect_true(is.list(info))
    expect_true(!is.null(info$path))
    expect_true(!is.null(info$backing))
    expect_true(!is.null(info$size))
    expect_true(!is.null(info$readonly))
    expect_true(!is.null(info$class_info))
    expect_true(!is.null(info$segment_info))
})

test_that("print.shard_shared works", {
    x <- letters
    shared <- share(x)

    expect_output(print(shared), "shard_shared")
    expect_output(print(shared), "Size:")

    close(shared)
})

test_that("share handles large objects", {
    # 1 million elements
    x <- rnorm(1e6)
    shared <- share(x)

    info <- shared_info(shared)
    expect_true(info$size >= 8e6)  # At least 8 bytes per double

    recovered <- materialize(shared)
    expect_equal(x, recovered)
})

test_that("share_open can reopen existing shared segment", {
    x <- letters
    shared1 <- share(x)

    path <- shared1$path
    backing <- shared1$backing
    size <- shared1$size

    skip_if(is.null(path), "Path is NULL")

    # Open the same segment
    shared2 <- share_open(path, backing = backing, size = size)

    # Read from reopened segment
    recovered <- fetch(shared2)
    expect_identical(x, recovered)

    # Close in order (shared2 doesn't unlink)
    close(shared2)
    close(shared1)
})

test_that("share respects backing type", {
    x <- letters

    # Test mmap backing
    shared_mmap <- share(x, backing = "mmap")
    expect_equal(shared_mmap$backing, "mmap")
    close(shared_mmap)

    # Test auto backing
    shared_auto <- share(x, backing = "auto")
    expect_s3_class(shared_auto, "shard_shared")
    close(shared_auto)
})

test_that("close releases resources", {
    x <- letters
    shared <- share(x)
    path <- shared$path

    # Close should work without error
    expect_silent(close(shared))

    # Segment should be gone (file-backed)
    if (!is.null(path) && shared$backing == "mmap") {
        expect_false(file.exists(path))
    }
})

test_that("share handles empty vectors", {
    x <- integer(0)
    shared <- share(x)

    recovered <- materialize(shared)
    expect_identical(x, recovered)
})

test_that("share handles zero-length vectors of all types", {
    # Double
    x <- double(0)
    shared <- share(x)
    recovered <- materialize(shared)
    expect_identical(x, recovered)

    # Logical
    x <- logical(0)
    shared <- share(x)
    recovered <- materialize(shared)
    expect_identical(x, recovered)

    # Character
    x <- character(0)
    shared <- share(x)
    recovered <- fetch(shared)
    expect_identical(x, recovered)
    close(shared)

    # Raw
    x <- raw(0)
    shared <- share(x)
    recovered <- materialize(shared)
    expect_identical(x, recovered)
})

test_that("share handles single-element vectors", {
    # Single integer
    x <- 1L
    shared <- share(x)
    recovered <- materialize(shared)
    expect_identical(x, recovered)

    # Single double
    x <- 3.14
    shared <- share(x)
    recovered <- materialize(shared)
    expect_equal(x, recovered)

    # Single character
    x <- "hello"
    shared <- share(x)
    recovered <- fetch(shared)
    expect_identical(x, recovered)
    close(shared)

    # Single logical
    x <- TRUE
    shared <- share(x)
    recovered <- materialize(shared)
    expect_identical(x, recovered)

    # Single NA
    x <- NA
    shared <- share(x)
    recovered <- materialize(shared)
    expect_identical(x, recovered)
})

test_that("share handles single-element matrix and array", {
    # 1x1 matrix
    mat <- matrix(42, nrow = 1, ncol = 1)
    shared <- share(mat)
    recovered <- materialize(shared)
    expect_identical(mat, recovered)

    # 1x1x1 array
    arr <- array(42, dim = c(1, 1, 1))
    shared <- share(arr)
    recovered <- materialize(shared)
    expect_identical(arr, recovered)
})

test_that("share handles empty data.frame", {
    # Zero-row data.frame
    df <- data.frame(a = integer(0), b = character(0), stringsAsFactors = FALSE)
    shared <- share(df)
    recovered <- fetch(shared)
    expect_identical(df, recovered)
    close(shared)

    # Zero-column data.frame
    df <- data.frame()
    shared <- share(df)
    recovered <- fetch(shared)
    expect_identical(df, recovered)
    close(shared)
})

test_that("share handles empty list", {
    lst <- list()
    shared <- share(lst)
    recovered <- fetch(shared)
    expect_identical(lst, recovered)
    close(shared)
})

test_that("share handles NULL elements in lists", {
    lst <- list(a = 1, b = NULL, c = 3)
    shared <- share(lst)

    recovered <- fetch(shared)
    expect_identical(lst, recovered)

    close(shared)
})

test_that("share handles complex objects", {
    # Factor
    f <- factor(c("a", "b", "a", "c"))
    shared <- share(f)
    recovered <- materialize(shared)
    expect_identical(f, recovered)

    # POSIXct
    dt <- as.POSIXct("2024-01-01 12:00:00")
    shared <- share(dt)
    recovered <- materialize(shared)
    expect_equal(dt, recovered)

    # Formula (environment-sensitive)
    # Skip this as formulas capture environments
})

# Tests for input validation of non-serializable objects

test_that("share rejects functions with clear error message", {
    fn <- function(x) x + 1

    expect_error(
        share(fn),
        "Cannot share functions"
    )
    expect_error(
        share(fn),
        "closures"
    )
})
test_that("share rejects functions nested in lists", {
    lst <- list(a = 1, fn = function(x) x)

    expect_error(
        share(lst),
        "Cannot share functions"
    )
    expect_error(
        share(lst),
        "x\\$fn"  # Shows the path to the problem
    )
})

test_that("share rejects external pointers with clear error message", {
    # Create a simple external pointer for testing
    # We can use file() to get a connection which has an external pointer
    skip_if_not(exists(".Call"))

    # Actually creating an external pointer requires C code, so we'll test
    # the error message pattern by checking a list containing one
    # For now, verify the error message format for functions (same pattern)
    expect_error(
        share(function(x) x),
        "Extract the data you need"
    )
})

test_that("share rejects environments with external pointers", {
    # Create an environment with a function (which will trigger the validation)
    env <- new.env()
    env$fn <- function(x) x

    expect_error(
        share(env),
        "Cannot share functions"
    )
    expect_error(
        share(env),
        "x\\$fn"
    )
})

test_that("share accepts environments without problematic content", {
    env <- new.env()
    env$a <- 1:10
    env$b <- "hello"

    shared <- share(env)
    recovered <- fetch(shared)

    expect_equal(recovered$a, 1:10)
    expect_equal(recovered$b, "hello")

    close(shared)
})

test_that("share error message shows path to nested problems", {
    # Deeply nested function
    lst <- list(
        level1 = list(
            level2 = list(
                fn = function(x) x
            )
        )
    )

    expect_error(
        share(lst),
        "x\\$level1\\$level2\\$fn"
    )
})

test_that("share error message shows index for unnamed list elements", {
    lst <- list(1, 2, function(x) x)

    expect_error(
        share(lst),
        "x\\[\\[3\\]\\]"
    )
})

# Tests for deep sharing of S4 objects

# Define S4 classes for testing
# Use "ANY" type for slots that will hold reconstructed shared data
setClass("TestS4DeepMatrix",
    slots = c(
        data = "ANY",
        name = "character"
    )
)

setClass("TestS4DeepList",
    slots = c(
        items = "ANY",
        label = "character"
    )
)

setClass("TestS4DeepEnv",
    slots = c(
        data = "ANY",
        env = "environment"
    )
)

setClass("TestS4DeepNested",
    slots = c(
        inner = "ANY",
        value = "numeric"
    )
)

test_that("share with deep=TRUE processes S4 object slots", {
    # Create a large matrix that will be shared
    big_mat <- matrix(rnorm(1e7), nrow = 1000)  # ~80MB
    obj <- new("TestS4DeepMatrix", data = big_mat, name = "test")

    # Share with deep=TRUE
    shared <- share(obj, deep = TRUE, min_bytes = 1e6)

    # Should return a shard_deep_shared object
    expect_s3_class(shared, "shard_deep_shared")

    # Fetch should reconstruct the original
    recovered <- fetch(shared)
    expect_true(isS4(recovered))
    expect_s4_class(recovered, "TestS4DeepMatrix")
    expect_equal(slot(recovered, "data"), big_mat)
    expect_equal(slot(recovered, "name"), "test")

    close(shared)
})

test_that("share with deep=TRUE recursively shares S4 list slots", {
    big_vec <- rnorm(1e7)  # ~80MB
    obj <- new("TestS4DeepList",
        items = list(big = big_vec, small = 1:10),
        label = "test"
    )

    shared <- share(obj, deep = TRUE, min_bytes = 1e6)
    expect_s3_class(shared, "shard_deep_shared")

    # Summary should show shared nodes
    expect_true(shared$summary$shared_count > 0)

    # Fetch should reconstruct
    recovered <- fetch(shared)
    expect_s4_class(recovered, "TestS4DeepList")
    expect_equal(slot(recovered, "items")$big, big_vec)
    expect_equal(slot(recovered, "items")$small, 1:10)

    close(shared)
})

test_that("share with deep=TRUE and mode='balanced' skips S4 environment slots", {
    big_mat <- matrix(rnorm(1e6), nrow = 100)
    env <- new.env()
    env$foo <- "bar"
    obj <- new("TestS4DeepEnv", data = big_mat, env = env)

    # Should not error with mode='balanced' (default)
    shared <- share(obj, deep = TRUE, min_bytes = 1e5, mode = "balanced")
    expect_s3_class(shared, "shard_deep_shared")

    # Fetch should preserve the environment
    recovered <- fetch(shared)
    expect_true(is.environment(slot(recovered, "env")))

    close(shared)
})

test_that("share with deep=TRUE and mode='strict' errors on S4 environment slots", {
    big_mat <- matrix(rnorm(1e6), nrow = 100)
    env <- new.env()
    obj <- new("TestS4DeepEnv", data = big_mat, env = env)

    expect_error(
        share(obj, deep = TRUE, min_bytes = 1e3, mode = "strict"),
        "contains environment"
    )
})

test_that("share with deep=TRUE uses @slot path notation for S4", {
    setClass("TestS4WithFunc",
        slots = c(
            fn = "function"
        )
    )

    obj <- new("TestS4WithFunc", fn = function(x) x)

    # When validating S4 with function slot, should show @slot notation
    expect_error(
        share(obj),
        "@fn"
    )

    removeClass("TestS4WithFunc")
})

test_that("share with deep=TRUE handles nested S4 objects", {
    inner_mat <- matrix(rnorm(1e7), nrow = 1000)
    inner <- new("TestS4DeepMatrix", data = inner_mat, name = "inner")
    outer <- new("TestS4DeepNested", inner = inner, value = 42)

    shared <- share(outer, deep = TRUE, min_bytes = 1e6)
    expect_s3_class(shared, "shard_deep_shared")

    recovered <- fetch(shared)
    expect_s4_class(recovered, "TestS4DeepNested")
    expect_s4_class(slot(recovered, "inner"), "TestS4DeepMatrix")
    expect_equal(slot(slot(recovered, "inner"), "data"), inner_mat)
    expect_equal(slot(recovered, "value"), 42)

    close(shared)
})

test_that("share with deep=TRUE preserves S4 class after fetch", {
    big_mat <- matrix(rnorm(1e7), nrow = 1000)
    obj <- new("TestS4DeepMatrix", data = big_mat, name = "preserved")

    shared <- share(obj, deep = TRUE, min_bytes = 1e6)
    recovered <- fetch(shared)

    expect_identical(class(recovered), class(obj))
    expect_s4_class(recovered, "TestS4DeepMatrix")

    close(shared)
})

# Clean up test classes
removeClass("TestS4DeepMatrix")
removeClass("TestS4DeepList")
removeClass("TestS4DeepEnv")
removeClass("TestS4DeepNested")

Try the shard package in your browser

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

shard documentation built on April 3, 2026, 9:08 a.m.