/* * Copyright © 2019 Benjamin Otte * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * * Authors: Benjamin Otte */ #include #include static void inc_counter (gpointer data) { guint *counter = data; *counter += 1; } static void test_property (void) { GValue value = G_VALUE_INIT; GtkExpression *expr; GtkExpressionWatch *watch; GtkStringFilter *filter; guint counter = 0; filter = GTK_STRING_FILTER (gtk_string_filter_new ()); expr = gtk_property_expression_new (GTK_TYPE_STRING_FILTER, NULL, "search"); watch = gtk_expression_watch (expr, filter, inc_counter, &counter, NULL); g_assert (gtk_expression_evaluate (expr, filter, &value)); g_assert_cmpstr (g_value_get_string (&value), ==, NULL); g_value_unset (&value); gtk_string_filter_set_search (filter, "Hello World"); g_assert_cmpint (counter, ==, 1); counter = 0; g_assert (gtk_expression_evaluate (expr, filter , &value)); g_assert_cmpstr (g_value_get_string (&value), ==, "Hello World"); g_value_unset (&value); gtk_expression_watch_unwatch (watch); g_assert_cmpint (counter, ==, 0); gtk_expression_unref (expr); g_object_unref (filter); } static char * print_filter_info (GtkStringFilter *filter, const char *search, gboolean ignore_case, GtkStringFilterMatchMode match_mode) { g_assert_cmpstr (search, ==, gtk_string_filter_get_search (filter)); g_assert_cmpint (ignore_case, ==, gtk_string_filter_get_ignore_case (filter)); g_assert_cmpint (match_mode, ==, gtk_string_filter_get_match_mode (filter)); return g_strdup ("OK"); } static void test_closure (void) { GValue value = G_VALUE_INIT; GtkExpression *expr, *pexpr[3]; GtkExpressionWatch *watch; GtkStringFilter *filter; guint counter = 0; filter = GTK_STRING_FILTER (gtk_string_filter_new ()); pexpr[0] = gtk_property_expression_new (GTK_TYPE_STRING_FILTER, NULL, "search"); pexpr[1] = gtk_property_expression_new (GTK_TYPE_STRING_FILTER, NULL, "ignore-case"); pexpr[2] = gtk_property_expression_new (GTK_TYPE_STRING_FILTER, NULL, "match-mode"); expr = gtk_cclosure_expression_new (G_TYPE_STRING, NULL, 3, pexpr, G_CALLBACK (print_filter_info), NULL, NULL); watch = gtk_expression_watch (expr, filter, inc_counter, &counter, NULL); g_assert (gtk_expression_evaluate (expr, filter, &value)); g_assert_cmpstr (g_value_get_string (&value), ==, "OK"); g_value_unset (&value); gtk_string_filter_set_search (filter, "Hello World"); g_assert_cmpint (counter, ==, 1); g_assert (gtk_expression_evaluate (expr, filter , &value)); g_assert_cmpstr (g_value_get_string (&value), ==, "OK"); g_value_unset (&value); gtk_string_filter_set_ignore_case (filter, FALSE); g_assert_cmpint (counter, ==, 2); g_assert (gtk_expression_evaluate (expr, filter , &value)); g_assert_cmpstr (g_value_get_string (&value), ==, "OK"); g_value_unset (&value); gtk_string_filter_set_search (filter, "Hello"); gtk_string_filter_set_ignore_case (filter, TRUE); gtk_string_filter_set_match_mode (filter, GTK_STRING_FILTER_MATCH_MODE_EXACT); g_assert_cmpint (counter, ==, 5); g_assert (gtk_expression_evaluate (expr, filter , &value)); g_assert_cmpstr (g_value_get_string (&value), ==, "OK"); g_value_unset (&value); gtk_expression_watch_unwatch (watch); g_assert_cmpint (counter, ==, 5); gtk_expression_unref (expr); g_object_unref (filter); } static void test_constant (void) { GtkExpression *expr; GValue value = G_VALUE_INIT; gboolean res; expr = gtk_constant_expression_new (G_TYPE_INT, 22); g_assert_cmpint (gtk_expression_get_value_type (expr), ==, G_TYPE_INT); g_assert_true (gtk_expression_is_static (expr)); res = gtk_expression_evaluate (expr, NULL, &value); g_assert_true (res); g_assert_cmpint (g_value_get_int (&value), ==, 22); gtk_expression_unref (expr); } /* Test that object expressions fail to evaluate when * the object is gone. */ static void test_object (void) { GtkExpression *expr; GObject *obj; GValue value = G_VALUE_INIT; gboolean res; obj = G_OBJECT (gtk_string_filter_new ()); expr = gtk_object_expression_new (obj); g_assert_true (!gtk_expression_is_static (expr)); g_assert_cmpint (gtk_expression_get_value_type (expr), ==, GTK_TYPE_STRING_FILTER); res = gtk_expression_evaluate (expr, NULL, &value); g_assert_true (res); g_assert_true (g_value_get_object (&value) == obj); g_value_unset (&value); g_clear_object (&obj); res = gtk_expression_evaluate (expr, NULL, &value); g_assert_false (res); gtk_expression_unref (expr); } /* Some basic tests that nested expressions work; in particular test * that watching works when things change deeper in the expression tree * * The setup we use is GtkFilterListModel -> GtkFilter -> "search" property, * which gives us an expression tree like * * GtkPropertyExpression "search" * -> GtkPropertyExpression "filter" * -> GtkObjectExpression listmodel * * We test setting both the search property and the filter property. */ static void test_nested (void) { GtkExpression *list_expr; GtkExpression *filter_expr; GtkExpression *expr; GtkFilter *filter; GListModel *list; GtkFilterListModel *filtered; GValue value = G_VALUE_INIT; gboolean res; GtkExpressionWatch *watch; guint counter = 0; filter = gtk_string_filter_new (); gtk_string_filter_set_search (GTK_STRING_FILTER (filter), "word"); list = G_LIST_MODEL (g_list_store_new (G_TYPE_OBJECT)); filtered = gtk_filter_list_model_new (list, filter); list_expr = gtk_object_expression_new (G_OBJECT (filtered)); filter_expr = gtk_property_expression_new (GTK_TYPE_FILTER_LIST_MODEL, list_expr, "filter"); expr = gtk_property_expression_new (GTK_TYPE_STRING_FILTER, filter_expr, "search"); g_assert_true (!gtk_expression_is_static (expr)); g_assert_cmpint (gtk_expression_get_value_type (expr), ==, G_TYPE_STRING); res = gtk_expression_evaluate (expr, NULL, &value); g_assert_true (res); g_assert_cmpstr (g_value_get_string (&value), ==, "word"); g_value_unset (&value); watch = gtk_expression_watch (expr, NULL, inc_counter, &counter, NULL); gtk_string_filter_set_search (GTK_STRING_FILTER (filter), "salad"); g_assert_cmpint (counter, ==, 1); counter = 0; res = gtk_expression_evaluate (expr, NULL, &value); g_assert_true (res); g_assert_cmpstr (g_value_get_string (&value), ==, "salad"); g_value_unset (&value); gtk_filter_list_model_set_filter (filtered, filter); g_assert_cmpint (counter, ==, 0); g_clear_object (&filter); filter = gtk_string_filter_new (); gtk_string_filter_set_search (GTK_STRING_FILTER (filter), "salad"); gtk_filter_list_model_set_filter (filtered, filter); g_assert_cmpint (counter, ==, 1); counter = 0; res = gtk_expression_evaluate (expr, NULL, &value); g_assert_true (res); g_assert_cmpstr (g_value_get_string (&value), ==, "salad"); g_value_unset (&value); gtk_string_filter_set_search (GTK_STRING_FILTER (filter), "bar"); g_assert_cmpint (counter, ==, 1); counter = 0; res = gtk_expression_evaluate (expr, NULL, &value); g_assert_true (res); g_assert_cmpstr (g_value_get_string (&value), ==, "bar"); g_value_unset (&value); gtk_filter_list_model_set_filter (filtered, NULL); g_assert_cmpint (counter, ==, 1); counter = 0; res = gtk_expression_evaluate (expr, NULL, &value); g_assert_false (res); gtk_expression_watch_unwatch (watch); g_assert_cmpint (counter, ==, 0); g_object_unref (filtered); g_object_unref (list); g_object_unref (filter); gtk_expression_unref (expr); } /* This test uses the same setup as the last test, but * passes the filter as the "this" object when creating * the watch. * * So when we set a new filter and the old one gets desroyed, * the watch should invalidate itself because its this object * is gone. */ static void test_nested_this_destroyed (void) { GtkExpression *list_expr; GtkExpression *filter_expr; GtkExpression *expr; GtkFilter *filter; GListModel *list; GtkFilterListModel *filtered; GValue value = G_VALUE_INIT; gboolean res; GtkExpressionWatch *watch; guint counter = 0; filter = gtk_string_filter_new (); gtk_string_filter_set_search (GTK_STRING_FILTER (filter), "word"); list = G_LIST_MODEL (g_list_store_new (G_TYPE_OBJECT)); filtered = gtk_filter_list_model_new (list, filter); list_expr = gtk_object_expression_new (G_OBJECT (filtered)); filter_expr = gtk_property_expression_new (GTK_TYPE_FILTER_LIST_MODEL, list_expr, "filter"); expr = gtk_property_expression_new (GTK_TYPE_STRING_FILTER, filter_expr, "search"); watch = gtk_expression_watch (expr, filter, inc_counter, &counter, NULL); gtk_expression_watch_ref (watch); res = gtk_expression_watch_evaluate (watch, &value); g_assert_true (res); g_assert_cmpstr (g_value_get_string (&value), ==, "word"); g_value_unset (&value); g_clear_object (&filter); g_assert_cmpint (counter, ==, 0); filter = gtk_string_filter_new (); gtk_string_filter_set_search (GTK_STRING_FILTER (filter), "salad"); gtk_filter_list_model_set_filter (filtered, filter); g_assert_cmpint (counter, ==, 1); counter = 0; res = gtk_expression_watch_evaluate (watch, &value); g_assert_false (res); gtk_string_filter_set_search (GTK_STRING_FILTER (filter), "bar"); g_assert_cmpint (counter, ==, 0); gtk_filter_list_model_set_filter (filtered, NULL); g_assert_cmpint (counter, ==, 0); res = gtk_expression_watch_evaluate (watch, &value); g_assert_false (res); g_assert_false (G_IS_VALUE (&value)); /* We unwatch on purpose here to make sure it doesn't do bad things. */ gtk_expression_watch_unwatch (watch); gtk_expression_watch_unref (watch); g_assert_cmpint (counter, ==, 0); g_object_unref (filtered); g_object_unref (list); g_object_unref (filter); gtk_expression_unref (expr); } /* Test that property expressions fail to evaluate if the * expression evaluates to an object of the wrong type */ static void test_type_mismatch (void) { GtkFilter *filter; GtkExpression *expr; GValue value = G_VALUE_INIT; gboolean res; filter = gtk_any_filter_new (); expr = gtk_property_expression_new (GTK_TYPE_STRING_FILTER, gtk_constant_expression_new (GTK_TYPE_ANY_FILTER, filter), "search"); res = gtk_expression_evaluate (expr, NULL, &value); g_assert_false (res); g_assert_false (G_IS_VALUE (&value)); gtk_expression_unref (expr); g_object_unref (filter); } /* Some basic tests around 'this' */ static void test_this (void) { GtkFilter *filter; GtkFilter *filter2; GtkExpression *expr; GValue value = G_VALUE_INIT; gboolean res; expr = gtk_property_expression_new (GTK_TYPE_STRING_FILTER, NULL, "search"); filter = gtk_string_filter_new (); gtk_string_filter_set_search (GTK_STRING_FILTER (filter), "word"); filter2 = gtk_string_filter_new (); gtk_string_filter_set_search (GTK_STRING_FILTER (filter2), "sausage"); res = gtk_expression_evaluate (expr, filter, &value); g_assert_true (res); g_assert_cmpstr (g_value_get_string (&value), ==, "word"); g_value_unset (&value); res = gtk_expression_evaluate (expr, filter2, &value); g_assert_true (res); g_assert_cmpstr (g_value_get_string (&value), ==, "sausage"); g_value_unset (&value); gtk_expression_unref (expr); g_object_unref (filter2); g_object_unref (filter); } /* Check that even for static expressions, watches can be created * and destroying the "this" argument does invalidate the * expression. */ static void test_constant_watch_this_destroyed (void) { GtkExpression *expr; GObject *this; guint counter = 0; this = g_object_new (G_TYPE_OBJECT, NULL); expr = gtk_constant_expression_new (G_TYPE_INT, 42); gtk_expression_watch (expr, this, inc_counter, &counter, NULL); g_assert_cmpint (counter, ==, 0); g_clear_object (&this); g_assert_cmpint (counter, ==, 1); gtk_expression_unref (expr); } /* Basic test of gtk_expression_bind */ static void test_bind (void) { GtkFilter *target; GtkFilter *source; GtkExpression *expr; GtkExpressionWatch *watch; GValue value = G_VALUE_INIT; gboolean res; expr = gtk_property_expression_new (GTK_TYPE_STRING_FILTER, NULL, "search"); target = gtk_string_filter_new (); gtk_string_filter_set_search (GTK_STRING_FILTER (target), "word"); g_assert_cmpstr (gtk_string_filter_get_search (GTK_STRING_FILTER (target)), ==, "word"); source = gtk_string_filter_new (); gtk_string_filter_set_search (GTK_STRING_FILTER (source), "sausage"); watch = gtk_expression_bind (expr, target, "search", source); gtk_expression_watch_ref (watch); g_assert_cmpstr (gtk_string_filter_get_search (GTK_STRING_FILTER (target)), ==, "sausage"); gtk_string_filter_set_search (GTK_STRING_FILTER (source), "salad"); g_assert_cmpstr (gtk_string_filter_get_search (GTK_STRING_FILTER (target)), ==, "salad"); res = gtk_expression_watch_evaluate (watch, &value); g_assert_true (res); g_assert_cmpstr (g_value_get_string (&value), ==, "salad"); g_value_unset (&value); g_object_unref (source); g_assert_cmpstr (gtk_string_filter_get_search (GTK_STRING_FILTER (target)), ==, "salad"); res = gtk_expression_watch_evaluate (watch, &value); g_assert_false (res); g_assert_false (G_IS_VALUE (&value)); g_object_unref (target); gtk_expression_watch_unref (watch); } /* Another test of bind, this time we watch ourselves */ static void test_bind_self (void) { GtkFilter *filter; GtkExpression *expr; expr = gtk_property_expression_new (GTK_TYPE_STRING_FILTER, NULL, "ignore-case"); filter = gtk_string_filter_new (); gtk_string_filter_set_search (GTK_STRING_FILTER (filter), "word"); g_assert_cmpstr (gtk_string_filter_get_search (GTK_STRING_FILTER (filter)), ==, "word"); gtk_expression_bind (expr, filter, "search", filter); g_assert_cmpstr (gtk_string_filter_get_search (GTK_STRING_FILTER (filter)), ==, "TRUE"); g_object_unref (filter); } /* Test bind does the right memory management if the target's * dispose() kills the source */ static void test_bind_child (void) { GtkFilter *filter; GtkFilterListModel *child, *target; GtkExpression *expr; expr = gtk_property_expression_new (GTK_TYPE_FILTER_LIST_MODEL, NULL, "filter"); filter = gtk_string_filter_new (); child = gtk_filter_list_model_new_for_type (G_TYPE_OBJECT); gtk_filter_list_model_set_filter (child, filter); target = gtk_filter_list_model_new (G_LIST_MODEL (child), NULL); g_object_unref (child); g_object_unref (filter); gtk_expression_bind (expr, target, "filter", child); g_assert_true (gtk_filter_list_model_get_filter (child) == gtk_filter_list_model_get_filter (target)); filter = gtk_string_filter_new (); gtk_filter_list_model_set_filter (child, filter); g_assert_true (filter == gtk_filter_list_model_get_filter (target)); g_assert_true (gtk_filter_list_model_get_filter (child) == gtk_filter_list_model_get_filter (target)); g_object_unref (filter); g_object_unref (target); } /* Another test of gtk_expression_bind that exercises the subwatch code paths */ static void test_nested_bind (void) { GtkFilter *filter; GtkFilter *filter2; GtkFilter *filter3; GListModel *list; GtkFilterListModel *filtered; GtkExpression *expr; GtkExpression *filter_expr; gboolean res; GValue value = G_VALUE_INIT; filter2 = gtk_string_filter_new (); gtk_string_filter_set_search (GTK_STRING_FILTER (filter2), "sausage"); list = G_LIST_MODEL (g_list_store_new (G_TYPE_OBJECT)); filtered = gtk_filter_list_model_new (list, filter2); filter_expr = gtk_property_expression_new (GTK_TYPE_FILTER_LIST_MODEL, gtk_object_expression_new (G_OBJECT (filtered)), "filter"); expr = gtk_property_expression_new (GTK_TYPE_STRING_FILTER, gtk_expression_ref (filter_expr), "search"); filter = gtk_string_filter_new (); gtk_string_filter_set_search (GTK_STRING_FILTER (filter), "word"); g_assert_cmpstr (gtk_string_filter_get_search (GTK_STRING_FILTER (filter)), ==, "word"); gtk_expression_bind (gtk_expression_ref (expr), filter, "search", NULL); gtk_string_filter_set_search (GTK_STRING_FILTER (filter2), "sausage"); g_assert_cmpstr (gtk_string_filter_get_search (GTK_STRING_FILTER (filter)), ==, "sausage"); filter3 = gtk_string_filter_new (); gtk_string_filter_set_search (GTK_STRING_FILTER (filter3), "banana"); gtk_filter_list_model_set_filter (filtered, filter3); /* check that the expressions evaluate correctly */ res = gtk_expression_evaluate (filter_expr, NULL, &value); g_assert_true (res); g_assert (g_value_get_object (&value) == filter3); g_value_unset (&value); res = gtk_expression_evaluate (expr, NULL, &value); g_assert_true (res); g_assert_cmpstr (g_value_get_string (&value), ==, "banana"); g_value_unset (&value); /* and the bind too */ g_assert_cmpstr (gtk_string_filter_get_search (GTK_STRING_FILTER (filter)), ==, "banana"); g_object_unref (filter); g_object_unref (filter2); g_object_unref (filter3); g_object_unref (filtered); g_object_unref (list); gtk_expression_unref (expr); gtk_expression_unref (filter_expr); } static char * some_cb (gpointer this, const char *search, gboolean ignore_case, gpointer data) { if (!search) return NULL; if (ignore_case) return g_utf8_strdown (search, -1); else return g_strdup (search); } /* Test that things work as expected when the same object is used multiple times in an * expression or its subexpressions. */ static void test_double_bind (void) { GtkStringFilter *filter1; GtkStringFilter *filter2; GtkExpression *expr; GtkExpression *filter_expr; GtkExpression *params[2]; filter1 = GTK_STRING_FILTER (gtk_string_filter_new ()); filter2 = GTK_STRING_FILTER (gtk_string_filter_new ()); filter_expr = gtk_object_expression_new (G_OBJECT (filter1)); params[0] = gtk_property_expression_new (GTK_TYPE_STRING_FILTER, gtk_expression_ref (filter_expr), "search"); params[1] = gtk_property_expression_new (GTK_TYPE_STRING_FILTER, gtk_expression_ref (filter_expr), "ignore-case"); expr = gtk_cclosure_expression_new (G_TYPE_STRING, NULL, 2, params, (GCallback)some_cb, NULL, NULL); gtk_expression_bind (gtk_expression_ref (expr), filter2, "search", NULL); gtk_string_filter_set_search (GTK_STRING_FILTER (filter1), "Banana"); g_assert_cmpstr (gtk_string_filter_get_search (GTK_STRING_FILTER (filter2)), ==, "banana"); gtk_string_filter_set_ignore_case (GTK_STRING_FILTER (filter1), FALSE); g_assert_cmpstr (gtk_string_filter_get_search (GTK_STRING_FILTER (filter2)), ==, "Banana"); gtk_expression_unref (expr); gtk_expression_unref (filter_expr); g_object_unref (filter1); g_object_unref (filter2); } /* Test that having multiple binds on the same object works. */ static void test_binds (void) { GtkStringFilter *filter1; GtkStringFilter *filter2; GtkStringFilter *filter3; GtkExpression *expr; GtkExpression *expr2; GtkExpression *filter1_expr; GtkExpression *filter2_expr; GtkExpression *params[2]; filter1 = GTK_STRING_FILTER (gtk_string_filter_new ()); filter2 = GTK_STRING_FILTER (gtk_string_filter_new ()); filter3 = GTK_STRING_FILTER (gtk_string_filter_new ()); filter1_expr = gtk_object_expression_new (G_OBJECT (filter1)); filter2_expr = gtk_object_expression_new (G_OBJECT (filter2)); params[0] = gtk_property_expression_new (GTK_TYPE_STRING_FILTER, gtk_expression_ref (filter1_expr), "search"); params[1] = gtk_property_expression_new (GTK_TYPE_STRING_FILTER, gtk_expression_ref (filter2_expr), "ignore-case"); expr = gtk_cclosure_expression_new (G_TYPE_STRING, NULL, 2, params, (GCallback)some_cb, NULL, NULL); expr2 = gtk_property_expression_new (GTK_TYPE_STRING_FILTER, gtk_expression_ref (filter2_expr), "ignore-case"); gtk_expression_bind (gtk_expression_ref (expr), filter3, "search", NULL); gtk_expression_bind (gtk_expression_ref (expr2), filter3, "ignore-case", NULL); gtk_string_filter_set_search (GTK_STRING_FILTER (filter1), "Banana"); g_assert_cmpstr (gtk_string_filter_get_search (GTK_STRING_FILTER (filter3)), ==, "banana"); g_assert_true (gtk_string_filter_get_ignore_case (GTK_STRING_FILTER (filter3))); gtk_string_filter_set_ignore_case (GTK_STRING_FILTER (filter2), FALSE); g_assert_cmpstr (gtk_string_filter_get_search (GTK_STRING_FILTER (filter3)), ==, "Banana"); g_assert_false (gtk_string_filter_get_ignore_case (GTK_STRING_FILTER (filter3))); /* invalidate the first bind */ g_object_unref (filter1); gtk_string_filter_set_ignore_case (GTK_STRING_FILTER (filter2), TRUE); g_assert_cmpstr (gtk_string_filter_get_search (GTK_STRING_FILTER (filter3)), ==, "Banana"); g_assert_true (gtk_string_filter_get_ignore_case (GTK_STRING_FILTER (filter3))); gtk_expression_unref (expr); gtk_expression_unref (expr2); gtk_expression_unref (filter1_expr); gtk_expression_unref (filter2_expr); g_object_unref (filter2); g_object_unref (filter3); } /* test that binds work ok with object expressions */ static void test_bind_object (void) { GtkFilter *filter; GListStore *store; GtkFilterListModel *model; GtkExpression *expr; filter = gtk_string_filter_new (); store = g_list_store_new (G_TYPE_OBJECT); model = gtk_filter_list_model_new (G_LIST_MODEL (store), NULL); expr = gtk_object_expression_new (G_OBJECT (filter)); gtk_expression_bind (gtk_expression_ref (expr), model, "filter", NULL); g_assert_true (gtk_filter_list_model_get_filter (model) == filter); g_object_unref (filter); g_assert_true (gtk_filter_list_model_get_filter (model) == filter); gtk_expression_unref (expr); g_object_unref (model); g_object_unref (store); } int main (int argc, char *argv[]) { gtk_test_init (&argc, &argv, NULL); setlocale (LC_ALL, "C"); g_test_add_func ("/expression/property", test_property); g_test_add_func ("/expression/closure", test_closure); g_test_add_func ("/expression/constant", test_constant); g_test_add_func ("/expression/constant-watch-this-destroyed", test_constant_watch_this_destroyed); g_test_add_func ("/expression/object", test_object); g_test_add_func ("/expression/nested", test_nested); g_test_add_func ("/expression/nested-this-destroyed", test_nested_this_destroyed); g_test_add_func ("/expression/type-mismatch", test_type_mismatch); g_test_add_func ("/expression/this", test_this); g_test_add_func ("/expression/bind", test_bind); g_test_add_func ("/expression/bind-self", test_bind_self); g_test_add_func ("/expression/bind-child", test_bind_child); g_test_add_func ("/expression/nested-bind", test_nested_bind); g_test_add_func ("/expression/double-bind", test_double_bind); g_test_add_func ("/expression/binds", test_binds); g_test_add_func ("/expression/bind-object", test_bind_object); return g_test_run (); }