diff --git a/libgimp/tests/README.md b/libgimp/tests/README.md
new file mode 100644
index 0000000000..23dcaa0c59
--- /dev/null
+++ b/libgimp/tests/README.md
@@ -0,0 +1,68 @@
+# Unit testing for libgimp
+
+We should test every function in our released libraries and ensure they return
+the correct data. This test infrastructure does this for the C library and the
+Python 3 binding.
+
+Every new test unit should be added both in C and Python 3.
+
+## Procedure to add the C unit
+
+C functions are tested in a real plug-in which is run by the unit test
+infrastructure. Most of the boiler-plate code is contained in `c-test-header.c`
+therefore you don't have to care about it.
+
+All you must do is create a `gimp_c_test_run()` function with the following
+template:
+
+```C
+static GimpValueArray *
+gimp_c_test_run (GimpProcedure *procedure,
+ GimpRunMode run_mode,
+ GimpImage *image,
+ gint n_drawables,
+ GimpDrawable **drawables,
+ GimpProcedureConfig *config,
+ gpointer run_data)
+{
+ /* Each test must be surrounded by GIMP_TEST_START() and GIMP_TEST_END()
+ * macros this way:
+ */
+ GIMP_TEST_START("Test name for easy debugging")
+ /* Run some code and finish by an assert-like test. */
+ GIMP_TEST_END(testme > 0)
+
+ /* Do more tests as needed. */
+
+ /* Mandatorily end the function by this macro: */
+ GIMP_TEST_RETURN
+}
+```
+
+This code must be in a file named only with alphanumeric letters and hyphens,
+and prepended with `test-`, such as: `test-palette.c`.
+
+The part between `test-` and `.c` must be added to the `tests` list in
+`libgimp/tests/meson.build`.
+
+## Procedure to add the Python 3 unit
+
+Unlike C, the Python 3 API is not run as a standalone plug-in, but as Python
+code directly interpreted through the `python-fu-eval` batch plug-in.
+
+Simply add your code in a file named the same as the C file, but with `.py`
+extension instead of `.c`.
+
+The file must mandatorily start with a shebang: `#!/usr/bin/env python3`
+
+For testing, use `gimp_assert()` as follows:
+
+```py
+#!/usr/bin/env python3
+
+# Add your test code here.
+# Then test that it succeeded with the assert-like test:
+gimp_assert('Test name for easy debugging', testme > 0)
+
+# Repeat with more tests as needed.
+```
diff --git a/libgimp/tests/c-test-header.c b/libgimp/tests/c-test-header.c
new file mode 100644
index 0000000000..2beb00e9c2
--- /dev/null
+++ b/libgimp/tests/c-test-header.c
@@ -0,0 +1,133 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995-1999 Spencer Kimball and Peter Mattis
+ *
+ * Copyright (C) 2024 Jehan
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include
+
+static GimpPDBStatusType status = GIMP_PDB_SUCCESS;
+static GError *error = NULL;
+static const gchar *testname = NULL;
+
+#define GIMP_TEST_RETURN \
+gimp_test_cleanup: \
+ return gimp_procedure_new_return_values (procedure, status, error);
+
+#define GIMP_TEST_START(name) if (error) goto gimp_test_cleanup; testname = name; printf ("Starting test '%s': ", testname);
+
+#define GIMP_TEST_END(test) \
+ if (! (test)) \
+ { \
+ printf ("FAILED\n"); \
+ error = g_error_new_literal (GIMP_PLUG_IN_ERROR, 0, testname); \
+ status = GIMP_PDB_EXECUTION_ERROR; \
+ } \
+ else \
+ { \
+ printf ("OK\n"); \
+ }
+
+typedef struct _GimpCTest GimpCTest;
+typedef struct _GimpCTestClass GimpCTestClass;
+
+struct _GimpCTest
+{
+ GimpPlugIn parent_instance;
+};
+
+struct _GimpCTestClass
+{
+ GimpPlugInClass parent_class;
+};
+
+
+/* Declare local functions.
+ */
+
+#define GIMP_C_TEST_TYPE (gimp_c_test_get_type ())
+#define GIMP_C_TEST (obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GIMP_C_TEST_TYPE, GimpCTest))
+
+GType gimp_c_test_get_type (void) G_GNUC_CONST;
+
+static GList * gimp_c_test_query_procedures (GimpPlugIn *plug_in);
+static GimpProcedure * gimp_c_test_create_procedure (GimpPlugIn *plug_in,
+ const gchar *name);
+
+static GimpValueArray * gimp_c_test_run (GimpProcedure *procedure,
+ GimpRunMode run_mode,
+ GimpImage *image,
+ gint n_drawables,
+ GimpDrawable **drawables,
+ GimpProcedureConfig *config,
+ gpointer run_data);
+
+
+G_DEFINE_TYPE (GimpCTest, gimp_c_test, GIMP_TYPE_PLUG_IN)
+
+GIMP_MAIN (GIMP_C_TEST_TYPE)
+
+
+static void
+gimp_c_test_class_init (GimpCTestClass *klass)
+{
+ GimpPlugInClass *plug_in_class = GIMP_PLUG_IN_CLASS (klass);
+
+ plug_in_class->query_procedures = gimp_c_test_query_procedures;
+ plug_in_class->create_procedure = gimp_c_test_create_procedure;
+}
+
+static void
+gimp_c_test_init (GimpCTest *ctest)
+{
+}
+
+static GList *
+gimp_c_test_query_procedures (GimpPlugIn *plug_in)
+{
+ gchar *testname = g_path_get_basename (__FILE__);
+ gchar *dot;
+
+ dot = g_strrstr (testname, ".");
+ *dot = '\0';
+
+ return g_list_append (NULL, testname);
+}
+
+static GimpProcedure *
+gimp_c_test_create_procedure (GimpPlugIn *plug_in,
+ const gchar *name)
+{
+ GimpProcedure *procedure = NULL;
+ gchar *testname = g_path_get_basename (__FILE__);
+ gchar *dot;
+
+ dot = g_strrstr (testname, ".");
+ *dot = '\0';
+
+ if (! strcmp (name, testname))
+ {
+ procedure = gimp_image_procedure_new (plug_in, name,
+ GIMP_PDB_PROC_TYPE_PLUGIN,
+ gimp_c_test_run, NULL, NULL);
+ gimp_procedure_set_image_types (procedure, "*");
+ gimp_procedure_set_sensitivity_mask(procedure, GIMP_PROCEDURE_SENSITIVE_ALWAYS);
+ }
+
+ g_free (testname);
+
+ return procedure;
+}
diff --git a/libgimp/tests/libgimp-run-c-test.sh b/libgimp/tests/libgimp-run-c-test.sh
new file mode 100644
index 0000000000..ffd8dac4db
--- /dev/null
+++ b/libgimp/tests/libgimp-run-c-test.sh
@@ -0,0 +1,13 @@
+#!/bin/sh
+
+GIMP_EXE=$1
+TEST_FILE=$2
+SRC_DIR=`dirname $TEST_FILE`
+SRC_DIR=`realpath $SRC_DIR`
+TEST_NAME=$3
+
+cmd="import os; import sys; sys.path.insert(0, '$SRC_DIR'); from pygimp.utils import gimp_c_assert;"
+cmd="$cmd proc = Gimp.get_pdb().lookup_procedure('$TEST_NAME'); gimp_c_assert('$TEST_FILE', 'Test PDB procedure does not exist: {}'.format('$TEST_NAME'), proc is not None);"
+cmd="$cmd result = proc.run(proc.create_config());"
+cmd="$cmd print('SUCCESS') if result.index(0) == Gimp.PDBStatusType.SUCCESS else gimp_c_assert('$TEST_FILE', result.index(1), False);"
+echo "$cmd" | "$GIMP_EXE" -nis --batch-interpreter "python-fu-eval" -b - --quit
diff --git a/libgimp/tests/meson.build b/libgimp/tests/meson.build
index 53bd0f4717..e2fd3844a2 100644
--- a/libgimp/tests/meson.build
+++ b/libgimp/tests/meson.build
@@ -14,6 +14,7 @@ env.set('GIMP_TESTING_MENUS_PATH', menu_paths)
env.set('GIMP_TESTING_PLUGINDIRS', meson.project_build_root() / 'plug-ins:')
env.append('GIMP_TESTING_PLUGINDIRS', meson.project_build_root() / 'plug-ins/python')
env.append('GIMP_TESTING_PLUGINDIRS', meson.project_build_root() / 'plug-ins/common/test-plug-ins/')
+env.append('GIMP_TESTING_PLUGINDIRS', meson.project_build_root() / 'libgimp/tests/c-tests/')
env.prepend('GI_TYPELIB_PATH', meson.project_build_root() / 'libgimp')
env.prepend('LD_LIBRARY_PATH', meson.project_build_root() / 'libgimp')
@@ -34,13 +35,44 @@ else
endif
run_python_test = find_program('./libgimp-run-python-test.sh')
+run_c_test = find_program('./libgimp-run-c-test.sh')
+cat = find_program('cat')
foreach test_name : tests
basename = 'test-' + test_name
- py_test = meson.current_source_dir() / basename + '.py'
+ py_test = meson.current_source_dir() / basename + '.py'
test(test_name, run_python_test,
args: [ gimp_exe, py_test ],
env: env,
suite: ['libgimp', 'python3'],
timeout: 60)
+
+ c_test_name = basename + '.c'
+ c_test = custom_target(c_test_name,
+ input: [ 'c-test-header.c', c_test_name ],
+ output: c_test_name,
+ command: [cat, '@INPUT@'],
+ capture: true,
+ install: false)
+ c_test_exe = executable(basename,
+ c_test,
+ dependencies: [ libgimp_dep, pango ],
+ install: false)
+
+ # Same ugly trick as in plug-ins/common/meson.build to detect plug-ins in a
+ # non-installed build directory.
+ custom_target(basename + '.dummy',
+ input: [ c_test_exe ],
+ output: [ basename + '.dummy' ],
+ command: [ python, meson.project_source_root() / '.gitlab/cp-plug-in-subfolder.py',
+ c_test_exe, meson.current_build_dir() / 'c-tests' / basename,
+ '@OUTPUT@' ],
+ build_by_default: true,
+ install: false)
+
+ test(test_name, run_c_test,
+ args: [ gimp_exe, meson.current_source_dir() / c_test_name, basename ],
+ env: env,
+ suite: ['libgimp', 'C'],
+ timeout: 60)
endforeach
diff --git a/libgimp/tests/pygimp/utils.py b/libgimp/tests/pygimp/utils.py
index 28172ed725..548c723168 100755
--- a/libgimp/tests/pygimp/utils.py
+++ b/libgimp/tests/pygimp/utils.py
@@ -19,3 +19,14 @@ def gimp_assert(subtest_name, test):
subtest_name))
sys.stderr.write("***** END FAILED SUBTEST ******\n\n")
assert test
+
+def gimp_c_assert(c_filename, error_msg, test):
+ '''
+ This is called by the platform only, and print out the GError message from the
+ C test plug-in.
+ '''
+ if not test:
+ sys.stderr.write("\n**** START FAILED SUBTEST *****\n")
+ sys.stderr.write("ERROR: {}: {}\n".format(c_filename, error_msg))
+ sys.stderr.write("***** END FAILED SUBTEST ******\n\n")
+ assert test
diff --git a/libgimp/tests/test-palette.c b/libgimp/tests/test-palette.c
new file mode 100644
index 0000000000..6fef0be2c5
--- /dev/null
+++ b/libgimp/tests/test-palette.c
@@ -0,0 +1,75 @@
+#define GIMP_TEST_PALETTE "Bears"
+#define GIMP_TEST_PALETTE_SIZE 256
+
+static GimpValueArray *
+gimp_c_test_run (GimpProcedure *procedure,
+ GimpRunMode run_mode,
+ GimpImage *image,
+ gint n_drawables,
+ GimpDrawable **drawables,
+ GimpProcedureConfig *config,
+ gpointer run_data)
+{
+ GimpPalette *palette;
+ GimpPalette *palette2;
+ GeglColor **colors;
+ gint n_colors;
+ GimpValueArray *retvals;
+
+ GIMP_TEST_START("gimp_palette_get_by_name()")
+ palette = gimp_palette_get_by_name (GIMP_TEST_PALETTE);
+ GIMP_TEST_END(GIMP_IS_PALETTE (palette))
+
+ GIMP_TEST_START("gimp_palette_get_by_name() is unique")
+ palette2 = gimp_palette_get_by_name (GIMP_TEST_PALETTE);
+ GIMP_TEST_END(GIMP_IS_PALETTE (palette2) && palette == palette2)
+
+ GIMP_TEST_START("gimp_palette_get_colors()")
+ colors = gimp_palette_get_colors (palette);
+ GIMP_TEST_END(colors != NULL && gimp_color_array_get_length (colors) == GIMP_TEST_PALETTE_SIZE)
+
+ GIMP_TEST_START("gimp_palette_get_color_count()")
+ n_colors = gimp_palette_get_color_count (palette);
+ GIMP_TEST_END(n_colors == gimp_color_array_get_length (colors))
+
+ /* Run the same tests through PDB. */
+
+ GIMP_TEST_START("gimp-palette-get-by-name")
+ retvals = gimp_procedure_run (gimp_pdb_lookup_procedure (gimp_get_pdb (), "gimp-palette-get-by-name"),
+ "name", GIMP_TEST_PALETTE, NULL);
+ GIMP_TEST_END(g_value_get_enum (gimp_value_array_index (retvals, 0)) == GIMP_PDB_SUCCESS)
+
+ GIMP_TEST_START("gimp-palette-get-by-name and gimp_palette_get_by_name() get identical result")
+ palette2 = g_value_get_object (gimp_value_array_index(retvals, 1));
+ GIMP_TEST_END(GIMP_IS_PALETTE (palette2) && palette == palette2)
+
+ gimp_value_array_unref (retvals);
+
+ GIMP_TEST_START("gimp-palette-get-colors")
+ retvals = gimp_procedure_run (gimp_pdb_lookup_procedure (gimp_get_pdb (), "gimp-palette-get-colors"),
+ "palette", palette, NULL);
+ GIMP_TEST_END(g_value_get_enum (gimp_value_array_index (retvals, 0)) == GIMP_PDB_SUCCESS)
+
+ GIMP_TEST_START("gimp-palette-get-colors returns GimpColorArray")
+ colors = g_value_get_boxed (gimp_value_array_index (retvals, 1));
+ GIMP_TEST_END(colors != NULL && gimp_color_array_get_length (colors) == GIMP_TEST_PALETTE_SIZE)
+
+ GIMP_TEST_START("gimp_value_array_get_color_array()")
+ colors = gimp_value_array_get_color_array (retvals, 1);
+ GIMP_TEST_END(colors != NULL && gimp_color_array_get_length (colors) == GIMP_TEST_PALETTE_SIZE)
+
+ gimp_value_array_unref (retvals);
+
+ GIMP_TEST_START("gimp-palette-get-color-count")
+ retvals = gimp_procedure_run (gimp_pdb_lookup_procedure (gimp_get_pdb (), "gimp-palette-get-color-count"),
+ "palette", palette, NULL);
+ GIMP_TEST_END(g_value_get_enum (gimp_value_array_index (retvals, 0)) == GIMP_PDB_SUCCESS)
+
+ GIMP_TEST_START("gimp-palette-get-color-count returns the right number of colors")
+ n_colors = g_value_get_int (gimp_value_array_index (retvals, 1));
+ GIMP_TEST_END(n_colors == GIMP_TEST_PALETTE_SIZE)
+
+ gimp_value_array_unref (retvals);
+
+ GIMP_TEST_RETURN
+}