Convert avifmetadatatest to C++ and GoogleTest

diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 93c4f40..b625499 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -52,13 +52,6 @@
 target_link_libraries(avifincrtest avifincrtest_helpers)
 add_test(NAME avifincrtest COMMAND avifincrtest ${CMAKE_CURRENT_SOURCE_DIR}/data/sofa_grid1x5_420.avif)
 
-add_executable(avifmetadatatest gtest/avifmetadatatest.c)
-if(AVIF_LOCAL_LIBGAV1)
-    set_target_properties(avifmetadatatest PROPERTIES LINKER_LANGUAGE "CXX")
-endif()
-target_link_libraries(avifmetadatatest avif ${AVIF_PLATFORM_LIBRARIES})
-add_test(NAME avifmetadatatest COMMAND avifmetadatatest)
-
 if(AVIF_ENABLE_GTEST)
     enable_language(CXX)
     set(CMAKE_CXX_STANDARD 11)
@@ -88,6 +81,11 @@
     target_include_directories(avifgridapitest PRIVATE ${GTEST_INCLUDE_DIRS})
     add_test(NAME avifgridapitest COMMAND avifgridapitest)
 
+    add_executable(avifmetadatatest gtest/avifmetadatatest.cc)
+    target_link_libraries(avifmetadatatest aviftest_helpers ${GTEST_BOTH_LIBRARIES})
+    target_include_directories(avifmetadatatest PRIVATE ${GTEST_INCLUDE_DIRS})
+    add_test(NAME avifmetadatatest COMMAND avifmetadatatest)
+
     add_executable(avify4mtest gtest/avify4mtest.cc)
     target_link_libraries(avify4mtest aviftest_helpers avif_apps ${GTEST_BOTH_LIBRARIES})
     target_include_directories(avify4mtest PRIVATE ${GTEST_INCLUDE_DIRS})
