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/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