Support multi-config in test_cmd* (#2781)

diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml
index f5d31b6..404fc98 100644
--- a/.github/workflows/ci-windows.yml
+++ b/.github/workflows/ci-windows.yml
@@ -36,6 +36,7 @@
       fail-fast: false
       matrix:
         os: [windows-latest]
+        generator: [Ninja, "Visual Studio 17 2022"]
 
     steps:
       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -58,7 +59,7 @@
 
       - name: Prepare libavif (cmake)
         run: >
-          cmake -G Ninja -S . -B build
+          cmake -G "${{ matrix.generator }}" -S . -B build
           -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF
           -DAVIF_CODEC_AOM=LOCAL -DAVIF_CODEC_DAV1D=LOCAL
           -DAVIF_CODEC_RAV1E=LOCAL -DAVIF_CODEC_SVT=LOCAL
@@ -75,7 +76,15 @@
         run: cmake --build build --config Release --parallel 4
       - name: Run AVIF Tests
         working-directory: ./build
-        run: ctest -j $Env:NUMBER_OF_PROCESSORS --output-on-failure
+        run: ctest -C Release -j $Env:NUMBER_OF_PROCESSORS --output-on-failure
+      - name: Set the config folder for Ninja
+        if: ${{ matrix.generator == 'Ninja' }}
+        run:
+          echo "CONFIG_DIR=" >> $env:GITHUB_ENV
+      - name: Set the config folder for multi-config MSVC
+        if: ${{ matrix.generator != 'Ninja' }}
+        run:
+          echo "CONFIG_DIR=Release\" >> $env:GITHUB_ENV
       - name: Check static link bundling
         run: >
           cl .\apps\avifenc.c .\apps\shared\*.c .\third_party\iccjpeg\iccjpeg.c /nologo
@@ -89,10 +98,10 @@
           -external:I.\build\_deps\libpng
           -external:I.\build\_deps\zlib
           /link
-          build\libjpeg\src\libjpeg-build\jpeg-static.lib
-          build\_deps\libpng\libpng16_static.lib
-          build\_deps\zlib\zlibstatic.lib
-          build\avif.lib
+          build\libjpeg\src\libjpeg-build\${env:CONFIG_DIR}jpeg-static.lib
+          build\_deps\libpng\${env:CONFIG_DIR}libpng16_static.lib
+          build\_deps\zlib\${env:CONFIG_DIR}zlibstatic.lib
+          build\${env:CONFIG_DIR}avif.lib
           ws2_32.lib ntdll.lib userenv.lib bcrypt.lib advapi32.lib
           /out:avifenc.exe
 
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 6d4b40d..c15df73 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -221,6 +221,20 @@
 ################################################################################
 # Bash tests
 
+# Macro to add a shell test. It takes multi-config generators into account.
+# The first argument is the name of the shell script, without .sh.
+# The following arguments are sent to the script.
+macro(add_cmd_test SHELL_SCRIPT)
+    get_property(IS_MULTI_CONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
+    if(${IS_MULTI_CONFIG})
+        add_test(NAME ${SHELL_SCRIPT} COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/${SHELL_SCRIPT}.sh $<CONFIG> ${CMAKE_BINARY_DIR}
+                                              ${ARGN}
+        )
+    else()
+        add_test(NAME ${SHELL_SCRIPT} COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/${SHELL_SCRIPT}.sh "" ${CMAKE_BINARY_DIR} ${ARGN})
+    endif()
+endmacro()
+
 if(AVIF_BUILD_APPS)
     # When building apps, test the avifenc/avifdec.
     # 'are_images_equal' is used to make sure inputs/outputs are unchanged.
@@ -233,38 +247,18 @@
         endif()
     endif()
     target_link_libraries(are_images_equal aviftest_helpers avif_enable_warnings)
-    add_test(NAME test_cmd COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cmd.sh ${CMAKE_BINARY_DIR}
-                                   ${CMAKE_CURRENT_SOURCE_DIR}/data
-    )
-    add_test(NAME test_cmd_animation COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cmd_animation.sh ${CMAKE_BINARY_DIR}
-                                             ${CMAKE_CURRENT_SOURCE_DIR}/data
-    )
-    add_test(NAME test_cmd_grid COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cmd_grid.sh ${CMAKE_BINARY_DIR}
-                                        ${CMAKE_CURRENT_SOURCE_DIR}/data
-    )
-    add_test(NAME test_cmd_icc_profile COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cmd_icc_profile.sh ${CMAKE_BINARY_DIR}
-                                               ${CMAKE_CURRENT_SOURCE_DIR}/data
-    )
-    add_test(NAME test_cmd_lossless COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cmd_lossless.sh ${CMAKE_BINARY_DIR}
-                                            ${CMAKE_CURRENT_SOURCE_DIR}/data
-    )
-    add_test(NAME test_cmd_metadata COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cmd_metadata.sh ${CMAKE_BINARY_DIR}
-                                            ${CMAKE_CURRENT_SOURCE_DIR}/data
-    )
-    add_test(NAME test_cmd_progressive COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cmd_progressive.sh ${CMAKE_BINARY_DIR}
-                                               ${CMAKE_CURRENT_SOURCE_DIR}/data
-    )
-    add_test(NAME test_cmd_stdin COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cmd_stdin.sh ${CMAKE_BINARY_DIR}
-                                         ${CMAKE_CURRENT_SOURCE_DIR}/data
-    )
-    add_test(NAME test_cmd_targetsize COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cmd_targetsize.sh ${CMAKE_BINARY_DIR}
-                                              ${CMAKE_CURRENT_SOURCE_DIR}/data
-    )
+    add_cmd_test(test_cmd ${CMAKE_CURRENT_SOURCE_DIR}/data)
+    add_cmd_test(test_cmd_animation ${CMAKE_CURRENT_SOURCE_DIR}/data)
+    add_cmd_test(test_cmd_grid ${CMAKE_CURRENT_SOURCE_DIR}/data)
+    add_cmd_test(test_cmd_icc_profile ${CMAKE_CURRENT_SOURCE_DIR}/data)
+    add_cmd_test(test_cmd_lossless ${CMAKE_CURRENT_SOURCE_DIR}/data)
+    add_cmd_test(test_cmd_metadata ${CMAKE_CURRENT_SOURCE_DIR}/data)
+    add_cmd_test(test_cmd_progressive ${CMAKE_CURRENT_SOURCE_DIR}/data)
+    add_cmd_test(test_cmd_stdin ${CMAKE_CURRENT_SOURCE_DIR}/data)
+    add_cmd_test(test_cmd_targetsize ${CMAKE_CURRENT_SOURCE_DIR}/data)
 
     if(AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION)
-        add_test(NAME test_cmd_avifgainmaputil COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cmd_avifgainmaputil.sh
-                                                       ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/data
-        )
+        add_cmd_test(test_cmd_avifgainmaputil ${CMAKE_CURRENT_SOURCE_DIR}/data)
     endif()
 
     if(AVIF_ENABLE_GOLDEN_TESTS AND AVIF_CODEC_AOM_ENCODE)
@@ -277,19 +271,13 @@
 
         set(GOLDEN_TESTS_OUTPUT_DIR "" CACHE STRING "Output path for golden tests (will be a temp dir if empty)")
 
-        add_test(NAME test_cmd_enc_boxes_golden
-                 COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cmd_enc_boxes_golden.sh ${CMAKE_BINARY_DIR} ${MP4BOX_DIR}
-                         ${CMAKE_CURRENT_SOURCE_DIR}/data ${GOLDEN_TESTS_OUTPUT_DIR}
-        )
+        add_cmd_test(test_cmd_enc_boxes_golden ${MP4BOX_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/data ${GOLDEN_TESTS_OUTPUT_DIR})
 
         if(AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION)
-            add_test(NAME test_cmd_gainmap COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cmd_gainmap.sh ${CMAKE_BINARY_DIR}
-                                                   ${CMAKE_CURRENT_SOURCE_DIR}/data
-            )
+            add_cmd_test(test_cmd_gainmap ${CMAKE_CURRENT_SOURCE_DIR}/data)
 
-            add_test(NAME test_cmd_enc_gainmap_boxes_golden
-                     COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cmd_enc_gainmap_boxes_golden.sh ${CMAKE_BINARY_DIR}
-                             ${MP4BOX_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/data ${GOLDEN_TESTS_OUTPUT_DIR}
+            add_cmd_test(
+                test_cmd_enc_gainmap_boxes_golden ${MP4BOX_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/data ${GOLDEN_TESTS_OUTPUT_DIR}
             )
         endif()
     endif()
@@ -325,12 +313,8 @@
     endif()
 
     if(AVIF_BUILD_APPS)
-        add_test(NAME test_cmd_avm COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cmd_avm.sh ${CMAKE_BINARY_DIR}
-                                           ${CMAKE_CURRENT_SOURCE_DIR}/data
-        )
-        add_test(NAME test_cmd_avm_lossless COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cmd_avm_lossless.sh ${CMAKE_BINARY_DIR}
-                                                    ${CMAKE_CURRENT_SOURCE_DIR}/data
-        )
+        add_cmd_test(test_cmd_avm ${CMAKE_CURRENT_SOURCE_DIR}/data)
+        add_cmd_test(test_cmd_avm_lossless ${CMAKE_CURRENT_SOURCE_DIR}/data)
     endif()
 
     # AV2 support is experimental and only available when avm is explicitly specified as the encoder.
diff --git a/tests/cmd_test_common.sh b/tests/cmd_test_common.sh
index 81bd3f4..18b5ca7 100644
--- a/tests/cmd_test_common.sh
+++ b/tests/cmd_test_common.sh
@@ -20,24 +20,33 @@
 # Very verbose but useful for debugging.
 set -eux
 
-if [[ "$#" -ge 1 ]]; then
+# The CMake config must always be passed. It can be empty if there is
+# no multi-config generator.
+CONFIG="$1"
+if [[ "$#" -ge 2 ]]; then
   # eval so that the passed in directory can contain variables.
-  BINARY_DIR="$(eval echo "$1")"
+  BINARY_DIR="$(eval echo "$2")"
 else
   # Assume "tests" is the current directory.
   BINARY_DIR="$(pwd)/.."
 fi
-if [[ "$#" -ge 2 ]]; then
-  TESTDATA_DIR="$(eval echo "$2")"
+if [[ "$#" -ge 3 ]]; then
+  TESTDATA_DIR="$(eval echo "$3")"
 else
   TESTDATA_DIR="$(pwd)/data"
 fi
-if [[ "$#" -ge 3 ]]; then
-  TMP_DIR="$(eval echo "$3")"
+if [[ "$#" -ge 4 ]]; then
+  TMP_DIR="$(eval echo "$4")"
 else
   TMP_DIR="$(mktemp -d)"
 fi
 
-AVIFENC="${BINARY_DIR}/avifenc"
-AVIFDEC="${BINARY_DIR}/avifdec"
-ARE_IMAGES_EQUAL="${BINARY_DIR}/tests/are_images_equal"
+if [[ ! -z "$CONFIG" ]]; then
+  AVIFENC="${BINARY_DIR}/${CONFIG}/avifenc"
+  AVIFDEC="${BINARY_DIR}/${CONFIG}/avifdec"
+  ARE_IMAGES_EQUAL="${BINARY_DIR}/tests/${CONFIG}/are_images_equal"
+else
+  AVIFENC="${BINARY_DIR}/avifenc"
+  AVIFDEC="${BINARY_DIR}/avifdec"
+  ARE_IMAGES_EQUAL="${BINARY_DIR}/tests/are_images_equal"
+fi
diff --git a/tests/golden_test_common.sh b/tests/golden_test_common.sh
index ec58223..e17e8d7 100755
--- a/tests/golden_test_common.sh
+++ b/tests/golden_test_common.sh
@@ -13,33 +13,36 @@
 
 set -eu
 
-if [[ "$#" -ge 1 ]]; then
-  # eval so that the passed in directory can contain variables.
-  ENCODER_DIR="$(eval echo "$1")"
-else
-  # Assume "tests" is the current directory.
-  ENCODER_DIR="$(pwd)/.."
-fi
+# The CMake config must always be passed. It can be empty if there is
+# no multi-config.
+CONFIG="$1"
 if [[ "$#" -ge 2 ]]; then
   # eval so that the passed in directory can contain variables.
-  MP4BOX_DIR="$(eval echo "$2")"
+  BINARY_DIR="$(eval echo "$2")"
+else
+  # Assume "tests" is the current directory.
+  BINARY_DIR="$(pwd)/.."
+fi
+if [[ "$#" -ge 3 ]]; then
+  # eval so that the passed in directory can contain variables.
+  MP4BOX_DIR="$(eval echo "$3")"
 else
   # Assume "tests" is the current directory.
   MP4BOX_DIR="$(pwd)/../ext/gpac/bin/gcc"
 fi
-if [[ "$#" -ge 3 ]]; then
-  TESTDATA_DIR="$(eval echo "$3")"
+if [[ "$#" -ge 4 ]]; then
+  TESTDATA_DIR="$(eval echo "$4")"
 else
   TESTDATA_DIR="$(pwd)/data"
 fi
-if [[ "$#" -ge 4 && ! -z "$4" ]]; then
-  OUTPUT_DIR="$(eval echo "$4")/test_cmd_enc_boxes_golden"
+if [[ "$#" -ge 5 && ! -z "$5" ]]; then
+  OUTPUT_DIR="$(eval echo "$5")/test_cmd_enc_boxes_golden"
 else
   OUTPUT_DIR="$(mktemp -d)"
 fi
 
 GOLDEN_DIR="${TESTDATA_DIR}/goldens"
-AVIFENC="${ENCODER_DIR}/avifenc"
+AVIFENC="${BINARY_DIR}/${CONFIG}/avifenc"
 MP4BOX="${MP4BOX_DIR}/MP4Box"
 
 # Colors for pretty formatting.
diff --git a/tests/test_cmd_avifgainmaputil.sh b/tests/test_cmd_avifgainmaputil.sh
index f9eb906..22fb2db 100755
--- a/tests/test_cmd_avifgainmaputil.sh
+++ b/tests/test_cmd_avifgainmaputil.sh
@@ -6,7 +6,11 @@
 
 source $(dirname "$0")/cmd_test_common.sh || exit
 
-AVIFGAINMAPUTIL="${BINARY_DIR}/avifgainmaputil"
+if [[ ! -z "$CONFIG" ]]; then
+  AVIFGAINMAPUTIL="${BINARY_DIR}/${CONFIG}/avifgainmaputil"
+else
+  AVIFGAINMAPUTIL="${BINARY_DIR}/avifgainmaputil"
+fi
 
 # Input file paths.
 INPUT_AVIF_GAINMAP_SDR="${TESTDATA_DIR}/seine_sdr_gainmap_srgb.avif"
diff --git a/tests/test_cmd_targetsize.sh b/tests/test_cmd_targetsize.sh
index 47f5297..779ec3a 100755
--- a/tests/test_cmd_targetsize.sh
+++ b/tests/test_cmd_targetsize.sh
@@ -18,9 +18,6 @@
 
 source $(dirname "$0")/cmd_test_common.sh || exit
 
-AVIFENC="${BINARY_DIR}/avifenc"
-AVIFDEC="${BINARY_DIR}/avifdec"
-
 # Input file paths.
 INPUT_Y4M="${TESTDATA_DIR}/kodim03_yuv420_8bpc.y4m"
 # Output file names.