Make sorcery details opaque and add extended fields.
authorJoshua Colp <jcolp@digium.com>
Sat, 22 Jun 2013 14:26:25 +0000 (14:26 +0000)
committerJoshua Colp <jcolp@digium.com>
Sat, 22 Jun 2013 14:26:25 +0000 (14:26 +0000)
Sorcery specific object information is now opaque and allocated with the object.
This means that modules do not need to be recompiled if the sorcery specific part
is changed. It also means that sorcery can store additional information on objects
and ensure it is freed or the reference count decreased when the object goes away.

To facilitate the above a generic sorcery allocator function has been added which
also ensures that allocated objects do not have a lock.

Extended fields have been added thanks to all of the above which allows specific fields
to be marked as extended, and thus simply stored as-is within the object. Type safety
is *NOT* enforced on these fields. A consumer of them has to query and ultimately perform
their own safety check. What does this mean? Extra modules can extend already defined
structures without having to modify them.

Tests have also been included to verify extended field functionality.

Review: https://reviewboard.asterisk.org/r/2585/

git-svn-id: https://origsvn.digium.com/svn/asterisk/trunk@392586 65c4cc65-6c06-0410-ace0-fbb531ad65f3

include/asterisk/sorcery.h
main/sorcery.c
res/res_sip/config_auth.c
res/res_sip/config_domain_aliases.c
res/res_sip/config_transport.c
res/res_sip/location.c
res/res_sip/sip_configuration.c
res/res_sip/sip_options.c
tests/test_sorcery.c

index ae97da5..464a83c 100644 (file)
@@ -247,13 +247,13 @@ struct ast_sorcery_observer {
        void (*loaded)(const char *object_type);
 };
 
+/*! \brief Opaque structure for internal sorcery object */
+struct ast_sorcery_object;
+
 /*! \brief Structure which contains details about a sorcery object */
 struct ast_sorcery_object_details {
-       /*! \brief Unique identifier of this object */
-       char id[AST_UUID_STR_LEN];
-
-       /*! \brief Type of object */
-       char type[MAX_OBJECT_TYPE];
+       /*! \brief Pointer to internal sorcery object information */
+       struct ast_sorcery_object *object;
 };
 
 /*! \brief Macro which must be used at the beginning of each sorcery capable object */
@@ -529,6 +529,17 @@ int ast_sorcery_objectset_apply(const struct ast_sorcery *sorcery, void *object,
 int ast_sorcery_changeset_create(const struct ast_variable *original, const struct ast_variable *modified, struct ast_variable **changes);
 
 /*!
+ * \brief Allocate a generic sorcery capable object
+ *
+ * \param size Size of the object
+ * \param destructor Optional destructor function
+ *
+ * \retval non-NULL success
+ * \retval NULL failure
+ */
+void *ast_sorcery_generic_alloc(size_t size, ao2_destructor_fn destructor);
+
+/*!
  * \brief Allocate an object
  *
  * \param sorcery Pointer to a sorcery structure
@@ -695,6 +706,34 @@ const char *ast_sorcery_object_get_id(const void *object);
  */
 const char *ast_sorcery_object_get_type(const void *object);
 
+/*!
+ * \brief Get an extended field value from a sorcery object
+ *
+ * \param object Pointer to a sorcery object
+ * \param name Name of the extended field value
+ *
+ * \retval non-NULL if found
+ * \retval NULL if not found
+ *
+ * \note The returned string does NOT need to be freed and is guaranteed to remain valid for the lifetime of the object
+ */
+const char *ast_sorcery_object_get_extended(const void *object, const char *name);
+
+/*!
+ * \brief Set an extended field value on a sorcery object
+ *
+ * \param object Pointer to a sorcery object
+ * \param name Name of the extended field
+ * \param value Value of the extended field
+ *
+ * \retval 0 success
+ * \retval -1 failure
+ *
+ * \note The field name MUST begin with '@' to indicate it is an extended field.
+ * \note If the extended field already exists it will be overwritten with the new value.
+ */
+int ast_sorcery_object_set_extended(const void *object, const char *name, const char *value);
+
 #if defined(__cplusplus) || defined(c_plusplus)
 }
 #endif
index 99e4c55..cf4cd02 100644 (file)
@@ -58,6 +58,21 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
 /*! \brief Thread pool for observers */
 static struct ast_threadpool *threadpool;
 