diff --git a/tests/gtest/avifmetadatatest.c b/tests/gtest/avifmetadatatest.c
deleted file mode 100644
index d714814..0000000
--- a/tests/gtest/avifmetadatatest.c
+++ /dev/null
@@ -1,224 +0,0 @@
-// Copyright 2022 Google LLC. All rights reserved.
-// SPDX-License-Identifier: BSD-2-Clause
-
-#include <assert.h>
-#include <inttypes.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-
-#include "avif/avif.h"
-
-//------------------------------------------------------------------------------
-
-// ICC color profiles are not checked by libavif so the content does not matter.
-// This is a truncated widespread ICC color profile.
-static const uint8_t sampleICC[] = { 0x00, 0x00, 0x02, 0x0c, 0x6c, 0x63, 0x6d, 0x73, 0x02, 0x10, 0x00, 0x00,
-                                     0x6d, 0x6e, 0x74, 0x72, 0x52, 0x47, 0x42, 0x20, 0x58, 0x59, 0x5a, 0x20 };
-static const size_t sampleICCSize = sizeof(sampleICC) / sizeof(sampleICC[0]);
-
-// Exif bytes are partially checked by libavif. This is a truncated widespread Exif metadata chunk.
-static const uint8_t sampleExif[] = { 0xff, 0x1,  0x45, 0x78, 0x69, 0x76, 0x32, 0xff, 0xe1, 0x12, 0x5a, 0x45,
-                                      0x78, 0x69, 0x66, 0x0,  0x0,  0x49, 0x49, 0x2a, 0x0,  0x8,  0x0,  0x0 };
-static const size_t sampleExifSize = sizeof(sampleExif) / sizeof(sampleExif[0]);
-
-// XMP bytes are not checked by libavif so the content does not matter.
-// This is a truncated widespread XMP metadata chunk.
-static const uint8_t sampleXMP[] = { 0x3c, 0x3f, 0x78, 0x70, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x20, 0x62, 0x65,
-                                     0x67, 0x69, 0x6e, 0x3d, 0x22, 0xef, 0xbb, 0xbf, 0x22, 0x20, 0x69, 0x64 };
-static const size_t sampleXMPSize = sizeof(sampleXMP) / sizeof(sampleXMP[0]);
-
-//------------------------------------------------------------------------------
-// TODO: Move these functions to a tests/common/helper.c shared with avifgridapitest.c
-
-// 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) {
-        printf("ERROR: avifImageCreate() failed\n");
-        return AVIF_FALSE;
-    }
-    avifImageAllocatePlanes(*image, createAlpha ? AVIF_PLANES_ALL : AVIF_PLANES_YUV);
-
-    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;
-}
-
-static avifBool createImage1x1(avifImage ** image)
-{
-    return createImage(/*width=*/1, /*height=*/1, /*depth=*/10, AVIF_PIXEL_FORMAT_YUV444, /*createAlpha=*/AVIF_TRUE, image);
-}
-
-//------------------------------------------------------------------------------
-
-// Encodes the image. Returns false in case of failure.
-static avifBool encode(const avifImage * image, avifRWData * output)
-{
-    avifBool success = AVIF_FALSE;
-    avifEncoder * encoder = avifEncoderCreate();
-    if (!encoder) {
-        printf("ERROR: avifEncoderCreate() failed\n");
-        goto cleanup;
-    }
-    encoder->speed = AVIF_SPEED_FASTEST;
-    if (avifEncoderWrite(encoder, image, output) != AVIF_RESULT_OK) {
-        printf("ERROR: avifEncoderWrite() failed\n");
-        goto cleanup;
-    }
-
-    success = AVIF_TRUE;
-cleanup:
-    if (encoder) {
-        avifEncoderDestroy(encoder);
-    }
-    return success;
-}
-
-// Decodes the data. Returns false in case of failure.
-static avifBool decode(const avifRWData * encodedAvif, avifImage ** image)
-{
-    avifBool success = AVIF_FALSE;
-    *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 = AVIF_TRUE;
-cleanup:
-    if (decoder) {
-        avifDecoderDestroy(decoder);
-    }
-    return success;
-}
-
-//------------------------------------------------------------------------------
-
-// Returns true if the decoded output metadata matches the input metadata.
-static avifBool metadataIsEqual(const avifRWData * inputItem, const avifRWData * outputItem)
-{
-    return (outputItem->size == inputItem->size) && (memcmp(outputItem->data, inputItem->data, inputItem->size) == 0);
-}
-
-// Encodes, decodes then compares the metadata of the input and decoded images.
-static avifBool encodeDecode(const avifImage * image)
-{
-    avifBool success = AVIF_FALSE;
-    avifRWData encodedAvif = AVIF_DATA_EMPTY;
-    avifImage * decodedImage = NULL;
-    if (!encode(image, &encodedAvif)) {
-        goto cleanup;
-    }
-    if (!decode(&encodedAvif, &decodedImage)) {
-        goto cleanup;
-    }
-
-    if (!metadataIsEqual(&image->icc, &decodedImage->icc)) {
-        printf("ERROR: icc mismatch\n");
-        goto cleanup;
-    }
-    if (!metadataIsEqual(&image->exif, &decodedImage->exif)) {
-        printf("ERROR: Exif metadata mismatch\n");
-        goto cleanup;
-    }
-    if (!metadataIsEqual(&image->xmp, &decodedImage->xmp)) {
-        printf("ERROR: XMP metadata mismatch\n");
-        goto cleanup;
-    }
-    success = AVIF_TRUE;
-cleanup:
-    avifRWDataFree(&encodedAvif);
-    if (decodedImage) {
-        avifImageDestroy(decodedImage);
-    }
-    return success;
-}
-
-//------------------------------------------------------------------------------
-
-// Encodes, decodes then verifies that the output metadata matches the input
-// metadata defined by the arguments.
-static avifBool encodeDecodeMetadataItems(avifBool useICC, avifBool useExif, avifBool useXMP)
-{
-    avifBool success = AVIF_FALSE;
-    avifImage * image = NULL;
-    if (!createImage1x1(&image)) {
-        goto cleanup;
-    }
-
-    if (useICC) {
-        avifImageSetProfileICC(image, sampleICC, sampleICCSize);
-    }
-    if (useExif) {
-        avifImageSetMetadataExif(image, sampleExif, sampleExifSize);
-    }
-    if (useXMP) {
-        avifImageSetMetadataXMP(image, sampleXMP, sampleXMPSize);
-    }
-
-    if (!encodeDecode(image)) {
-        goto cleanup;
-    }
-    success = AVIF_TRUE;
-cleanup:
-    if (image) {
-        avifImageDestroy(image);
-    }
-    return success;
-}
-
-int main(void)
-{
-    if (!encodeDecodeMetadataItems(/*useICC=*/AVIF_TRUE, /*useExif=*/AVIF_FALSE, /*useXMP=*/AVIF_FALSE)) {
-        return EXIT_FAILURE;
-    }
-    if (!encodeDecodeMetadataItems(/*useICC=*/AVIF_FALSE, /*useExif=*/AVIF_TRUE, /*useXMP=*/AVIF_FALSE)) {
-        return EXIT_FAILURE;
-    }
-    if (!encodeDecodeMetadataItems(/*useICC=*/AVIF_FALSE, /*useExif=*/AVIF_FALSE, /*useXMP=*/AVIF_TRUE)) {
-        return EXIT_FAILURE;
-    }
-    // TODO: Negative test
-    // TODO: Multi xmp test
-    printf("Test passed.\n");
-    return EXIT_SUCCESS;
-}
diff --git a/tests/gtest/avifmetadatatest.cc b/tests/gtest/avifmetadatatest.cc
new file mode 100644
index 0000000..b943eec
--- /dev/null
+++ b/tests/gtest/avifmetadatatest.cc
@@ -0,0 +1,101 @@
+// Copyright 2022 Google LLC. All rights reserved.
+// SPDX-License-Identifier: BSD-2-Clause
+
+#include "avif/avif.h"
+
+#include <array>
+#include <tuple>
+
+#include "aviftest_helpers.h"
+#include "gtest/gtest.h"
+
+using ::testing::Bool;
+using ::testing::Combine;
+
+namespace libavif
+{
+namespace
+{
+
+//------------------------------------------------------------------------------
+
+// ICC color profiles are not checked by libavif so the content does not matter.
+// This is a truncated widespread ICC color profile.
+const std::array<uint8_t, 24> sampleICC = { 0x00, 0x00, 0x02, 0x0c, 0x6c, 0x63, 0x6d, 0x73, 0x02, 0x10, 0x00, 0x00,
+                                            0x6d, 0x6e, 0x74, 0x72, 0x52, 0x47, 0x42, 0x20, 0x58, 0x59, 0x5a, 0x20 };
+
+// Exif bytes are partially checked by libavif. This is a truncated widespread Exif metadata chunk.
+const std::array<uint8_t, 24> sampleExif = { 0xff, 0x1,  0x45, 0x78, 0x69, 0x76, 0x32, 0xff, 0xe1, 0x12, 0x5a, 0x45,
+                                             0x78, 0x69, 0x66, 0x0,  0x0,  0x49, 0x49, 0x2a, 0x0,  0x8,  0x0,  0x0 };
+
+// XMP bytes are not checked by libavif so the content does not matter.
+// This is a truncated widespread XMP metadata chunk.
+const std::array<uint8_t, 24> sampleXMP = { 0x3c, 0x3f, 0x78, 0x70, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x20, 0x62, 0x65,
+                                            0x67, 0x69, 0x6e, 0x3d, 0x22, 0xef, 0xbb, 0xbf, 0x22, 0x20, 0x69, 0x64 };
+
+//------------------------------------------------------------------------------
+
+class MetadataTest : public testing::TestWithParam<std::tuple</*useICC=*/bool, /*useExif=*/bool, /*useXMP=*/bool>>
+{
+};
+
+// Encodes, decodes then verifies that the output metadata matches the input metadata defined by the parameters.
+TEST_P(MetadataTest, EncodeDecode)
+{
+    const bool useICC = std::get<0>(GetParam());
+    const bool useExif = std::get<1>(GetParam());
+    const bool useXMP = std::get<2>(GetParam());
+
+    testutil::avifImagePtr image =
+        testutil::createImage(/*width=*/12, /*height=*/34, /*depth=*/10, AVIF_PIXEL_FORMAT_YUV444, AVIF_PLANES_ALL);
+    ASSERT_NE(image, nullptr);
+    testutil::fillImageGradient(image.get()); // The pixels do not matter.
+    if (useICC) {
+        avifImageSetProfileICC(image.get(), sampleICC.data(), sampleICC.size());
+    }
+    if (useExif) {
+        avifImageSetMetadataExif(image.get(), sampleExif.data(), sampleExif.size());
+    }
+    if (useXMP) {
+        avifImageSetMetadataXMP(image.get(), sampleXMP.data(), sampleXMP.size());
+    }
+
+    // Encode.
+    testutil::avifEncoderPtr encoder(avifEncoderCreate(), avifEncoderDestroy);
+    ASSERT_NE(encoder, nullptr);
+    encoder->speed = AVIF_SPEED_FASTEST;
+    testutil::avifRWDataCleaner encodedAvif;
+    ASSERT_EQ(avifEncoderWrite(encoder.get(), image.get(), &encodedAvif), AVIF_RESULT_OK);
+
+    // Decode.
+    testutil::avifImagePtr decoded(avifImageCreateEmpty(), avifImageDestroy);
+    ASSERT_NE(decoded, nullptr);
+    testutil::avifDecoderPtr decoder(avifDecoderCreate(), avifDecoderDestroy);
+    ASSERT_NE(decoder, nullptr);
+    ASSERT_EQ(avifDecoderReadMemory(decoder.get(), decoded.get(), encodedAvif.data, encodedAvif.size), AVIF_RESULT_OK);
+
+    // Compare input and output metadata.
+    if (useICC) {
+        ASSERT_EQ(decoded->icc.size, sampleICC.size());
+        EXPECT_TRUE(std::equal(sampleICC.begin(), sampleICC.end(), decoded->icc.data));
+    } else {
+        EXPECT_EQ(decoded->icc.size, 0u);
+    }
+    if (useExif) {
+        ASSERT_EQ(decoded->exif.size, sampleExif.size());
+        EXPECT_TRUE(std::equal(sampleExif.begin(), sampleExif.end(), decoded->exif.data));
+    } else {
+        EXPECT_EQ(decoded->exif.size, 0u);
+    }
+    if (useXMP) {
+        ASSERT_EQ(decoded->xmp.size, sampleXMP.size());
+        EXPECT_TRUE(std::equal(sampleXMP.begin(), sampleXMP.end(), decoded->xmp.data));
+    } else {
+        EXPECT_EQ(decoded->xmp.size, 0u);
+    }
+}
+
+INSTANTIATE_TEST_SUITE_P(All, MetadataTest, Combine(/*useICC=*/Bool(), /*useExif=*/Bool(), /*useXMP=*/Bool()));
+
+} // namespace
+} // namespace libavif