tests: add test for json-streamer.c error recovery
Before rewriting the error recovery code to work in a push parsing setup, make sure that we have tests for it. Cover various cases of invalid JSON, to check that structural recovery based on balanced brackets and braces works; and lexer-based recovery which documents "\f" as a sure fire way to reset the lexer. Reviewed-by: Markus Armbruster <armbru@redhat.com> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
Paolo Bonzini committed
Mar 31, 2026 at 10:22 UTC
b1d5be8011592ae4b1a5fe8eb4d3383a104ed615
2 files changed
+160
tests/unit/check-json-parser.c
new
+159
@@ -0,0 +1,159 @@
1
+/*
2
+ * Unit tests for JSON Parser error recovery
3
+ *
4
+ * Copyright 2026 Red Hat
5
+ * Author: Paolo Bonzini <pbonzini@redhat.com>
6
+ *
7
+ * This work is licensed under the terms of the GNU LGPL, version 2.1 or later.
8
+ * See the COPYING.LIB file in the top-level directory.
9
+ */
10
+
11
+/*
12
+ * Missing tests:
13
+ * - multiple JSON values in a single stream
14
+ * - multiple invocations of json_message_parser_feed()
15
+ * (does not really matter much because of how
16
+ * json_lexer_feed() is implemented)
17
+ * - most JSON types are only covered by check-json.c.
18
+ */
19
+
20
+#include "qemu/osdep.h"
21
+
22
+#include "qapi/error.h"
23
+#include "qobject/qbool.h"
24
+#include "qobject/json-parser.h"
25
+
26
+typedef struct ParseResult {
27
+ int errors;
28
+ QObject *result;
29
+} ParseResult;
30
+
31
+static void parse_emit(void *opaque, QObject *json, Error *err)
32
+{
33
+ ParseResult *r = opaque;
34
+
35
+ g_assert_cmpint(!json, !=, !err);
36
+ if (err) {
37
+ r->errors++;
38
+ error_free(err);
39
+ } else {
40
+ g_assert_null(r->result);
41
+ r->result = json;
42
+ }
43
+}
44
+
45
+static ParseResult do_parse(const char *input)
46
+{
47
+ ParseResult r = { 0, NULL };
48
+ JSONMessageParser parser;
49
+
50
+ json_message_parser_init(&parser, parse_emit, &r, NULL);
51
+ json_message_parser_feed(&parser, input, strlen(input));
52
+ json_message_parser_flush(&parser);
53
+ json_message_parser_destroy(&parser);
54
+ return r;
55
+}
56
+
57
+static void check_result(const char *input, int expected_errors,
58
+ QType expected_type)
59
+{
60
+ ParseResult r = do_parse(input);
61
+
62
+ g_assert_cmpint(r.errors, ==, expected_errors);
63
+ g_assert_nonnull(r.result);
64
+ g_assert_cmpint(qobject_type(r.result), ==, expected_type);
65
+ qobject_unref(r.result);
66
+}
67
+
68
+static void check_result_error(const char *input, int expected_errors)
69
+{
70
+ ParseResult r = do_parse(input);
71
+
72
+ g_assert_cmpint(r.errors, ==, expected_errors);
73
+ g_assert_null(r.result);
74
+}
75
+
76
+static void test_simple(void)
77
+{
78
+ check_result("false", 0, QTYPE_QBOOL);
79
+}
80
+
81
+static void test_whitespace(void)
82
+{
83
+ check_result(" false", 0, QTYPE_QBOOL);
84
+}
85
+
86
+static void test_extra_closing_braces(void)
87
+{
88
+ check_result("}}false", 2, QTYPE_QBOOL);
89
+}
90
+
91
+static void test_bad_dict(void)
92
+{
93
+ check_result("{ 'abc' }false", 1, QTYPE_QBOOL);
94
+}
95
+
96
+static void test_trailing_comma(void)
97
+{
98
+ check_result("[ 'abc', ]false", 1, QTYPE_QBOOL);
99
+}
100
+
101
+static void test_lexer_recovery(void)
102
+{
103
+ check_result("\f{}", 1, QTYPE_QDICT);
104
+ check_result("\f[]", 1, QTYPE_QLIST);
105
+ check_result("\f:false", 2, QTYPE_QBOOL);
106
+ check_result("\f,false", 2, QTYPE_QBOOL);
107
+
108
+ /*
109
+ * Alphabetic characters do not start a new parsing. This is
110
+ * slightly weird but it keeps the lexer simple and works well for
111
+ * QMP (where valid input is a sequence of dictionaries).
112
+ */
113
+ check_result_error("\ffalse", 1);
114
+ check_result_error("\f'str'", 1);
115
+ check_result_error("\f\"str\"", 1);
116
+}
117
+
118
+static void test_lexer_recovery_nested(void)
119
+{
120
+ check_result("{[{\f{}", 1, QTYPE_QDICT);
121
+ check_result("{[{\f[]", 1, QTYPE_QLIST);
122
+ check_result("{[{\f:false", 2, QTYPE_QBOOL);
123
+ check_result("{[{\f,false", 2, QTYPE_QBOOL);
124
+
125
+ /*
126
+ * As in test_lexer_recovery, these do not produce a successful
127
+ * parse after \f.
128
+ */
129
+ check_result_error("{[{\ffalse", 1);
130
+ check_result_error("{[{\f'str'", 1);
131
+ check_result_error("{[{\f\"str\"", 1);
132
+}
133
+
134
+static void test_nested(void)
135
+{
136
+ check_result("[{'a']}false", 1, QTYPE_QBOOL);
137
+}
138
+
139
+static void test_nested_multiple(void)
140
+{
141
+ check_result("[{'a']}[{'a']}false", 2, QTYPE_QBOOL);
142
+}
143
+
144
+int main(int argc, char **argv)
145
+{
146
+ g_test_init(&argc, &argv, NULL);
147
+
148
+ g_test_add_func("/json-parser/simple", test_simple);
149
+ g_test_add_func("/json-parser/whitespace", test_whitespace);
150
+ g_test_add_func("/json-parser/error-recovery/extra-closing-braces", test_extra_closing_braces);
151
+ g_test_add_func("/json-parser/error-recovery/bad-dict", test_bad_dict);
152
+ g_test_add_func("/json-parser/error-recovery/trailing-comma", test_trailing_comma);
153
+ g_test_add_func("/json-parser/error-recovery/lexer", test_lexer_recovery);
154
+ g_test_add_func("/json-parser/error-recovery/lexer/nested", test_lexer_recovery_nested);
155
+ g_test_add_func("/json-parser/error-recovery/nested", test_nested);
156
+ g_test_add_func("/json-parser/error-recovery/nested/multiple", test_nested_multiple);
157
+
158
+ return g_test_run();
159
+}
tests/unit/meson.build
+1
@@ -10,6 +10,7 @@ tests = {
10
'check-qnull': [],
11
'check-qobject': [],
12
'check-qjson': [],
13
+ 'check-json-parser': [],
14
'check-qlit': [],
15
'test-error-report': [],
16
'test-qobject-output-visitor': [testqapi],