Convert some tests to C++ and GoogleTest

Keep aviftest in C with no GoogleTest dependency as a minimum test
suite when GoogleTest is unavailable.
Convert feature-oriented, unit tests to C++ and to the GoogleTest
testing framework. Migrating remaining C behaviors will be done in
another change (goto, casts, etc.).

Add extern "C" in apps/shared headers.
Add AVIF_ENABLE_GTEST and AVIF_LOCAL_GTEST CMake options.

Build GoogleTest and run all tests in the GitHub workflow.

Set CMAKE_CXX_STANDARD to C++11.
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 31936d5..31f3e30 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -52,11 +52,24 @@
       if: steps.cache-ext.outputs.cache-hit != 'true'
       working-directory: ./ext
       run: bash libyuv.cmd
+    - name: Build GoogleTest
+      # TODO: if: steps.cache-ext.outputs.cache-hit != 'true'
+      working-directory: ./ext
+      # Note: "apt install googletest" is sometimes insufficient for find_package(GTest) so build in ext/ instead.
+      run: bash googletest.cmd
 
     - name: Prepare libavif (cmake)
       run: |
         mkdir build && cd build
-        cmake .. -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DAVIF_CODEC_AOM=ON -DAVIF_LOCAL_AOM=ON -DAVIF_CODEC_DAV1D=ON -DAVIF_LOCAL_DAV1D=ON -DAVIF_CODEC_RAV1E=ON -DAVIF_LOCAL_RAV1E=ON -DAVIF_CODEC_SVT=ON -DAVIF_LOCAL_SVT=ON -DAVIF_CODEC_LIBGAV1=ON -DAVIF_LOCAL_LIBGAV1=ON -DAVIF_LOCAL_LIBYUV=ON -DAVIF_BUILD_EXAMPLES=ON -DAVIF_BUILD_APPS=ON -DAVIF_BUILD_TESTS=ON
+        cmake .. -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF \
+          -DAVIF_CODEC_AOM=ON -DAVIF_LOCAL_AOM=ON \
+          -DAVIF_CODEC_DAV1D=ON -DAVIF_LOCAL_DAV1D=ON \
+          -DAVIF_CODEC_RAV1E=ON -DAVIF_LOCAL_RAV1E=ON \
+          -DAVIF_CODEC_SVT=ON -DAVIF_LOCAL_SVT=ON \
+          -DAVIF_CODEC_LIBGAV1=ON -DAVIF_LOCAL_LIBGAV1=ON \
+          -DAVIF_LOCAL_LIBYUV=ON \
+          -DAVIF_BUILD_EXAMPLES=ON -DAVIF_BUILD_APPS=ON \
+          -DAVIF_BUILD_TESTS=ON -DAVIF_ENABLE_GTEST=ON -DAVIF_LOCAL_GTEST=ON
     - name: Build libavif (make)
       working-directory: ./build
       run: make -j $(($(nproc) + 1))
diff --git a/.gitignore b/.gitignore
index 90b5532..fa481e6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
 /obj*
 /ext/aom
 /ext/dav1d
+/ext/googletest
 /ext/libjpeg
 /ext/libgav1
 /ext/libpng
diff --git a/CMakeLists.txt b/CMakeLists.txt
index c6c3aab..673387b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -51,6 +51,9 @@
 option(AVIF_LOCAL_SVT
        "Build the SVT-AV1 codec by providing your own copy of the repo in ext/SVT-AV1 (see Local Builds in README)" OFF
 )
+option(AVIF_LOCAL_GTEST
+       "Build the GoogleTest framework by providing your own copy of the repo in ext/googletest (see Local Builds in README)" OFF
+)
 
 if(AVIF_LOCAL_LIBGAV1)
     enable_language(CXX)
@@ -410,7 +413,7 @@
     endif()
 endif()
 
-option(AVIF_BUILD_EXAMPLES "Build avif Examples." OFF)
+option(AVIF_BUILD_EXAMPLES "Build avif examples." OFF)
 if(AVIF_BUILD_EXAMPLES)
     set(AVIF_EXAMPLES avif_example_decode_memory avif_example_decode_file avif_example_decode_streaming avif_example_encode)
 
@@ -437,8 +440,11 @@
 
 option(AVIF_BUILD_APPS "Build avif apps." OFF)
 option(AVIF_BUILD_TESTS "Build avif tests." OFF)
+option(AVIF_ENABLE_GTEST
+       "Build avif C++ tests, which depend on GoogleTest. Requires GoogleTest. Has no effect unless AVIF_BUILD_TESTS is ON." ON
+)
 
-if(AVIF_BUILD_APPS OR AVIF_BUILD_TESTS)
+if(AVIF_BUILD_APPS OR (AVIF_BUILD_TESTS AND AVIF_ENABLE_GTEST))
     find_package(ZLIB REQUIRED)
     find_package(PNG REQUIRED)
     find_package(JPEG REQUIRED)
diff --git a/README.md b/README.md
index 1497c30..8a270bb 100644
--- a/README.md
+++ b/README.md
@@ -36,7 +36,7 @@
 a child CMake project, the appropriate CMake target must already exist
 by the time libavif's CMake scripts are executed.
 
-## Local / Static Builds
+### Local / Static Builds
 
 The `ext/` subdirectory contains a handful of basic scripts which each pull
 down a known-good copy of an AV1 codec and make a local static library build.
@@ -49,6 +49,15 @@
 peek inside of each script to see where the current known-good SHA is for each
 codec.
 
+### Tests
+
+A few tests written in C can be built by enabling the `AVIF_BUILD_TESTS` CMake
+option.
+
+The remaining tests can be built by enabling the `AVIF_BUILD_TESTS` and
+`AVIF_ENABLE_GTEST` CMake options. They require GoogleTest to be built locally
+with ext/googletest.cmd or installed on the system.
+
 ## Prebuilt Library (Windows)
 
 If you're building on Windows with Visual Studio 2019 and want to try out
