From f2160378cef554d3a2f0bcbd69f9af35f796e816 Mon Sep 17 00:00:00 2001 From: Daniel Jette Date: Sun, 15 Mar 2026 12:39:35 -0400 Subject: [PATCH 1/2] Fix OOM crashes during screenshot comparison - Recycle captured bitmap in createBitmapFromActivity() after saving to disk, eliminating ~9.6 MB leaked per test invocation - Switch IntBuffer allocation from heap (IntBuffer.allocate) to native memory (ByteBuffer.allocateDirect) to bypass JVM heap constraints - Add overflow guard for large capacity * INTEGER_BYTES computations - Wrap ParallelPixelProcessor analyze() and transform() in try/finally to guarantee buffer cleanup on exceptions - Recycle diff bitmap in HighContrastDiff.generate() after saving - Update ImageBufferTest to reflect direct buffer allocation behavior --- .../testify/core/processor/ImageBufferTest.kt | 11 ++- .../java/dev/testify/ScreenshotUtility.kt | 2 + .../core/exception/LowMemoryException.kt | 2 +- .../dev/testify/core/processor/ImageBuffer.kt | 11 ++- .../core/processor/ParallelPixelProcessor.kt | 74 ++++++++++--------- .../core/processor/diff/HighContrastDiff.kt | 15 ++-- 6 files changed, 68 insertions(+), 47 deletions(-) diff --git a/Library/src/androidTest/java/dev/testify/core/processor/ImageBufferTest.kt b/Library/src/androidTest/java/dev/testify/core/processor/ImageBufferTest.kt index c3cb31f6..710baa6a 100644 --- a/Library/src/androidTest/java/dev/testify/core/processor/ImageBufferTest.kt +++ b/Library/src/androidTest/java/dev/testify/core/processor/ImageBufferTest.kt @@ -23,9 +23,6 @@ */ package dev.testify.core.processor -import android.app.ActivityManager -import android.content.Context.ACTIVITY_SERVICE -import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import dev.testify.core.exception.ImageBufferAllocationException import dev.testify.core.exception.LowMemoryException import org.junit.Assert.assertEquals @@ -69,14 +66,16 @@ class ImageBufferTest { @Test(expected = LowMemoryException::class) fun allocate_fails_on_oom() { - val activityManager = getInstrumentation().targetContext.getSystemService(ACTIVITY_SERVICE) as ActivityManager - val requestedSize: Int = activityManager.memoryClass * 1_048_576 / 2 + // Request a capacity whose byte size (capacity * 4) exceeds Int.MAX_VALUE, + // which cannot be fulfilled by a single direct ByteBuffer. + val requestedSize: Int = Int.MAX_VALUE / 2 ImageBuffers.allocate(width = 1, height = requestedSize, allocateDiffBuffer = false) } @Test fun can_allocate_a_reasonable_amount() { - val requestedSize: Int = Runtime.getRuntime().freeMemory().toInt() / 10 + // Allocate buffers equivalent to a 1080x1920 screen (a typical device resolution) + val requestedSize = 1080 * 1920 val buffers = ImageBuffers.allocate(width = 1, height = requestedSize, allocateDiffBuffer = false) assertNotNull(buffers.baselineBuffer) assertNotNull(buffers.currentBuffer) diff --git a/Library/src/main/java/dev/testify/ScreenshotUtility.kt b/Library/src/main/java/dev/testify/ScreenshotUtility.kt index bbb3a40c..6a12182a 100644 --- a/Library/src/main/java/dev/testify/ScreenshotUtility.kt +++ b/Library/src/main/java/dev/testify/ScreenshotUtility.kt @@ -163,6 +163,8 @@ fun createBitmapFromActivity( val destination = getDestination(activity, fileName) saveBitmapToDestination(activity, currentActivityBitmap[0], destination) + currentActivityBitmap[0]?.recycle() + currentActivityBitmap[0] = null return destination.loadBitmap(preferredBitmapOptions) } diff --git a/Library/src/main/java/dev/testify/core/exception/LowMemoryException.kt b/Library/src/main/java/dev/testify/core/exception/LowMemoryException.kt index 80b83793..f33012d5 100644 --- a/Library/src/main/java/dev/testify/core/exception/LowMemoryException.kt +++ b/Library/src/main/java/dev/testify/core/exception/LowMemoryException.kt @@ -50,7 +50,7 @@ import dev.testify.core.formatDeviceString @Suppress("ktlint:standard:argument-list-wrapping") class LowMemoryException( targetContext: Context, - requestedAllocation: Int, + requestedAllocation: Long, memoryInfo: String, cause: OutOfMemoryError ) : TestifyException( diff --git a/Library/src/main/java/dev/testify/core/processor/ImageBuffer.kt b/Library/src/main/java/dev/testify/core/processor/ImageBuffer.kt index 17d8477e..7626dbf3 100644 --- a/Library/src/main/java/dev/testify/core/processor/ImageBuffer.kt +++ b/Library/src/main/java/dev/testify/core/processor/ImageBuffer.kt @@ -34,6 +34,8 @@ import androidx.annotation.IntRange import androidx.test.platform.app.InstrumentationRegistry import dev.testify.core.exception.ImageBufferAllocationException import dev.testify.core.exception.LowMemoryException +import java.nio.ByteBuffer +import java.nio.ByteOrder import java.nio.IntBuffer private const val INTEGER_BYTES: Int = 4 @@ -122,11 +124,16 @@ internal data class ImageBuffers( * @throws LowMemoryException if the allocation fails twice */ internal fun allocateSafely(capacity: Int, retry: Boolean = true): IntBuffer { - val requestedBytes = capacity * INTEGER_BYTES + val requestedBytes = capacity.toLong() * INTEGER_BYTES return try { val memoryState = formatMemoryState() Log.v(LOG_TAG, "Allocating $requestedBytes bytes\n$memoryState") - IntBuffer.allocate(capacity) + if (requestedBytes > Int.MAX_VALUE) { + throw OutOfMemoryError("Requested allocation of $requestedBytes bytes exceeds maximum direct buffer size") + } + ByteBuffer.allocateDirect(requestedBytes.toInt()) + .order(ByteOrder.nativeOrder()) + .asIntBuffer() } catch (e: OutOfMemoryError) { val memoryState = formatMemoryState() Log.e(LOG_TAG, "Error allocating $requestedBytes bytes\n$memoryState", e) diff --git a/Library/src/main/java/dev/testify/core/processor/ParallelPixelProcessor.kt b/Library/src/main/java/dev/testify/core/processor/ParallelPixelProcessor.kt index da02a454..23820268 100644 --- a/Library/src/main/java/dev/testify/core/processor/ParallelPixelProcessor.kt +++ b/Library/src/main/java/dev/testify/core/processor/ParallelPixelProcessor.kt @@ -133,23 +133,26 @@ class ParallelPixelProcessor private constructor( */ fun analyze(analyzer: AnalyzePixelFunction): Boolean { val buffers = prepareBuffers() - val chunkData = getChunkData(buffers.width, buffers.height) - val results = BitSet(chunkData.chunks).apply { set(0, chunkData.chunks) } - - runBlockingInChunks(chunkData) { chunk, index -> - val position = getPosition(index, buffers.width) - val baselinePixel = buffers.baselineBuffer[index] - val currentPixel = buffers.currentBuffer[index] - if (!analyzer(baselinePixel, currentPixel, position)) { - results.clear(chunk) - false - } else { - true + try { + val chunkData = getChunkData(buffers.width, buffers.height) + val results = BitSet(chunkData.chunks).apply { set(0, chunkData.chunks) } + + runBlockingInChunks(chunkData) { chunk, index -> + val position = getPosition(index, buffers.width) + val baselinePixel = buffers.baselineBuffer[index] + val currentPixel = buffers.currentBuffer[index] + if (!analyzer(baselinePixel, currentPixel, position)) { + results.clear(chunk) + false + } else { + true + } } - } - buffers.free() - return results.cardinality() == chunkData.chunks + return results.cardinality() == chunkData.chunks + } finally { + buffers.free() + } } /** @@ -163,25 +166,30 @@ class ParallelPixelProcessor private constructor( transformer: (baselinePixel: Int, currentPixel: Int, position: Pair) -> Int ): TransformResult { val buffers = prepareBuffers(allocateDiffBuffer = true) - val chunkData = getChunkData(buffers.width, buffers.height) - val diffBuffer = buffers.diffBuffer - - runBlockingInChunks(chunkData) { _, index -> - val position = getPosition(index, buffers.width) - val baselinePixel = buffers.baselineBuffer[index] - val currentPixel = buffers.currentBuffer[index] - diffBuffer.put(index, transformer(baselinePixel, currentPixel, position)) - true - } - - val result = TransformResult( - width = buffers.width, - height = buffers.height, - pixels = diffBuffer.array() - ) + try { + val chunkData = getChunkData(buffers.width, buffers.height) + val diffBuffer = buffers.diffBuffer + + runBlockingInChunks(chunkData) { _, index -> + val position = getPosition(index, buffers.width) + val baselinePixel = buffers.baselineBuffer[index] + val currentPixel = buffers.currentBuffer[index] + diffBuffer.put(index, transformer(baselinePixel, currentPixel, position)) + true + } - buffers.free() - return result + val pixels = IntArray(buffers.width * buffers.height) + diffBuffer.position(0) + diffBuffer.get(pixels) + + return TransformResult( + width = buffers.width, + height = buffers.height, + pixels = pixels + ) + } finally { + buffers.free() + } } /** diff --git a/Library/src/main/java/dev/testify/core/processor/diff/HighContrastDiff.kt b/Library/src/main/java/dev/testify/core/processor/diff/HighContrastDiff.kt index 664beeea..6ac404c8 100644 --- a/Library/src/main/java/dev/testify/core/processor/diff/HighContrastDiff.kt +++ b/Library/src/main/java/dev/testify/core/processor/diff/HighContrastDiff.kt @@ -100,11 +100,16 @@ class HighContrastDiff private constructor( } } - saveBitmapToDestination( - context = context, - bitmap = transformResult.createBitmap(), - destination = getDestination(context, "$fileName.diff") - ) + val diffBitmap = transformResult.createBitmap() + try { + saveBitmapToDestination( + context = context, + bitmap = diffBitmap, + destination = getDestination(context, "$fileName.diff") + ) + } finally { + diffBitmap.recycle() + } } private var exactness: Float? = null From b20d9ae09619e763454a84b5182fafdd48ebda00 Mon Sep 17 00:00:00 2001 From: Daniel Jette Date: Mon, 16 Mar 2026 10:50:22 -0400 Subject: [PATCH 2/2] Refactor pixel processing to use stripe-based approach Replace full-image IntBuffer allocation with stripe-based processing to dramatically reduce peak Java heap usage during bitmap comparison. - Process bitmaps in horizontal stripes using Bitmap.createBitmap() sub-regions and copyPixelsToBuffer(), keeping per-stripe heap usage under ~1 MB instead of ~19 MB for full-image buffers - Stripe height is dynamically calculated based on available heap memory - Revert ByteBuffer.allocateDirect() change (no benefit on Android where direct buffers use VMRuntime.newNonMovableArray on the Java heap) - Add mockBitmapCreateBitmap() test helper and update all processor tests --- .../testify/core/processor/ImageBufferTest.kt | 11 +- .../core/exception/LowMemoryException.kt | 2 +- .../dev/testify/core/processor/ImageBuffer.kt | 11 +- .../core/processor/ParallelPixelProcessor.kt | 125 ++++++++++++------ .../core/processor/BitmapTestHelpers.kt | 49 +++++++ .../processor/ParallelPixelProcessorTest.kt | 1 + .../processor/compare/FuzzyCompareTest.kt | 2 + .../processor/compare/RegionCompareTest.kt | 19 +++ .../processor/diff/HighContrastDiffTest.kt | 2 + 9 files changed, 167 insertions(+), 55 deletions(-) diff --git a/Library/src/androidTest/java/dev/testify/core/processor/ImageBufferTest.kt b/Library/src/androidTest/java/dev/testify/core/processor/ImageBufferTest.kt index 710baa6a..c3cb31f6 100644 --- a/Library/src/androidTest/java/dev/testify/core/processor/ImageBufferTest.kt +++ b/Library/src/androidTest/java/dev/testify/core/processor/ImageBufferTest.kt @@ -23,6 +23,9 @@ */ package dev.testify.core.processor +import android.app.ActivityManager +import android.content.Context.ACTIVITY_SERVICE +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import dev.testify.core.exception.ImageBufferAllocationException import dev.testify.core.exception.LowMemoryException import org.junit.Assert.assertEquals @@ -66,16 +69,14 @@ class ImageBufferTest { @Test(expected = LowMemoryException::class) fun allocate_fails_on_oom() { - // Request a capacity whose byte size (capacity * 4) exceeds Int.MAX_VALUE, - // which cannot be fulfilled by a single direct ByteBuffer. - val requestedSize: Int = Int.MAX_VALUE / 2 + val activityManager = getInstrumentation().targetContext.getSystemService(ACTIVITY_SERVICE) as ActivityManager + val requestedSize: Int = activityManager.memoryClass * 1_048_576 / 2 ImageBuffers.allocate(width = 1, height = requestedSize, allocateDiffBuffer = false) } @Test fun can_allocate_a_reasonable_amount() { - // Allocate buffers equivalent to a 1080x1920 screen (a typical device resolution) - val requestedSize = 1080 * 1920 + val requestedSize: Int = Runtime.getRuntime().freeMemory().toInt() / 10 val buffers = ImageBuffers.allocate(width = 1, height = requestedSize, allocateDiffBuffer = false) assertNotNull(buffers.baselineBuffer) assertNotNull(buffers.currentBuffer) diff --git a/Library/src/main/java/dev/testify/core/exception/LowMemoryException.kt b/Library/src/main/java/dev/testify/core/exception/LowMemoryException.kt index f33012d5..80b83793 100644 --- a/Library/src/main/java/dev/testify/core/exception/LowMemoryException.kt +++ b/Library/src/main/java/dev/testify/core/exception/LowMemoryException.kt @@ -50,7 +50,7 @@ import dev.testify.core.formatDeviceString @Suppress("ktlint:standard:argument-list-wrapping") class LowMemoryException( targetContext: Context, - requestedAllocation: Long, + requestedAllocation: Int, memoryInfo: String, cause: OutOfMemoryError ) : TestifyException( diff --git a/Library/src/main/java/dev/testify/core/processor/ImageBuffer.kt b/Library/src/main/java/dev/testify/core/processor/ImageBuffer.kt index 7626dbf3..17d8477e 100644 --- a/Library/src/main/java/dev/testify/core/processor/ImageBuffer.kt +++ b/Library/src/main/java/dev/testify/core/processor/ImageBuffer.kt @@ -34,8 +34,6 @@ import androidx.annotation.IntRange import androidx.test.platform.app.InstrumentationRegistry import dev.testify.core.exception.ImageBufferAllocationException import dev.testify.core.exception.LowMemoryException -import java.nio.ByteBuffer -import java.nio.ByteOrder import java.nio.IntBuffer private const val INTEGER_BYTES: Int = 4 @@ -124,16 +122,11 @@ internal data class ImageBuffers( * @throws LowMemoryException if the allocation fails twice */ internal fun allocateSafely(capacity: Int, retry: Boolean = true): IntBuffer { - val requestedBytes = capacity.toLong() * INTEGER_BYTES + val requestedBytes = capacity * INTEGER_BYTES return try { val memoryState = formatMemoryState() Log.v(LOG_TAG, "Allocating $requestedBytes bytes\n$memoryState") - if (requestedBytes > Int.MAX_VALUE) { - throw OutOfMemoryError("Requested allocation of $requestedBytes bytes exceeds maximum direct buffer size") - } - ByteBuffer.allocateDirect(requestedBytes.toInt()) - .order(ByteOrder.nativeOrder()) - .asIntBuffer() + IntBuffer.allocate(capacity) } catch (e: OutOfMemoryError) { val memoryState = formatMemoryState() Log.e(LOG_TAG, "Error allocating $requestedBytes bytes\n$memoryState", e) diff --git a/Library/src/main/java/dev/testify/core/processor/ParallelPixelProcessor.kt b/Library/src/main/java/dev/testify/core/processor/ParallelPixelProcessor.kt index 23820268..d530d5a9 100644 --- a/Library/src/main/java/dev/testify/core/processor/ParallelPixelProcessor.kt +++ b/Library/src/main/java/dev/testify/core/processor/ParallelPixelProcessor.kt @@ -30,17 +30,25 @@ import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import java.nio.IntBuffer import java.util.BitSet import kotlin.math.ceil +import kotlin.math.min typealias AnalyzePixelFunction = (baselinePixel: Int, currentPixel: Int, position: Pair) -> Boolean +private const val BYTES_PER_PIXEL = 4 + /** * A class that allows for parallel processing of pixels in a bitmap. * * Uses coroutines to process pixels in two [Bitmap] objects in parallel. * [analyze] is used to compare two bitmaps in parallel. * [transform] is used to transform two bitmaps in parallel. + * + * Processes images in horizontal stripes to limit heap memory usage. Each stripe's pixels are + * read via [Bitmap.getPixels] into temporary arrays sized for just that stripe, avoiding the + * need to allocate full-image buffers on the Java heap. */ class ParallelPixelProcessor private constructor( private val configuration: ParallelProcessorConfiguration @@ -66,17 +74,17 @@ class ParallelPixelProcessor private constructor( } /** - * Prepare the bitmaps for parallel processing. + * Calculate the number of rows to process per stripe based on available heap memory. + * Targets using no more than 1/4 of free heap for the two per-stripe IntArrays. */ - private fun prepareBuffers(allocateDiffBuffer: Boolean = false): ImageBuffers { - return ImageBuffers.allocate( - width = currentBitmap.width, - height = currentBitmap.height, - allocateDiffBuffer = allocateDiffBuffer - ).apply { - baselineBitmap.copyPixelsToBuffer(baselineBuffer) - currentBitmap.copyPixelsToBuffer(currentBuffer) - } + @VisibleForTesting + internal fun calculateStripeHeight(width: Int, height: Int): Int { + val runtime = Runtime.getRuntime() + val freeMemory = runtime.maxMemory() - (runtime.totalMemory() - runtime.freeMemory()) + val targetBudget = freeMemory / 4 + val bytesPerRow = width.toLong() * BYTES_PER_PIXEL * 2 // two IntArrays (baseline + current) + val maxRows = (targetBudget / bytesPerRow).toInt().coerceAtLeast(1) + return min(maxRows, height) } /** @@ -128,20 +136,40 @@ class ParallelPixelProcessor private constructor( * Analyze the two bitmaps in parallel. * The analyzer function is called for each pixel in the bitmaps. * + * Processes the image in horizontal stripes to minimize heap usage. + * * @param analyzer The analyzer function to call for each pixel. * @return True if all pixels pass the analyzer function, false otherwise. */ fun analyze(analyzer: AnalyzePixelFunction): Boolean { - val buffers = prepareBuffers() - try { - val chunkData = getChunkData(buffers.width, buffers.height) + val width = currentBitmap.width + val height = currentBitmap.height + val stripeHeight = calculateStripeHeight(width, height) + + for (stripeY in 0 until height step stripeHeight) { + val h = min(stripeHeight, height - stripeY) + val stripeSize = width * h + + val baselineBuffer = IntBuffer.allocate(stripeSize) + val currentBuffer = IntBuffer.allocate(stripeSize) + + val baselineStripe = Bitmap.createBitmap(baselineBitmap, 0, stripeY, width, h) + val currentStripe = Bitmap.createBitmap(currentBitmap, 0, stripeY, width, h) + try { + baselineStripe.copyPixelsToBuffer(baselineBuffer) + currentStripe.copyPixelsToBuffer(currentBuffer) + } finally { + if (baselineStripe !== baselineBitmap) baselineStripe.recycle() + if (currentStripe !== currentBitmap) currentStripe.recycle() + } + + val chunkData = getChunkData(width, h) val results = BitSet(chunkData.chunks).apply { set(0, chunkData.chunks) } runBlockingInChunks(chunkData) { chunk, index -> - val position = getPosition(index, buffers.width) - val baselinePixel = buffers.baselineBuffer[index] - val currentPixel = buffers.currentBuffer[index] - if (!analyzer(baselinePixel, currentPixel, position)) { + val x = index % width + val y = stripeY + index / width + if (!analyzer(baselineBuffer[index], currentBuffer[index], x to y)) { results.clear(chunk) false } else { @@ -149,47 +177,64 @@ class ParallelPixelProcessor private constructor( } } - return results.cardinality() == chunkData.chunks - } finally { - buffers.free() + if (results.cardinality() != chunkData.chunks) { + return false + } } + + return true } /** * Transform the two bitmaps in parallel. * The transformer function is called for each pixel in the bitmaps. * + * Processes the image in horizontal stripes to minimize heap usage. + * * @param transformer The transformer function to call for each pixel. * @return A [TransformResult] containing the transformed pixels. */ fun transform( transformer: (baselinePixel: Int, currentPixel: Int, position: Pair) -> Int ): TransformResult { - val buffers = prepareBuffers(allocateDiffBuffer = true) - try { - val chunkData = getChunkData(buffers.width, buffers.height) - val diffBuffer = buffers.diffBuffer + val width = currentBitmap.width + val height = currentBitmap.height + val stripeHeight = calculateStripeHeight(width, height) + val outputPixels = IntArray(width * height) + + for (stripeY in 0 until height step stripeHeight) { + val h = min(stripeHeight, height - stripeY) + val stripeSize = width * h + + val baselineBuffer = IntBuffer.allocate(stripeSize) + val currentBuffer = IntBuffer.allocate(stripeSize) + + val baselineStripe = Bitmap.createBitmap(baselineBitmap, 0, stripeY, width, h) + val currentStripe = Bitmap.createBitmap(currentBitmap, 0, stripeY, width, h) + try { + baselineStripe.copyPixelsToBuffer(baselineBuffer) + currentStripe.copyPixelsToBuffer(currentBuffer) + } finally { + if (baselineStripe !== baselineBitmap) baselineStripe.recycle() + if (currentStripe !== currentBitmap) currentStripe.recycle() + } + + val chunkData = getChunkData(width, h) + val stripeOffset = stripeY * width runBlockingInChunks(chunkData) { _, index -> - val position = getPosition(index, buffers.width) - val baselinePixel = buffers.baselineBuffer[index] - val currentPixel = buffers.currentBuffer[index] - diffBuffer.put(index, transformer(baselinePixel, currentPixel, position)) + val x = index % width + val y = stripeY + index / width + outputPixels[stripeOffset + index] = transformer( + baselineBuffer[index], + currentBuffer[index], + x to y + ) true } - - val pixels = IntArray(buffers.width * buffers.height) - diffBuffer.position(0) - diffBuffer.get(pixels) - - return TransformResult( - width = buffers.width, - height = buffers.height, - pixels = pixels - ) - } finally { - buffers.free() } + + return TransformResult(width = width, height = height, pixels = outputPixels) } /** diff --git a/Library/src/test/java/dev/testify/core/processor/BitmapTestHelpers.kt b/Library/src/test/java/dev/testify/core/processor/BitmapTestHelpers.kt index e7660e65..f37a8d42 100644 --- a/Library/src/test/java/dev/testify/core/processor/BitmapTestHelpers.kt +++ b/Library/src/test/java/dev/testify/core/processor/BitmapTestHelpers.kt @@ -27,6 +27,7 @@ import android.graphics.Bitmap import android.graphics.Rect import io.mockk.every import io.mockk.mockk +import io.mockk.mockkStatic import io.mockk.slot import java.nio.Buffer import java.nio.IntBuffer @@ -34,6 +35,54 @@ import java.nio.IntBuffer const val DEFAULT_BITMAP_WIDTH = 1080 const val DEFAULT_BITMAP_HEIGHT = 2220 +/** + * Set up the static mock for [Bitmap.createBitmap] so that stripe-based processing works in unit tests. + * Call this from @Before in tests that use [ParallelPixelProcessor]. + */ +fun mockBitmapCreateBitmap() { + mockkStatic(Bitmap::class) + every { Bitmap.createBitmap(any(), any(), any(), any(), any()) } answers { + val source = arg(0) + val x = arg(1) + val y = arg(2) + val w = arg(3) + val h = arg(4) + createSubBitmapMock(source, x, y, w, h) + } +} + +/** + * Creates a mock sub-bitmap that extracts pixel data from the source bitmap's region. + */ +private fun createSubBitmapMock(source: Bitmap, srcX: Int, srcY: Int, w: Int, h: Int): Bitmap { + // Read the source region's pixels via copyPixelsToBuffer on the full source, + // then extract just the sub-region. + val fullWidth = source.width + val fullHeight = source.height + val fullBuffer = IntBuffer.allocate(fullWidth * fullHeight) + source.copyPixelsToBuffer(fullBuffer) + + val subBuffer = IntBuffer.allocate(w * h) + for (row in 0 until h) { + for (col in 0 until w) { + val srcIndex = (srcY + row) * fullWidth + (srcX + col) + subBuffer.put(row * w + col, fullBuffer[srcIndex]) + } + } + + return mockk(relaxed = true) { + every { this@mockk.width } returns w + every { this@mockk.height } returns h + val slotBuffer = slot() + every { this@mockk.copyPixelsToBuffer(capture(slotBuffer)) } answers { + val outputBuffer = slotBuffer.captured as IntBuffer + for (i in 0 until w * h) { + outputBuffer.put(subBuffer[i]) + } + } + } +} + fun mockBitmap( width: Int = DEFAULT_BITMAP_WIDTH, height: Int = DEFAULT_BITMAP_HEIGHT, diff --git a/Library/src/test/java/dev/testify/core/processor/ParallelPixelProcessorTest.kt b/Library/src/test/java/dev/testify/core/processor/ParallelPixelProcessorTest.kt index e3d95bd2..22326e56 100644 --- a/Library/src/test/java/dev/testify/core/processor/ParallelPixelProcessorTest.kt +++ b/Library/src/test/java/dev/testify/core/processor/ParallelPixelProcessorTest.kt @@ -67,6 +67,7 @@ class ParallelPixelProcessorTest { fun setUp() { mockkStatic(::formatMemoryState) every { formatMemoryState() } returns "" + mockBitmapCreateBitmap() } @After diff --git a/Library/src/test/java/dev/testify/core/processor/compare/FuzzyCompareTest.kt b/Library/src/test/java/dev/testify/core/processor/compare/FuzzyCompareTest.kt index 829e4d75..0bf6fc43 100644 --- a/Library/src/test/java/dev/testify/core/processor/compare/FuzzyCompareTest.kt +++ b/Library/src/test/java/dev/testify/core/processor/compare/FuzzyCompareTest.kt @@ -29,6 +29,7 @@ import dev.testify.core.processor.ParallelPixelProcessor import dev.testify.core.processor.ParallelProcessorConfiguration import dev.testify.core.processor.formatMemoryState import dev.testify.core.processor.mockBitmap +import dev.testify.core.processor.mockBitmapCreateBitmap import dev.testify.internal.helpers.ManifestPlaceholder import dev.testify.internal.helpers.getMetaDataValue import io.mockk.clearAllMocks @@ -59,6 +60,7 @@ class FuzzyCompareTest { mockkObject(ParallelPixelProcessor.Companion) mockkStatic("dev.testify.internal.helpers.ManifestHelpersKt") mockkStatic(::formatMemoryState) + mockBitmapCreateBitmap() every { any().getMetaDataValue() } returns null every { formatMemoryState() } returns "" } diff --git a/Library/src/test/java/dev/testify/core/processor/compare/RegionCompareTest.kt b/Library/src/test/java/dev/testify/core/processor/compare/RegionCompareTest.kt index 6148d010..4f9d642b 100644 --- a/Library/src/test/java/dev/testify/core/processor/compare/RegionCompareTest.kt +++ b/Library/src/test/java/dev/testify/core/processor/compare/RegionCompareTest.kt @@ -29,6 +29,7 @@ import android.graphics.Rect import dev.testify.core.TestifyConfiguration import dev.testify.core.processor.ParallelProcessorConfiguration import dev.testify.core.processor.formatMemoryState +import dev.testify.core.processor.mockBitmapCreateBitmap import dev.testify.core.processor.mockRect import dev.testify.internal.helpers.ManifestPlaceholder import dev.testify.internal.helpers.getMetaDataValue @@ -61,6 +62,7 @@ class RegionCompareTest { Dispatchers.setMain(mainThreadSurrogate) mockkStatic("dev.testify.internal.helpers.ManifestHelpersKt") mockkStatic(::formatMemoryState) + mockBitmapCreateBitmap() every { any().getMetaDataValue() } returns null every { formatMemoryState() } returns "" } @@ -157,6 +159,23 @@ class RegionCompareTest { } } } + every { getPixels(any(), any(), any(), any(), any(), any(), any()) } answers { + val pixels = arg(0) + val offset = arg(1) + val stride = arg(2) + val startX = arg(3) + val startY = arg(4) + val regionWidth = arg(5) + val regionHeight = arg(6) + for (row in 0 until regionHeight) { + for (col in 0 until regionWidth) { + val x = startX + col + val y = startY + row + pixels[offset + row * stride + col] = + if (alternateColor != null) alternateColor(color, x, y) else color + } + } + } } } } diff --git a/Library/src/test/java/dev/testify/core/processor/diff/HighContrastDiffTest.kt b/Library/src/test/java/dev/testify/core/processor/diff/HighContrastDiffTest.kt index c0d3f4e1..45f12a20 100644 --- a/Library/src/test/java/dev/testify/core/processor/diff/HighContrastDiffTest.kt +++ b/Library/src/test/java/dev/testify/core/processor/diff/HighContrastDiffTest.kt @@ -31,6 +31,7 @@ import dev.testify.core.processor.ParallelProcessorConfiguration import dev.testify.core.processor.createBitmap import dev.testify.core.processor.formatMemoryState import dev.testify.core.processor.mockBitmap +import dev.testify.core.processor.mockBitmapCreateBitmap import dev.testify.core.processor.mockRect import dev.testify.internal.helpers.ManifestPlaceholder import dev.testify.internal.helpers.getMetaDataValue @@ -76,6 +77,7 @@ class HighContrastDiffTest { mockkStatic("dev.testify.core.processor.BitmapExtentionsKt") mockkStatic("dev.testify.internal.helpers.ManifestHelpersKt") mockkStatic(::formatMemoryState) + mockBitmapCreateBitmap() every { formatMemoryState() } returns "" every { any().createBitmap() } answers {