Optimized duplicate detection and removal

This commit is contained in:
Olof hagsand 2024-12-19 16:58:35 +01:00
parent ead9e8d666
commit 06e1a48480
7 changed files with 364 additions and 125 deletions

View file

@ -17,7 +17,9 @@ Expected: January 2025
### Features ### Features
* C-API: New no-copy `xmldb_get_cache` function for performance * Performance optimization
* New no-copy `xmldb_get_cache` function for performance
* Optimized duplicate detection
* New: CLI generic pipe callbacks * New: CLI generic pipe callbacks
* Add scripts in `CLICON_CLI_PIPE_DIR` * Add scripts in `CLICON_CLI_PIPE_DIR`
* New: [feature request: support xpath functions for strings](https://github.com/clicon/clixon/issues/556) * New: [feature request: support xpath functions for strings](https://github.com/clicon/clixon/issues/556)

View file

@ -667,13 +667,20 @@ from_client_edit_config(clixon_handle h,
goto done; goto done;
/* Disable duplicate check in NETCONF messages. */ /* Disable duplicate check in NETCONF messages. */
if (clicon_option_bool(h, "CLICON_NETCONF_DUPLICATE_ALLOW")){ if (clicon_option_bool(h, "CLICON_NETCONF_DUPLICATE_ALLOW")){
if ((ret = xml_duplicate_remove_recurse(xc, &xret)) < 0) if (xml_duplicate_remove_recurse(xc) < 0)
goto done; goto done;
} }
else if ((ret = xml_yang_validate_unique_recurse(xc, &xret)) < 0) else {
if ((ret = xml_yang_validate_unique_recurse(xc, &xret)) < 0)
goto done; goto done;
if (ret == 0){
if (clixon_xml2cbuf(cbret, xret, 0, 0, NULL, -1, 0) < 0)
goto done;
goto ok;
}
}
/* xmldb_put (difflist handling) requires list keys */ /* xmldb_put (difflist handling) requires list keys */
if (ret == 1 && (ret = xml_yang_validate_list_key_only(xc, &xret)) < 0) if ((ret = xml_yang_validate_list_key_only(xc, &xret)) < 0)
goto done; goto done;
if (ret == 0){ if (ret == 0){
if (clixon_xml2cbuf(cbret, xret, 0, 0, NULL, -1, 0) < 0) if (clixon_xml2cbuf(cbret, xret, 0, 0, NULL, -1, 0) < 0)

View file

@ -47,6 +47,6 @@ int xml_yang_validate_minmax(cxobj *xt, int presence, cxobj **xret);
int xml_yang_validate_minmax_recurse(cxobj *xt, cxobj **xret); int xml_yang_validate_minmax_recurse(cxobj *xt, cxobj **xret);
int xml_yang_validate_unique(cxobj *xt, cxobj **xret); int xml_yang_validate_unique(cxobj *xt, cxobj **xret);
int xml_yang_validate_unique_recurse(cxobj *xt, cxobj **xret); int xml_yang_validate_unique_recurse(cxobj *xt, cxobj **xret);
int xml_duplicate_remove_recurse(cxobj *xt, cxobj **xret); int xml_duplicate_remove_recurse(cxobj *xt);
#endif /* _CLIXON_VALIDATE_MINMAX_H_ */ #endif /* _CLIXON_VALIDATE_MINMAX_H_ */

View file

@ -250,6 +250,7 @@ cxobj *xml_child_i(cxobj *xn, int i);
cxobj *xml_child_i_type(cxobj *xn, int i, enum cxobj_type type); cxobj *xml_child_i_type(cxobj *xn, int i, enum cxobj_type type);
cxobj *xml_child_i_set(cxobj *xt, int i, cxobj *xc); cxobj *xml_child_i_set(cxobj *xt, int i, cxobj *xc);
int xml_child_order(cxobj *xn, cxobj *xc); int xml_child_order(cxobj *xn, cxobj *xc);
int xml_vector_decrement(cxobj *x, int nr);
cxobj *xml_child_each(cxobj *xparent, cxobj *xprev, enum cxobj_type type); cxobj *xml_child_each(cxobj *xparent, cxobj *xprev, enum cxobj_type type);
cxobj *xml_child_each_attr(cxobj *xparent, cxobj *xprev); cxobj *xml_child_each_attr(cxobj *xparent, cxobj *xprev);
int xml_child_insert_pos(cxobj *x, cxobj *xc, int pos); int xml_child_insert_pos(cxobj *x, cxobj *xc, int pos);

View file

@ -46,6 +46,7 @@
#include <errno.h> #include <errno.h>
#include <ctype.h> #include <ctype.h>
#include <string.h> #include <string.h>
#include <assert.h>
#include <syslog.h> #include <syslog.h>
#include <fcntl.h> #include <fcntl.h>
#include <arpa/inet.h> #include <arpa/inet.h>
@ -79,6 +80,19 @@
#include "clixon_xml_bind.h" #include "clixon_xml_bind.h"
#include "clixon_validate_minmax.h" #include "clixon_validate_minmax.h"
/*
* Local types
*/
/*! List used to order values, object and strings for leaf-lists, cvk:s for lists
* consider union
*/
struct vec_order {
cxobj *vo_xml;
char **vo_strvec;
size_t vo_slen; /* length of vo_strvec (is actually global to vector) */
};
/*! New element last in list, check if already exists if so return -1 /*! New element last in list, check if already exists if so return -1
* *
* @param[in] vec Vector of existing entries (new is last) * @param[in] vec Vector of existing entries (new is last)
@ -253,18 +267,19 @@ check_unique_list_direct(cxobj *x,
/* No keys: no checks necessary */ /* No keys: no checks necessary */
goto ok; goto ok;
} }
/* x need not be child 0, which could make the vector larger than necessary */ /* Vector of key values, k00,k01,..,k0n,k10,k11,..
* Ie, if nr of keys is n, and nr of children is m, then length is n*m
* x need not be child 0, which could make the vector larger than necessary */
if ((vec = calloc(clen*xml_child_nr(xt), sizeof(char*))) == NULL){ if ((vec = calloc(clen*xml_child_nr(xt), sizeof(char*))) == NULL){
clixon_err(OE_UNIX, errno, "calloc"); clixon_err(OE_UNIX, errno, "calloc");
goto done; goto done;
} }
/* Vector of xml objects (children), not really necessary, is a direct copy of x_childvec */
if ((xvec = calloc(xml_child_nr(xt), sizeof(cxobj*))) == NULL){ if ((xvec = calloc(xml_child_nr(xt), sizeof(cxobj*))) == NULL){
clixon_err(OE_UNIX, errno, "calloc"); clixon_err(OE_UNIX, errno, "calloc");
goto done; goto done;
} }
/* A vector is built with key-values, for each iteration check "backward" in the vector /* Loop over children, then over each key, then search "backwards" */
* for duplicates
*/
i = 0; /* x element index */ i = 0; /* x element index */
do { do {
xvec[i] = x; xvec[i] = x;
@ -1035,118 +1050,253 @@ xml_yang_validate_unique_recurse(cxobj *xt,
goto done; goto done;
} }
/*----------- New linear vector code -----------------*/
static int
vec_free(struct vec_order *vec,
size_t vlen)
{
int v;
for (v=0; v<vlen; v++){
if (vec[v].vo_strvec)
free(vec[v].vo_strvec);
}
free(vec);
return 0;
}
/*! Leaf-list qsort comparison function
*/
static int
cmp_list_qsort(const void *arg1,
const void *arg2)
{
struct vec_order *v1 = (struct vec_order *)arg1;
struct vec_order *v2 = (struct vec_order *)arg2;
int i1;
int i2;
int eq;
int i;
eq = 0;
for (i=0; i<v1->vo_slen; i++){
assert(v1->vo_strvec[i]);
assert(v2->vo_strvec[i]);
if ((eq = strcmp(v1->vo_strvec[i], v2->vo_strvec[i])) != 0)
break;
}
if (eq != 0)
return eq;
i1 = xml_enumerate_get(v1->vo_xml);
i2 = xml_enumerate_get(v2->vo_xml);
if (i1 > i2)
return 1;
else if (i1 < i2)
return -1;
else
return 0;
}
/*! Remove duplicates list
*/
static int
remove_duplicates_list(struct vec_order *vec,
size_t vlen,
int *nr)
{
int retval = -1;
int v;
int i;
if (nr)
*nr = 0;
for (v=1; v<vlen; v++){
for (i=0; i<vec[v-1].vo_slen; i++){
if (clicon_strcmp(vec[v-1].vo_strvec[i], vec[v].vo_strvec[i]) != 0)
break;
}
if (i==vec[v-1].vo_slen){
if (xml_purge(vec[v-1].vo_xml) < 0)
goto done;
if (nr)
(*nr)++;
}
}
retval = 0;
done:
return retval;
}
static int
vec_order_analyze(yang_stmt *y,
struct vec_order *vec,
size_t vlen,
cxobj *x)
{
int retval = -1;
int nr = 0;
if (yang_find(y, Y_ORDERED_BY, "user") != NULL)
qsort(vec, vlen, sizeof(*vec), cmp_list_qsort);
if (remove_duplicates_list(vec, vlen, &nr) < 0)
goto done;
if (x && nr)
xml_vector_decrement(x, nr);
retval = 0;
done:
return retval;
}
/*! YANG unique check and remove duplicates, keep last /*! YANG unique check and remove duplicates, keep last
* *
* Assume xt:s children are sorted and yang populated. * Assume xt:s children are sorted and yang populated.
* @param[in] xt XML parent (may have lists w unique constraints as child) * @param[in] xt XML parent (may have lists w unique constraints as child)
* @param[out] xret Error XML tree. Free with xml_free after use * @param[out] xret Error XML tree. Free with xml_free after use
* @retval 2 Locally abort this subtree, continue with others * @retval 0 OK
* @retval 1 Abort, dont continue with others, return 1 to end user * @retval -1 Error
* @retval 0 OK, continue
* @retval -1 Error, aborted at first error encounter, return -1 to end user
* @see xml_yang_validate_minmax which include these unique tests * @see xml_yang_validate_minmax which include these unique tests
*/ */
static int static int
xml_duplicate_remove(cxobj *xt, xml_duplicate_remove(cxobj *xt)
void *arg)
{ {
int retval = -1; int retval = -1;
cxobj **xret = (cxobj **)arg;
cxobj *x; cxobj *x;
yang_stmt *y; yang_stmt *y;
yang_stmt *yprev; yang_stmt *y0;
enum rfc_6020 keyw; enum rfc_6020 keyw;
int again; char *b;
int ret; size_t vlen = 0;
struct vec_order *vec = NULL;
cvec *cvk;
cg_var *cvi;
size_t clen;
size_t slen0; /* sanity check, ensure all vectors are equal length */
char *str;
cxobj *xi;
int v;
again = 1; xml_enumerate_children(xt); // Could be done in-line
while (again){ y0 = NULL;
again = 0; slen0 = 0;
yprev = NULL;
x = NULL; x = NULL;
while ((x = xml_child_each(xt, x, CX_ELMNT)) != NULL){ while ((x = xml_child_each(xt, x, CX_ELMNT)) != NULL) {
if ((y = xml_spec(x)) == NULL) if ((y = xml_spec(x)) == NULL)
continue; continue;
keyw = yang_keyword_get(y); if (y != y0 && vec != NULL){ /* New */
if (keyw == Y_LIST || keyw == Y_LEAF_LIST){ if (vec_order_analyze(y0, vec, vlen, x) < 0)
if (y == yprev){ /* equal: continue, assume list check does look-forward */ goto done;
continue; if (vec_free(vec, vlen) < 0)
goto done;
vec = NULL;
vlen = 0;
slen0 = 0;
} }
/* new list check */ keyw = yang_keyword_get(y);
switch (keyw){ switch (keyw){
case Y_LIST: case Y_LIST:
if ((ret = xml_yang_minmax_new_list(x, xt, y, XML_FLAG_DEL, xret)) < 0) if ((cvk = yang_cvec_get(y)) == NULL)
continue;
if ((clen = cvec_len(cvk)) == 0)
continue;
if (vec>0 && slen0 != clen){ /* Sanity check */
clixon_err(OE_YANG, 0, "List key vector mismatch %lu != %lu", slen0, clen);
goto done; goto done;
if (ret == 0){
if (xml_tree_prune_flags1(xt, XML_FLAG_DEL, XML_FLAG_DEL, 0, &again) < 0)
goto done;
if (again){
if (xret && *xret){
xml_free(*xret);
*xret = NULL;
} }
/* see check_unique_list_direct */
if ((vec = realloc(vec, (vlen+1)*sizeof(*vec))) == NULL){
clixon_err(OE_UNIX, errno, "cvec_new");
goto done;
}
vec[vlen].vo_slen = clen;
vec[vlen].vo_xml = x;
if ((vec[vlen].vo_strvec = calloc(vec[vlen].vo_slen , sizeof(char*))) == NULL){
clixon_err(OE_UNIX, errno, "calloc");
goto done;
}
cvi = NULL;
v = 0;
while ((cvi = cvec_each(cvk, cvi)) != NULL){
if ((str = cv_string_get(cvi)) == NULL)
break; break;
if ((xi = xml_find(x, str)) == NULL)
break;
if ((b = xml_body(xi)) == NULL)
vec[vlen].vo_strvec[v++] = "";
else
vec[vlen].vo_strvec[v++] = b;
} }
goto fail; if (cvi != NULL){ /* No key or null: revert and skip */
free(vec[vlen].vo_strvec);
memset(&vec[vlen], 0, sizeof(*vec));
vec[vlen].vo_strvec = NULL;
}
else{
slen0 = clen;
vlen++;
} }
break; break;
case Y_LEAF_LIST: case Y_LEAF_LIST:
if ((ret = xml_yang_minmax_new_leaf_list(x, xt, y, XML_FLAG_DEL, xret)) < 0) if (vec>0 && slen0 != 1){ /* Sanity check */
clixon_err(OE_YANG, 0, "Leaf-list key vector mismatch %lu != 1", slen0);
goto done; goto done;
if (ret == 0){ }
if (xml_tree_prune_flags1(xt, XML_FLAG_DEL, XML_FLAG_DEL, 0, &again) < 0) if ((vec = realloc(vec, (vlen+1)*sizeof(struct vec_order))) == NULL){
clixon_err(OE_UNIX, errno, "cvec_new");
goto done; goto done;
if (again){
if (xret && *xret){
xml_free(*xret);
*xret = NULL;
} }
break; vec[vlen].vo_xml = x;
} vec[vlen].vo_slen = 1;
goto fail; if ((vec[vlen].vo_strvec = calloc(vec[vlen].vo_slen, sizeof(char*))) == NULL){
clixon_err(OE_UNIX, errno, "calloc");
goto done;
} }
b = xml_body(x);
vec[vlen].vo_strvec[0] = b;
slen0 = 1;
vlen++;
break; break;
default: default:
break; break;
} }
if (again) y0 = y;
break;
yprev = y;
}
} }
if (y0 && vec != NULL){
if (vec_order_analyze(y0, vec, vlen, NULL) < 0)
goto done;
if (vec_free(vec, vlen) < 0)
goto done;
vec = NULL;
vlen = 0;
} }
retval = 0; retval = 0;
done: done:
if (vec)
free(vec);
return retval; return retval;
fail:
retval = 1;
goto done;
} }
/*! Recursive YANG unique check and remove duplicates, keep last /*! Recursive YANG unique check and remove duplicates, keep last
* *
* @param[in] xt XML parent (may have lists w unique constraints as child) * @param[in] xt XML parent (may have lists w unique constraints as child)
* @param[out] xret Error XML tree. Free with xml_free after use * @retval 0 Validation OK
* @retval 1 Validation OK
* @retval 0 Validation failed (xret set)
* @retval -1 Error * @retval -1 Error
* @see xml_yang_validate_unique_recurse * @see xml_yang_validate_unique_recurse This function destructively removes
*/ */
int int
xml_duplicate_remove_recurse(cxobj *xt, xml_duplicate_remove_recurse(cxobj *xt)
cxobj **xret)
{ {
int retval = -1; int retval = -1;
int ret; cxobj *x;
if ((ret = xml_apply0(xt, CX_ELMNT, xml_duplicate_remove, xret)) < 0) if (xml_duplicate_remove(xt) < 0)
goto done; goto done;
if (ret == 1) x = NULL;
goto fail; while ((x = xml_child_each(xt, x, CX_ELMNT)) != NULL) {
retval = 1; if (xml_duplicate_remove_recurse(x) < 0)
goto done;
}
retval = 0;
done: done:
return retval; return retval;
fail:
retval = 0;
goto done;
} }

View file

@ -909,6 +909,16 @@ xml_child_order(cxobj *xp,
return -1; return -1;
} }
/*! Advanced function to decrement _x_vector_i if objects have been removed
*/
int
xml_vector_decrement(cxobj *x,
int nr)
{
x->_x_vector_i -= nr;
return 0;
}
/*! Iterator over xml children objects /*! Iterator over xml children objects
* *
* @param[in] xparent xml tree node whose children should be iterated * @param[in] xparent xml tree node whose children should be iterated

View file

@ -58,6 +58,10 @@ module unique{
leaf-list b{ leaf-list b{
type string; type string;
} }
leaf-list buser{
ordered-by user;
type string;
}
} }
} }
EOF EOF
@ -98,7 +102,7 @@ expecteof_netconf "$clixon_netconf -qf $cfg" 0 "$DEFAULTHELLO" "<rpc $DEFAULTNS>
</server> </server>
</c></config></edit-config></rpc>" "" "<rpc-reply $DEFAULTNS><rpc-error><error-type>application</error-type><error-tag>operation-failed</error-tag><error-app-tag>data-not-unique</error-app-tag><error-severity>error</error-severity><error-info><non-unique xmlns=\"urn:ietf:params:xml:ns:yang:1\">/rpc/edit-config/config/c/server[name=\"one\"]/name</non-unique></error-info></rpc-error></rpc-reply>" </c></config></edit-config></rpc>" "" "<rpc-reply $DEFAULTNS><rpc-error><error-type>application</error-type><error-tag>operation-failed</error-tag><error-app-tag>data-not-unique</error-app-tag><error-severity>error</error-severity><error-info><non-unique xmlns=\"urn:ietf:params:xml:ns:yang:1\">/rpc/edit-config/config/c/server[name=\"one\"]/name</non-unique></error-info></rpc-error></rpc-reply>"
new "Add list with duplicate" new "Add list with duplicate 2"
expecteof_netconf "$clixon_netconf -qf $cfg" 0 "$DEFAULTHELLO" "<rpc $DEFAULTNS><edit-config><target><candidate/></target><default-operation>replace</default-operation><config><c xmlns=\"urn:example:clixon\"> expecteof_netconf "$clixon_netconf -qf $cfg" 0 "$DEFAULTHELLO" "<rpc $DEFAULTNS><edit-config><target><candidate/></target><default-operation>replace</default-operation><config><c xmlns=\"urn:example:clixon\">
<server> <server>
<name>one</name> <name>one</name>
@ -142,9 +146,8 @@ if [ $BE -ne 0 ]; then
fi fi
# Check CLICON_NETCONF_DUPLICATE_ALLOW # Check CLICON_NETCONF_DUPLICATE_ALLOW
if [ $BE -ne 0 ]; then if [ $BE -ne 0 ]; then
new "start backend -s init -f $cfg" new "start backend -s init -f $cfg -o CLICON_NETCONF_DUPLICATE_ALLOW=true"
# start new backend # start new backend
start_backend -s init -f $cfg -o CLICON_NETCONF_DUPLICATE_ALLOW=true start_backend -s init -f $cfg -o CLICON_NETCONF_DUPLICATE_ALLOW=true
fi fi
@ -246,16 +249,82 @@ expecteof_netconf "$clixon_netconf -qf $cfg" 0 "$DEFAULTHELLO" "<rpc $DEFAULTNS>
new "netconf discard-changes" new "netconf discard-changes"
expecteof_netconf "$clixon_netconf -qf $cfg" 0 "$DEFAULTHELLO" "<rpc $DEFAULTNS><discard-changes/></rpc>" "" "<rpc-reply $DEFAULTNS><ok/></rpc-reply>" expecteof_netconf "$clixon_netconf -qf $cfg" 0 "$DEFAULTHELLO" "<rpc $DEFAULTNS><discard-changes/></rpc>" "" "<rpc-reply $DEFAULTNS><ok/></rpc-reply>"
new "Add leaf-list with duplicate" new "leaf-list with dups"
expecteof_netconf "$clixon_netconf -qf $cfg" 0 "$DEFAULTHELLO" "<rpc $DEFAULTNS><edit-config><target><candidate/></target><default-operation>replace</default-operation><config><c xmlns=\"urn:example:clixon\"> expecteof_netconf "$clixon_netconf -qf $cfg" 0 "$DEFAULTHELLO" "<rpc $DEFAULTNS><edit-config><target><candidate/></target><default-operation>replace</default-operation><config><c xmlns=\"urn:example:clixon\">
<b>aaa</b> <b>ccc</b>
<b>aaa</b> <b>aaa</b>
<b>bbb</b> <b>bbb</b>
<b>aaa</b>
</c></config></edit-config></rpc>" "" "<rpc-reply $DEFAULTNS><ok/></rpc-reply>" </c></config></edit-config></rpc>" "" "<rpc-reply $DEFAULTNS><ok/></rpc-reply>"
new "Check leaf-list no duplicates" new "Check leaf-list with dups"
expecteof_netconf "$clixon_netconf -qf $cfg" 0 "$DEFAULTHELLO" "<rpc $DEFAULTNS><get-config><source><candidate/></source></get-config></rpc>" "" "<rpc-reply $DEFAULTNS><data><c xmlns=\"urn:example:clixon\"><b>aaa</b><b>bbb</b></c></data></rpc-reply>" expecteof_netconf "$clixon_netconf -qf $cfg" 0 "$DEFAULTHELLO" "<rpc $DEFAULTNS><get-config><source><candidate/></source></get-config></rpc>" "" "<rpc-reply $DEFAULTNS><data><c xmlns=\"urn:example:clixon\"><b>aaa</b><b>bbb</b><b>ccc</b></c></data></rpc-reply>"
new "leaf-list with dups ordered-by user"
expecteof_netconf "$clixon_netconf -qf $cfg" 0 "$DEFAULTHELLO" "<rpc $DEFAULTNS><edit-config><target><candidate/></target><default-operation>replace</default-operation><config><c xmlns=\"urn:example:clixon\">
<buser>CCC</buser>
<buser>AAA</buser>
<buser>BBB</buser>
<buser>AAA</buser>
<buser>AAA</buser>
</c></config></edit-config></rpc>" "" "<rpc-reply $DEFAULTNS><ok/></rpc-reply>"
new "Check"
expecteof_netconf "$clixon_netconf -qf $cfg" 0 "$DEFAULTHELLO" "<rpc $DEFAULTNS><get-config><source><candidate/></source></get-config></rpc>" "" "<rpc-reply $DEFAULTNS><data><c xmlns=\"urn:example:clixon\"><buser>CCC</buser><buser>BBB</buser><buser>AAA</buser></c></data></rpc-reply>"
new "list/leaf-list mix"
expecteof_netconf "$clixon_netconf -qf $cfg" 0 "$DEFAULTHELLO" "<rpc $DEFAULTNS><edit-config><target><candidate/></target><default-operation>replace</default-operation><config><c xmlns=\"urn:example:clixon\">
<b>ccc</b>
<buser>CCC</buser>
<user>
<name>bbb</name>
<value>foo</value>
</user>
<b>aaa</b>
<buser>AAA</buser>
<user>
<name>aaa</name>
<value>foo</value>
</user>
<b>bbb</b>
<buser>BBB</buser>
<user>
<name>bbb</name>
<value>foo</value>
</user>
<b>aaa</b>
<buser>AAA</buser>
</c></config></edit-config></rpc>" "" "<rpc-reply $DEFAULTNS><ok/></rpc-reply>"
new "Check mix"
expecteof_netconf "$clixon_netconf -qf $cfg" 0 "$DEFAULTHELLO" "<rpc $DEFAULTNS><get-config><source><candidate/></source></get-config></rpc>" "" "<rpc-reply $DEFAULTNS><data><c xmlns=\"urn:example:clixon\"><user><name>aaa</name><value>foo</value></user><user><name>bbb</name><value>foo</value></user><b>aaa</b><b>bbb</b><b>ccc</b><buser>CCC</buser><buser>BBB</buser><buser>AAA</buser></c></data></rpc-reply>"
new "Mix with empty"
expecteof_netconf "$clixon_netconf -qf $cfg" 0 "$DEFAULTHELLO" "<rpc $DEFAULTNS><edit-config><target><candidate/></target><default-operation>replace</default-operation><config><c xmlns=\"urn:example:clixon\">
<b>ccc</b>
<buser>CCC</buser>
<user>
<name></name>
<value>foo</value>
</user>
<b></b>
<buser>AAA</buser>
<user>
<name>aaa</name>
<value>foo</value>
</user>
<b>bbb</b>
<buser>BBB</buser>
<user>
<name></name>
<value>foo</value>
</user>
<b></b>
<buser>AAA</buser>
</c></config></edit-config></rpc>" "" "<rpc-reply $DEFAULTNS><ok/></rpc-reply>"
new "Check mix w empty"
expecteof_netconf "$clixon_netconf -qf $cfg" 0 "$DEFAULTHELLO" "<rpc $DEFAULTNS><get-config><source><candidate/></source></get-config></rpc>" "" "<rpc-reply $DEFAULTNS><data><c xmlns=\"urn:example:clixon\"><user><name>aaa</name><value>foo</value></user><user><name/><value>foo</value></user><b/><b>bbb</b><b>ccc</b><buser>CCC</buser><buser>BBB</buser><buser>AAA</buser></c></data></rpc-reply>"
if [ $BE -ne 0 ]; then if [ $BE -ne 0 ]; then
new "Kill backend" new "Kill backend"