tests/testthat/test-morph-final-push.R

# ==============================================================================
# Final push tests to maximize morph coverage
# ==============================================================================

# ------------------------------------------------------------------------------
# morph_pixel.R: mp4 output path (lines 394-414)
# ------------------------------------------------------------------------------

test_that("pixel_morph_animate saves mp4 when av available", {
  skip_if_not_installed("magick")
  skip_if_not_installed("av")

  imgA <- system.file("extdata/icons/circleA_40.png", package = "couplr")
  imgB <- system.file("extdata/icons/circleB_40.png", package = "couplr")
  skip_if(imgA == "" || imgB == "")

  outfile <- tempfile(fileext = ".mp4")
  on.exit(unlink(outfile), add = TRUE)

  result <- pixel_morph_animate(
    imgA, imgB,
    format = "mp4",
    outfile = outfile,
    n_frames = 3,
    show = FALSE
  )

  expect_true(file.exists(outfile))
})

# ------------------------------------------------------------------------------
# morph_pixel.R: size warnings for exact mode (lines 224-234)
# ------------------------------------------------------------------------------

# Large exact mode warning tests skipped - they require large LAP computations
# that are too slow for automated testing. The warnings are tested via
# visual inspection during development.

# ------------------------------------------------------------------------------
# morph_pixel.R: size warnings for large tiles (lines 237-249)
# ------------------------------------------------------------------------------

test_that("pixel_morph_animate warns for large tiles", {
  skip_if_not_installed("magick")

  imgA <- system.file("extdata/icons/circleA_40.png", package = "couplr")
  imgB <- system.file("extdata/icons/circleB_40.png", package = "couplr")
  skip_if(imgA == "" || imgB == "")

  expect_warning(
    pixel_morph_animate(
      imgA, imgB,
      mode = "exact",
      patch_size = 25,  # 25x25 = 625 pixels per tile > 400
      n_frames = 2,
      show = FALSE
    ),
    "Tile is large for LAP"
  )
})

# ------------------------------------------------------------------------------
# morph_pixel.R: color_walk large image warning (lines 252-263)
# ------------------------------------------------------------------------------

# Large image color_walk test skipped - too slow for automated testing
# The warning threshold is 250000 pixels; we skip to avoid timeout

# ------------------------------------------------------------------------------
# morph_utils.R: .to_array_rgb error paths (lines 28, 55)
# These are hard to trigger through normal usage since magick always
# returns properly formatted data
# ------------------------------------------------------------------------------

# The error at line 28 requires magick to return non-3D data - very rare
# The error at line 55 requires unexpected dim ordering - also rare

# ------------------------------------------------------------------------------
# morph_utils.R: color walk pipeline - remaining A pixels path (lines 454-476)
# ------------------------------------------------------------------------------

test_that("morph_utils color walk handles remaining pixels", {
  H <- 6
  W <- 6
  N <- H * W

  # Create images where color groups don't perfectly align
  set.seed(999)
  A_planar <- sample(0:255, N * 3, replace = TRUE)
  B_planar <- sample(0:255, N * 3, replace = TRUE)

  result <- couplr:::.solve_color_walk_pipeline(
    A_planar, B_planar, H, W,
    quantize_bits = 2,  # Very few colors = large groups
    method = "jv",
    maximize = FALSE
  )

  expect_equal(length(result), N)
  # Should be a valid assignment
  expect_true(all(result >= 1 & result <= N))
})

# ------------------------------------------------------------------------------
# morph_pixel.R: .solve_color_walk_pipeline remaining pixels (lines 1211-1218)
# ------------------------------------------------------------------------------

test_that("morph_pixel color walk handles sparse groups", {
  H <- 8
  W <- 8
  N <- H * W

  # Create images with very different colors to force remaining pixel handling
  set.seed(42)
  A_planar <- c(
    sample(c(0, 128, 255), N, replace = TRUE),  # R
    sample(c(0, 128, 255), N, replace = TRUE),  # G
    sample(c(0, 128, 255), N, replace = TRUE)   # B
  )
  B_planar <- c(
    sample(c(32, 160, 224), N, replace = TRUE),  # Different R range
    sample(c(32, 160, 224), N, replace = TRUE),  # Different G range
    sample(c(32, 160, 224), N, replace = TRUE)   # Different B range
  )

  result <- couplr:::.solve_color_walk_pipeline(
    A_planar, B_planar, H, W,
    quantize_bits = 4,
    method = "jv",
    maximize = FALSE
  )

  expect_equal(length(result), N)
})

# ------------------------------------------------------------------------------
# morph_tiling.R: .recursive_tiling_solver rest width/height (lines 1128-1162)
# ------------------------------------------------------------------------------

test_that(".recursive_tiling_solver handles images with odd dimensions", {
  # 7x5 has rest_w=1 and rest_h=1 when divided by 2
  H <- 5
  W <- 7
  N <- H * W

  set.seed(123)
  A_planar <- runif(N * 3) * 255
  B_planar <- runif(N * 3) * 255

  result <- couplr:::.recursive_tiling_solver(
    A_planar, B_planar, H, W,
    patch_size = 2,
    alpha = 1, beta = 0.1
  )

  expect_equal(length(result), N)
  # Should cover all pixels
  expect_true(all(result >= 1 & result <= N))
})

test_that(".recursive_tiling_solver handles both rest_w and rest_h > 0", {
  # 9x9 has rest_w=1 and rest_h=1 for 2x2 tiling
  H <- 9
  W <- 9
  N <- H * W

  set.seed(456)
  A_planar <- runif(N * 3) * 255
  B_planar <- runif(N * 3) * 255

  result <- couplr:::.recursive_tiling_solver(
    A_planar, B_planar, H, W,
    patch_size = 2,
    alpha = 0.5, beta = 0.5
  )

  expect_equal(length(result), N)
})

