tests/testthat/test-recursive.R

# Recursive Conversion and Advanced Types (Phase 3)

test_that("Recursive conversion works for nested maps", {
  doc <- am_create()

  # Single-call nested structure
  am_put(
    doc,
    AM_ROOT,
    "user",
    list(
      name = "Alice",
      age = 30L,
      active = TRUE
    )
  )

  user <- am_get(doc, AM_ROOT, "user")
  expect_s3_class(user, "am_object")

  expect_equal(am_get(doc, user, "name"), "Alice")
  expect_equal(am_get(doc, user, "age"), 30L)
  expect_equal(am_get(doc, user, "active"), TRUE)
})

test_that("Recursive conversion handles deep nesting", {
  doc <- am_create()

  # Multi-level nested structure (3 levels deep)
  am_put(
    doc,
    AM_ROOT,
    "company",
    list(
      name = "Acme Corp",
      office = list(
        location = "Boston",
        address = list(
          street = "123 Main St",
          city = "Boston",
          zip = 02101L
        )
      )
    )
  )

  company <- am_get(doc, AM_ROOT, "company")
  expect_equal(am_get(doc, company, "name"), "Acme Corp")

  office <- am_get(doc, company, "office")
  expect_equal(am_get(doc, office, "location"), "Boston")

  address <- am_get(doc, office, "address")
  expect_equal(am_get(doc, address, "street"), "123 Main St")
  expect_equal(am_get(doc, address, "city"), "Boston")
  expect_equal(am_get(doc, address, "zip"), 02101L)
})

test_that("Recursive conversion handles mixed maps and lists", {
  doc <- am_create()

  # Map containing lists
  am_put(
    doc,
    AM_ROOT,
    "data",
    list(
      tags = list("r", "automerge", "crdt"), # Unnamed = list
      metadata = list(version = "1.0", author = "user") # Named = map
    )
  )

  data <- am_get(doc, AM_ROOT, "data")

  tags <- am_get(doc, data, "tags")
  expect_equal(am_length(doc, tags), 3L)
  expect_equal(am_get(doc, tags, 1), "r") # 1-based indexing
  expect_equal(am_get(doc, tags, 2), "automerge")
  expect_equal(am_get(doc, tags, 3), "crdt")

  metadata <- am_get(doc, data, "metadata")
  expect_equal(am_get(doc, metadata, "version"), "1.0")
  expect_equal(am_get(doc, metadata, "author"), "user")
})

test_that("Explicit type constructors work", {
  doc <- am_create()

  # Explicit list type (empty)
  am_put(doc, AM_ROOT, "items", am_list())
  items <- am_get(doc, AM_ROOT, "items")
  expect_s3_class(items, "am_object")
  expect_equal(am_length(doc, items), 0L)

  # Explicit list type (populated)
  am_put(doc, AM_ROOT, "tags", am_list("a", "b", "c"))
  tags <- am_get(doc, AM_ROOT, "tags")
  expect_equal(am_length(doc, tags), 3L)
  expect_equal(am_get(doc, tags, 1), "a")

  # Explicit map type (empty)
  am_put(doc, AM_ROOT, "config", am_map())
  config <- am_get(doc, AM_ROOT, "config")
  expect_s3_class(config, "am_object")
  expect_equal(am_length(doc, config), 0L)

  # Explicit map type (populated)
  am_put(doc, AM_ROOT, "settings", am_map(key1 = "val1", key2 = "val2"))
  settings <- am_get(doc, AM_ROOT, "settings")
  expect_equal(am_get(doc, settings, "key1"), "val1")
  expect_equal(am_get(doc, settings, "key2"), "val2")
})

test_that("POSIXct timestamps work", {
  doc <- am_create()

  # Store timestamp
  now <- Sys.time()
  am_put(doc, AM_ROOT, "created", now)

  # Retrieve and verify (allowing for small rounding error)
  retrieved <- am_get(doc, AM_ROOT, "created")
  expect_s3_class(retrieved, "POSIXct")
  expect_equal(as.numeric(retrieved), as.numeric(now), tolerance = 0.001)
})

test_that("am_counter type works", {
  doc <- am_create()

  # Store counter
  am_put(doc, AM_ROOT, "score", am_counter(0))

  # Retrieve and verify
  score <- am_get(doc, AM_ROOT, "score")
  expect_s3_class(score, "am_counter")
  expect_equal(as.integer(score), 0L)

  # Store counter with initial value
  am_put(doc, AM_ROOT, "points", am_counter(100L))
  points <- am_get(doc, AM_ROOT, "points")
  expect_equal(as.integer(points), 100L)
})

test_that("am_text type creates text objects", {
  doc <- am_create()

  # Empty text
  am_put(doc, AM_ROOT, "doc1", am_text())
  text1 <- am_get(doc, AM_ROOT, "doc1")
  expect_s3_class(text1, "am_object")
  # Text object should be empty initially
  expect_equal(am_length(doc, text1), 0L)

  # Text with initial content
  am_put(doc, AM_ROOT, "doc2", am_text("Hello, World!"))
  text2 <- am_get(doc, AM_ROOT, "doc2")
  expect_s3_class(text2, "am_object")
  # Text object should have 13 characters
  expect_equal(am_length(doc, text2), 13L)
})

test_that("text objects vs strings behave differently", {
  doc <- am_create()

  # Regular string (last-write-wins)
  am_put(doc, AM_ROOT, "title", "String Value")
  expect_type(am_get(doc, AM_ROOT, "title"), "character")
  expect_equal(am_get(doc, AM_ROOT, "title"), "String Value")

  # Text object (CRDT)
  am_put(doc, AM_ROOT, "content", am_text("Text Object"))
  text_obj <- am_get(doc, AM_ROOT, "content")
  expect_s3_class(text_obj, "am_object")
  # Length should be character count
  expect_equal(am_length(doc, text_obj), 11L)
})

