app: new save-and-export tests calling the API with actual GIMP process.

Issue #15763 is again mostly false positive crashes of the unit tests
because the code is only partly GIMP code, arranged differently as what
a full GIMP process would actually do.

The new tests just runs on the real GIMP code. Now the difference is
that we don't test the UI by doing this, but this can be debated whether
the previous tests were actually running on the UI themselves (they were
mostly running some core code directly and sometimes activating some
actions or raising dialogs (gimp_test_utils_create_image_from_dialog()),
but again not by actually clicking or hitting keys as a human would do.

It's not proper UI testing (cf. #9339 of #13376 for further discussions
on this topic).

So in the meantime, let's go with this intermediate step. At least now,
such tests would run actual GIMP code and would catch issues which could
really happen in a GIMP process.
This commit is contained in:
Jehan 2026-02-03 10:29:13 +01:00
parent fd58ab3bee
commit 608ad0a528
5 changed files with 124 additions and 401 deletions

View file

@ -128,7 +128,6 @@ libapp_dep = declare_dependency(
# Those subdirs need to link against the first ones
subdir('config')
subdir('tests')
console_libapps += [ libappconfig ]

View file

@ -35,7 +35,6 @@ apptests_links += libapptestutils
app_tests = [
'core',
'gimpidtable',
'save-and-export',
#'session-2-8-compatibility-multi-window',
#'session-2-8-compatibility-single-window',
'single-window-mode',
@ -73,3 +72,41 @@ foreach test_name : app_tests
prio = prio - 10
endforeach
## Newer Tests using GIMP directly ##
if not meson.can_run_host_binaries()
warning('XCF loading unit testing disabled in cross-building or similar environments.')
subdir_done()
endif
tests = [
{
'name': 'save-and-export',
}
]
test_env=gimp_run_env
test_env.set('GIMP_TESTING_ABS_TOP_SRCDIR', meson.project_source_root())
test_env.set('GIMP_TESTING_ABS_TOP_BUILDDIR', meson.project_build_root())
run_python_test = find_program(meson.project_source_root() / 'libgimp/tests/libgimp-run-python-test.py')
foreach testinfo : tests
test_name = testinfo['name']
basename = 'test-' + testinfo['name']
py_test = meson.current_source_dir() / basename + '.py'
if testinfo.has_key('input')
test(test_name, run_python_test,
args: [ gimp_exe.full_path(), py_test, testinfo['input']],
env: test_env,
suite: ['app'],
timeout: 90)
else
test(test_name, run_python_test,
args: [ gimp_exe.full_path(), py_test ],
env: test_env,
suite: ['app'],
timeout: 90)
endif
endforeach

View file

@ -1,399 +0,0 @@
/* GIMP - The GNU Image Manipulation Program
* Copyright (C) 2009 Martin Nordholts <martinn@src.gnome.org>
*
* 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 <https://www.gnu.org/licenses/>.
*/
#include "config.h"
#include <stdlib.h>
#include <string.h>
#include <gegl.h>
#include <glib/gstdio.h>
#include <gtk/gtk.h>
#include "libgimpbase/gimpbase.h"
#include "libgimpmath/gimpmath.h"
#include "libgimpwidgets/gimpwidgets.h"
#include "dialogs/dialogs-types.h"
#include "core/gimp.h"
#include "core/gimpchannel.h"
#include "core/gimpcontext.h"
#include "core/gimpimage.h"
#include "core/gimplayer.h"
#include "core/gimptoolinfo.h"
#include "core/gimptooloptions.h"
#include "plug-in/gimppluginmanager-file.h"
#include "file/file-open.h"
#include "file/file-save.h"
#include "widgets/gimpdialogfactory.h"
#include "widgets/gimpdock.h"
#include "widgets/gimpdockable.h"
#include "widgets/gimpdockbook.h"
#include "widgets/gimpdocked.h"
#include "widgets/gimpdockwindow.h"
#include "widgets/gimphelp-ids.h"
#include "widgets/gimpsessioninfo.h"
#include "widgets/gimptoolbox.h"
#include "widgets/gimptooloptionseditor.h"
#include "widgets/gimpuimanager.h"
#include "widgets/gimpwidgets-utils.h"
#include "display/gimpdisplay.h"
#include "display/gimpdisplayshell.h"
#include "display/gimpdisplayshell-scale.h"
#include "display/gimpdisplayshell-transform.h"
#include "display/gimpimagewindow.h"
#include "gimpcoreapp.h"
#include "gimp-app-test-utils.h"
#include "tests.h"
#define ADD_TEST(function) \
g_test_add_data_func ("/gimp-save-and-export/" #function, gimp, function);
typedef gboolean (*GimpUiTestFunc) (GObject *object);
/**
* new_file_has_no_files:
* @data:
*
* Tests that the URIs are correct for a newly created image.
**/
static void
new_file_has_no_files (gconstpointer data)
{
Gimp *gimp = GIMP (data);
GimpImage *image = gimp_test_utils_create_image_from_dialog (gimp);
g_assert_true (gimp_image_get_file (image) == NULL);
g_assert_true (gimp_image_get_imported_file (image) == NULL);
g_assert_true (gimp_image_get_exported_file (image) == NULL);
}
/**
* opened_xcf_file_files:
* @data:
*
* Tests that GimpImage URIs are correct for an XCF file that has just
* been opened.
**/
static void
opened_xcf_file_files (gconstpointer data)
{
Gimp *gimp = GIMP (data);
GimpImage *image;
GFile *file;
gchar *filename;
GimpPDBStatusType status;
filename = g_build_filename (g_getenv ("GIMP_TESTING_ABS_TOP_SRCDIR"),
"app/tests/files/gimp-2-6-file.xcf",
NULL);
file = g_file_new_for_path (filename);
g_free (filename);
image = file_open_image (gimp,
gimp_get_user_context (gimp),
NULL /*progress*/,
file,
0, 0, /* vector width, height */
TRUE, /* vector keep ratio */
FALSE /*as_new*/,
NULL /*file_proc*/,
GIMP_RUN_NONINTERACTIVE,
NULL, /* file_proc_handles_vector */
&status,
NULL /*mime_type*/,
NULL /*error*/);
g_assert_true (g_file_equal (gimp_image_get_file (image), file));
g_assert_true (gimp_image_get_imported_file (image) == NULL);
g_assert_true (gimp_image_get_exported_file (image) == NULL);
g_object_unref (file);
}
/**
* imported_file_files:
* @data:
*
* Tests that URIs are correct for an imported image.
**/
static void
imported_file_files (gconstpointer data)
{
Gimp *gimp = GIMP (data);
GimpImage *image;
GFile *file;
gchar *filename;
GimpPDBStatusType status;
filename = g_build_filename (g_getenv ("GIMP_TESTING_ABS_TOP_BUILDDIR"),
"gimp-data/images/logo/gimp64x64.png",
NULL);
g_assert_true (g_file_test (filename, G_FILE_TEST_EXISTS));
file = g_file_new_for_path (filename);
g_free (filename);
image = file_open_image (gimp,
gimp_get_user_context (gimp),
NULL /*progress*/,
file,
0, 0, /* vector width, height */
TRUE, /* vector keep ratio */
FALSE /*as_new*/,
NULL /*file_proc*/,
GIMP_RUN_NONINTERACTIVE,
NULL, /* file_proc_handles_vector */
&status,
NULL /*mime_type*/,
NULL /*error*/);
g_assert_true (gimp_image_get_file (image) == NULL);
g_assert_true (g_file_equal (gimp_image_get_imported_file (image), file));
g_assert_true (gimp_image_get_exported_file (image) == NULL);
g_object_unref (file);
}
/**
* saved_imported_file_files:
* @data:
*
* Tests that the URIs are correct for an image that has been imported
* and then saved.
**/
static void
saved_imported_file_files (gconstpointer data)
{
Gimp *gimp = GIMP (data);
GimpImage *image;
GFile *import_file;
gchar *import_filename;
GFile *save_file;
gchar *save_filename;
GimpPDBStatusType status;
GimpPlugInProcedure *proc;
import_filename = g_build_filename (g_getenv ("GIMP_TESTING_ABS_TOP_BUILDDIR"),
"gimp-data/images/logo/gimp64x64.png",
NULL);
import_file = g_file_new_for_path (import_filename);
g_free (import_filename);
save_filename = g_build_filename (g_get_tmp_dir (), "gimp-test.xcf", NULL);
save_file = g_file_new_for_path (save_filename);
g_free (save_filename);
/* Import */
image = file_open_image (gimp,
gimp_get_user_context (gimp),
NULL /*progress*/,
import_file,
0, 0, /* vector width, height */
TRUE, /* vector keep ratio */
FALSE /*as_new*/,
NULL /*file_proc*/,
GIMP_RUN_NONINTERACTIVE,
NULL, /* file_proc_handles_vector */
&status,
NULL /*mime_type*/,
NULL /*error*/);
g_object_unref (import_file);
/* Save */
proc = gimp_plug_in_manager_file_procedure_find (image->gimp->plug_in_manager,
GIMP_FILE_PROCEDURE_GROUP_SAVE,
save_file,
NULL /*error*/);
file_save (gimp,
image,
NULL /*progress*/,
save_file,
proc,
GIMP_RUN_NONINTERACTIVE,
TRUE /*change_saved_state*/,
FALSE /*export_backward*/,
FALSE /*export_forward*/,
NULL /*error*/);
/* Assert */
g_assert_true (g_file_equal (gimp_image_get_file (image), save_file));
g_assert_true (gimp_image_get_imported_file (image) == NULL);
g_assert_true (gimp_image_get_exported_file (image) == NULL);
g_file_delete (save_file, NULL, NULL);
g_object_unref (save_file);
}
/**
* exported_file_files:
* @data:
*
* Tests that the URIs for an exported, newly created file are
* correct.
**/
static void
exported_file_files (gconstpointer data)
{
GFile *save_file;
gchar *save_filename;
GimpPlugInProcedure *proc;
Gimp *gimp = GIMP (data);
GimpImage *image = gimp_test_utils_create_image_from_dialog (gimp);
save_filename = g_build_filename (g_get_tmp_dir (), "gimp-test.png", NULL);
save_file = g_file_new_for_path (save_filename);
g_free (save_filename);
proc = gimp_plug_in_manager_file_procedure_find (image->gimp->plug_in_manager,
GIMP_FILE_PROCEDURE_GROUP_EXPORT,
save_file,
NULL /*error*/);
file_save (gimp,
image,
NULL /*progress*/,
save_file,
proc,
GIMP_RUN_NONINTERACTIVE,
FALSE /*change_saved_state*/,
FALSE /*export_backward*/,
TRUE /*export_forward*/,
NULL /*error*/);
g_assert_true (gimp_image_get_file (image) == NULL);
g_assert_true (gimp_image_get_imported_file (image) == NULL);
g_assert_true (g_file_equal (gimp_image_get_exported_file (image), save_file));
g_file_delete (save_file, NULL, NULL);
g_object_unref (save_file);
}
/**
* clear_import_file_after_export:
* @data:
*
* Tests that after a XCF file that was imported has been exported,
* the import URI is cleared. An image can not be considered both
* imported and exported at the same time.
**/
static void
clear_import_file_after_export (gconstpointer data)
{
Gimp *gimp = GIMP (data);
GimpImage *image;
GFile *file;
gchar *filename;
GFile *save_file;
gchar *save_filename;
GimpPlugInProcedure *proc;
GimpPDBStatusType status;
filename = g_build_filename (g_getenv ("GIMP_TESTING_ABS_TOP_BUILDDIR"),
"gimp-data/images/logo/gimp64x64.png",
NULL);
file = g_file_new_for_path (filename);
g_free (filename);
image = file_open_image (gimp,
gimp_get_user_context (gimp),
NULL /*progress*/,
file,
0, 0, /* vector width, height */
TRUE, /* vector keep ratio */
FALSE /*as_new*/,
NULL /*file_proc*/,
GIMP_RUN_NONINTERACTIVE,
NULL, /* file_proc_handles_vector */
&status,
NULL /*mime_type*/,
NULL /*error*/);
g_assert_true (gimp_image_get_file (image) == NULL);
g_assert_true (g_file_equal (gimp_image_get_imported_file (image), file));
g_assert_true (gimp_image_get_exported_file (image) == NULL);
g_object_unref (file);
save_filename = g_build_filename (g_get_tmp_dir (), "gimp-test.png", NULL);
save_file = g_file_new_for_path (save_filename);
g_free (save_filename);
proc = gimp_plug_in_manager_file_procedure_find (image->gimp->plug_in_manager,
GIMP_FILE_PROCEDURE_GROUP_EXPORT,
save_file,
NULL /*error*/);
file_save (gimp,
image,
NULL /*progress*/,
save_file,
proc,
GIMP_RUN_NONINTERACTIVE,
FALSE /*change_saved_state*/,
FALSE /*export_backward*/,
TRUE /*export_forward*/,
NULL /*error*/);
g_assert_true (gimp_image_get_file (image) == NULL);
g_assert_true (gimp_image_get_imported_file (image) == NULL);
g_assert_true (g_file_equal (gimp_image_get_exported_file (image), save_file));
g_file_delete (save_file, NULL, NULL);
g_object_unref (save_file);
}
int
main(int argc,
char **argv)
{
Gimp *gimp = NULL;
gint result = -1;
gimp_test_bail_if_no_display ();
gtk_test_init (&argc, &argv, NULL);
gimp_test_utils_setup_menus_path ();
/* Start up GIMP */
gimp = gimp_init_for_gui_testing (TRUE /*show_gui*/);
gimp_test_run_mainloop_until_idle ();
ADD_TEST (new_file_has_no_files);
ADD_TEST (opened_xcf_file_files);
ADD_TEST (imported_file_files);
ADD_TEST (saved_imported_file_files);
ADD_TEST (exported_file_files);
ADD_TEST (clear_import_file_after_export);
/* Run the tests and return status */
g_application_run (gimp->app, 0, NULL);
result = gimp_core_app_get_exit_status (GIMP_CORE_APP (gimp->app));
g_application_quit (G_APPLICATION (gimp->app));
g_clear_object (&gimp->app);
return result;
}

View file

@ -0,0 +1,85 @@
#!/usr/bin/env python3
# NOTE: these tests were originally mostly supposed to be GUI tests, but
# the early tests were much too flimsy. And to be fair, they were not
# proper GUI tests either, as they would hack-call some core functions,
# but without actually loading the full process and clicking on buttons
# or actually interacting with the interface.
# These replacement tests are further away since they use the public API
# (but this one does use core code too!).
#
# Now the ideal implementation would use dedicated GUI testing
# frameworks, which actually simulate human interactions with GUI
# applications through automated scripts. When we'll have these, we may
# replace the below tests.
tmpdir = Gimp.temp_directory()
# Tests that the URIs are correct for a newly created image.
image = Gimp.Image.new(800, 600, Gimp.ImageBaseType.RGB)
#Gimp.Display.new(image)
gimp_assert("New image has no file associated", image.get_xcf_file() is None)
gimp_assert("New image has no imported file", image.get_imported_file() is None);
gimp_assert("New image has no exported file", image.get_exported_file() is None);
image.delete()
# Tests that GimpImage URIs are correct for an XCF file that has just been opened.
path = os.path.join(os.environ['GIMP_TESTING_ABS_TOP_SRCDIR'],
'app/tests/files/gimp-2-6-file.xcf')
gimp_assert("XCF file to load exists", GLib.file_test(path, GLib.FileTest.EXISTS))
file = Gio.File.new_for_path(path)
image = Gimp.file_load(Gimp.RunMode.NONINTERACTIVE, file)
gimp_assert("Loaded XCF's file equals the loaded file", image.get_xcf_file().equal(file));
gimp_assert("Loaded XCF has no imported file", image.get_imported_file() is None);
gimp_assert("Loaded XCF has no exported file", image.get_exported_file() is None);
# Tests that the URIs are correct for an image that has been imported and then saved.
path = os.path.join(tmpdir, 'gimp-test.xcf')
saved_file = Gio.File.new_for_path(path)
gimp_assert(f"Making sure {path} does not exists", not GLib.file_test(path, GLib.FileTest.EXISTS))
Gimp.file_save(Gimp.RunMode.NONINTERACTIVE, image, saved_file, None)
gimp_assert("XCF file was properly saved", GLib.file_test(path, GLib.FileTest.EXISTS))
gimp_assert("Saved XCF's file is correct", image.get_xcf_file().equal(saved_file));
gimp_assert("Saved XCF has no imported file", image.get_imported_file() is None);
gimp_assert("Saved XCF has no exported file", image.get_exported_file() is None);
# Tests that the URIs for an exported, newly created file are correct.
path = os.path.join(tmpdir, 'gimp-test.png')
exported_file = Gio.File.new_for_path(path)
gimp_assert(f"Making sure {path} does not exists", not GLib.file_test(path, GLib.FileTest.EXISTS))
Gimp.file_save(Gimp.RunMode.NONINTERACTIVE, image, exported_file, None)
gimp_assert("PNG file was properly saved", GLib.file_test(path, GLib.FileTest.EXISTS))
gimp_assert("Exported image's file still equals the saved file", image.get_xcf_file().equal(saved_file));
gimp_assert("Exported image has no imported file", image.get_imported_file() is None);
gimp_assert("Exported image's exported file is correct", image.get_exported_file().equal(exported_file));
saved_file.delete()
exported_file.delete()
image.delete()
# Tests that URIs are correct for an imported image.
path = os.path.join(os.environ['GIMP_TESTING_ABS_TOP_BUILDDIR'],
'gimp-data/images/logo/gimp64x64.png')
gimp_assert("Image file to import exists", GLib.file_test(path, GLib.FileTest.EXISTS))
file = Gio.File.new_for_path(path)
image = Gimp.file_load(Gimp.RunMode.NONINTERACTIVE, file)
gimp_assert("Imported image has no file associated", image.get_xcf_file() is None)
gimp_assert("Imported image's imported file equals the loaded file", image.get_imported_file().equal(file));
gimp_assert("Imported image has no exported file", image.get_exported_file() is None);
# Tests that after a XCF file was imported then exported, the import URI is cleared.
# An image can not be considered both imported and exported at the same time.
gimp_assert(f"Making sure {exported_file.peek_path()} does not exists",
not GLib.file_test(exported_file.peek_path(), GLib.FileTest.EXISTS))
Gimp.file_save(Gimp.RunMode.NONINTERACTIVE, image, exported_file, None)
gimp_assert("Imported then exported image has no file associated", image.get_xcf_file() is None)
gimp_assert("Imported then exported image has no imported file", image.get_imported_file() is None)
gimp_assert("Imported then exported image has a correct exported file", image.get_exported_file().equal(exported_file))
exported_file.delete()
image.delete()

View file

@ -2090,6 +2090,7 @@ gimp_exe = find_program('tools'/'in-build-gimp.py')
subdir('gimp-data/images/')
# Unit testing
subdir('app/tests')
subdir('libgimp/tests')
# Docs