From: "Daniel P. Berrange" <berrange(a)redhat.com>
Comparing JSON docs using strcmp is simple, but is not flexible
as it is sensitive to whitespace used in the doc generation.
When comparing objects it may also be desirable to treat the
existance of keys in the actual object but not expected object
as non-fatal. Introduce a virJSONStringCompare function which
takes two strings representing expected and actual JSON docs
and then does a DOM comparison. Comparison is controled with
the ignore_contexts and flags parameters. No comparison is
done on context paths specified in ignore_contexts. The
VIR_JSON_COMPARE_IGNORE_EXPECTED_NULL flag can be used to
ignore actual values that have changed from an expected value
of null.
Signed-off-by: Daniel P. Berrange <berrange(a)redhat.com>
Signed-off-by: Jim Fehlig <jfehlig(a)suse.com>
---
Beyond rebasing, unchanged from V3.
src/libvirt_private.syms | 1 +
src/util/virjson.c | 242 +++++++++++++++++++++++++++++++++++++++++++++++
src/util/virjson.h | 16 ++++
3 files changed, 259 insertions(+)
diff --git a/src/libvirt_private.syms b/src/libvirt_private.syms
index fdf4548..b0c0625 100644
--- a/src/libvirt_private.syms
+++ b/src/libvirt_private.syms
@@ -1446,6 +1446,7 @@ virISCSIScanTargets;
# util/virjson.h
+virJSONStringCompare;
virJSONValueArrayAppend;
virJSONValueArrayGet;
virJSONValueArraySize;
diff --git a/src/util/virjson.c b/src/util/virjson.c
index ec83b2f..73b71f4 100644
--- a/src/util/virjson.c
+++ b/src/util/virjson.c
@@ -47,6 +47,11 @@
VIR_LOG_INIT("util.json");
+VIR_ENUM_DECL(virJSONType)
+VIR_ENUM_IMPL(virJSONType, VIR_JSON_TYPE_LAST,
+ "object", "array", "string",
+ "number", "boolean", "null")
+
typedef struct _virJSONParserState virJSONParserState;
typedef virJSONParserState *virJSONParserStatePtr;
struct _virJSONParserState {
@@ -91,6 +96,7 @@ virJSONValueFree(virJSONValuePtr value)
break;
case VIR_JSON_TYPE_BOOLEAN:
case VIR_JSON_TYPE_NULL:
+ case VIR_JSON_TYPE_LAST:
break;
}
@@ -1107,6 +1113,204 @@ virJSONParserHandleEndArray(void *ctx)
}
+static bool
+virJSONValueCompare(virJSONValuePtr expect,
+ virJSONValuePtr actual,
+ const char *context,
+ const char **ignore_contexts,
+ unsigned int flags)
+{
+ size_t i, j;
+
+ if (expect->type != actual->type) {
+ if (expect->type == VIR_JSON_TYPE_NULL &&
+ (flags & VIR_JSON_COMPARE_IGNORE_EXPECTED_NULL))
+ return true;
+
+ const char *expectType = virJSONTypeTypeToString(expect->type);
+ const char *actualType = virJSONTypeTypeToString(actual->type);
+
+ virReportError(VIR_ERR_INTERNAL_ERROR,
+ _("Expected value type '%s' but got value type
'%s' at '%s'"),
+ expectType, actualType, context);
+ return false;
+ }
+
+ switch (expect->type) {
+ case VIR_JSON_TYPE_OBJECT:
+ /* Ensure actual data contains all expected data */
+ for (i = 0; i < expect->data.object.npairs; i++) {
+ bool found = false;
+ char *childcontext;
+
+ if (virAsprintf(&childcontext, "%s%s%s",
+ context,
+ STREQ(context, "/") ? "" :
"/",
+ expect->data.object.pairs[i].key) < 0)
+ return false;
+
+ /* Bypass paths we've been asked to ignore */
+ if (ignore_contexts) {
+ bool ignored = false;
+
+ j = 0;
+ while (ignore_contexts[j]) {
+ if (STREQ(childcontext, ignore_contexts[j])) {
+ ignored = true;
+ break;
+ }
+ j++;
+ }
+
+ if (ignored)
+ continue;
+ }
+
+ for (j = 0; j < actual->data.object.npairs; j++) {
+ if (STREQ(expect->data.object.pairs[i].key,
+ actual->data.object.pairs[j].key)) {
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ virReportError(VIR_ERR_INTERNAL_ERROR,
+ _("Expected object key '%s' not found in
actual object at '%s'"),
+ expect->data.object.pairs[i].key, context);
+ VIR_FREE(childcontext);
+ return false;
+ }
+
+ if (!virJSONValueCompare(expect->data.object.pairs[i].value,
+ actual->data.object.pairs[j].value,
+ childcontext,
+ ignore_contexts,
+ flags)) {
+ VIR_FREE(childcontext);
+ return false;
+ }
+ VIR_FREE(childcontext);
+ }
+
+ /* Ensure expected data contains all actual data */
+ for (i = 0; i < actual->data.object.npairs; i++) {
+ bool found = false;
+ char *childcontext;
+
+ if (virAsprintf(&childcontext, "%s%s%s",
+ context,
+ STREQ(context, "/") ? "" :
"/",
+ actual->data.object.pairs[i].key) < 0)
+ return false;
+
+ /* Bypass paths we've been asked to ignore */
+ if (ignore_contexts) {
+ bool ignored = false;
+
+ j = 0;
+ while (ignore_contexts[j]) {
+ if (STREQ(childcontext, ignore_contexts[j])) {
+ ignored = true;
+ break;
+ }
+ j++;
+ }
+
+ if (ignored)
+ continue;
+ }
+
+ for (j = 0; j < expect->data.object.npairs; j++) {
+ if (STREQ(actual->data.object.pairs[i].key,
+ expect->data.object.pairs[j].key)) {
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ virReportError(VIR_ERR_INTERNAL_ERROR,
+ _("Actual object key '%s' not found in
expected object at '%s'"),
+ actual->data.object.pairs[i].key, context);
+ VIR_FREE(childcontext);
+ return false;
+ }
+
+ if (!virJSONValueCompare(actual->data.object.pairs[i].value,
+ expect->data.object.pairs[j].value,
+ childcontext,
+ ignore_contexts,
+ flags)) {
+ VIR_FREE(childcontext);
+ }
+ }
+ break;
+
+ case VIR_JSON_TYPE_ARRAY:
+ if (expect->data.array.nvalues !=
+ actual->data.array.nvalues) {
+ virReportError(VIR_ERR_INTERNAL_ERROR,
+ _("Expected array size '%zu' but got size
'%zu' at '%s'"),
+ expect->data.array.nvalues,
+ actual->data.array.nvalues,
+ context);
+ return false;
+ }
+
+ for (i = 0; i < expect->data.array.nvalues; i++) {
+ if (!virJSONValueCompare(expect->data.array.values[i],
+ actual->data.array.values[i],
+ context,
+ ignore_contexts,
+ flags))
+ return false;
+ }
+ break;
+
+ case VIR_JSON_TYPE_STRING:
+ if (STRNEQ(expect->data.string,
+ actual->data.string)) {
+ virReportError(VIR_ERR_INTERNAL_ERROR,
+ _("Expected string value '%s' but got
'%s' at '%s'"),
+ expect->data.string, actual->data.string, context);
+ return false;
+ }
+ break;
+
+ case VIR_JSON_TYPE_NUMBER:
+ if (STRNEQ(expect->data.number,
+ actual->data.number)) {
+ virReportError(VIR_ERR_INTERNAL_ERROR,
+ _("Expected number value '%s' but got
'%s' at '%s'"),
+ expect->data.number, actual->data.number, context);
+ return false;
+ }
+ break;
+
+ case VIR_JSON_TYPE_BOOLEAN:
+ if (expect->data.boolean !=
+ actual->data.boolean) {
+ virReportError(VIR_ERR_INTERNAL_ERROR,
+ _("Expected bool value '%d' but got '%d'
at '%s'"),
+ expect->data.boolean, actual->data.boolean, context);
+ return false;
+ }
+ break;
+
+ case VIR_JSON_TYPE_NULL:
+ /* trivially equal */
+ break;
+
+ case VIR_JSON_TYPE_LAST:
+ /* nothing */
+ break;
+ }
+
+ return true;
+}
+
+
static const yajl_callbacks parserCallbacks = {
virJSONParserHandleNull,
virJSONParserHandleBoolean,
@@ -1306,6 +1510,30 @@ virJSONValueToString(virJSONValuePtr object,
}
+bool
+virJSONStringCompare(const char *expect,
+ const char *actual,
+ const char **ignore_contexts,
+ unsigned int flags)
+{
+ virJSONValuePtr expectVal = NULL;
+ virJSONValuePtr actualVal = NULL;
+ int ret = false;
+
+ if (!(expectVal = virJSONValueFromString(expect)))
+ goto cleanup;
+ if (!(actualVal = virJSONValueFromString(actual)))
+ goto cleanup;
+
+ ret = virJSONValueCompare(expectVal, actualVal, "/", ignore_contexts,
flags);
+
+ cleanup:
+ virJSONValueFree(expectVal);
+ virJSONValueFree(actualVal);
+ return ret;
+}
+
+
#else
virJSONValuePtr
virJSONValueFromString(const char *jsonstring ATTRIBUTE_UNUSED)
@@ -1324,4 +1552,18 @@ virJSONValueToString(virJSONValuePtr object ATTRIBUTE_UNUSED,
_("No JSON parser implementation is available"));
return NULL;
}
+
+
+bool
+virJSONStringCompare(const char *expect ATTRIBUTE_UNUSED,
+ const char *actual ATTRIBUTE_UNUSED,
+ const char **ignore_contexts ATTRIBUTE_UNUSED,
+ unsigned int flags)
+{
+ virCheckFlags(VIR_JSON_COMPARE_IGNORE_EXPECTED_NULL, false);
+
+ virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+ _("No JSON parser implementation is available"));
+ return false;
+}
#endif
diff --git a/src/util/virjson.h b/src/util/virjson.h
index 9487729..5dc948f 100644
--- a/src/util/virjson.h
+++ b/src/util/virjson.h
@@ -34,6 +34,8 @@ typedef enum {
VIR_JSON_TYPE_NUMBER,
VIR_JSON_TYPE_BOOLEAN,
VIR_JSON_TYPE_NULL,
+
+ VIR_JSON_TYPE_LAST,
} virJSONType;
typedef struct _virJSONValue virJSONValue;
@@ -141,4 +143,18 @@ virJSONValuePtr virJSONValueFromString(const char *jsonstring);
char *virJSONValueToString(virJSONValuePtr object,
bool pretty);
+typedef enum {
+ /*
+ * when comparing two values, if their types are different,
+ * and the 'expected' value type is 'null', then this will
+ * be considered non-fatal.
+ */
+ VIR_JSON_COMPARE_IGNORE_EXPECTED_NULL = (1 << 0),
+} virJSONCompareFlags;
+
+bool virJSONStringCompare(const char *expect,
+ const char *actual,
+ const char **ignore_contexts,
+ unsigned int flags);
+
#endif /* __VIR_JSON_H_ */
--
1.8.4.5