--- /dev/null
+/*
+ * 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+/**
+ * $Id$
+ *
+ * @file src/lib/util/edit.c
+ * @brief Functions to edit pair lists, and track undo operations
+ *
+ * @copyright 2021 Network RADIUS SAS (legal@networkradius.com)
+ */
+
+RCSID("$Id$")
+
+#include <freeradius-devel/util/value.h>
+#include "edit.h"
+
+/** Track a series of edits.
+ *
+ */
+struct fr_edit_list_s {
+ /*
+ * List of edits to be made, in order.
+ */
+ fr_dlist_head_t list;
+
+ /*
+ * VPs which were inserted, and then over-written by a
+ * later edit.
+ */
+ fr_pair_list_t deleted_pairs;
+};
+
+/** Track one particular edit.
+ */
+typedef struct {
+ fr_edit_op_t op; //!< edit operation to perform
+ fr_dlist_t entry; //!< linked list of edits
+
+ fr_pair_t *vp; //!< pair edited, deleted, or inserted
+
+ union {
+ fr_value_box_t data; //!< original data
+ fr_pair_list_t children; //!< original child list,
+ //!< for T_OP_SET of structural entries
+
+ struct {
+ fr_pair_list_t *list; //!< parent list
+ fr_pair_t *ref; //!< reference pair for delete, insert before/after
+ };
+ };
+} fr_edit_t;
+
+
+/** Undo one particular edit.
+ */
+static int edit_undo(fr_edit_t *e)
+{
+ fr_pair_t *vp = e->vp;
+#ifndef NDEBUG
+ int rcode;
+#endif
+
+ switch (e->op) {
+ case FR_EDIT_INVALID:
+ return -1;
+
+ case FR_EDIT_VALUE:
+ if (fr_type_is_leaf(vp->vp_type)) {
+ if (!fr_type_is_fixed_size(vp->vp_type)) fr_value_box_clear(&vp->data);
+ fr_value_box_copy_shallow(NULL, &vp->data, &e->data);
+ } else {
+ fr_assert(fr_type_is_structural(vp->vp_type));
+
+ fr_pair_list_free(&vp->vp_group);
+ fr_pair_list_append(&vp->vp_group, &e->children);
+ }
+ break;
+
+ case FR_EDIT_DELETE:
+#ifndef NDEBUG
+ rcode =
+#endif
+ fr_pair_insert_after(e->list, e->ref, vp);
+ fr_assert(rcode == 0);
+ break;
+
+ case FR_EDIT_INSERT_BEFORE:
+ case FR_EDIT_INSERT_AFTER:
+ /*
+ * We can free the VP here, as any edits to its'
+ * children MUST come after the creation of the
+ * VP. And any deletion of VPs after this one
+ * must come after this VP was created.
+ */
+ fr_pair_delete(e->list, vp);
+ talloc_free(vp);
+ break;
+ }
+
+ return 0;
+}
+
+/** Abort the entries in an edit list.
+ *
+ * After this call, the input list(s) are unchanged from before any
+ * edits were made.
+ *
+ * the caller still has to call talloc_free(el);
+ */
+void fr_edit_list_abort(fr_edit_list_t *el)
+{
+ fr_edit_t *e;
+
+ if (!el) return;
+
+ /*
+ * Undo edits in reverse order, as later edits depend on
+ * earlier ones. We don't have multiple edits of the
+ * same VP, but we can create a VP, and then later edit
+ * its children.
+ */
+ while ((e = fr_dlist_pop_tail(&el->list)) != NULL) {
+ edit_undo(e);
+ };
+}
+
+/** Record one particular edit
+ *
+ * For INSERT / DELETE, this function will also insert / delete the
+ * VP.
+ *
+ * For VALUE changes, this function must be called BEFORE the value
+ * is changed. Once this function has been called, it is then safe
+ * to edit the value in place.
+ *
+ * Note that VALUE changes for structural types are allowed ONLY when
+ * using T_OP_SET, which over-writes previous values. For every
+ * other modification to structural types, we MUST instead call
+ * insert / delete on the vp_group.
+ */
+int fr_edit_list_record(fr_edit_list_t *el, fr_edit_op_t op, fr_pair_t *vp, fr_pair_list_t *list, fr_pair_t *ref)
+{
+ fr_edit_t *e;
+
+ if (!el) return 0;
+
+ /*
+ * Search for previous edits.
+ *
+ * @todo - if we're modifying values of a child VP, and
+ * it's parent is marked as INSERT, then we don't need to
+ * record FR_EDIT_VALUE changes to the children. It's
+ * not yet clear how best to track this.
+ */
+ for (e = fr_dlist_head(&el->list);
+ e != NULL;
+ e = fr_dlist_next(&el->list, e)) {
+ fr_assert(e->vp != NULL);
+
+ if (e->vp != vp) continue;
+
+ switch (op) {
+ case FR_EDIT_INVALID:
+ return -1;
+
+ /*
+ * We're editing a previous edit.
+ * There's no need to record anything
+ * new, as we've already recorded the
+ * original value.
+ *
+ * Note that we can insert a pair and
+ * then edit it. The undo list only
+ * saves the insert, as the later edit is
+ * irrelevant. If we're undoing, we
+ * simply delete the new attribute which
+ * was inserted.
+ */
+ case FR_EDIT_VALUE:
+ /*
+ * If we delete a pair, we can't later
+ * edit it. That indicates serious
+ * issues with the code.
+ */
+ fr_assert(e->op != FR_EDIT_DELETE);
+
+ /*
+ * We're over-writing (again) the
+ * children of a structural type.
+ *
+ * Save the attributes we added in the
+ * previous T_OP_SET operation. Some
+ * other edit may be changing them, so we
+ * don't want to delete the attributes
+ * until we're done.
+ */
+ if (fr_type_is_structural(vp->vp_type) && !fr_pair_list_empty(&vp->vp_group)) {
+ fr_pair_list_append(&el->deleted_pairs, &vp->vp_group);
+ }
+ return 0;
+
+ /*
+ * We're inserting a new pair.
+ *
+ * We can't have previously edited this
+ * pair (inserted, deleted, or updated
+ * the value), as the pair is new!
+ */
+ case FR_EDIT_INSERT_BEFORE:
+ case FR_EDIT_INSERT_AFTER:
+ fr_assert(0);
+ return -1;
+
+ /*
+ * We're being asked to delete something
+ * we previously inserted, or previously
+ * edited.
+ */
+ case FR_EDIT_DELETE:
+ /*
+ * We can't delete something which was
+ * already deleted.
+ */
+ fr_assert(e->op != FR_EDIT_DELETE);
+
+ /*
+ * We had previously inserted it. So
+ * just delete the insert operation, and
+ * delete the VP from the list.
+ *
+ * Other edits may refer to children of
+ * this pair. So we don't free the VP
+ * immediately, but instead reparent it
+ * to the edit list. So that when the
+ * edit list is freed, the VP will be
+ * freed.
+ */
+ if ((e->op == FR_EDIT_INSERT_BEFORE) ||
+ (e->op == FR_EDIT_INSERT_AFTER)) {
+ fr_assert(e->list == list);
+
+ fr_pair_remove(list, vp);
+ fr_pair_append(&el->deleted_pairs, vp);
+
+ fr_dlist_remove(&el->list, e);
+ talloc_free(e);
+ return 0;
+ }
+
+ /*
+ * We had previously changed the value,
+ * but now we're going to delete it.
+ *
+ * Since it had previously existed, we
+ * have to reset its value to the
+ * original one, and then track the
+ * deletion.
+ */
+ fr_assert(e->op == FR_EDIT_VALUE);
+ edit_undo(e);
+ e->op = FR_EDIT_DELETE;
+ goto delete;
+ }
+ } /* loop over existing edits */
+
+ /*
+ * No edit for this pair exists. Create a new edit entry.
+ */
+ e = talloc_zero(el, fr_edit_t);
+ if (!e) return -1;
+
+ e->op = op;
+ e->vp = vp;
+
+ switch (op) {
+ case FR_EDIT_INVALID:
+ talloc_free(e);
+ return -1;
+
+ case FR_EDIT_VALUE:
+ fr_assert(list == NULL);
+ fr_assert(ref == NULL);
+
+ if (fr_type_is_leaf(vp->vp_type)) {
+ fr_value_box_copy_shallow(NULL, &e->data, &vp->data);
+ if (!fr_type_is_fixed_size(vp->vp_type)) fr_value_box_memdup_shallow(&vp->data, vp->data.enumv,
+ e->data.vb_octets, e->data.vb_length,
+ e->data.tainted);
+ } else {
+ fr_assert(fr_type_is_structural(vp->vp_type));
+
+ fr_pair_list_init(&e->children);
+ fr_pair_list_append(&e->children, &vp->vp_group);
+ }
+ break;
+
+ case FR_EDIT_INSERT_BEFORE:
+ fr_assert(list != NULL);
+
+ /*
+ * There's no need to record "prev". On undo, we
+ * just delete this pair from the list.
+ */
+ e->list = list;
+ fr_pair_insert_before(list, ref, vp);
+ break;
+
+ case FR_EDIT_INSERT_AFTER:
+ fr_assert(list != NULL);
+
+ /*
+ * There's no need to record "prev". On undo, we
+ * just delete this pair from the list.
+ */
+ e->list = list;
+ fr_pair_insert_after(list, ref, vp);
+ break;
+
+ case FR_EDIT_DELETE:
+ delete:
+ fr_assert(list != NULL);
+ fr_assert(ref == NULL);
+
+ e->list = list;
+ e->ref = fr_pair_list_prev(list, vp);
+
+ fr_pair_remove(list, vp);
+ break;
+ }
+
+ fr_dlist_insert_tail(&el->list, e);
+ return 0;
+}
+
+/** Finalize the edits when we destroy the edit list.
+ *
+ * Which in large part means freeing the VPs which have been deleted,
+ * and then deleting the edit list.
+ */
+static int _edit_list_destructor(fr_edit_list_t *el)
+{
+ fr_edit_t *e;
+
+ if (!el) return 0;
+
+ for (e = fr_dlist_head(&el->list);
+ e != NULL;
+ e = fr_dlist_next(&el->list, e)) {
+ switch (e->op) {
+ case FR_EDIT_INVALID:
+ fr_assert(0);
+ break;
+
+ case FR_EDIT_INSERT_BEFORE:
+ case FR_EDIT_INSERT_AFTER:
+ break;
+
+ case FR_EDIT_DELETE:
+ fr_assert(e->vp != NULL);
+ talloc_free(e->vp);
+ break;
+
+ case FR_EDIT_VALUE:
+ if (fr_type_is_leaf(e->vp->vp_type)) {
+ fr_value_box_clear(&e->data);
+ } else {
+ fr_assert(fr_type_is_structural(e->vp->vp_type));
+ fr_pair_list_free(&e->children);
+ }
+ break;
+ }
+ }
+
+ fr_pair_list_free(&el->deleted_pairs);
+
+ talloc_free(el);
+
+ return 0;
+}
+
+fr_edit_list_t *fr_edit_list_alloc(TALLOC_CTX *ctx)
+{
+ fr_edit_list_t *el;
+
+ el = talloc_zero(ctx, fr_edit_list_t);
+ if (!el) return NULL;
+
+ fr_dlist_init(&el->list, fr_edit_t, entry);
+
+ fr_pair_list_init(&el->deleted_pairs);
+
+ talloc_set_destructor(el, _edit_list_destructor);
+
+ return el;
+}
+
+/** Notes
+ *
+ * Unlike "update" sections, edits are _not_ hierarchical. If we're
+ * editing values a list, then the list has to exist. If we're
+ * inserting pairs in a list, then we find the lowest existing pair,
+ * and add pairs there.
+ *
+ * The functions tmpl_extents_find() and tmpl_extents_build_to_leaf()
+ * should help us figure out where the VPs exist or not.
+ *
+ * The overall "update" algorithm is now:
+ *
+ * alloc(edit list)
+ *
+ * foreach entry in the things to do
+ * expand LHS if needed to local TMPL
+ * expand RHS if needed to local box / cursor / TMPL
+ *
+ * use LHS/RHS cursors to find VPs
+ * edit VPs, recording edits
+ *
+ * free temporary map
+ * talloc_free(edit list)
+ */
--- /dev/null
+/*
+ * 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+/** Tests for a AVP manipulation and search API.
+ *
+ * @file src/lib/util/edit_tests.c
+ * @author Alan DeKok (aland@networkradius.com)
+ * @copyright 2021 Network RADIUS SAS (legal@networkradius.com)
+ */
+
+/**
+ * The 'TEST_INIT' macro provided by 'acutest.h' allowing to register a function to be called
+ * before call the unit tests. Therefore, It calls the function ALL THE TIME causing an overhead.
+ * That is why we are initializing test_init() by "__attribute__((constructor));" reducing the
+ * test execution by 50% of the time.
+ */
+#define USE_CONSTRUCTOR
+
+/*
+ * It should be declared before include the "acutest.h"
+ */
+#ifdef USE_CONSTRUCTOR
+static void test_init(void) __attribute__((constructor));
+#else
+static void test_init(void);
+# define TEST_INIT test_init()
+#endif
+
+#include <freeradius-devel/util/acutest.h>
+#include <freeradius-devel/util/acutest_helpers.h>
+#include <freeradius-devel/util/pair_test_helpers.h>
+
+#include <freeradius-devel/util/conf.h>
+#include <freeradius-devel/util/dict.h>
+#include <freeradius-devel/util/edit.h>
+
+#ifdef HAVE_GPERFTOOLS_PROFILER_H
+# include <gperftools/profiler.h>
+#endif
+
+static TALLOC_CTX *autofree;
+static fr_pair_list_t test_pairs;
+static fr_dict_t *test_dict;
+
+/** Global initialisation
+ */
+static void test_init(void)
+{
+ autofree = talloc_autofree_context();
+ if (!autofree) {
+ error:
+ fr_perror("edit_tests");
+ fr_exit_now(EXIT_FAILURE);
+ }
+
+ /*
+ * Mismatch between the binary and the libraries it depends on
+ */
+ if (fr_check_lib_magic(RADIUSD_MAGIC_NUMBER) < 0) goto error;
+
+ if (fr_dict_test_init(autofree, &test_dict, NULL) < 0) goto error;
+
+ /* Initialize the "test_pairs" list */
+ fr_pair_list_init(&test_pairs);
+
+ if (fr_pair_test_list_alloc(autofree, &test_pairs, NULL) < 0) goto error;
+}
+
+static void add_pairs(fr_pair_list_t *local_pairs)
+{
+ int count;
+
+ fr_pair_list_init(local_pairs);
+
+ fr_pair_append(local_pairs, fr_pair_afrom_da(autofree, fr_dict_attr_test_uint32));
+ fr_pair_append(local_pairs, fr_pair_afrom_da(autofree, fr_dict_attr_test_octets));
+ fr_pair_append(local_pairs, fr_pair_afrom_da(autofree, fr_dict_attr_test_tlv));
+
+ count = fr_pair_list_len(local_pairs);
+ TEST_CASE("Expected (count == 3)");
+ TEST_CHECK(count == 3);
+}
+
+static void expect3(fr_pair_list_t *local_pairs)
+{
+ int count;
+ fr_pair_t *vp;
+
+ count = fr_pair_list_len(local_pairs);
+ TEST_CASE("Expected (count == 3) after undoing the edits");
+ TEST_CHECK(count == 3);
+
+ vp = fr_pair_list_head(local_pairs);
+ TEST_CHECK(vp->da == fr_dict_attr_test_uint32);
+
+ vp = fr_pair_list_tail(local_pairs);
+ TEST_CHECK(vp->da == fr_dict_attr_test_tlv);
+
+ fr_pair_list_free(local_pairs);
+}
+
+/*
+ * Tests functions
+ */
+static void test_pair_delete_head(void)
+{
+ fr_pair_t *vp;
+ fr_pair_list_t local_pairs;
+ size_t count;
+ fr_edit_list_t *el;
+ int rcode;
+
+ TEST_CASE("Add 3 pairs and delete the first one");
+
+ add_pairs(&local_pairs);
+
+ vp = fr_pair_list_head(&local_pairs);
+
+ el = fr_edit_list_alloc(NULL);
+ TEST_CHECK(el != NULL);
+
+ rcode = fr_edit_list_record(el, FR_EDIT_DELETE, vp, &local_pairs, NULL);
+ TEST_CHECK(rcode == 0);
+
+ talloc_free(el);
+
+ count = fr_pair_list_len(&local_pairs);
+ TEST_CASE("Expected (count == 2) after deleting the head");
+ TEST_CHECK(count == 2);
+
+ vp = fr_pair_list_head(&local_pairs);
+ TEST_CASE("head is now what was the second pair");
+ TEST_CHECK(vp->da == fr_dict_attr_test_octets);
+
+ fr_pair_list_free(&local_pairs);
+}
+
+static void test_pair_delete_head_abort(void)
+{
+ fr_pair_t *vp;
+ fr_pair_list_t local_pairs;
+ size_t count;
+ fr_edit_list_t *el;
+ int rcode;
+
+ TEST_CASE("Add 3 pairs and delete the first one");
+
+ add_pairs(&local_pairs);
+
+ vp = fr_pair_list_head(&local_pairs);
+
+ el = fr_edit_list_alloc(NULL);
+ TEST_CHECK(el != NULL);
+
+ rcode = fr_edit_list_record(el, FR_EDIT_DELETE, vp, &local_pairs, NULL);
+ TEST_CHECK(rcode == 0);
+
+ count = fr_pair_list_len(&local_pairs);
+ TEST_CASE("Expected (count == 2) after deleting the head");
+ TEST_CHECK(count == 2);
+
+ /*
+ * Abort the edit
+ */
+ fr_edit_list_abort(el);
+
+ talloc_free(el);
+
+ expect3(&local_pairs);
+}
+
+static void test_pair_delete_middle(void)
+{
+ fr_pair_t *vp;
+ fr_pair_list_t local_pairs;
+ size_t count;
+ fr_edit_list_t *el;
+ int rcode;
+
+ TEST_CASE("Add 3 pairs and delete the middle one");
+
+ add_pairs(&local_pairs);
+
+ vp = fr_pair_list_head(&local_pairs);
+ vp = fr_pair_list_next(&local_pairs, vp);
+ TEST_CHECK(vp != NULL);
+
+ el = fr_edit_list_alloc(NULL);
+ TEST_CHECK(el != NULL);
+
+ rcode = fr_edit_list_record(el, FR_EDIT_DELETE, vp, &local_pairs, NULL);
+ TEST_CHECK(rcode == 0);
+
+ talloc_free(el);
+
+ /* let's count */
+ count = fr_pair_list_len(&local_pairs);
+ TEST_CASE("Expected (count == 2) after deleting the middle");
+ TEST_CHECK(count == 2);
+
+ vp = fr_pair_list_head(&local_pairs);
+ TEST_CHECK(vp->da == fr_dict_attr_test_uint32);
+
+ vp = fr_pair_list_tail(&local_pairs);
+ TEST_CHECK(vp->da == fr_dict_attr_test_tlv);
+
+ fr_pair_list_free(&local_pairs);
+}
+
+static void test_pair_delete_middle_abort(void)
+{
+ fr_pair_t *vp, *middle;
+ fr_pair_list_t local_pairs;
+ size_t count;
+ fr_edit_list_t *el;
+ int rcode;
+
+ TEST_CASE("Add 3 pairs and delete the middle one, then abort");
+
+ add_pairs(&local_pairs);
+
+ vp = fr_pair_list_head(&local_pairs);
+ middle = fr_pair_list_next(&local_pairs, vp);
+ TEST_CHECK(middle != NULL);
+ TEST_CHECK(middle->da == fr_dict_attr_test_octets);
+
+ el = fr_edit_list_alloc(NULL);
+ TEST_CHECK(el != NULL);
+
+ rcode = fr_edit_list_record(el, FR_EDIT_DELETE, middle, &local_pairs, NULL);
+ TEST_CHECK(rcode == 0);
+
+ count = fr_pair_list_len(&local_pairs);
+ TEST_CASE("Expected (count == 2) after deleting the middle");
+ TEST_CHECK(count == 2);
+
+ vp = fr_pair_list_head(&local_pairs);
+ TEST_CHECK(vp->da == fr_dict_attr_test_uint32);
+
+ vp = fr_pair_list_tail(&local_pairs);
+ TEST_CHECK(vp->da == fr_dict_attr_test_tlv);
+
+ /*
+ * Abort the edit
+ */
+ fr_edit_list_abort(el);
+
+ talloc_free(el);
+
+ expect3(&local_pairs);
+}
+
+static void test_pair_delete_multiple(void)
+{
+ fr_pair_t *vp;
+ fr_pair_list_t local_pairs;
+ size_t count;
+ fr_edit_list_t *el;
+ int rcode;
+
+ TEST_CASE("Add 3 pairs and delete the last 2");
+
+ add_pairs(&local_pairs);
+
+ vp = fr_pair_list_head(&local_pairs);
+ vp = fr_pair_list_next(&local_pairs, vp);
+ TEST_CHECK(vp != NULL);
+
+ el = fr_edit_list_alloc(NULL);
+ TEST_CHECK(el != NULL);
+
+ rcode = fr_edit_list_record(el, FR_EDIT_DELETE, vp, &local_pairs, NULL); /* middle */
+ TEST_CHECK(rcode == 0);
+
+ vp = fr_pair_list_tail(&local_pairs);
+ TEST_CHECK(vp != NULL);
+
+ rcode = fr_edit_list_record(el, FR_EDIT_DELETE, vp, &local_pairs, NULL); /* tail */
+ TEST_CHECK(rcode == 0);
+
+ talloc_free(el);
+
+ count = fr_pair_list_len(&local_pairs);
+ TEST_CASE("Expected (count == 1) after deleting the last 2");
+ TEST_CHECK(count == 1);
+
+ vp = fr_pair_list_head(&local_pairs);
+ TEST_CHECK(vp->da == fr_dict_attr_test_uint32);
+
+ vp = fr_pair_list_tail(&local_pairs);
+ TEST_CHECK(vp->da == fr_dict_attr_test_uint32); /* head == tail */
+
+ fr_pair_list_free(&local_pairs);
+}
+
+static void test_pair_delete_multiple_abort(void)
+{
+ fr_pair_t *vp;
+ fr_pair_list_t local_pairs;
+ size_t count;
+ fr_edit_list_t *el;
+ int rcode;
+
+ TEST_CASE("Add 3 pairs and delete the last two, then abort");
+
+ add_pairs(&local_pairs);
+
+ vp = fr_pair_list_head(&local_pairs);
+ vp = fr_pair_list_next(&local_pairs, vp);
+ TEST_CHECK(vp->da == fr_dict_attr_test_octets);
+
+ el = fr_edit_list_alloc(NULL);
+ TEST_CHECK(el != NULL);
+
+ rcode = fr_edit_list_record(el, FR_EDIT_DELETE, vp, &local_pairs, NULL); /* middle */
+ TEST_CHECK(rcode == 0);
+
+ vp = fr_pair_list_tail(&local_pairs);
+ TEST_CHECK(vp != NULL);
+
+ rcode = fr_edit_list_record(el, FR_EDIT_DELETE, vp, &local_pairs, NULL); /* tail */
+ TEST_CHECK(rcode == 0);
+
+ count = fr_pair_list_len(&local_pairs);
+ TEST_CASE("Expected (count == 1) after deleting the last 2");
+ TEST_CHECK(count == 1);
+
+ vp = fr_pair_list_head(&local_pairs);
+ TEST_CHECK(vp->da == fr_dict_attr_test_uint32);
+
+ vp = fr_pair_list_tail(&local_pairs);
+ TEST_CHECK(vp->da == fr_dict_attr_test_uint32);
+
+ /*
+ * Abort the edit
+ */
+ fr_edit_list_abort(el);
+
+ talloc_free(el);
+
+ expect3(&local_pairs);
+}
+
+
+static void test_pair_edit_value(void)
+{
+ fr_pair_t *vp;
+ fr_pair_list_t local_pairs;
+ fr_edit_list_t *el;
+ int rcode;
+
+ TEST_CASE("Add 3 pairs and change the value of the first one");
+
+ add_pairs(&local_pairs);
+
+ vp = fr_pair_list_head(&local_pairs);
+ TEST_CHECK(vp != NULL);
+
+ el = fr_edit_list_alloc(NULL);
+ TEST_CHECK(el != NULL);
+
+ rcode = fr_edit_list_record(el, FR_EDIT_VALUE, vp, NULL, NULL);
+ TEST_CHECK(rcode == 0);
+
+ TEST_CHECK(vp->vp_uint32 == 0);
+
+ vp->vp_uint32 = 1;
+ TEST_CHECK(vp->vp_uint32 == 1);
+
+ talloc_free(el);
+
+ vp = fr_pair_list_head(&local_pairs);
+ TEST_CHECK(vp->da == fr_dict_attr_test_uint32);
+ TEST_CHECK(vp->vp_uint32 == 1);
+
+ expect3(&local_pairs);
+}
+
+static void test_pair_edit_value_abort(void)
+{
+ fr_pair_t *vp;
+ fr_pair_list_t local_pairs;
+ fr_edit_list_t *el;
+ int rcode;
+
+ TEST_CASE("Add 3 pairs and change the value of the first one, then abort");
+
+ add_pairs(&local_pairs);
+
+ vp = fr_pair_list_head(&local_pairs);
+ TEST_CHECK(vp != NULL);
+
+ el = fr_edit_list_alloc(NULL);
+ TEST_CHECK(el != NULL);
+
+ rcode = fr_edit_list_record(el, FR_EDIT_VALUE, vp, NULL, NULL);
+ TEST_CHECK(rcode == 0);
+
+ TEST_CHECK(vp->vp_uint32 == 0);
+
+ vp->vp_uint32 = 1;
+ TEST_CHECK(vp->vp_uint32 == 1);
+
+ /*
+ * Abort the edit
+ */
+ fr_edit_list_abort(el);
+
+ talloc_free(el);
+
+ vp = fr_pair_list_head(&local_pairs);
+ TEST_CHECK(vp->da == fr_dict_attr_test_uint32);
+ TEST_CHECK(vp->vp_uint32 == 0);
+
+ expect3(&local_pairs);
+}
+
+TEST_LIST = {
+ /*
+ * Deletion.
+ */
+ { "pair_delete_head", test_pair_delete_head },
+ { "pair_delete_head_abort", test_pair_delete_head_abort },
+
+ { "pair_delete_middle", test_pair_delete_middle },
+ { "pair_delete_middle_abort", test_pair_delete_middle_abort },
+
+ { "pair_delete_multiple", test_pair_delete_multiple },
+ { "pair_delete_multiple_abort", test_pair_delete_multiple_abort },
+
+ /*
+ * Insert before
+ */
+
+ /*
+ * Insert after
+ */
+
+ /*
+ * Value modification
+ */
+ { "pair_edit_value", test_pair_edit_value },
+ { "pair_edit_value_abort", test_pair_edit_value_abort },
+
+ { NULL }
+};