tests/testthat/test-convenience.R

# Tests for Convenience Functions

# Path-based Access Tests -------------------------------------------------

test_that("am_get_path navigates nested structures", {
  doc <- am_create()
  am_put(
    doc,
    AM_ROOT,
    "user",
    list(
      name = "Alice",
      address = list(
        city = "NYC",
        zip = 10001L
      )
    )
  )

  # Navigate to nested values
  expect_equal(am_get_path(doc, c("user", "name")), "Alice")
  expect_equal(am_get_path(doc, c("user", "address", "city")), "NYC")
  expect_equal(am_get_path(doc, c("user", "address", "zip")), 10001L)
})

test_that("am_get_path handles missing paths", {
  doc <- am_create()
  doc$user <- list(name = "Alice")

  # Missing path returns NULL
  expect_null(am_get_path(doc, c("user", "address", "city")))
  expect_null(am_get_path(doc, c("nonexistent")))
})

test_that("am_get_path supports mixed string and numeric indices", {
  doc <- am_create()

  # Create structure with both maps and lists
  am_put(
    doc,
    AM_ROOT,
    "users",
    list(
      list(name = "Alice", age = 30L),
      list(name = "Bob", age = 25L)
    )
  )

  # Navigate: map key "users" -> list index 1 -> map key "name"
  name <- am_get_path(doc, list("users", 1, "name"))
  expect_equal(name, "Alice")

  # Navigate to second user's age
  age <- am_get_path(doc, list("users", 2, "age"))
  expect_equal(age, 25L)
})

test_that("am_get_path validates inputs", {
  doc <- am_create()

  expect_error(am_get_path(doc, list()), "path cannot be empty")
  expect_error(am_get_path(doc, NULL), "path must be")
  expect_error(
    am_get_path("not a doc", c("key")),
    "must be an Automerge document"
  )
})

test_that("am_put_path creates nested structures", {
  doc <- am_create()

  # Create nested structure with automatic intermediate objects
  am_put_path(doc, c("user", "address", "city"), "Boston")
  am_put_path(doc, c("user", "address", "zip"), 02101L)
  am_put_path(doc, c("user", "name"), "Alice")

  # Verify structure was created
  expect_equal(am_get_path(doc, c("user", "name")), "Alice")
  expect_equal(am_get_path(doc, c("user", "address", "city")), "Boston")
  expect_equal(am_get_path(doc, c("user", "address", "zip")), 02101L)
})

test_that("am_put_path can disable intermediate creation", {
  doc <- am_create()

  # Should fail without create_intermediate
  expect_error(
    am_put_path(
      doc,
      c("user", "address", "city"),
      "NYC",
      create_intermediate = FALSE
    ),
    "Path component at position 1 does not exist"
  )

  # Create parent first (must use am_map() to create a MAP, not a LIST)
  doc$user <- am_map()
  expect_error(
    am_put_path(
      doc,
      c("user", "address", "city"),
      "NYC",
      create_intermediate = FALSE
    ),
    "Path component at position 2 does not exist"
  )
})

test_that("am_put_path validates inputs", {
  doc <- am_create()

  expect_error(am_put_path(doc, list(), "value"), "path cannot be empty")
  expect_error(am_put_path(doc, NULL, "value"), "path must be")
  expect_error(
    am_put_path("not a doc", c("key"), "value"),
    "must be an Automerge document"
  )
})

test_that("am_put_path returns document invisibly", {
  doc <- am_create()

  result <- withVisible(am_put_path(doc, c("key"), "value"))
  expect_false(result$visible)
  expect_identical(result$value, doc)
})

test_that("am_delete_path removes nested keys", {
  doc <- am_create()
  am_put_path(doc, c("user", "address", "city"), "NYC")
  am_put_path(doc, c("user", "name"), "Alice")

  # Delete nested key
  am_delete_path(doc, c("user", "address"))

  # Address should be gone
  expect_null(am_get_path(doc, c("user", "address")))
  # But name should remain
  expect_equal(am_get_path(doc, c("user", "name")), "Alice")
})

test_that("am_delete_path handles missing paths gracefully", {
  doc <- am_create()
  am_put(doc, AM_ROOT, "user", am_map(name = "Alice"))

  # Should warn for missing intermediate path
  expect_warning(am_delete_path(doc, c("user", "nonexistent", "key")))

  # Deleting nonexistent root key should succeed without warning (single element path)
  expect_no_condition(am_delete_path(doc, c("nonexistent")))
})