@@ -66,9 +75,10 @@
 sources from the top-level folder:
 
 ```sh
-clang-format -i apps/*.c apps/shared/avifjpeg.* apps/shared/avifpng.* \
-                apps/shared/avifutil.* apps/shared/y4m.* \
-                examples/*.c include/avif/*.h src/*.c tests/*.h tests/*.c
+clang-format -style=file:.clang-format -i \
+  apps/*.c apps/shared/avifjpeg.* apps/shared/avifpng.* \
+  apps/shared/avifutil.* apps/shared/y4m.* examples/*.c \
+  include/avif/*.h src/*.c tests/*.h tests/*.c tests/*.cc
 ```
 
 Use [cmake-format](https://github.com/cheshirekow/cmake_format) to format the
diff --git a/apps/shared/avifjpeg.h b/apps/shared/avifjpeg.h
index 36df980..305375d 100644
--- a/apps/shared/avifjpeg.h
+++ b/apps/shared/avifjpeg.h
@@ -6,7 +6,15 @@
 
 #include "avif/avif.h"
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 avifBool avifJPEGRead(const char * inputFilename, avifImage * avif, avifPixelFormat requestedFormat, uint32_t requestedDepth);
 avifBool avifJPEGWrite(const char * outputFilename, const avifImage * avif, int jpegQuality, avifChromaUpsampling chromaUpsampling);
 
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
 #endif // ifndef LIBAVIF_APPS_SHARED_AVIFJPEG_H
diff --git a/apps/shared/avifpng.h b/apps/shared/avifpng.h
index d3dba8e..5e130ce 100644
--- a/apps/shared/avifpng.h
+++ b/apps/shared/avifpng.h
@@ -6,6 +6,10 @@
 
 #include "avif/avif.h"
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 // if (requestedDepth == 0), do best-fit
 avifBool avifPNGRead(const char * inputFilename, avifImage * avif, avifPixelFormat requestedFormat, uint32_t requestedDepth, uint32_t * outPNGDepth);
 avifBool avifPNGWrite(const char * outputFilename,
@@ -14,4 +18,8 @@
                       avifChromaUpsampling chromaUpsampling,
                       int compressionLevel);
 
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
 #endif // ifndef LIBAVIF_APPS_SHARED_AVIFPNG_H
diff --git a/apps/shared/avifutil.h b/apps/shared/avifutil.h
index 8e95458..22dfb03 100644
--- a/apps/shared/avifutil.h
+++ b/apps/shared/avifutil.h
@@ -6,6 +6,10 @@
 
 #include "avif/avif.h"
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 // The %z format specifier is not available in the old Windows CRT msvcrt,
 // hence the %I format specifier must be used instead to print out `size_t`.
 // The new Windows CRT UCRT, which is used by Visual Studio 2015 or later,
@@ -56,4 +60,8 @@
 // This must match the cited fallback for "--yuv auto" in avifenc.c's syntax() function.
 #define AVIF_APP_DEFAULT_PIXEL_FORMAT AVIF_PIXEL_FORMAT_YUV444
 
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
 #endif // ifndef LIBAVIF_APPS_SHARED_AVIFUTIL_H
diff --git a/apps/shared/y4m.h b/apps/shared/y4m.h
index be08d28..3770769 100644
--- a/apps/shared/y4m.h
+++ b/apps/shared/y4m.h
@@ -8,6 +8,10 @@
 
 #include "avifutil.h"
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 // Optionally pass one of these pointers (set to NULL) on a fresh input. If it successfully reads in
 // a frame and sees that there is more data to be read, it will allocate an internal structure remembering
 // the y4m header and FILE position and return it. Pass in this pointer to continue reading frames.
@@ -17,4 +21,8 @@
 avifBool y4mRead(const char * inputFilename, avifImage * avif, avifAppSourceTiming * sourceTiming, struct y4mFrameIterator ** iter);
 avifBool y4mWrite(const char * outputFilename, const avifImage * avif);
 
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
 #endif // ifndef LIBAVIF_APPS_SHARED_Y4M_H
diff --git a/ext/googletest.cmd b/ext/googletest.cmd
new file mode 100755
index 0000000..2e2587b
--- /dev/null
+++ b/ext/googletest.cmd
@@ -0,0 +1,15 @@
+: # If you want to use a local build of googletest, you must clone the googletest repo in this directory.
+
+: # The odd choice of comment style in this file is to try to share this script between *nix and win32.
+
+: # cmake must be in your PATH.
+
+: # If you're running this on Windows, be sure you've already run this (from your VC2019 install dir):
+: #     "C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\VC\Auxiliary\Build\vcvars64.bat"
+
+git clone -b release-1.11.0 --depth 1 https://github.com/google/googletest.git
+cd googletest
+mkdir build
+cd build
+cmake .. -DCMAKE_BUILD_TYPE=Release -DBUILD_GMOCK=OFF
+cmake --build .
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 44d532d..717ab9d 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -4,9 +4,6 @@
 # With testing enabled, all targets referenced by add_test() can be run
 # at once with CMake's ctest command line tool from the build folder.
 enable_testing()
-# Using a CMake FIXTURES_SETUP/FIXTURES_CLEANUP to create and delete a
-# subdirectory would be cleaner but this is way simpler.
-set(AVIF_TEST_TMP_DIR ${PROJECT_BINARY_DIR})
 
 add_executable(aviftest aviftest.c)
 if(AVIF_LOCAL_LIBGAV1)
@@ -15,13 +12,6 @@
 target_link_libraries(aviftest avif ${AVIF_PLATFORM_LIBRARIES})
 add_test(NAME aviftest COMMAND aviftest ${CMAKE_CURRENT_SOURCE_DIR}/data)
 
-add_executable(avifgridapitest avifgridapitest.c)
-if(AVIF_LOCAL_LIBGAV1)
-    set_target_properties(avifgridapitest PROPERTIES LINKER_LANGUAGE "CXX")
-endif()
-target_link_libraries(avifgridapitest avif ${AVIF_PLATFORM_LIBRARIES})
-add_test(NAME avifgridapitest COMMAND avifgridapitest)
-
 add_executable(avifincrtest avifincrtest.c avifincrtest_helpers.c)
 if(AVIF_LOCAL_LIBGAV1)
     set_target_properties(avifincrtest PROPERTIES LINKER_LANGUAGE "CXX")
@@ -36,14 +26,6 @@
 target_link_libraries(avifmetadatatest avif ${AVIF_PLATFORM_LIBRARIES})
 add_test(NAME avifmetadatatest COMMAND avifmetadatatest)
 
-add_executable(avify4mtest avify4mtest.c)
-if(AVIF_LOCAL_LIBGAV1)
-    set_target_properties(avify4mtest PROPERTIES LINKER_LANGUAGE "CXX")
-endif()
-target_link_libraries(avify4mtest avif avif_apps ${AVIF_PLATFORM_LIBRARIES})
-add_test(NAME avify4mtest COMMAND avify4mtest AVIF_TEST_TMP_DIR)
-set_tests_properties(avify4mtest PROPERTIES ENVIRONMENT "AVIF_TEST_TMP_DIR=${AVIF_TEST_TMP_DIR}")
-
 add_executable(avifyuv avifyuv.c)
 if(AVIF_LOCAL_LIBGAV1)
     set_target_properties(avifyuv PROPERTIES LINKER_LANGUAGE "CXX")
@@ -53,6 +35,40 @@
     add_test(NAME avifyuv_${AVIFYUV_MODE} COMMAND avifyuv -m ${AVIFYUV_MODE})
 endforeach()
 
+if(AVIF_ENABLE_GTEST)
+    enable_language(CXX)
+    set(CMAKE_CXX_STANDARD 11)
+    if(AVIF_LOCAL_GTEST)
+        set(GTEST_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/ext/googletest/googletest/include)
+        set(GTEST_LIBRARIES ${CMAKE_SOURCE_DIR}/ext/googletest/build/lib/libgtest${CMAKE_STATIC_LIBRARY_SUFFIX})
+        set(GTEST_MAIN_LIBRARIES ${CMAKE_SOURCE_DIR}/ext/googletest/build/lib/libgtest_main${CMAKE_STATIC_LIBRARY_SUFFIX})
+        set(GTEST_BOTH_LIBRARIES ${GTEST_LIBRARIES} ${GTEST_MAIN_LIBRARIES})
+        if(NOT EXISTS ${GTEST_INCLUDE_DIRS}/gtest/gtest.h)
+            message(FATAL_ERROR "googletest(AVIF_LOCAL_GTEST): ${GTEST_INCLUDE_DIRS}/gtest/gtest.h is missing, bailing out")
+        elseif(NOT EXISTS ${GTEST_LIBRARIES})
+            message(FATAL_ERROR "googletest(AVIF_LOCAL_GTEST): ${GTEST_LIBRARIES} is missing, bailing out")
+        elseif(NOT EXISTS ${GTEST_MAIN_LIBRARIES})
+            message(FATAL_ERROR "googletest(AVIF_LOCAL_GTEST): ${GTEST_MAIN_LIBRARIES} is missing, bailing out")
+        else()
+            message(STATUS "Found local ext/googletest")
+        endif()
+    else()
+        find_package(GTest REQUIRED)
+    endif()
+
+    add_executable(avifgridapitest avifgridapitest.cc)
+    target_link_libraries(avifgridapitest avif ${AVIF_PLATFORM_LIBRARIES} ${GTEST_BOTH_LIBRARIES})
+    target_include_directories(avifgridapitest PRIVATE ${GTEST_INCLUDE_DIRS})
+    add_test(NAME avifgridapitest COMMAND avifgridapitest)
+
+    add_executable(avify4mtest avify4mtest.cc)
+    target_link_libraries(avify4mtest avif avif_apps ${AVIF_PLATFORM_LIBRARIES} ${GTEST_BOTH_LIBRARIES})
+    target_include_directories(avify4mtest PRIVATE ${GTEST_INCLUDE_DIRS})
+    add_test(NAME avify4mtest COMMAND avify4mtest)
+else()
+    message(STATUS "Most tests are disabled because AVIF_ENABLE_GTEST is OFF.")
+endif()
+
 if(AVIF_ENABLE_COVERAGE)
     add_custom_target(
         avif_coverage
diff --git a/tests/avifgridapitest.c b/tests/avifgridapitest.c
deleted file mode 100644
index dd74500..0000000
--- a/tests/avifgridapitest.c
+++ /dev/null
@@ -1,258 +0,0 @@
-// Copyright 2022 Google LLC. All rights reserved.
-// SPDX-License-Identifier: BSD-2-Clause
-
-#include "avif/avif.h"
-
-#include <assert.h>
-#include <inttypes.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-
-//------------------------------------------------------------------------------
-
-// Fills a plane with a repeating gradient.
-static void fillPlane(int width, int height, int depth, uint8_t * row, uint32_t rowBytes)
-{
-    assert(depth == 8 || depth == 10 || depth == 12); // Values allowed by AV1.
-    const int maxValuePlusOne = 1 << depth;
-    for (int y = 0; y < height; ++y) {
-        if (depth == 8) {
-            memset(row, y % maxValuePlusOne, width);
-        } else {
-            for (int x = 0; x < width; ++x) {
-                ((uint16_t *)row)[x] = (uint16_t)(y % maxValuePlusOne);
-            }
-        }
-        row += rowBytes;
-    }
-}
-
-// Creates an image where the pixel values are defined but do not matter.
-// Returns false in case of memory failure.
-static avifBool createImage(int width, int height, int depth, avifPixelFormat yuvFormat, avifBool createAlpha, avifImage ** image)
-{
-    *image = avifImageCreate(width, height, depth, yuvFormat);
-    if (*image == NULL) {
-        printf("ERROR: avifImageCreate() failed\n");
-        return AVIF_FALSE;
-    }
-    avifImageAllocatePlanes(*image, createAlpha ? AVIF_PLANES_ALL : AVIF_PLANES_YUV);
-    if (width * height == 0) {
-        return AVIF_TRUE;
-    }
-
-    avifPixelFormatInfo formatInfo;
-    avifGetPixelFormatInfo((*image)->yuvFormat, &formatInfo);
-    uint32_t uvWidth = ((*image)->width + formatInfo.chromaShiftX) >> formatInfo.chromaShiftX;
-    uint32_t uvHeight = ((*image)->height + formatInfo.chromaShiftY) >> formatInfo.chromaShiftY;
-
-    const int planeCount = formatInfo.monochrome ? 1 : AVIF_PLANE_COUNT_YUV;
-    for (int plane = 0; plane < planeCount; ++plane) {
-        fillPlane((plane == AVIF_CHAN_Y) ? (*image)->width : uvWidth,
-                  (plane == AVIF_CHAN_Y) ? (*image)->height : uvHeight,
-                  (*image)->depth,
-                  (*image)->yuvPlanes[plane],
-                  (*image)->yuvRowBytes[plane]);
-    }
-
-    if (createAlpha) {
-        fillPlane((*image)->width, (*image)->height, (*image)->depth, (*image)->alphaPlane, (*image)->alphaRowBytes);
-    }
-    return AVIF_TRUE;
-}
-
-// Generates then encodes a grid image. Returns false in case of failure.
-static avifBool encodeGrid(int columns, int rows, int cellWidth, int cellHeight, int depth, avifPixelFormat yuvFormat, avifBool createAlpha, avifRWData * output)
-{
-    avifBool success = AVIF_FALSE;
-    avifEncoder * encoder = NULL;
-    avifImage ** cellImages = avifAlloc(sizeof(avifImage *) * columns * rows);
-    memset(cellImages, 0, sizeof(avifImage *) * columns * rows);
-    for (int iCell = 0; iCell < columns * rows; ++iCell) {
-        if (!createImage(cellWidth, cellHeight, depth, yuvFormat, createAlpha, &cellImages[iCell])) {
-            goto cleanup;
-        }
-    }
-
-    encoder = avifEncoderCreate();
-    if (encoder == NULL) {
-        printf("ERROR: avifEncoderCreate() failed\n");
-        goto cleanup;
-    }
-    encoder->speed = AVIF_SPEED_FASTEST;
-    if (avifEncoderAddImageGrid(encoder, columns, rows, (const avifImage * const *)cellImages, AVIF_ADD_IMAGE_FLAG_SINGLE) !=
-        AVIF_RESULT_OK) {
-        printf("ERROR: avifEncoderAddImageGrid() failed\n");
-        goto cleanup;
-    }
-    if (avifEncoderFinish(encoder, output) != AVIF_RESULT_OK) {
-        printf("ERROR: avifEncoderFinish() failed\n");
-        goto cleanup;
-    }
-
-    success = AVIF_TRUE;
-cleanup:
-    if (encoder != NULL) {
-        avifEncoderDestroy(encoder);
-    }
-    if (cellImages != NULL) {
-        for (int i = 0; i < columns * rows; ++i) {
-            if (cellImages[i] != NULL) {
-                avifImageDestroy(cellImages[i]);
-            }
-        }
-        avifFree(cellImages);
-    }
-    return success;
-}
-
-//------------------------------------------------------------------------------
-
-// Decodes the data. Returns false in case of failure.
-static avifBool decode(const avifRWData * encodedAvif)
-{
-    avifBool success = AVIF_FALSE;
-    avifImage * const image = avifImageCreateEmpty();
-    avifDecoder * const decoder = avifDecoderCreate();
-    if (image == NULL || decoder == NULL) {
-        printf("ERROR: memory allocation failed\n");
-        goto cleanup;
-    }
-    if (avifDecoderReadMemory(decoder, image, encodedAvif->data, encodedAvif->size) != AVIF_RESULT_OK) {
-        printf("ERROR: avifDecoderReadMemory() failed\n");
-        goto cleanup;
-    }
-    success = AVIF_TRUE;
-cleanup:
-    if (image != NULL) {
-        avifImageDestroy(image);
-    }
-    if (decoder != NULL) {
-        avifDecoderDestroy(decoder);
-    }
-    return success;
-}
-
-//------------------------------------------------------------------------------
-
-// Generates, encodes then decodes a grid image.
-static avifBool encodeDecode(int columns, int rows, int cellWidth, int cellHeight, int depth, avifPixelFormat yuvFormat, avifBool createAlpha, avifBool expectedSuccess)
-{
-    avifBool success = AVIF_FALSE;
-    avifRWData encodedAvif = { 0 };
-    if (encodeGrid(columns, rows, cellWidth, cellHeight, depth, yuvFormat, createAlpha, &encodedAvif) != expectedSuccess) {
-        goto cleanup;
-    }
-    // Only decode if the encoding was expected to succeed.
-    // Any successful encoding shall result in a valid decoding.
-    if (expectedSuccess && !decode(&encodedAvif)) {
-        goto cleanup;
-    }
-    success = AVIF_TRUE;
-cleanup:
-    avifRWDataFree(&encodedAvif);
-    return success;
-}
-
-//------------------------------------------------------------------------------
-
-// For each bit depth, with and without alpha, generates, encodes then decodes a grid image.
-static avifBool encodeDecodeDepthsAlpha(int columns, int rows, int cellWidth, int cellHeight, avifPixelFormat yuvFormat, avifBool expectedSuccess)
-{
-    const int depths[] = { 8, 10, 12 }; // See avifEncoderAddImageInternal()
-    for (size_t d = 0; d < sizeof(depths) / sizeof(depths[0]); ++d) {
-        for (avifBool createAlpha = AVIF_FALSE; createAlpha <= AVIF_TRUE; ++createAlpha) {
-            if (!encodeDecode(columns, rows, cellWidth, cellHeight, depths[d], yuvFormat, createAlpha, expectedSuccess)) {
-                return AVIF_FALSE;
-            }
-        }
-    }
-    return AVIF_TRUE;
-}
-
-// For each dimension, for each combination of cell count and size, generates, encodes then decodes a grid image for several depths and alpha.
-static avifBool encodeDecodeSizes(const int columnsCellWidths[][2],
-                                  int horizontalCombinationCount,
-                                  const int rowsCellHeights[][2],
-                                  int verticalCombinationCount,
-                                  avifPixelFormat yuvFormat,
-                                  avifBool expectedSuccess)
-{
-    for (int i = 0; i < horizontalCombinationCount; ++i) {
-        for (int j = 0; j < verticalCombinationCount; ++j) {
-            if (!encodeDecodeDepthsAlpha(/*columns=*/columnsCellWidths[i][0],
-                                         /*rows=*/rowsCellHeights[j][0],
-                                         /*cellWidth=*/columnsCellWidths[i][1],
-                                         /*cellHeight=*/rowsCellHeights[j][1],
-                                         yuvFormat,
-                                         expectedSuccess)) {
-                return AVIF_FALSE;
-            }
-        }
-    }
-    return AVIF_TRUE;
-}
-
-int main(void)
-{
-    // Pairs of cell count and cell size for a single dimension.
-    // A cell cannot be smaller than 64px in any dimension if there are several cells.
-    // A cell cannot have an odd size in any dimension if there are several cells and chroma subsampling.
-    // Image size must be a multiple of cell size.
-    const int validCellCountsSizes[][2] = { { 1, 64 }, { 1, 66 }, { 2, 64 }, { 3, 68 } };
-    const int validCellCountsSizeCount = sizeof(validCellCountsSizes) / sizeof(validCellCountsSizes[0]);
-    const int invalidCellCountsSizes[][2] = { { 0, 0 }, { 0, 1 }, { 1, 0 }, { 2, 1 }, { 2, 2 }, { 2, 3 }, { 2, 63 } };
-    const int invalidCellCountsSizeCount = sizeof(invalidCellCountsSizes) / sizeof(invalidCellCountsSizes[0]);
-
-    for (int yuvFormat = AVIF_PIXEL_FORMAT_YUV444; yuvFormat <= AVIF_PIXEL_FORMAT_YUV400; ++yuvFormat) {
-        if (!encodeDecodeSizes(validCellCountsSizes,
-                               validCellCountsSizeCount,
-                               validCellCountsSizes,
-                               validCellCountsSizeCount,
-                               yuvFormat,
-                               /*expectedSuccess=*/AVIF_TRUE)) {
-            return EXIT_FAILURE;
-        }
-
-        if (!encodeDecodeSizes(validCellCountsSizes,
-                               validCellCountsSizeCount,
-                               invalidCellCountsSizes,
-                               invalidCellCountsSizeCount,
-                               yuvFormat,
-                               /*expectedSuccess=*/AVIF_FALSE) ||
-            !encodeDecodeSizes(invalidCellCountsSizes,
-                               invalidCellCountsSizeCount,
-                               validCellCountsSizes,
-                               validCellCountsSizeCount,
-                               yuvFormat,
-                               /*expectedSuccess=*/AVIF_FALSE) ||
-            !encodeDecodeSizes(invalidCellCountsSizes,
-                               invalidCellCountsSizeCount,
-                               invalidCellCountsSizes,
-                               invalidCellCountsSizeCount,
-                               yuvFormat,
-                               /*expectedSuccess=*/AVIF_FALSE)) {
-            return EXIT_FAILURE;
-        }
-
-        // Special case depending on the cell count and the chroma subsampling.
-        for (int rows = 1; rows <= 2; ++rows) {
-            avifBool expectedSuccess = (rows == 1) || (yuvFormat != AVIF_PIXEL_FORMAT_YUV420);
-            if (!encodeDecodeDepthsAlpha(/*columns=*/1, rows, /*cellWidth=*/64, /*cellHeight=*/65, yuvFormat, expectedSuccess)) {
-                return EXIT_FAILURE;
-            }
-        }
-
-        // Special case depending on the cell count and the cell size.
-        for (int columns = 1; columns <= 2; ++columns) {
-            for (int rows = 1; rows <= 2; ++rows) {
-                avifBool expectedSuccess = (columns * rows == 1);
-                if (!encodeDecodeDepthsAlpha(columns, rows, /*cellWidth=*/1, /*cellHeight=*/65, yuvFormat, expectedSuccess)) {
-                    return EXIT_FAILURE;
-                }
-            }
-        }
-    }
-    return EXIT_SUCCESS;
-}
diff --git a/tests/avifgridapitest.cc b/tests/avifgridapitest.cc
new file mode 100644
index 0000000..c939e29
--- /dev/null
+++ b/tests/avifgridapitest.cc
@@ -0,0 +1,277 @@
+// Copyright 2022 Google LLC. All rights reserved.
+// SPDX-License-Identifier: BSD-2-Clause
+
+#include "avif/avif.h"
+
+#include <cassert>
+#include <cinttypes>
+#include <cstdio>
+#include <cstring>
+#include <tuple>
+
+#include "gtest/gtest.h"
+
+using testing::Combine;
+using testing::Values;
+using testing::ValuesIn;
+
+namespace
+{
+
+//------------------------------------------------------------------------------
+
+// Fills a plane with a repeating gradient.
+void fillPlane(int width, int height, int depth, uint8_t * row, uint32_t rowBytes)
+{
+    assert(depth == 8 || depth == 10 || depth == 12); // Values allowed by AV1.
+    const int maxValuePlusOne = 1 << depth;
+    for (int y = 0; y < height; ++y) {
+        if (depth == 8) {
+            memset(row, y % maxValuePlusOne, width);
+        } else {
+            for (int x = 0; x < width; ++x) {
+                ((uint16_t *)row)[x] = (uint16_t)(y % maxValuePlusOne);
+            }
+        }
+        row += rowBytes;
+    }
+}
+
+// Creates an image where the pixel values are defined but do not matter.
+// Returns false in case of memory failure.
+bool createImage(int width, int height, int depth, avifPixelFormat yuvFormat, bool createAlpha, avifImage ** image)
+{
+    *image = avifImageCreate(width, height, depth, yuvFormat);
+    if (!*image) {
+        printf("ERROR: avifImageCreate() failed\n");
+        return false;
+    }
+    avifImageAllocatePlanes(*image, createAlpha ? AVIF_PLANES_ALL : AVIF_PLANES_YUV);
+    if (width * height == 0) {
+        return true;
+    }
+
+    avifPixelFormatInfo formatInfo;
+    avifGetPixelFormatInfo((*image)->yuvFormat, &formatInfo);
+    uint32_t uvWidth = ((*image)->width + formatInfo.chromaShiftX) >> formatInfo.chromaShiftX;
+    uint32_t uvHeight = ((*image)->height + formatInfo.chromaShiftY) >> formatInfo.chromaShiftY;
+
+    const int planeCount = formatInfo.monochrome ? 1 : AVIF_PLANE_COUNT_YUV;
+    for (int plane = 0; plane < planeCount; ++plane) {
+        fillPlane((plane == AVIF_CHAN_Y) ? (*image)->width : uvWidth,
+                  (plane == AVIF_CHAN_Y) ? (*image)->height : uvHeight,
+                  (*image)->depth,
+                  (*image)->yuvPlanes[plane],
+                  (*image)->yuvRowBytes[plane]);
+    }
+
+    if (createAlpha) {
+        fillPlane((*image)->width, (*image)->height, (*image)->depth, (*image)->alphaPlane, (*image)->alphaRowBytes);
+    }
+    return true;
+}
+
+// Generates then encodes a grid image. Returns false in case of failure.
+bool encodeGrid(int columns, int rows, int cellWidth, int cellHeight, int depth, avifPixelFormat yuvFormat, bool createAlpha, avifRWData * output)
+{
+    bool success = false;
+    avifEncoder * encoder = NULL;
+    avifImage ** cellImages = (avifImage **)avifAlloc(sizeof(avifImage *) * columns * rows);
+    memset(cellImages, 0, sizeof(avifImage *) * columns * rows);
+    for (int iCell = 0; iCell < columns * rows; ++iCell) {
+        if (!createImage(cellWidth, cellHeight, depth, yuvFormat, createAlpha, &cellImages[iCell])) {
+            goto cleanup;
+        }
+    }
+
+    encoder = avifEncoderCreate();
+    if (!encoder) {
+        printf("ERROR: avifEncoderCreate() failed\n");
+        goto cleanup;
+    }
+    encoder->speed = AVIF_SPEED_FASTEST;
+    if (avifEncoderAddImageGrid(encoder, columns, rows, (const avifImage * const *)cellImages, AVIF_ADD_IMAGE_FLAG_SINGLE) !=
+        AVIF_RESULT_OK) {
+        printf("ERROR: avifEncoderAddImageGrid() failed\n");
+        goto cleanup;
+    }
+    if (avifEncoderFinish(encoder, output) != AVIF_RESULT_OK) {
+        printf("ERROR: avifEncoderFinish() failed\n");
+        goto cleanup;
+    }
+
+    success = true;
+cleanup:
+    if (encoder) {
+        avifEncoderDestroy(encoder);
+    }
+    if (cellImages) {
+        for (int i = 0; i < columns * rows; ++i) {
+            if (cellImages[i]) {
+                avifImageDestroy(cellImages[i]);
+            }
+        }
+        avifFree(cellImages);
+    }
+    return success;
+}
+
+//------------------------------------------------------------------------------
+
+// Decodes the data. Returns false in case of failure.
+bool decode(const avifRWData * encodedAvif)
+{
+    bool success = false;
+    avifImage * const image = avifImageCreateEmpty();
+    avifDecoder * const decoder = avifDecoderCreate();
+    if (!image || !decoder) {
+        printf("ERROR: memory allocation failed\n");
+        goto cleanup;
+    }
+    if (avifDecoderReadMemory(decoder, image, encodedAvif->data, encodedAvif->size) != AVIF_RESULT_OK) {
+        printf("ERROR: avifDecoderReadMemory() failed\n");
+        goto cleanup;
+    }
+    success = true;
+cleanup:
+    if (image) {
+        avifImageDestroy(image);
+    }
+    if (decoder) {
+        avifDecoderDestroy(decoder);
+    }
+    return success;
+}
+
+//------------------------------------------------------------------------------
+
+// Generates, encodes then decodes a grid image.
+bool encodeDecode(int columns, int rows, int cellWidth, int cellHeight, int depth, avifPixelFormat yuvFormat, bool createAlpha, bool expectedSuccess)
+{
+    bool success = false;
+    avifRWData encodedAvif = { nullptr, 0 };
+    if (encodeGrid(columns, rows, cellWidth, cellHeight, depth, yuvFormat, createAlpha, &encodedAvif) != expectedSuccess) {
+        goto cleanup;
+    }
+    // Only decode if the encoding was expected to succeed.
+    // Any successful encoding shall result in a valid decoding.
+    if (expectedSuccess && !decode(&encodedAvif)) {
+        goto cleanup;
+    }
+    success = true;
+cleanup:
+    avifRWDataFree(&encodedAvif);
+    return success;
+}
+
+//------------------------------------------------------------------------------
+
+// Pair of cell count and cell size for a single dimension.
+struct Cell
+{
+    int count, size;
+};
+
+class GridApiTest
+    : public testing::TestWithParam<std::tuple</*horizontal=*/Cell, /*vertical=*/Cell, /*bitDepth=*/int, /*yuvFormat=*/avifPixelFormat, /*createAlpha=*/bool, /*expectedSuccess=*/bool>>
+{
+};
+
+TEST_P(GridApiTest, EncodeDecode)
+{
+    const Cell horizontal = std::get<0>(GetParam());
+    const Cell vertical = std::get<1>(GetParam());
+    const int bitDepth = std::get<2>(GetParam());
+    const avifPixelFormat yuvFormat = std::get<3>(GetParam());
+    const bool createAlpha = std::get<4>(GetParam());
+    const bool expectedSuccess = std::get<5>(GetParam());
+
+    EXPECT_TRUE(encodeDecode(/*columns=*/horizontal.count,
+                             /*rows=*/vertical.count,
+                             /*cellWidth=*/horizontal.size,
+                             /*cellHeight=*/vertical.size,
+                             bitDepth,
+                             yuvFormat,
+                             createAlpha,
+                             expectedSuccess));
+}
+
+// A cell cannot be smaller than 64px in any dimension if there are several cells.
+// A cell cannot have an odd size in any dimension if there are several cells and chroma subsampling.
+// Image size must be a multiple of cell size.
+constexpr Cell kValidCells[] = { { 1, 64 }, { 1, 66 }, { 2, 64 }, { 3, 68 } };
+constexpr Cell kInvalidCells[] = { { 0, 0 }, { 0, 1 }, { 1, 0 }, { 2, 1 }, { 2, 2 }, { 2, 3 }, { 2, 63 } };
+constexpr int kBitDepths[] = { 8, 10, 12 };
+constexpr avifPixelFormat kPixelFormats[] = { AVIF_PIXEL_FORMAT_YUV444, AVIF_PIXEL_FORMAT_YUV422, AVIF_PIXEL_FORMAT_YUV420, AVIF_PIXEL_FORMAT_YUV400 };
+
+INSTANTIATE_TEST_SUITE_P(Valid,
+                         GridApiTest,
+                         Combine(/*horizontal=*/ValuesIn(kValidCells),
+                                 /*vertical=*/ValuesIn(kValidCells),
+                                 ValuesIn(kBitDepths),
+                                 ValuesIn(kPixelFormats),
+                                 /*createAlpha=*/Values(false, true),
+                                 /*expectedSuccess=*/Values(true)));
+
+INSTANTIATE_TEST_SUITE_P(InvalidVertically,
+                         GridApiTest,
+                         Combine(/*horizontal=*/ValuesIn(kValidCells),
+                                 /*vertical=*/ValuesIn(kInvalidCells),
+                                 ValuesIn(kBitDepths),
+                                 ValuesIn(kPixelFormats),
+                                 /*createAlpha=*/Values(false, true),
+                                 /*expectedSuccess=*/Values(false)));
+INSTANTIATE_TEST_SUITE_P(InvalidHorizontally,
+                         GridApiTest,
+                         Combine(/*horizontal=*/ValuesIn(kInvalidCells),
+                                 /*vertical=*/ValuesIn(kValidCells),
+                                 ValuesIn(kBitDepths),
+                                 ValuesIn(kPixelFormats),
+                                 /*createAlpha=*/Values(false, true),
+                                 /*expectedSuccess=*/Values(false)));
+INSTANTIATE_TEST_SUITE_P(InvalidBoth,
+                         GridApiTest,
+                         Combine(/*horizontal=*/ValuesIn(kInvalidCells),
+                                 /*vertical=*/ValuesIn(kInvalidCells),
+                                 ValuesIn(kBitDepths),
+                                 ValuesIn(kPixelFormats),
+                                 /*createAlpha=*/Values(false, true),
+                                 /*expectedSuccess=*/Values(false)));
+
+// Special case depending on the cell count and the chroma subsampling.
+INSTANTIATE_TEST_SUITE_P(ValidOddHeight,
+                         GridApiTest,
+                         Combine(/*horizontal=*/Values(Cell { 1, 64 }),
+                                 /*vertical=*/Values(Cell { 1, 65 }, Cell { 2, 65 }),
+                                 ValuesIn(kBitDepths),
+                                 Values(AVIF_PIXEL_FORMAT_YUV444, AVIF_PIXEL_FORMAT_YUV422, AVIF_PIXEL_FORMAT_YUV400),
+                                 /*createAlpha=*/Values(false, true),
+                                 /*expectedSuccess=*/Values(true)));
+INSTANTIATE_TEST_SUITE_P(InvalidOddHeight,
+                         GridApiTest,
+                         Combine(/*horizontal=*/Values(Cell { 1, 64 }),
+                                 /*vertical=*/Values(Cell { 2, 65 }),
+                                 ValuesIn(kBitDepths),
+                                 Values(AVIF_PIXEL_FORMAT_YUV420),
+                                 /*createAlpha=*/Values(false, true),
+                                 /*expectedSuccess=*/Values(false)));
+
+// Special case depending on the cell count and the cell size.
+INSTANTIATE_TEST_SUITE_P(ValidOddDimensions,
+                         GridApiTest,
+                         Combine(/*horizontal=*/Values(Cell { 1, 1 }),
+                                 /*vertical=*/Values(Cell { 1, 65 }),
+                                 ValuesIn(kBitDepths),
+                                 ValuesIn(kPixelFormats),
+                                 /*createAlpha=*/Values(false, true),
+                                 /*expectedSuccess=*/Values(true)));
+INSTANTIATE_TEST_SUITE_P(InvalidOddDimensions,
+                         GridApiTest,
+                         Combine(/*horizontal=*/Values(Cell { 2, 1 }),
+                                 /*vertical=*/Values(Cell { 1, 65 }, Cell { 2, 65 }),
+                                 ValuesIn(kBitDepths),
+                                 ValuesIn(kPixelFormats),
+                                 /*createAlpha=*/Values(false, true),
+                                 /*expectedSuccess=*/Values(false)));
+
+} // namespace
diff --git a/tests/avify4mtest.c b/tests/avify4mtest.cc
similarity index 61%
rename from tests/avify4mtest.c
rename to tests/avify4mtest.cc
index cd9f723..cde23a4 100644
--- a/tests/avify4mtest.c
+++ b/tests/avify4mtest.cc
@@ -5,21 +5,30 @@
 
 #include "y4m.h"
 
-#include <assert.h>
-#include <inttypes.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
+#include <cassert>
+#include <cinttypes>
+#include <cstdio>
+#include <cstring>
+#include <sstream>
+#include <tuple>
+
+#include "gtest/gtest.h"
+
+using testing::Combine;
+using testing::Values;
+
+namespace
+{
 
 //------------------------------------------------------------------------------
 
 // Returns true if image1 and image2 are identical.
-static avifBool compareYUVA(const avifImage * image1, const avifImage * image2)
+bool compareYUVA(const avifImage * image1, const avifImage * image2)
 {
     if (image1->width != image2->width || image1->height != image2->height || image1->depth != image2->depth ||
         image1->yuvFormat != image2->yuvFormat || image1->yuvRange != image2->yuvRange) {
         printf("ERROR: input mismatch\n");
-        return AVIF_FALSE;
+        return false;
     }
     assert(image1->width * image1->height > 0);
 
@@ -38,17 +47,17 @@
         for (uint32_t y = 0; y < height; ++y) {
             if (memcmp(row1, row2, widthByteCount) != 0) {
                 printf("ERROR: different px at row %" PRIu32 ", channel %" PRIu32 "\n", y, plane);
-                return AVIF_FALSE;
+                return false;
             }
             row1 += image1->yuvRowBytes[plane];
             row2 += image2->yuvRowBytes[plane];
         }
     }
 
-    if (image1->alphaPlane != NULL || image2->alphaPlane != NULL) {
-        if (image1->alphaPlane == NULL || image2->alphaPlane == NULL || image1->alphaPremultiplied != image2->alphaPremultiplied) {
+    if (image1->alphaPlane || image2->alphaPlane) {
+        if (!image1->alphaPlane || !image2->alphaPlane || image1->alphaPremultiplied != image2->alphaPremultiplied) {
             printf("ERROR: input mismatch\n");
-            return AVIF_FALSE;
+            return false;
         }
         const uint32_t widthByteCount = image1->width * ((image1->depth > 8) ? sizeof(uint16_t) : sizeof(uint8_t));
         const uint8_t * row1 = image1->alphaPlane;
@@ -56,26 +65,26 @@
         for (uint32_t y = 0; y < image1->height; ++y) {
             if (memcmp(row1, row2, widthByteCount) != 0) {
                 printf("ERROR: different px at row %" PRIu32 ", alpha\n", y);
-                return AVIF_FALSE;
+                return false;
             }
             row1 += image1->alphaRowBytes;
             row2 += image2->alphaRowBytes;
         }
     }
-    return AVIF_TRUE;
+    return true;
 }
 
 //------------------------------------------------------------------------------
 
 // Fills each plane of the image with the maximum allowed value.
-static void fillPlanes(avifImage * image)
+void fillPlanes(avifImage * image)
 {
     const uint16_t yuvValue = (image->yuvRange == AVIF_RANGE_LIMITED) ? (235 << (image->depth - 8)) : ((1 << image->depth) - 1);
     avifPixelFormatInfo formatInfo;
     avifGetPixelFormatInfo(image->yuvFormat, &formatInfo);
     const int planeCount = formatInfo.monochrome ? 1 : AVIF_PLANE_COUNT_YUV;
     for (int plane = 0; plane < planeCount; ++plane) {
-        if (image->yuvPlanes[plane] != NULL) {
+        if (image->yuvPlanes[plane]) {
             const uint32_t planeWidth =
                 (plane == AVIF_CHAN_Y) ? image->width : ((image->width + formatInfo.chromaShiftX) >> formatInfo.chromaShiftX);
             const uint32_t planeHeight =
@@ -92,7 +101,7 @@
             }
         }
     }
-    if (image->alphaPlane != NULL) {
+    if (image->alphaPlane) {
         const uint16_t alphaValue = (1 << image->depth) - 1;
         for (uint32_t y = 0; y < image->height; ++y) {
             uint8_t * const row = image->alphaPlane + y * image->alphaRowBytes;
@@ -108,18 +117,18 @@
 }
 
 // Creates an image and encodes then decodes it as a y4m file.
-static avifBool encodeDecodeY4m(uint32_t width,
-                                uint32_t height,
-                                uint32_t depth,
-                                avifPixelFormat yuvFormat,
-                                avifRange yuvRange,
-                                avifBool createAlpha,
-                                const char filePath[])
+bool encodeDecodeY4m(uint32_t width,
+                     uint32_t height,
+                     uint32_t depth,
+                     avifPixelFormat yuvFormat,
+                     avifRange yuvRange,
+                     bool createAlpha,
+                     const char filePath[])
 {
-    avifBool success = AVIF_FALSE;
+    bool success = false;
     avifImage * image = avifImageCreateEmpty();
     avifImage * decoded = avifImageCreateEmpty();
-    if (image == NULL || decoded == NULL) {
+    if (!image || !decoded) {
         printf("ERROR: avifImageCreate() failed\n");
         goto cleanup;
     }
@@ -144,12 +153,12 @@
         goto cleanup;
     }
 
-    success = AVIF_TRUE;
+    success = true;
 cleanup:
-    if (image != NULL) {
+    if (image) {
         avifImageDestroy(image);
     }
-    if (decoded != NULL) {
+    if (decoded) {
         avifImageDestroy(decoded);
     }
     return success;
@@ -157,47 +166,42 @@
 
 //------------------------------------------------------------------------------
 
-int main(int argc, char * argv[])
+class Y4mTest
+    : public testing::TestWithParam<std::tuple</*width=*/int, /*height=*/int, /*bitDepth=*/int, /*yuvFormat=*/avifPixelFormat, /*yuvRange=*/avifRange, /*createAlpha=*/bool>>
 {
-    if (argc != 2 || !strlen(argv[1])) {
-        fprintf(stderr, "Missing temporary directory path environment variable name argument\n");
-        return EXIT_FAILURE;
-    }
-    const char * testTmpdir = getenv(argv[1]);
-    if (testTmpdir == NULL || !strlen(testTmpdir)) {
-        fprintf(stderr, "The environment variable %s is missing or is an empty string\n", argv[1]);
-        return EXIT_FAILURE;
-    }
-    char filePath[256];
-    const int result = snprintf(filePath, sizeof(filePath), "%s/avify4mtest.y4m", testTmpdir);
-    if (result < 0 || result >= (int)sizeof(filePath)) {
-        fprintf(stderr, "Could not generate a temporary file path\n");
-        return EXIT_FAILURE;
-    }
+};
 
-    // Try several configurations.
-    const uint32_t depths[] = { 8, 10, 12 };
-    const uint32_t widths[] = { 1, 2, 3 };
-    const uint32_t heights[] = { 1, 2, 3 };
-    for (uint32_t d = 0; d < sizeof(depths) / sizeof(depths[0]); ++d) {
-        for (int yuvFormat = AVIF_PIXEL_FORMAT_YUV444; yuvFormat <= AVIF_PIXEL_FORMAT_YUV400; ++yuvFormat) {
-            for (avifBool createAlpha = AVIF_FALSE; createAlpha <= AVIF_TRUE; ++createAlpha) {
-                if (createAlpha && (depths[d] != 8 || yuvFormat != AVIF_PIXEL_FORMAT_YUV444)) {
-                    continue; // writing alpha is currently only supported in 8bpc YUV444
-                }
-
-                for (int yuvRange = AVIF_RANGE_LIMITED; yuvRange <= AVIF_RANGE_FULL; ++yuvRange) {
-                    for (uint32_t w = 0; w < sizeof(widths) / sizeof(widths[0]); ++w) {
-                        for (uint32_t h = 0; h < sizeof(heights) / sizeof(heights[0]); ++h) {
-                            if (!encodeDecodeY4m(widths[w], heights[h], depths[d], yuvFormat, yuvRange, createAlpha, filePath)) {
-                                return EXIT_FAILURE;
-                            }
-                        }
-                    }
-                }
-            }
-        }
-    }
-
-    return EXIT_SUCCESS;
+TEST_P(Y4mTest, EncodeDecode)
+{
+    const int width = std::get<0>(GetParam());
+    const int height = std::get<1>(GetParam());
+    const int bitDepth = std::get<2>(GetParam());
+    const avifPixelFormat yuvFormat = std::get<3>(GetParam());
+    const avifRange yuvRange = std::get<4>(GetParam());
+    const bool createAlpha = std::get<5>(GetParam());
+    std::ostringstream filePath;
+    filePath << testing::TempDir() << "avify4mtest_" << width << "_" << height << "_" << bitDepth << "_" << yuvFormat << "_"
+             << yuvRange << "_" << createAlpha;
+    EXPECT_TRUE(encodeDecodeY4m(width, height, bitDepth, yuvFormat, yuvRange, createAlpha, filePath.str().c_str()));
 }
+
+INSTANTIATE_TEST_SUITE_P(OpaqueCombinations,
+                         Y4mTest,
+                         Combine(/*width=*/Values(1, 2, 3),
+                                 /*height=*/Values(1, 2, 3),
+                                 /*depths=*/Values(8, 10, 12),
+                                 Values(AVIF_PIXEL_FORMAT_YUV444, AVIF_PIXEL_FORMAT_YUV422, AVIF_PIXEL_FORMAT_YUV420, AVIF_PIXEL_FORMAT_YUV400),
+                                 Values(AVIF_RANGE_LIMITED, AVIF_RANGE_FULL),
+                                 /*createAlpha=*/Values(false)));
+
+// Writing alpha is currently only supported in 8bpc YUV444.
+INSTANTIATE_TEST_SUITE_P(AlphaCombinations,
+                         Y4mTest,
+                         Combine(/*width=*/Values(1, 2, 3),
+                                 /*height=*/Values(1, 2, 3),
+                                 /*depths=*/Values(8),
+                                 Values(AVIF_PIXEL_FORMAT_YUV444),
+                                 Values(AVIF_RANGE_LIMITED, AVIF_RANGE_FULL),
+                                 /*createAlpha=*/Values(true)));
+
+} // namespace