test_that("Recursive conversion handles NULL values", {
  doc <- am_create()

  am_put(
    doc,
    AM_ROOT,
    "data",
    list(
      present = "value",
      missing = NULL,
      nested = list(
        also_missing = NULL,
        also_present = 42L
      )
    )
  )

  data <- am_get(doc, AM_ROOT, "data")
  expect_equal(am_get(doc, data, "present"), "value")
  expect_null(am_get(doc, data, "missing"))

  nested <- am_get(doc, data, "nested")
  expect_null(am_get(doc, nested, "also_missing"))
  expect_equal(am_get(doc, nested, "also_present"), 42L)
})

test_that("Recursive conversion handles all primitive types", {
  doc <- am_create()

  raw_data <- as.raw(c(0x01, 0x02, 0x03))

  am_put(
    doc,
    AM_ROOT,
    "all_types",
    list(
      null_val = NULL,
      bool_val = TRUE,
      int_val = 42L,
      double_val = 3.14159,
      string_val = "hello",
      raw_val = raw_data,
      timestamp_val = Sys.time(),
      counter_val = am_counter(10L)
    )
  )

  obj <- am_get(doc, AM_ROOT, "all_types")

  expect_null(am_get(doc, obj, "null_val"))
  expect_equal(am_get(doc, obj, "bool_val"), TRUE)
  expect_equal(am_get(doc, obj, "int_val"), 42L)
  expect_equal(am_get(doc, obj, "double_val"), 3.14159, tolerance = 1e-6)
  expect_equal(am_get(doc, obj, "string_val"), "hello")
  expect_equal(am_get(doc, obj, "raw_val"), raw_data)
  expect_s3_class(am_get(doc, obj, "timestamp_val"), "POSIXct")
  expect_s3_class(am_get(doc, obj, "counter_val"), "am_counter")
})

test_that("Recursive conversion integrates with commit/save/load", {
  doc1 <- am_create()

  # Create complex nested structure
  am_put(
    doc1,
    AM_ROOT,
    "project",
    list(
      name = "MyProject",
      version = "1.0.0",
      metadata = list(
        created = Sys.time(),
        tags = list("important", "active"),
        stats = list(
          commits = am_counter(42L),
          stars = am_counter(100L)
        )
      )
    )
  )

  am_commit(doc1, "Added project data")

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

  # Verify structure is preserved
  project <- am_get(doc2, AM_ROOT, "project")
  expect_equal(am_get(doc2, project, "name"), "MyProject")

  metadata <- am_get(doc2, project, "metadata")
  expect_s3_class(am_get(doc2, metadata, "created"), "POSIXct")

  tags <- am_get(doc2, metadata, "tags")
  expect_equal(am_length(doc2, tags), 2L)

  stats <- am_get(doc2, metadata, "stats")
  commits <- am_get(doc2, stats, "commits")
  expect_s3_class(commits, "am_counter")
  expect_equal(as.integer(commits), 42L)
})

test_that("Very deep nesting (5+ levels) works", {
  doc <- am_create()

  # 6 levels deep
  am_put(
    doc,
    AM_ROOT,
    "level1",
    list(
      level2 = list(
        level3 = list(
          level4 = list(
            level5 = list(
              level6 = "deep value"
            )
          )
        )
      )
    )
  )

  # Navigate down
  l1 <- am_get(doc, AM_ROOT, "level1")
  l2 <- am_get(doc, l1, "level2")
  l3 <- am_get(doc, l2, "level3")
  l4 <- am_get(doc, l3, "level4")
  l5 <- am_get(doc, l4, "level5")
  value <- am_get(doc, l5, "level6")

  expect_equal(value, "deep value")
})

test_that("Empty nested structures work", {
  doc <- am_create()

  # Empty map in map
  am_put(doc, AM_ROOT, "outer", list(inner = list()))
  outer <- am_get(doc, AM_ROOT, "outer")
  inner <- am_get(doc, outer, "inner")
  expect_equal(am_length(doc, inner), 0L)

  # Empty list in map
  am_put(doc, AM_ROOT, "container", list(items = am_list()))
  container <- am_get(doc, AM_ROOT, "container")
  items <- am_get(doc, container, "items")
  expect_equal(am_length(doc, items), 0L)
})

test_that("Extremely deep nesting (150 levels) works without stack overflow", {
  doc <- am_create()

  # Build a deeply nested structure programmatically (150 levels)
  # This exceeds the old MAX_RECURSION_DEPTH=100 limit
  depth <- 150

  # Build nested list structure
  build_nested <- function(n) {
    if (n == 0) {
      return("bottom value")
    }
    list(child = build_nested(n - 1))
  }

  nested_data <- build_nested(depth - 1)

  # This should not crash - test that recursive conversion handles it
  expect_error(
    am_put(doc, AM_ROOT, "deep", nested_data),
    NA  # NA means we expect NO error
  )

  # Navigate down to verify the structure was created
  current <- am_get(doc, AM_ROOT, "deep")
  expect_s3_class(current, "am_object")

  # Navigate several levels down (not all 150, that would be tedious)
  for (i in 1:10) {
    current <- am_get(doc, current, "child")
    if (i < 10) {
      expect_s3_class(current, "am_object")
    }
  }

  # Verify we can commit and save without issues
  expect_error(am_commit(doc, "Deep structure test"), NA)
  expect_error(am_save(doc), NA)
})

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.