test_that("am_delete_path validates inputs", {
  doc <- am_create()

  expect_error(am_delete_path(doc, list()), "path cannot be empty")
  expect_error(am_delete_path(doc, NULL), "path must be")
  expect_error(
    am_delete_path("not a doc", c("key")),
    "must be an Automerge document"
  )
})

test_that("am_delete_path returns document invisibly", {
  doc <- am_create()
  doc$key <- "value"

  result <- withVisible(am_delete_path(doc, c("key")))
  expect_false(result$visible)
  expect_identical(result$value, doc)
})

test_that("path-based access works with deep nesting", {
  doc <- am_create()

  # Create 6-level deep structure
  am_put_path(doc, c("a", "b", "c", "d", "e", "f"), "deep_value")

  # Should be able to retrieve it
  expect_equal(am_get_path(doc, c("a", "b", "c", "d", "e", "f")), "deep_value")

  # Modify intermediate level
  am_put_path(doc, c("a", "b", "c", "modified"), TRUE)
  expect_true(am_get_path(doc, c("a", "b", "c", "modified")))

  # Original deep value still accessible
  expect_equal(am_get_path(doc, c("a", "b", "c", "d", "e", "f")), "deep_value")
})

# Conversion Functions Tests ----------------------------------------------

test_that("as_automerge converts simple lists", {
  data <- list(name = "Alice", age = 30L, active = TRUE)
  doc <- as_automerge(data)

  expect_s3_class(doc, "am_doc")
  expect_equal(doc[["name"]], "Alice")
  expect_equal(doc[["age"]], 30L)
  expect_equal(doc[["active"]], TRUE)
})

test_that("as_automerge converts nested lists", {
  data <- list(
    name = "Alice",
    age = 30L,
    address = list(
      city = "NYC",
      zip = 10001L
    )
  )

  doc <- as_automerge(data)

  expect_equal(doc[["name"]], "Alice")
  expect_equal(am_get_path(doc, c("address", "city")), "NYC")
  expect_equal(am_get_path(doc, c("address", "zip")), 10001L)
})

test_that("as_automerge converts deeply nested structures", {
  data <- list(
    company = list(
      name = "Acme Corp",
      office = list(
        address = list(
          street = "123 Main St",
          city = "Boston"
        )
      )
    )
  )

  doc <- as_automerge(data)

  expect_equal(am_get_path(doc, c("company", "name")), "Acme Corp")
  expect_equal(
    am_get_path(doc, c("company", "office", "address", "city")),
    "Boston"
  )
})

test_that("as_automerge handles scalar values", {
  # Single value
  doc <- as_automerge(42L)
  expect_equal(doc[["value"]], 42L)

  # Single string
  doc <- as_automerge("test")
  expect_equal(doc[["value"]], "test")
})

test_that("as_automerge handles vectors", {
  # Vector becomes a list
  doc <- as_automerge(c(1, 2, 3))
  values <- doc[["values"]]
  expect_s3_class(values, "am_object")
  # am_object is now the external pointer directly
  expect_equal(am_length(doc, values), 3)
})

test_that("as_automerge can use existing document", {
  doc <- am_create()
  doc$existing <- "data"

  # Add to existing document
  as_automerge(list(new = "value"), doc = doc)

  expect_equal(doc$existing, "data")
  expect_equal(doc$new, "value")
})

test_that("as_automerge can specify actor_id", {
  doc <- as_automerge(list(x = 1), actor_id = NULL)
  expect_s3_class(doc, "am_doc")

  # Verify actor ID was set (should be random bytes)
  actor <- am_get_actor(doc)
  expect_type(actor, "raw")
  expect_true(length(actor) > 0)
})

test_that("as_automerge validates inputs", {
  expect_error(
    as_automerge(list(x = 1), doc = "not a doc"),
    "must be an Automerge document"
  )
})

test_that("from_automerge converts to R list", {
  doc <- am_create()
  doc$name <- "Alice"
  doc$age <- 30L
  doc$active <- TRUE

  result <- from_automerge(doc)

  expect_type(result, "list")
  expect_equal(result$name, "Alice")
  expect_equal(result$age, 30L)
  expect_equal(result$active, TRUE)
})

test_that("from_automerge handles nested structures", {
  doc <- am_create()
  am_put(
    doc,
    AM_ROOT,
    "user",
    list(
      name = "Bob",
      address = list(city = "NYC", zip = 10001L)
    )
  )

  result <- from_automerge(doc)

  expect_type(result$user, "list")
  expect_equal(result$user$name, "Bob")
  expect_type(result$user$address, "list")
  expect_equal(result$user$address$city, "NYC")
  expect_equal(result$user$address$zip, 10001L)
})

