How to Test GEGL Operations in GIMP

This article provides a practical guide for developers looking to write unit tests for GEGL (Generic Graphics Library) operations integrated into GIMP. It covers the setup of the testing environment using GLib's testing framework and Meson, constructing graph-based image processing pipelines, feeding test buffers into GEGL nodes, and asserting expected pixel and metadata outputs within both isolated and GIMP-dependent environments.


Understanding the Testing Scope

When testing GEGL operations within GIMP, tests fall into two categories:

  1. Pure GEGL Unit Tests: Isolated tests verifying mathematical and color transformation logic of custom GEGL nodes without launching GIMP core components.
  2. GIMP-Integrated Tests: Tests that verify how GIMP handles the operation through its internal wrappers, tool interfaces, or the Procedural Database (PDB).

For pure node logic, write tests using the GEGL API directly. For integration verification, write tests within GIMP's test directory (app/tests/).


Setting Up the Test Runner

GIMP and GEGL utilize the standard GLib Testing Framework (gtest). A minimal test file initializes the GLib test harness and the GEGL runtime environment.

#include <glib.h>
#include <gegl.h>

static void
test_gegl_operation_basic (void)
{
  /* Test logic goes here */
}

int
main (int argc, char **argv)
{
  g_test_init (&argc, &argv, NULL);
  gegl_init (&argc, &argv);

  g_test_add_func ("/gegl-ops/custom-filter/basic", test_gegl_operation_basic);

  int result = g_test_run ();

  gegl_exit ();
  return result;
}

Constructing the Test Pipeline

To test an operation, construct a Directed Acyclic Graph (DAG) consisting of a source node, the operation under test, and a sink node.

  1. Source Node: Provides synthetic input data (such as a solid color, pattern, or known pixel buffer).
  2. Operation Node: The specific gegl:operation being tested.
  3. Sink Node: Captures the processed output into a GeglBuffer for inspection.
static void
test_brightness_contrast_operation (void)
{
  GeglNode *graph   = gegl_node_new ();
  GeglColor *color  = gegl_color_new ("rgb(0.5, 0.5, 0.5)");

  /* Create Nodes */
  GeglNode *source  = gegl_node_new_child (graph,
                                           "operation", "gegl:color",
                                           "value", color,
                                           NULL);

  GeglNode *filter  = gegl_node_new_child (graph,
                                           "operation", "gegl:brightness-contrast",
                                           "brightness", 0.2,
                                           "contrast", 0.0,
                                           NULL);

  GeglNode *sink    = gegl_node_new_child (graph,
                                           "operation", "gegl:buffer-sink",
                                           NULL);

  /* Connect Nodes */
  gegl_node_link_many (source, filter, sink, NULL);

  /* Process Graph */
  GeglBuffer *output_buffer = NULL;
  gegl_node_process (sink);
  gegl_node_get (sink, "buffer", &output_buffer, NULL);

  g_assert_nonnull (output_buffer);

  /* Validate output here */

  g_object_unref (output_buffer);
  g_object_unref (color);
  g_object_unref (graph);
}

Validating Buffer Outputs

To ensure the GEGL operation functions correctly, inspect the pixel values stored in the resulting GeglBuffer. Read the buffer back using a defined format (e.g., Babl format "RGBA float") to compare against expected values.

static void
assert_pixel_values (GeglBuffer *buffer, gfloat expected_r, gfloat expected_g, gfloat expected_b)
{
  GeglRectangle rect = { 0, 0, 1, 1 };
  gfloat pixel[4];

  gegl_buffer_get (buffer, &rect, 1.0, babl_format ("RGBA float"),
                   pixel, GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE);

  /* Use a delta for floating-point comparisons */
  const gfloat epsilon = 0.001f;
  g_assert_cmpfloat_with_epsilon (pixel[0], expected_r, epsilon);
  g_assert_cmpfloat_with_epsilon (pixel[1], expected_g, epsilon);
  g_assert_cmpfloat_with_epsilon (pixel[2], expected_b, epsilon);
}

Testing GIMP-Specific Integrations

When an operation is wired directly into GIMP (e.g., using GimpGeglProcedure or GIMP tools), initialize GIMP's subsystem within the test:

  1. Use gimp_init_test () or link against libgimpbase and libgimpcore.
  2. Instantiate a GimpImage and GimpLayer.
  3. Apply the operation via GIMP's internal API (gimp_drawable_apply_operation () or corresponding procedural functions).
  4. Assert that the underlying projection updates properly and undo/redo stacks maintain integrity.
/* Example: Verifying an operation applied to a GimpDrawable */
GimpDrawable *drawable = GIMP_DRAWABLE (layer);
gimp_gegl_apply_operation (drawable, NULL, "Processing", filter_node);
gimp_drawable_update (drawable, 0, 0, width, height);

/* Verify layer buffer changes match expected result */
GeglBuffer *layer_buffer = gimp_drawable_get_buffer (drawable);
assert_pixel_values (layer_buffer, 0.7f, 0.7f, 0.7f);

Configuring the Build System (Meson)

Register the test in the relevant meson.build file to integrate it with continuous integration and local testing workflows:

test_gegl_operation = executable(
  'test-gegl-operation',
  'test-gegl-operation.c',
  dependencies: [gegl_dep, glib_dep, babl_dep],
  include_directories: [include_directories('.')],
)

test('gegl-operation-test', test_gegl_operation)

Run the compiled test suite from the build directory using Meson:

meson test -C build --verbose gegl-operation-test