# ------------------------------------------------------------------------------
# morph_utils.R: .patch_cost_matrix with multiple patches (lines 261-275)
# ------------------------------------------------------------------------------

test_that(".patch_cost_matrix handles multiple patches", {
  patches_a <- list(
    colors = matrix(c(
      100, 100, 100,
      200, 50, 50,
      50, 200, 50
    ), nrow = 3, ncol = 3, byrow = TRUE),
    centers = matrix(c(
      0, 0,
      10, 0,
      5, 10
    ), nrow = 3, ncol = 2, byrow = TRUE)
  )
  patches_b <- list(
    colors = matrix(c(
      105, 95, 100,
      190, 60, 45,
      55, 210, 55
    ), nrow = 3, ncol = 3, byrow = TRUE),
    centers = matrix(c(
      2, 1,
      8, 2,
      6, 9
    ), nrow = 3, ncol = 2, byrow = TRUE)
  )

  result <- couplr:::.patch_cost_matrix(patches_a, patches_b, alpha = 1, beta = 0.2, H = 20, W = 20)

  expect_equal(dim(result), c(3, 3))
  expect_true(all(is.finite(result)))
})

# ------------------------------------------------------------------------------
# morph_utils.R: .expand_patch_assignment with multiple patches (lines 280-291)
# ------------------------------------------------------------------------------

test_that(".expand_patch_assignment handles multiple patches", {
  patch_assign <- list(2L, 1L, 3L)  # Patch 1->B2, Patch 2->B1, Patch 3->B3
  patches_a <- list(
    indices = list(c(1L, 2L), c(3L, 4L), c(5L, 6L))
  )
  patches_b <- list(
    indices = list(c(7L, 8L), c(9L, 10L), c(11L, 12L))
  )

  result <- couplr:::.expand_patch_assignment(patch_assign, patches_a, patches_b, N = 6)

  expect_equal(length(result), 6)
  # Patch 1 maps to B patch 2
  expect_equal(result[1], 9L)
  expect_equal(result[2], 10L)
  # Patch 2 maps to B patch 1
  expect_equal(result[3], 7L)
  expect_equal(result[4], 8L)
})

# ------------------------------------------------------------------------------
# morph_tiling.R: .generate_square_tiles boundary conditions
# ------------------------------------------------------------------------------

test_that(".generate_square_tiles handles exact division", {
  # 6x6 with P=2 divides exactly
  tiles <- couplr:::.generate_square_tiles(W = 6, H = 6, P = 2)

  # Should have 9 tiles of size 2
  expect_equal(length(tiles), 9)
  expect_true(all(sapply(tiles, function(t) t$size) == 2))
})

test_that(".generate_square_tiles handles P > both dimensions", {
  tiles <- couplr:::.generate_square_tiles(W = 2, H = 3, P = 5)

  # Should fill with smaller tiles
  covered <- matrix(FALSE, 3, 2)
  for (tile in tiles) {
    for (dx in 0:(tile$size - 1)) {
      for (dy in 0:(tile$size - 1)) {
        covered[tile$y0 + dy + 1, tile$x0 + dx + 1] <- TRUE
      }
    }
  }
  expect_true(all(covered))
})

# ------------------------------------------------------------------------------
# morph_pixel.R: pixel_morph with downscale (lines 707-718)
# ------------------------------------------------------------------------------

test_that("pixel_morph works with downscale_steps", {
  skip_if_not_installed("magick")

  imgA <- system.file("extdata/icons/circleA_40.png", package = "couplr")
  imgB <- system.file("extdata/icons/circleB_40.png", package = "couplr")
  skip_if(imgA == "" || imgB == "")

  result <- pixel_morph(
    imgA, imgB,
    downscale_steps = 1,
    n_frames = 2,
    show = FALSE
  )

  expect_s3_class(result, "magick-image")
})

# ------------------------------------------------------------------------------
# morph_pixel.R: fractional upscale path (lines 365-371, 757-763)
# ------------------------------------------------------------------------------

test_that("pixel_morph applies fractional upscale correctly", {
  skip_if_not_installed("magick")

  imgA <- system.file("extdata/icons/circleA_40.png", package = "couplr")
  imgB <- system.file("extdata/icons/circleB_40.png", package = "couplr")
  skip_if(imgA == "" || imgB == "")

  result <- pixel_morph(
    imgA, imgB,
    upscale = 1.5,  # Non-integer
    n_frames = 2,
    show = FALSE
  )

  info <- magick::image_info(result)
  # 40 * 1.5 = 60
  expect_equal(info$width, 60)
  expect_equal(info$height, 60)
})

# ------------------------------------------------------------------------------
# morph_pixel.R: GIF format save (lines 419-420)
# ------------------------------------------------------------------------------

test_that("pixel_morph_animate saves GIF correctly", {
  skip_if_not_installed("magick")

  imgA <- system.file("extdata/icons/circleA_40.png", package = "couplr")
  imgB <- system.file("extdata/icons/circleB_40.png", package = "couplr")
  skip_if(imgA == "" || imgB == "")

  outfile <- tempfile(fileext = ".gif")
  on.exit(unlink(outfile), add = TRUE)

  result <- pixel_morph_animate(
    imgA, imgB,
    format = "gif",
    outfile = outfile,
    n_frames = 2,
    show = FALSE
  )

  expect_true(file.exists(outfile))
  expect_true(file.size(outfile) > 0)
})

Try the couplr package in your browser

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

couplr documentation built on Jan. 20, 2026, 5:07 p.m.