From: Florian Forster Date: Mon, 19 Nov 2012 21:37:18 +0000 (+0100) Subject: src/utils_vl_lookup.[ch]: Support selecting values by regex. X-Git-Tag: collectd-5.3.0~70 X-Git-Url: https://git.tokkee.org/?a=commitdiff_plain;h=16202999622d778521903ca2feef7d55c5b1b5b3;p=collectd.git src/utils_vl_lookup.[ch]: Support selecting values by regex. The name generated by the Aggregation plugin currently includes the regular expression, which is suboptimal. We need to come up with something clever there I guess. --- diff --git a/src/aggregation.c b/src/aggregation.c index db33c177..7ca26ca4 100644 --- a/src/aggregation.c +++ b/src/aggregation.c @@ -34,9 +34,12 @@ #include +#define AGG_MATCHES_ALL(str) (strcmp ("/.*/", str) == 0) + struct aggregation_s /* {{{ */ { identifier_t ident; + unsigned int group_by; _Bool calc_num; _Bool calc_sum; @@ -135,17 +138,17 @@ static agg_instance_t *agg_instance_create (data_set_t const *ds, /* {{{ */ inst->ds_type = ds->ds[0].type; -#define COPY_FIELD(fld) do { \ - sstrncpy (inst->ident.fld, \ - LU_IS_ANY (agg->ident.fld) ? vl->fld : agg->ident.fld, \ - sizeof (inst->ident.fld)); \ +#define COPY_FIELD(field, group_mask) do { \ + sstrncpy (inst->ident.field, \ + (agg->group_by & group_mask) ? vl->field : agg->ident.field, \ + sizeof (inst->ident.field)); \ } while (0) - COPY_FIELD (host); - COPY_FIELD (plugin); - COPY_FIELD (plugin_instance); - COPY_FIELD (type); - COPY_FIELD (type_instance); + COPY_FIELD (host, LU_GROUP_BY_HOST); + COPY_FIELD (plugin, LU_GROUP_BY_PLUGIN); + COPY_FIELD (plugin_instance, LU_GROUP_BY_PLUGIN_INSTANCE); + COPY_FIELD (type, /* group_mask = */ 0); + COPY_FIELD (type_instance, LU_GROUP_BY_TYPE_INSTANCE); #undef COPY_FIELD @@ -291,23 +294,23 @@ static int agg_instance_read (agg_instance_t *inst, cdtime_t t) /* {{{ */ } meta_data_add_boolean (vl.meta, "aggregation:created", 1); - if (LU_IS_ALL (inst->ident.host)) + if (AGG_MATCHES_ALL (inst->ident.host)) sstrncpy (vl.host, "global", sizeof (vl.host)); else sstrncpy (vl.host, inst->ident.host, sizeof (vl.host)); sstrncpy (vl.plugin, "aggregation", sizeof (vl.plugin)); - if (LU_IS_ALL (inst->ident.plugin)) + if (AGG_MATCHES_ALL (inst->ident.plugin)) { - if (LU_IS_ALL (inst->ident.plugin_instance)) + if (AGG_MATCHES_ALL (inst->ident.plugin_instance)) sstrncpy (pi_prefix, "", sizeof (pi_prefix)); else sstrncpy (pi_prefix, inst->ident.plugin_instance, sizeof (pi_prefix)); } else { - if (LU_IS_ALL (inst->ident.plugin_instance)) + if (AGG_MATCHES_ALL (inst->ident.plugin_instance)) sstrncpy (pi_prefix, inst->ident.plugin, sizeof (pi_prefix)); else ssnprintf (pi_prefix, sizeof (pi_prefix), @@ -316,7 +319,7 @@ static int agg_instance_read (agg_instance_t *inst, cdtime_t t) /* {{{ */ sstrncpy (vl.type, inst->ident.type, sizeof (vl.type)); - if (!LU_IS_ALL (inst->ident.type_instance)) + if (!AGG_MATCHES_ALL (inst->ident.type_instance)) sstrncpy (vl.type_instance, inst->ident.type_instance, sizeof (vl.type_instance)); @@ -424,14 +427,13 @@ static int agg_config_handle_group_by (oconfig_item_t const *ci, /* {{{ */ value = ci->values[i].value.string; if (strcasecmp ("Host", value) == 0) - sstrncpy (agg->ident.host, LU_ANY, sizeof (agg->ident.host)); + agg->group_by |= LU_GROUP_BY_HOST; else if (strcasecmp ("Plugin", value) == 0) - sstrncpy (agg->ident.plugin, LU_ANY, sizeof (agg->ident.plugin)); + agg->group_by |= LU_GROUP_BY_PLUGIN; else if (strcasecmp ("PluginInstance", value) == 0) - sstrncpy (agg->ident.plugin_instance, LU_ANY, - sizeof (agg->ident.plugin_instance)); + agg->group_by |= LU_GROUP_BY_PLUGIN_INSTANCE; else if (strcasecmp ("TypeInstance", value) == 0) - sstrncpy (agg->ident.type_instance, LU_ANY, sizeof (agg->ident.type_instance)); + agg->group_by |= LU_GROUP_BY_TYPE_INSTANCE; else if (strcasecmp ("Type", value) == 0) ERROR ("aggregation plugin: Grouping by type is not supported."); else @@ -457,12 +459,12 @@ static int agg_config_aggregation (oconfig_item_t *ci) /* {{{ */ } memset (agg, 0, sizeof (*agg)); - sstrncpy (agg->ident.host, LU_ALL, sizeof (agg->ident.host)); - sstrncpy (agg->ident.plugin, LU_ALL, sizeof (agg->ident.plugin)); - sstrncpy (agg->ident.plugin_instance, LU_ALL, + sstrncpy (agg->ident.host, "/.*/", sizeof (agg->ident.host)); + sstrncpy (agg->ident.plugin, "/.*/", sizeof (agg->ident.plugin)); + sstrncpy (agg->ident.plugin_instance, "/.*/", sizeof (agg->ident.plugin_instance)); - sstrncpy (agg->ident.type, LU_ALL, sizeof (agg->ident.type)); - sstrncpy (agg->ident.type_instance, LU_ALL, + sstrncpy (agg->ident.type, "/.*/", sizeof (agg->ident.type)); + sstrncpy (agg->ident.type_instance, "/.*/", sizeof (agg->ident.type_instance)); for (i = 0; i < ci->children_num; i++) @@ -505,7 +507,7 @@ static int agg_config_aggregation (oconfig_item_t *ci) /* {{{ */ /* Sanity checking */ is_valid = 1; - if (LU_IS_ALL (agg->ident.type)) /* {{{ */ + if (strcmp ("/.*/", agg->ident.type) == 0) /* {{{ */ { ERROR ("aggregation plugin: It appears you did not specify the required " "\"Type\" option in this aggregation. " @@ -518,15 +520,15 @@ static int agg_config_aggregation (oconfig_item_t *ci) /* {{{ */ else if (strchr (agg->ident.type, '/') != NULL) { ERROR ("aggregation plugin: The \"Type\" may not contain the '/' " - "character. Especially, it may not be a wildcard. The current " + "character. Especially, it may not be a regex. The current " "value is \"%s\".", agg->ident.type); is_valid = 0; } /* }}} */ - if (!LU_IS_ALL (agg->ident.host) /* {{{ */ - && !LU_IS_ALL (agg->ident.plugin) - && !LU_IS_ALL (agg->ident.plugin_instance) - && !LU_IS_ALL (agg->ident.type_instance)) + if (!AGG_MATCHES_ALL (agg->ident.host) /* {{{ */ + && !AGG_MATCHES_ALL (agg->ident.plugin) + && !AGG_MATCHES_ALL (agg->ident.plugin_instance) + && !AGG_MATCHES_ALL (agg->ident.type_instance)) { ERROR ("aggregation plugin: An aggregation must contain at least one " "wildcard. This is achieved by leaving at least one of the \"Host\", " @@ -557,7 +559,7 @@ static int agg_config_aggregation (oconfig_item_t *ci) /* {{{ */ return (-1); } /* }}} */ - status = lookup_add (lookup, &agg->ident, agg); + status = lookup_add (lookup, &agg->ident, agg->group_by, agg); if (status != 0) { ERROR ("aggregation plugin: lookup_add failed with status %i.", status); diff --git a/src/collectd.conf.pod b/src/collectd.conf.pod index aef92267..1c8b7a4f 100644 --- a/src/collectd.conf.pod +++ b/src/collectd.conf.pod @@ -297,6 +297,12 @@ aggregations. The following options are valid inside B blocks: Selects the value lists to be added to this aggregation. B must be a valid data set name, see L for details. +If the string starts with and ends with a slash (C), the string is +interpreted as a I. The regex flavor used are POSIX +extended regular expressions as described in L. Example usage: + + Host "/^db[0-9]\\.example\\.com$/" + =item B B|B|B|B Group valued by the specified field. The B option may be repeated to diff --git a/src/utils_vl_lookup.c b/src/utils_vl_lookup.c index 2dada247..722c4523 100644 --- a/src/utils_vl_lookup.c +++ b/src/utils_vl_lookup.c @@ -25,6 +25,9 @@ **/ #include "collectd.h" + +#include + #include "common.h" #include "utils_vl_lookup.h" #include "utils_avltree.h" @@ -41,6 +44,26 @@ /* * Types */ +struct part_match_s +{ + char str[DATA_MAX_NAME_LEN]; + regex_t regex; + _Bool is_regex; +}; +typedef struct part_match_s part_match_t; + +struct identifier_match_s +{ + part_match_t host; + part_match_t plugin; + part_match_t plugin_instance; + part_match_t type; + part_match_t type_instance; + + unsigned int group_by; +}; +typedef struct identifier_match_s identifier_match_t; + struct lookup_s { c_avl_tree_t *by_type_tree; @@ -64,7 +87,7 @@ struct user_obj_s struct user_class_s { void *user_class; - identifier_t ident; + identifier_match_t match; user_obj_t *user_obj_list; /* list of user_obj */ }; typedef struct user_class_s user_class_t; @@ -87,6 +110,87 @@ typedef struct by_type_entry_s by_type_entry_t; /* * Private functions */ +static _Bool lu_part_matches (part_match_t const *match, /* {{{ */ + char const *str) +{ + if (match->is_regex) + { + /* Short cut popular catch-all regex. */ + if (strcmp (".*", match->str) == 0) + return (1); + + int status = regexec (&match->regex, str, + /* nmatch = */ 0, /* pmatch = */ NULL, + /* flags = */ 0); + if (status == 0) + return (1); + else + return (0); + } + else if (strcmp (match->str, str) == 0) + return (1); + else + return (0); +} /* }}} _Bool lu_part_matches */ + +static int lu_copy_ident_to_match_part (part_match_t *match_part, /* {{{ */ + char const *ident_part) +{ + size_t len = strlen (ident_part); + int status; + + if ((len < 3) || (ident_part[0] != '/') || (ident_part[len - 1] != '/')) + { + sstrncpy (match_part->str, ident_part, sizeof (match_part->str)); + match_part->is_regex = 0; + return (0); + } + + /* Copy string without the leading slash. */ + sstrncpy (match_part->str, ident_part + 1, sizeof (match_part->str)); + assert (sizeof (match_part->str) > len); + /* strip trailing slash */ + match_part->str[len - 2] = 0; + + status = regcomp (&match_part->regex, match_part->str, + /* flags = */ REG_EXTENDED); + if (status != 0) + { + char errbuf[1024]; + regerror (status, &match_part->regex, errbuf, sizeof (errbuf)); + ERROR ("utils_vl_lookup: Compiling regular expression \"%s\" failed: %s", + match_part->str, errbuf); + return (EINVAL); + } + match_part->is_regex = 1; + + return (0); +} /* }}} int lu_copy_ident_to_match_part */ + +static int lu_copy_ident_to_match (identifier_match_t *match, /* {{{ */ + identifier_t const *ident, unsigned int group_by) +{ + memset (match, 0, sizeof (*match)); + + match->group_by = group_by; + +#define COPY_FIELD(field) do { \ + int status = lu_copy_ident_to_match_part (&match->field, ident->field); \ + if (status != 0) \ + return (status); \ +} while (0) + + COPY_FIELD (host); + COPY_FIELD (plugin); + COPY_FIELD (plugin_instance); + COPY_FIELD (type); + COPY_FIELD (type_instance); + +#undef COPY_FIELD + + return (0); +} /* }}} int lu_copy_ident_to_match */ + static void *lu_create_user_obj (lookup_t *obj, /* {{{ */ data_set_t const *ds, value_list_t const *vl, user_class_t *user_class) @@ -110,21 +214,21 @@ static void *lu_create_user_obj (lookup_t *obj, /* {{{ */ return (NULL); } - sstrncpy (user_obj->ident.host, - LU_IS_ALL (user_class->ident.host) ? "/all/" : vl->host, - sizeof (user_obj->ident.host)); - sstrncpy (user_obj->ident.plugin, - LU_IS_ALL (user_class->ident.plugin) ? "/all/" : vl->plugin, - sizeof (user_obj->ident.plugin)); - sstrncpy (user_obj->ident.plugin_instance, - LU_IS_ALL (user_class->ident.plugin_instance) ? "/all/" : vl->plugin_instance, - sizeof (user_obj->ident.plugin_instance)); - sstrncpy (user_obj->ident.type, - LU_IS_ALL (user_class->ident.type) ? "/all/" : vl->type, - sizeof (user_obj->ident.type)); - sstrncpy (user_obj->ident.type_instance, - LU_IS_ALL (user_class->ident.type_instance) ? "/all/" : vl->type_instance, - sizeof (user_obj->ident.type_instance)); +#define COPY_FIELD(field, group_mask) do { \ + if (user_class->match.field.is_regex \ + && ((user_class->match.group_by & group_mask) == 0)) \ + sstrncpy (user_obj->ident.field, "/.*/", sizeof (user_obj->ident.field)); \ + else \ + sstrncpy (user_obj->ident.field, vl->field, sizeof (user_obj->ident.field)); \ +} while (0) + + COPY_FIELD (host, LU_GROUP_BY_HOST); + COPY_FIELD (plugin, LU_GROUP_BY_PLUGIN); + COPY_FIELD (plugin_instance, LU_GROUP_BY_PLUGIN_INSTANCE); + COPY_FIELD (type, 0); + COPY_FIELD (type_instance, LU_GROUP_BY_TYPE_INSTANCE); + +#undef COPY_FIELD if (user_class->user_obj_list == NULL) { @@ -150,14 +254,21 @@ static user_obj_t *lu_find_user_obj (user_class_t *user_class, /* {{{ */ ptr != NULL; ptr = ptr->next) { - if (!LU_IS_ALL (ptr->ident.host) - && (strcmp (ptr->ident.host, vl->host) != 0)) + if (user_class->match.host.is_regex + && (user_class->match.group_by & LU_GROUP_BY_HOST) + && (strcmp (vl->host, ptr->ident.host) != 0)) + continue; + if (user_class->match.plugin.is_regex + && (user_class->match.group_by & LU_GROUP_BY_PLUGIN) + && (strcmp (vl->plugin, ptr->ident.plugin) != 0)) continue; - if (!LU_IS_ALL (ptr->ident.plugin_instance) - && (strcmp (ptr->ident.plugin_instance, vl->plugin_instance) != 0)) + if (user_class->match.plugin_instance.is_regex + && (user_class->match.group_by & LU_GROUP_BY_PLUGIN_INSTANCE) + && (strcmp (vl->plugin_instance, ptr->ident.plugin_instance) != 0)) continue; - if (!LU_IS_ALL (ptr->ident.type_instance) - && (strcmp (ptr->ident.type_instance, vl->type_instance) != 0)) + if (user_class->match.type_instance.is_regex + && (user_class->match.group_by & LU_GROUP_BY_TYPE_INSTANCE) + && (strcmp (vl->type_instance, ptr->ident.type_instance) != 0)) continue; return (ptr); @@ -173,21 +284,14 @@ static int lu_handle_user_class (lookup_t *obj, /* {{{ */ user_obj_t *user_obj; int status; - assert (strcmp (vl->type, user_class->ident.type) == 0); - assert (LU_IS_WILDCARD (user_class->ident.plugin) - || (strcmp (vl->plugin, user_class->ident.plugin) == 0)); + assert (strcmp (vl->type, user_class->match.type.str) == 0); + assert (user_class->match.plugin.is_regex + || (strcmp (vl->plugin, user_class->match.plugin.str)) == 0); - /* When we get here, type and plugin already match the user class. Now check - * the rest of the fields. */ - if (!LU_IS_WILDCARD (user_class->ident.type_instance) - && (strcmp (vl->type_instance, user_class->ident.type_instance) != 0)) - return (1); - if (!LU_IS_WILDCARD (user_class->ident.plugin_instance) - && (strcmp (vl->plugin_instance, - user_class->ident.plugin_instance) != 0)) - return (1); - if (!LU_IS_WILDCARD (user_class->ident.host) - && (strcmp (vl->host, user_class->ident.host) != 0)) + if (!lu_part_matches (&user_class->match.type_instance, vl->type_instance) + || !lu_part_matches (&user_class->match.plugin_instance, vl->plugin_instance) + || !lu_part_matches (&user_class->match.plugin, vl->plugin) + || !lu_part_matches (&user_class->match.host, vl->host)) return (1); user_obj = lu_find_user_obj (user_class, vl); @@ -292,14 +396,15 @@ static by_type_entry_t *lu_search_by_type (lookup_t *obj, /* {{{ */ } /* }}} by_type_entry_t *lu_search_by_type */ static int lu_add_by_plugin (by_type_entry_t *by_type, /* {{{ */ - identifier_t const *ident, user_class_list_t *user_class_list) + user_class_list_t *user_class_list) { user_class_list_t *ptr = NULL; + identifier_match_t const *match = &user_class_list->entry.match; /* Lookup user_class_list from the per-plugin structure. If this is the first * user_class to be added, the blocks return immediately. Otherwise they will * set "ptr" to non-NULL. */ - if (LU_IS_WILDCARD (ident->plugin)) + if (match->plugin.is_regex) { if (by_type->wildcard_plugin_list == NULL) { @@ -314,11 +419,11 @@ static int lu_add_by_plugin (by_type_entry_t *by_type, /* {{{ */ int status; status = c_avl_get (by_type->by_plugin_tree, - ident->plugin, (void *) &ptr); + match->plugin.str, (void *) &ptr); if (status != 0) /* plugin not yet in tree */ { - char *plugin_copy = strdup (ident->plugin); + char *plugin_copy = strdup (match->plugin.str); if (plugin_copy == NULL) { @@ -478,7 +583,7 @@ void lookup_destroy (lookup_t *obj) /* {{{ */ } /* }}} void lookup_destroy */ int lookup_add (lookup_t *obj, /* {{{ */ - identifier_t const *ident, void *user_class) + identifier_t const *ident, unsigned int group_by, void *user_class) { by_type_entry_t *by_type = NULL; user_class_list_t *user_class_obj; @@ -495,11 +600,11 @@ int lookup_add (lookup_t *obj, /* {{{ */ } memset (user_class_obj, 0, sizeof (*user_class_obj)); user_class_obj->entry.user_class = user_class; - memmove (&user_class_obj->entry.ident, ident, sizeof (*ident)); + lu_copy_ident_to_match (&user_class_obj->entry.match, ident, group_by); user_class_obj->entry.user_obj_list = NULL; user_class_obj->next = NULL; - return (lu_add_by_plugin (by_type, ident, user_class_obj)); + return (lu_add_by_plugin (by_type, user_class_obj)); } /* }}} int lookup_add */ /* returns the number of successful calls to the callback function */ diff --git a/src/utils_vl_lookup.h b/src/utils_vl_lookup.h index c006fc7f..31787f53 100644 --- a/src/utils_vl_lookup.h +++ b/src/utils_vl_lookup.h @@ -63,12 +63,11 @@ struct identifier_s }; typedef struct identifier_s identifier_t; -#define LU_ANY "/any/" -#define LU_ALL "/all/" - -#define LU_IS_ANY(str) (strcmp (str, LU_ANY) == 0) -#define LU_IS_ALL(str) (strcmp (str, LU_ALL) == 0) -#define LU_IS_WILDCARD(str) (LU_IS_ANY(str) || LU_IS_ALL(str)) +#define LU_GROUP_BY_HOST 0x01 +#define LU_GROUP_BY_PLUGIN 0x02 +#define LU_GROUP_BY_PLUGIN_INSTANCE 0x04 +/* #define LU_GROUP_BY_TYPE 0x00 */ +#define LU_GROUP_BY_TYPE_INSTANCE 0x10 /* * Functions @@ -81,7 +80,7 @@ lookup_t *lookup_create (lookup_class_callback_t, void lookup_destroy (lookup_t *obj); int lookup_add (lookup_t *obj, - identifier_t const *ident, void *user_class); + identifier_t const *ident, unsigned int group_by, void *user_class); /* TODO(octo): Pass lookup_obj_callback_t to lookup_search()? */ int lookup_search (lookup_t *obj, diff --git a/src/utils_vl_lookup_test.c b/src/utils_vl_lookup_test.c index 6265b321..bbb3a67f 100644 --- a/src/utils_vl_lookup_test.c +++ b/src/utils_vl_lookup_test.c @@ -82,7 +82,8 @@ static void *lookup_class_callback (data_set_t const *ds, static void checked_lookup_add (lookup_t *obj, /* {{{ */ char const *host, char const *plugin, char const *plugin_instance, - char const *type, char const *type_instance) + char const *type, char const *type_instance, + unsigned int group_by) { identifier_t ident; void *user_class; @@ -98,7 +99,7 @@ static void checked_lookup_add (lookup_t *obj, /* {{{ */ user_class = malloc (sizeof (ident)); memmove (user_class, &ident, sizeof (ident)); - status = lookup_add (obj, &ident, user_class); + status = lookup_add (obj, &ident, group_by, user_class); assert (status == 0); } /* }}} void test_add */ @@ -143,7 +144,7 @@ static void testcase0 (void) { lookup_t *obj = checked_lookup_create (); - checked_lookup_add (obj, "/any/", "test", "", "test", "/all/"); + checked_lookup_add (obj, "/.*/", "test", "", "test", "/.*/", LU_GROUP_BY_HOST); checked_lookup_search (obj, "host0", "test", "", "test", "0", /* expect new = */ 1); checked_lookup_search (obj, "host0", "test", "", "test", "1", @@ -160,7 +161,7 @@ static void testcase1 (void) { lookup_t *obj = checked_lookup_create (); - checked_lookup_add (obj, "/any/", "/all/", "/all/", "test", "/all/"); + checked_lookup_add (obj, "/.*/", "/.*/", "/.*/", "test", "/.*/", LU_GROUP_BY_HOST); checked_lookup_search (obj, "host0", "plugin0", "", "test", "0", /* expect new = */ 1); checked_lookup_search (obj, "host0", "plugin0", "", "test", "1", @@ -186,8 +187,8 @@ static void testcase2 (void) lookup_t *obj = checked_lookup_create (); int status; - checked_lookup_add (obj, "/any/", "plugin0", "", "test", "/all/"); - checked_lookup_add (obj, "/any/", "/all/", "", "test", "ti0"); + checked_lookup_add (obj, "/.*/", "plugin0", "", "test", "/.*/", LU_GROUP_BY_HOST); + checked_lookup_add (obj, "/.*/", "/.*/", "", "test", "ti0", LU_GROUP_BY_HOST); status = checked_lookup_search (obj, "host0", "plugin1", "", "test", "", /* expect new = */ 0); @@ -205,10 +206,39 @@ static void testcase2 (void) lookup_destroy (obj); } +static void testcase3 (void) +{ + lookup_t *obj = checked_lookup_create (); + + checked_lookup_add (obj, "/^db[0-9]\\./", "cpu", "/.*/", "cpu", "/.*/", + LU_GROUP_BY_TYPE_INSTANCE); + checked_lookup_search (obj, "db0.example.com", "cpu", "0", "cpu", "user", + /* expect new = */ 1); + checked_lookup_search (obj, "db0.example.com", "cpu", "0", "cpu", "idle", + /* expect new = */ 1); + checked_lookup_search (obj, "db0.example.com", "cpu", "1", "cpu", "user", + /* expect new = */ 0); + checked_lookup_search (obj, "db0.example.com", "cpu", "1", "cpu", "idle", + /* expect new = */ 0); + checked_lookup_search (obj, "app0.example.com", "cpu", "0", "cpu", "user", + /* expect new = */ 0); + checked_lookup_search (obj, "app0.example.com", "cpu", "0", "cpu", "idle", + /* expect new = */ 0); + checked_lookup_search (obj, "db1.example.com", "cpu", "0", "cpu", "user", + /* expect new = */ 0); + checked_lookup_search (obj, "db1.example.com", "cpu", "0", "cpu", "idle", + /* expect new = */ 0); + checked_lookup_search (obj, "db1.example.com", "cpu", "0", "cpu", "system", + /* expect new = */ 1); + + lookup_destroy (obj); +} + int main (int argc, char **argv) /* {{{ */ { testcase0 (); testcase1 (); testcase2 (); + testcase3 (); return (EXIT_SUCCESS); } /* }}} int main */