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 +}