test_that("from_automerge validates inputs", {
  expect_error(from_automerge("not a doc"), "must be an Automerge document")
})

test_that("as_automerge and from_automerge are inverses", {
  original <- list(
    name = "Alice",
    age = 30L,
    scores = list(85L, 90L, 95L),
    metadata = list(active = TRUE)
  )

  doc <- as_automerge(original)
  result <- from_automerge(doc)

  # Check top-level fields
  expect_equal(result$name, original$name)
  expect_equal(result$age, original$age)
  expect_equal(result$metadata$active, original$metadata$active)
})

# Integration Tests -------------------------------------------------------

test_that("convenience functions work together", {
  # Create document from R data
  data <- list(
    users = list(
      list(name = "Alice", role = "admin"),
      list(name = "Bob", role = "user")
    ),
    config = list(
      database = list(host = "localhost", port = 5432L)
    )
  )

  doc <- as_automerge(data)

  # Use path-based access to modify
  am_put_path(doc, c("config", "database", "port"), 5433L)
  # Must use list() for mixed string/numeric paths to avoid coercion
  am_put_path(doc, list("users", 1, "active"), TRUE)

  # Verify via path access
  expect_equal(am_get_path(doc, c("config", "database", "port")), 5433L)
  expect_true(am_get_path(doc, list("users", 1, "active")))

  # Fork and modify
  doc2 <- am_fork(doc)
  am_put_path(doc2, c("config", "version"), "2.0")

  # Merge back using am_merge
  am_merge(doc, doc2)

  # Convert back to R
  result <- from_automerge(doc)

  expect_equal(result$config$database$port, 5433L)
  expect_equal(result$config$version, "2.0")
})

test_that("path-based functions work with save/load/merge", {
  doc1 <- am_create()
  am_put_path(doc1, c("user", "name"), "Alice")
  am_put_path(doc1, c("user", "location", "city"), "NYC")
  am_commit(doc1)

  # Save and load
  bytes <- am_save(doc1)
  doc_loaded <- am_load(bytes)

  expect_equal(am_get_path(doc_loaded, c("user", "name")), "Alice")
  expect_equal(am_get_path(doc_loaded, c("user", "location", "city")), "NYC")

  # Fork and modify different paths
  doc2 <- am_fork(doc1)
  am_put_path(doc2, c("user", "location", "state"), "NY")
  am_commit(doc2)

  # Merge
  am_merge(doc1, doc2)

  # Both paths should exist
  expect_equal(am_get_path(doc1, c("user", "location", "city")), "NYC")
  expect_equal(am_get_path(doc1, c("user", "location", "state")), "NY")
})

# Edge Cases ------------------------------------------------------------------

test_that("as_automerge handles data.frame", {
  df <- data.frame(x = 1:3, y = letters[1:3], stringsAsFactors = FALSE)

  result <- tryCatch(
    as_automerge(df),
    error = function(e) "error"
  )

  if (inherits(result, "am_doc")) {
    expect_s3_class(result, "am_doc")
  } else {
    expect_equal(result, "error")
  }
})

test_that("as_automerge handles matrix", {
  mat <- matrix(1:9, nrow = 3)

  result <- tryCatch(
    as_automerge(mat),
    error = function(e) "error"
  )

  if (inherits(result, "am_doc")) {
    expect_s3_class(result, "am_doc")
  } else {
    expect_equal(result, "error")
  }
})

test_that("as_automerge handles empty list", {
  doc <- as_automerge(list())
  expect_s3_class(doc, "am_doc")
  expect_equal(am_length(doc, AM_ROOT), 0)
})

test_that("as_automerge handles list with NULL names", {
  data <- list("a", "b", "c")
  doc <- as_automerge(data)
  expect_s3_class(doc, "am_doc")
})

test_that("as_automerge handles partially named list", {
  data <- list(a = 1, "b", c = 3)
  doc <- as_automerge(data)
  expect_s3_class(doc, "am_doc")
})

test_that("as_automerge handles nested empty lists", {
  data <- list(
    outer = list(
      inner = list()
    )
  )
  doc <- as_automerge(data)
  expect_s3_class(doc, "am_doc")

  inner <- am_get_path(doc, c("outer", "inner"))
  expect_s3_class(inner, "am_object")
  # am_object is now the external pointer directly
  expect_equal(am_length(doc, inner), 0)
})