+/*! \brief Structure for internal sorcery object information */
+struct ast_sorcery_object {
+       /*! \brief Unique identifier of this object */
+       char id[AST_UUID_STR_LEN];
+
+       /*! \brief Type of object */
+       char type[MAX_OBJECT_TYPE];
+
+       /*! \brief Optional object destructor */
+       ao2_destructor_fn destructor;
+
+       /*! \brief Extended object fields */
+       struct ast_variable *extended;
+};
+
 /*! \brief Structure for registered object type */
 struct ast_sorcery_object_type {
        /*! \brief Unique name of the object type */
@@ -525,6 +540,24 @@ int __ast_sorcery_apply_default(struct ast_sorcery *sorcery, const char *type, c
        return sorcery_apply_wizard_mapping(sorcery, type, module, name, data, 0);
 }
 
+static int sorcery_extended_config_handler(const struct aco_option *opt, struct ast_variable *var, void *obj)
+{
+       return ast_sorcery_object_set_extended(obj, var->name, var->value);
+}
+
+static int sorcery_extended_fields_handler(const void *obj, struct ast_variable **fields)
+{
+       const struct ast_sorcery_object_details *details = obj;
+
+       if (details->object->extended) {
+               *fields = ast_variables_dup(details->object->extended);
+       } else {
+               *fields = NULL;
+       }
+
+       return 0;
+}
+
 int ast_sorcery_object_register(struct ast_sorcery *sorcery, const char *type, aco_type_item_alloc alloc, sorcery_transform_handler transform, sorcery_apply_handler apply)
 {
        RAII_VAR(struct ast_sorcery_object_type *, object_type, ao2_find(sorcery->types, type, OBJ_KEY), ao2_cleanup);
@@ -547,6 +580,10 @@ int ast_sorcery_object_register(struct ast_sorcery *sorcery, const char *type, a
                return -1;
        }
 
+       if (ast_sorcery_object_fields_register(sorcery, type, "^@", sorcery_extended_config_handler, sorcery_extended_fields_handler)) {
+               return -1;
+       }
+
        return 0;
 }
 
@@ -784,7 +821,7 @@ void ast_sorcery_ref(struct ast_sorcery *sorcery)
 struct ast_variable *ast_sorcery_objectset_create(const struct ast_sorcery *sorcery, const void *object)
 {
        const struct ast_sorcery_object_details *details = object;
-       RAII_VAR(struct ast_sorcery_object_type *, object_type, ao2_find(sorcery->types, details->type, OBJ_KEY), ao2_cleanup);
+       RAII_VAR(struct ast_sorcery_object_type *, object_type, ao2_find(sorcery->types, details->object->type, OBJ_KEY), ao2_cleanup);
        struct ao2_iterator i;
        struct ast_sorcery_object_field *object_field;
        struct ast_variable *fields = NULL;
@@ -816,7 +853,7 @@ struct ast_variable *ast_sorcery_objectset_create(const struct ast_sorcery *sorc
                        continue;
                }
 
-               if (!res) {
+               if (!res && tmp) {
                        tmp->next = fields;
                        fields = tmp;
                }
@@ -836,7 +873,7 @@ struct ast_variable *ast_sorcery_objectset_create(const struct ast_sorcery *sorc
 struct ast_json *ast_sorcery_objectset_json_create(const struct ast_sorcery *sorcery, const void *object)
 {
        const struct ast_sorcery_object_details *details = object;
-       RAII_VAR(struct ast_sorcery_object_type *, object_type, ao2_find(sorcery->types, details->type, OBJ_KEY), ao2_cleanup);
+       RAII_VAR(struct ast_sorcery_object_type *, object_type, ao2_find(sorcery->types, details->object->type, OBJ_KEY), ao2_cleanup);
        struct ao2_iterator i;
        struct ast_sorcery_object_field *object_field;
        struct ast_json *json = ast_json_object_create();
@@ -898,7 +935,7 @@ struct ast_json *ast_sorcery_objectset_json_create(const struct ast_sorcery *sor
 int ast_sorcery_objectset_apply(const struct ast_sorcery *sorcery, void *object, struct ast_variable *objectset)
 {
        const struct ast_sorcery_object_details *details = object;
-       RAII_VAR(struct ast_sorcery_object_type *, object_type, ao2_find(sorcery->types, details->type, OBJ_KEY), ao2_cleanup);
+       RAII_VAR(struct ast_sorcery_object_type *, object_type, ao2_find(sorcery->types, details->object->type, OBJ_KEY), ao2_cleanup);
        RAII_VAR(struct ast_variable *, transformed, NULL, ast_variables_destroy);
        struct ast_variable *field;
        int res = 0;
@@ -914,7 +951,7 @@ int ast_sorcery_objectset_apply(const struct ast_sorcery *sorcery, void *object,
        }
 
        for (; field; field = field->next) {
-               if ((res = aco_process_var(&object_type->type, details->id, field, object))) {
+               if ((res = aco_process_var(&object_type->type, details->object->id, field, object))) {
                        break;
                }
        }
@@ -977,6 +1014,32 @@ int ast_sorcery_changeset_create(const struct ast_variable *original, const stru
        return res;
 }
 
+static void sorcery_object_destructor(void *object)
+{
+       struct ast_sorcery_object_details *details = object;
+
+       if (details->object->destructor) {
+               details->object->destructor(object);
+       }
+
+       ast_variables_destroy(details->object->extended);
+}
+
+void *ast_sorcery_generic_alloc(size_t size, ao2_destructor_fn destructor)
+{
+       void *object = ao2_alloc_options(size + sizeof(struct ast_sorcery_object), sorcery_object_destructor, AO2_ALLOC_OPT_LOCK_NOLOCK);
+       struct ast_sorcery_object_details *details = object;
+
+       if (!object) {
+               return NULL;
+       }
+
+       details->object = object + size;
+       details->object->destructor = destructor;
+
+       return object;
+}
+
 void *ast_sorcery_alloc(const struct ast_sorcery *sorcery, const char *type, const char *id)
 {
        RAII_VAR(struct ast_sorcery_object_type *, object_type, ao2_find(sorcery->types, type, OBJ_KEY), ao2_cleanup);
@@ -988,12 +1051,12 @@ void *ast_sorcery_alloc(const struct ast_sorcery *sorcery, const char *type, con
        }
 
        if (ast_strlen_zero(id)) {
-               ast_uuid_generate_str(details->id, sizeof(details->id));
+               ast_uuid_generate_str(details->object->id, sizeof(details->object->id));
        } else {
-               ast_copy_string(details->id, id, sizeof(details->id));
+               ast_copy_string(details->object->id, id, sizeof(details->object->id));
        }
 
-       ast_copy_string(details->type, type, sizeof(details->type));
+       ast_copy_string(details->object->type, type, sizeof(details->object->type));
 
        if (aco_set_defaults(&object_type->type, id, details)) {
                ao2_ref(details, -1);
@@ -1006,8 +1069,8 @@ void *ast_sorcery_alloc(const struct ast_sorcery *sorcery, const char *type, con
 void *ast_sorcery_copy(const struct ast_sorcery *sorcery, const void *object)
 {
        const struct ast_sorcery_object_details *details = object;
-       RAII_VAR(struct ast_sorcery_object_type *, object_type, ao2_find(sorcery->types, details->type, OBJ_KEY), ao2_cleanup);
-       struct ast_sorcery_object_details *copy = ast_sorcery_alloc(sorcery, details->type, details->id);
+       RAII_VAR(struct ast_sorcery_object_type *, object_type, ao2_find(sorcery->types, details->object->type, OBJ_KEY), ao2_cleanup);
+       struct ast_sorcery_object_details *copy = ast_sorcery_alloc(sorcery, details->object->type, details->object->id);
        RAII_VAR(struct ast_variable *, objectset, NULL, ast_variables_destroy);
        int res = 0;
 
@@ -1120,7 +1183,6 @@ void *ast_sorcery_retrieve_by_fields(const struct ast_sorcery *sorcery, const ch
        unsigned int cached = 0;
 
        if (!object_type) {
-               ast_log(LOG_NOTICE, "Can't find object type '%s'\n", type);
                return NULL;
        }
 
@@ -1222,7 +1284,7 @@ static int sorcery_observers_notify_create(void *data)
 int ast_sorcery_create(const struct ast_sorcery *sorcery, void *object)
 {
        const struct ast_sorcery_object_details *details = object;
-       RAII_VAR(struct ast_sorcery_object_type *, object_type, ao2_find(sorcery->types, details->type, OBJ_KEY), ao2_cleanup);
+       RAII_VAR(struct ast_sorcery_object_type *, object_type, ao2_find(sorcery->types, details->object->type, OBJ_KEY), ao2_cleanup);
        RAII_VAR(struct ast_sorcery_object_wizard *, object_wizard, NULL, ao2_cleanup);
        struct sorcery_details sdetails = {
                .sorcery = sorcery,
@@ -1281,7 +1343,7 @@ static int sorcery_wizard_update(void *obj, void *arg, int flags)
 int ast_sorcery_update(const struct ast_sorcery *sorcery, void *object)
 {
        const struct ast_sorcery_object_details *details = object;
-       RAII_VAR(struct ast_sorcery_object_type *, object_type, ao2_find(sorcery->types, details->type, OBJ_KEY), ao2_cleanup);
+       RAII_VAR(struct ast_sorcery_object_type *, object_type, ao2_find(sorcery->types, details->object->type, OBJ_KEY), ao2_cleanup);
        RAII_VAR(struct ast_sorcery_object_wizard *, object_wizard, NULL, ao2_cleanup);
        struct sorcery_details sdetails = {
                .sorcery = sorcery,
@@ -1340,7 +1402,7 @@ static int sorcery_wizard_delete(void *obj, void *arg, int flags)
 int ast_sorcery_delete(const struct ast_sorcery *sorcery, void *object)
 {
        const struct ast_sorcery_object_details *details = object;
-       RAII_VAR(struct ast_sorcery_object_type *, object_type, ao2_find(sorcery->types, details->type, OBJ_KEY), ao2_cleanup);
+       RAII_VAR(struct ast_sorcery_object_type *, object_type, ao2_find(sorcery->types, details->object->type, OBJ_KEY), ao2_cleanup);
        RAII_VAR(struct ast_sorcery_object_wizard *, object_wizard, NULL, ao2_cleanup);
        struct sorcery_details sdetails = {
                .sorcery = sorcery,
@@ -1371,13 +1433,55 @@ void ast_sorcery_unref(struct ast_sorcery *sorcery)
 const char *ast_sorcery_object_get_id(const void *object)
 {
        const struct ast_sorcery_object_details *details = object;
-       return details->id;
+       return details->object->id;
 }
 
 const char *ast_sorcery_object_get_type(const void *object)
 {
        const struct ast_sorcery_object_details *details = object;
-       return details->type;
+       return details->object->type;
+}
+
+const char *ast_sorcery_object_get_extended(const void *object, const char *name)
+{
+       const struct ast_sorcery_object_details *details = object;
+       struct ast_variable *field;
+
+       for (field = details->object->extended; field; field = field->next) {
+               if (!strcmp(field->name + 1, name)) {
+                       return field->value;
+               }
+       }
+
+       return NULL;
+}
+
+int ast_sorcery_object_set_extended(const void *object, const char *name, const char *value)
+{
+       RAII_VAR(struct ast_variable *, field, NULL, ast_variables_destroy);
+       struct ast_variable *extended = ast_variable_new(name, value, ""), *previous = NULL;
+       const struct ast_sorcery_object_details *details = object;
+
+       if (!extended) {
+               return -1;
+       }
+
+       for (field = details->object->extended; field; previous = field, field = field->next) {
+               if (!strcmp(field->name, name)) {
+                       if (previous) {
+                               previous->next = field->next;
+                       } else {
+                               details->object->extended = field->next;
+                       }
+                       field->next = NULL;
+                       break;
+               }
+       }
+
+       extended->next = details->object->extended;
+       details->object->extended = extended;
+
+       return 0;
 }
 
 int ast_sorcery_observer_add(const struct ast_sorcery *sorcery, const char *type, const struct ast_sorcery_observer *callbacks)
index 9881dd8..47ff42d 100644 (file)
@@ -32,7 +32,7 @@ static void auth_destroy(void *obj)
 
 static void *auth_alloc(const char *name)
 {
-       struct ast_sip_auth *auth = ao2_alloc(sizeof(*auth), auth_destroy);
+       struct ast_sip_auth *auth = ast_sorcery_generic_alloc(sizeof(*auth), auth_destroy);
 
        if (!auth) {
                return NULL;
index 86b4636..90cd82f 100644 (file)
@@ -33,7 +33,7 @@ static void domain_alias_destroy(void *obj)
 
 static void *domain_alias_alloc(const char *name)
 {
-       struct ast_sip_domain_alias *alias = ao2_alloc(sizeof(*alias), domain_alias_destroy);
+        struct ast_sip_domain_alias *alias = ast_sorcery_generic_alloc(sizeof(*alias), domain_alias_destroy);
 
        if (!alias) {
                return NULL;
index 1d60274..30c1362 100644 (file)
@@ -62,7 +62,7 @@ static void transport_destroy(void *obj)
 /*! \brief Allocator for transport */
 static void *transport_alloc(const char *name)
 {
-       struct ast_sip_transport *transport = ao2_alloc(sizeof(*transport), transport_destroy);
+       struct ast_sip_transport *transport = ast_sorcery_generic_alloc(sizeof(*transport), transport_destroy);
 
        if (!transport) {
                return NULL;
index d0b0a28..bc1b521 100644 (file)
@@ -41,7 +41,7 @@ static void aor_destroy(void *obj)
 /*! \brief Allocator for AOR */
 static void *aor_alloc(const char *name)
 {
-       struct ast_sip_aor *aor = ao2_alloc_options(sizeof(struct ast_sip_aor), aor_destroy, AO2_ALLOC_OPT_LOCK_NOLOCK);
+       struct ast_sip_aor *aor = ast_sorcery_generic_alloc(sizeof(struct ast_sip_aor), aor_destroy);
        if (!aor) {
                return NULL;
        }
@@ -60,7 +60,7 @@ static void contact_destroy(void *obj)
 /*! \brief Allocator for contact */
 static void *contact_alloc(const char *name)
 {
-       struct ast_sip_contact *contact = ao2_alloc_options(sizeof(*contact), contact_destroy, AO2_ALLOC_OPT_LOCK_NOLOCK);
+       struct ast_sip_contact *contact = ast_sorcery_generic_alloc(sizeof(*contact), contact_destroy);
 
        if (!contact) {
                return NULL;
index 5864bde..49c2da2 100644 (file)
@@ -515,7 +515,7 @@ static int named_groups_handler(const struct aco_option *opt,
 
 static void *sip_nat_hook_alloc(const char *name)
 {
-       return ao2_alloc(sizeof(struct ast_sip_nat_hook), NULL);
+       return ast_sorcery_generic_alloc(sizeof(struct ast_sip_nat_hook), NULL);
 }
 
 /*! \brief Destructor function for persistent endpoint information */
@@ -722,7 +722,7 @@ static void endpoint_destructor(void* obj)
 
 void *ast_sip_endpoint_alloc(const char *name)
 {
-       struct ast_sip_endpoint *endpoint = ao2_alloc(sizeof(*endpoint), endpoint_destructor);
+       struct ast_sip_endpoint *endpoint = ast_sorcery_generic_alloc(sizeof(*endpoint), endpoint_destructor);
        if (!endpoint) {
                return NULL;
        }
index 4c8a9f6..4f21238 100644 (file)
@@ -42,8 +42,7 @@ static int qualify_contact(struct ast_sip_contact *contact);
  */
 static void *contact_status_alloc(const char *name)
 {
-       struct ast_sip_contact_status *status = ao2_alloc_options(
-               sizeof(*status), NULL, AO2_ALLOC_OPT_LOCK_NOLOCK);
+       struct ast_sip_contact_status *status = ast_sorcery_generic_alloc(sizeof(*status), NULL);
 
        if (!status) {
                ast_log(LOG_ERROR, "Unable to allocate ast_sip_contact_status\n");
index e1d4b7f..868c582 100644 (file)
@@ -49,7 +49,7 @@ struct test_sorcery_object {
 /*! \brief Internal function to allocate a test object */
 static void *test_sorcery_object_alloc(const char *id)
 {
-       return ao2_alloc(sizeof(struct test_sorcery_object), NULL);
+       return ast_sorcery_generic_alloc(sizeof(struct test_sorcery_object), NULL);
 }
 
 /*! \brief Internal function for object set transformation */
@@ -1300,6 +1300,71 @@ AST_TEST_DEFINE(objectset_apply_fields)
        return res;
 }
 
+AST_TEST_DEFINE(extended_fields)
+{
+       int res = AST_TEST_PASS;
+       RAII_VAR(struct ast_sorcery *, sorcery, NULL, ast_sorcery_unref);
+       RAII_VAR(struct test_sorcery_object *, obj, NULL, ao2_cleanup);
+       RAII_VAR(struct ast_variable *, objset, NULL, ast_variables_destroy);
+       const char *value;
+
+       switch (cmd) {
+       case TEST_INIT:
+               info->name = "extended_fields";
+               info->category = "/main/sorcery/";
+               info->summary = "sorcery object extended fields unit test";
+               info->description =
+                       "Test extended fields support in sorcery";
+               return AST_TEST_NOT_RUN;
+       case TEST_EXECUTE:
+               break;
+       }
+
+       if (!(sorcery = alloc_and_initialize_sorcery())) {
+               ast_test_status_update(test, "Failed to open sorcery structure\n");
+               return AST_TEST_FAIL;
+       }
+
+       if (!(obj = ast_sorcery_alloc(sorcery, "test", "blah"))) {
+               ast_test_status_update(test, "Failed to allocate a known object type\n");
+               return AST_TEST_FAIL;
+       }
+
+       if (!(objset = ast_variable_new("@testing", "toast", ""))) {
+               ast_test_status_update(test, "Failed to create an object set, test could not occur\n");
+               res = AST_TEST_FAIL;
+       } else if (ast_sorcery_objectset_apply(sorcery, obj, objset)) {
+               ast_test_status_update(test, "Failed to apply valid object set to object\n");
+               res = AST_TEST_FAIL;
+       } else if (!(value = ast_sorcery_object_get_extended(obj, "testing"))) {
+               ast_test_status_update(test, "Extended field, which was set using object set, could not be found\n");
+               res = AST_TEST_FAIL;
+       } else if (strcmp(value, "toast")) {
+               ast_test_status_update(test, "Extended field does not contain expected value\n");
+               res = AST_TEST_FAIL;
+       } else if (ast_sorcery_object_set_extended(obj, "@tacos", "supreme")) {
+               ast_test_status_update(test, "Extended field could not be set\n");
+               res = AST_TEST_FAIL;
+       } else if (!(value = ast_sorcery_object_get_extended(obj, "tacos"))) {
+               ast_test_status_update(test, "Extended field, which was set using the API, could not be found\n");
+               res = AST_TEST_FAIL;
+       } else if (strcmp(value, "supreme")) {
+               ast_test_status_update(test, "Extended field does not contain expected value\n");
+               res = AST_TEST_FAIL;
+       } else if (ast_sorcery_object_set_extended(obj, "@tacos", "canadian")) {
+               ast_test_status_update(test, "Extended field could not be set a second time\n");
+               res = AST_TEST_FAIL;
+       } else if (!(value = ast_sorcery_object_get_extended(obj, "tacos"))) {
+               ast_test_status_update(test, "Extended field, which was set using the API, could not be found\n");
+               res = AST_TEST_FAIL;
+       } else if (strcmp(value, "canadian")) {
+               ast_test_status_update(test, "Extended field does not contain expected value\n");
+               res = AST_TEST_FAIL;
+       }
+
+       return res;
+}
+
 AST_TEST_DEFINE(changeset_create)
 {
        int res = AST_TEST_PASS;
@@ -2604,6 +2669,7 @@ static int unload_module(void)
        AST_TEST_UNREGISTER(objectset_apply_invalid);
        AST_TEST_UNREGISTER(objectset_transform);
        AST_TEST_UNREGISTER(objectset_apply_fields);
+       AST_TEST_UNREGISTER(extended_fields);
        AST_TEST_UNREGISTER(changeset_create);
        AST_TEST_UNREGISTER(changeset_create_unchanged);
        AST_TEST_UNREGISTER(object_create);
@@ -2651,6 +2717,7 @@ static int load_module(void)
        AST_TEST_REGISTER(objectset_apply_invalid);
        AST_TEST_REGISTER(objectset_transform);
        AST_TEST_REGISTER(objectset_apply_fields);
+       AST_TEST_REGISTER(extended_fields);
        AST_TEST_REGISTER(changeset_create);
        AST_TEST_REGISTER(changeset_create_unchanged);
        AST_TEST_REGISTER(object_create);