From 608ad0a528b5b31101c021d96aeb95558d207497 Mon Sep 17 00:00:00 2001 From: Jehan Date: Tue, 3 Feb 2026 10:29:13 +0100 Subject: [PATCH] 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. --- app/meson.build | 1 - app/tests/meson.build | 39 ++- app/tests/test-save-and-export.c | 399 ------------------------------ app/tests/test-save-and-export.py | 85 +++++++ meson.build | 1 + 5 files changed, 124 insertions(+), 401 deletions(-) delete mode 100644 app/tests/test-save-and-export.c create mode 100644 app/tests/test-save-and-export.py diff --git a/app/meson.build b/app/meson.build index fb6efb55e7..94a5d44a2e 100644 --- a/app/meson.build +++ b/app/meson.build @@ -128,7 +128,6 @@ libapp_dep = declare_dependency( # Those subdirs need to link against the first ones subdir('config') -subdir('tests') console_libapps += [ libappconfig ] diff --git a/app/tests/meson.build b/app/tests/meson.build index 18e6c434ad..bca4405a96 100644 --- a/app/tests/meson.build +++ b/app/tests/meson.build @@ -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 diff --git a/app/tests/test-save-and-export.c b/app/tests/test-save-and-export.c deleted file mode 100644 index ddb25080af..0000000000 --- a/app/tests/test-save-and-export.c +++ /dev/null @@ -1,399 +0,0 @@ -/* GIMP - The GNU Image Manipulation Program - * Copyright (C) 2009 Martin Nordholts - * - * 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 "config.h" - -#include -#include - -#include -#include -#include - -#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; -} diff --git a/app/tests/test-save-and-export.py b/app/tests/test-save-and-export.py new file mode 100644 index 0000000000..c3c2bf1fdb --- /dev/null +++ b/app/tests/test-save-and-export.py @@ -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() diff --git a/meson.build b/meson.build index 3540950a85..0697adbdc6 100644 --- a/meson.build +++ b/meson.build @@ -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