test_that("as_automerge handles very large structures", {
  large_list <- lapply(1:100, function(i) {
    list(id = i, value = paste0("value", i))
  })

  doc <- as_automerge(list(data = large_list))
  expect_s3_class(doc, "am_doc")
})

test_that("from_automerge handles empty document", {
  doc <- am_create()
  result <- from_automerge(doc)

  expect_type(result, "list")
  expect_length(result, 0)
})

test_that("from_automerge handles document with deleted keys", {
  doc <- am_create()
  am_put(doc, AM_ROOT, "key1", "value1")
  am_put(doc, AM_ROOT, "key2", "value2")
  am_delete(doc, AM_ROOT, "key1")

  result <- from_automerge(doc)
  expect_false("key1" %in% names(result))
  expect_true("key2" %in% names(result))
  expect_equal(result$key2, "value2")
})

test_that("from_automerge preserves POSIXct timestamps", {
  timestamp <- Sys.time()
  doc <- am_create()
  am_put(doc, AM_ROOT, "time", timestamp)

  result <- from_automerge(doc)
  expect_s3_class(result$time, "POSIXct")
  expect_equal(as.numeric(result$time), as.numeric(timestamp))
})

test_that("am_get_path with single element path", {
  doc <- am_create()
  am_put(doc, AM_ROOT, "key", "value")

  expect_equal(am_get_path(doc, "key"), "value")
})

test_that("am_put_path overwrites existing values", {
  doc <- am_create()
  am_put_path(doc, c("key", "nested"), "original")
  am_put_path(doc, c("key", "nested"), "updated")

  expect_equal(am_get_path(doc, c("key", "nested")), "updated")
})

test_that("am_delete_path on intermediate path", {
  doc <- am_create()
  am_put_path(doc, c("a", "b", "c"), "value1")
  am_put_path(doc, c("a", "b", "d"), "value2")
  am_put_path(doc, c("a", "e"), "value3")

  am_delete_path(doc, c("a", "b"))

  expect_null(am_get_path(doc, c("a", "b")))
  expect_null(am_get_path(doc, c("a", "b", "c")))
  expect_equal(am_get_path(doc, c("a", "e")), "value3")
})

test_that("am_get_path handles numeric indices in lists", {
  doc <- am_create()
  doc$items <- am_list("first", "second", "third")

  expect_equal(am_get_path(doc, list("items", 1)), "first")
  expect_equal(am_get_path(doc, list("items", 2)), "second")
  expect_equal(am_get_path(doc, list("items", 3)), "third")
})

test_that("am_get_path returns NULL for out-of-bounds list index", {
  doc <- am_create()
  doc$items <- am_list("first", "second")

  expect_null(am_get_path(doc, list("items", 0)))
  expect_null(am_get_path(doc, list("items", 99)))
})

test_that("am_put_path with list index extends list", {
  doc <- am_create()
  doc$items <- am_list()

  am_put_path(doc, list("items", "end"), "first")
  am_put_path(doc, list("items", "end"), "second")

  expect_equal(am_get_path(doc, list("items", 1)), "first")
  expect_equal(am_get_path(doc, list("items", 2)), "second")
})

test_that("round-trip conversion preserves structure", {
  original <- list(
    name = "Alice",
    scores = list(85, 90, 95),
    metadata = list(
      created = TRUE,
      tags = list("a", "b", "c")
    )
  )

  doc <- as_automerge(original)
  result <- from_automerge(doc)

  expect_equal(result$name, original$name)
  expect_equal(result$metadata$created, original$metadata$created)
})

test_that("as_automerge handles special numeric values carefully", {
  doc <- tryCatch(
    as_automerge(list(inf = Inf, neg_inf = -Inf, nan = NaN)),
    error = function(e) "error"
  )

  expect_true(inherits(doc, "am_doc") || identical(doc, "error"))
})

test_that("as_automerge handles nested lists with mixed types", {
  data <- list(
    int = 1L,
    dbl = 3.14,
    str = "text",
    bool = TRUE,
    null = NULL,
    nested = list(a = 1, b = "two")
  )

  doc <- as_automerge(data)
  expect_s3_class(doc, "am_doc")
  expect_equal(doc$int, 1L)
  expect_equal(doc$str, "text")
  expect_null(doc$null)
})

Try the automerge package in your browser

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

automerge documentation built on Feb. 5, 2026, 5:08 p.m.