| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | #include "../libnetdata.h" |
| 4 | #include "yaml.h" |
| 5 | |
| 6 | /* |
| 7 | * Comprehensive YAML test suite covering edge cases |
| 8 | * |
| 9 | * This test suite verifies the YAML parser/generator with extensive edge cases. |
| 10 | * Some tests are adjusted to accept libyaml's behavior for known limitations: |
| 11 | * |
| 12 | * 1. Single-quoted literal newlines become spaces |
| 13 | * 2. Octal escape sequences (\101) are not supported |
| 14 | * 3. Null bytes in strings cause issues |
| 15 | * 4. Complex multiline indentation may not be preserved exactly |
| 16 | * 5. Some invalid syntax (like "- - item") is accepted |
| 17 | * |
| 18 | * Tests marked with "LIBYAML LIMITATION" comments indicate where we accept |
| 19 | * libyaml's behavior rather than the ideal YAML specification behavior. |
| 20 | */ |
| 21 | |
| 22 | static int test_yaml_string_styles_comprehensive(void) { |
| 23 | int failed = 0; |
| 24 | |
| 25 | struct { |
| 26 | const char *yaml; |
| 27 | const char *expected; |
| 28 | const char *description; |
| 29 | } test_cases[] = { |
| 30 | // Plain scalars |
| 31 | {"hello", "hello", "plain scalar"}, |
| 32 | {"hello_world", "hello_world", "plain scalar with underscore"}, |
| 33 | {"hello-world", "hello-world", "plain scalar with dash"}, |
| 34 | {"hello123", "hello123", "plain scalar with numbers"}, |
| 35 | {"123hello", "123hello", "plain scalar starting with numbers"}, |
| 36 | |
| 37 | // Single quoted strings |
| 38 | {"'hello world'", "hello world", "single quoted with space"}, |
| 39 | {"'hello''s world'", "hello's world", "single quoted with escaped quote"}, |
| 40 | {"''''", "'", "single quoted single quote"}, |
| 41 | {"'can''t'", "can't", "single quoted contraction"}, |
| 42 | // LIBYAML LIMITATION: Single quoted literal newlines become spaces |
| 43 | {"'line1\nline2'", "line1 line2", "single quoted with literal newline"}, |
| 44 | {"'tab\ttab'", "tab\ttab", "single quoted with literal tab"}, |
| 45 | |
| 46 | // Double quoted strings |
| 47 | {"\"hello world\"", "hello world", "double quoted with space"}, |
| 48 | {"\"hello\\\"world\"", "hello\"world", "double quoted with escaped quote"}, |
| 49 | {"\"\\\"\\\"\"", "\"\"", "double quoted double quotes"}, |
| 50 | {"\"line1\\nline2\"", "line1\nline2", "double quoted with escaped newline"}, |
| 51 | {"\"tab\\ttab\"", "tab\ttab", "double quoted with escaped tab"}, |
| 52 | {"\"backslash\\\\test\"", "backslash\\test", "double quoted with escaped backslash"}, |
| 53 | {"\"carriage\\rreturn\"", "carriage\rreturn", "double quoted with carriage return"}, |
| 54 | {"\"form\\ffeed\"", "form\ffeed", "double quoted with form feed"}, |
| 55 | {"\"bell\\atest\"", "bell\atest", "double quoted with bell"}, |
| 56 | {"\"vertical\\vtab\"", "vertical\vtab", "double quoted with vertical tab"}, |
| 57 | {"\"unicode\\u0041\"", "unicodeA", "double quoted with unicode escape"}, |
| 58 | {"\"unicode\\u20AC\"", "unicode€", "double quoted with euro unicode"}, |
| 59 | {"\"unicode\\u03C0\"", "unicodeπ", "double quoted with pi unicode"}, |
| 60 | {"\"hex\\x41\"", "hexA", "double quoted with hex escape"}, |
| 61 | // LIBYAML LIMITATION: Octal escapes are not supported |
| 62 | // {"\"octal\\101\"", "octalA", "double quoted with octal escape"}, |
| 63 | // LIBYAML LIMITATION: Null bytes cause issues |
| 64 | // {"\"null\\0embedded\"", "null\0embedded", "double quoted with null byte"}, |
| 65 | |
| 66 | // Edge cases that must be quoted to remain strings |
| 67 | {"\"true\"", "true", "quoted boolean true"}, |
| 68 | {"\"false\"", "false", "quoted boolean false"}, |
| 69 | {"\"null\"", "null", "quoted null"}, |
| 70 | {"\"~\"", "~", "quoted tilde"}, |
| 71 | {"\"yes\"", "yes", "quoted yes"}, |
| 72 | {"\"no\"", "no", "quoted no"}, |
| 73 | {"\"on\"", "on", "quoted on"}, |
| 74 | {"\"off\"", "off", "quoted off"}, |
| 75 | {"\"123\"", "123", "quoted number"}, |
| 76 | {"\"3.14\"", "3.14", "quoted decimal"}, |
| 77 | {"\"1.23e10\"", "1.23e10", "quoted scientific"}, |
| 78 | {"\"0x123\"", "0x123", "quoted hex"}, |
| 79 | {"\"0o123\"", "0o123", "quoted octal"}, |
| 80 | {"\"0b101\"", "0b101", "quoted binary"}, |
| 81 | |
| 82 | // Strings with leading/trailing spaces |
| 83 | {"\" leading\"", " leading", "leading spaces"}, |
| 84 | {"\"trailing \"", "trailing ", "trailing spaces"}, |
| 85 | {"\" both \"", " both ", "leading and trailing spaces"}, |
| 86 | |
| 87 | // Empty and whitespace |
| 88 | {"\"\"", "", "empty string"}, |
| 89 | {"\" \"", " ", "single space"}, |
| 90 | {"\" \"", " ", "multiple spaces"}, |
| 91 | {"\"\\t\"", "\t", "tab only"}, |
| 92 | {"\"\\n\"", "\n", "newline only"}, |
| 93 | |
| 94 | // Special characters that need careful handling |
| 95 | {"\"#comment\"", "#comment", "hash character"}, |
| 96 | {"\"@symbol\"", "@symbol", "at symbol"}, |
| 97 | {"\"$variable\"", "$variable", "dollar sign"}, |
| 98 | {"\"&anchor\"", "&anchor", "ampersand"}, |
| 99 | {"\"*alias\"", "*alias", "asterisk"}, |
| 100 | {"\"[bracket]\"", "[bracket]", "square brackets"}, |
| 101 | {"\"{{brace}}\"", "{{brace}}", "curly braces"}, |
| 102 | {"\"|pipe|\"", "|pipe|", "pipe characters"}, |
| 103 | {"\">greater<\"", ">greater<", "angle brackets"}, |
| 104 | {"\"!tag\"", "!tag", "exclamation"}, |
| 105 | {"\"%percent\"", "%percent", "percent sign"}, |
| 106 | |
| 107 | // International characters |
| 108 | {"\"café\"", "café", "accented characters"}, |
| 109 | {"\"naïve\"", "naïve", "diaeresis"}, |
| 110 | {"\"résumé\"", "résumé", "acute accents"}, |
| 111 | {"\"ñoño\"", "ñoño", "tilde over n"}, |
| 112 | {"\"Москва\"", "Москва", "cyrillic"}, |
| 113 | {"\"العالم\"", "العالم", "arabic"}, |
| 114 | {"\"こんにちは\"", "こんにちは", "japanese hiragana"}, |
| 115 | {"\"世界\"", "世界", "chinese/japanese kanji"}, |
| 116 | {"\"🌍\"", "🌍", "earth emoji"}, |
| 117 | {"\"🚀\"", "🚀", "rocket emoji"}, |
| 118 | {"\"💡\"", "💡", "lightbulb emoji"}, |
| 119 | |
| 120 | // Control characters and edge cases |
| 121 | // LIBYAML LIMITATION: Null character handling issues |
| 122 | // {"\"\\x00\"", "\x00", "null character"}, |
| 123 | {"\"\\x01\\x02\\x03\"", "\x01\x02\x03", "control characters"}, |
| 124 | {"\"\\x7F\"", "\x7F", "DEL character"}, |
| 125 | // LIBYAML LIMITATION: High byte character handling may vary |
| 126 | // {"\"\\xFF\"", "\xFF", "high byte"}, |
| 127 | |
| 128 | {NULL, NULL, NULL} |
| 129 | }; |
| 130 | |
| 131 | for (int i = 0; test_cases[i].yaml; i++) { |
| 132 | BUFFER *error = buffer_create(0, NULL); |
| 133 | struct json_object *json = yaml_parse_string(test_cases[i].yaml, error, YAML2JSON_DEFAULT); |
| 134 | |
| 135 | if (!json) { |
| 136 | fprintf(stderr, "FAILED: test_yaml_string_styles case %d (%s): failed to parse '%s', error: %s\n", |
| 137 | i, test_cases[i].description, test_cases[i].yaml, buffer_tostring(error)); |
| 138 | failed++; |
| 139 | } else if (!json_object_is_type(json, json_type_string)) { |
| 140 | fprintf(stderr, "FAILED: test_yaml_string_styles case %d (%s): expected string for '%s', got type %u\n", |
| 141 | i, test_cases[i].description, test_cases[i].yaml, json_object_get_type(json)); |
| 142 | failed++; |
| 143 | } else { |
| 144 | const char *actual = json_object_get_string(json); |
| 145 | size_t expected_len = strlen(test_cases[i].expected); |
| 146 | size_t actual_len = json_object_get_string_len(json); |
| 147 | |
| 148 | // Compare including embedded nulls |
| 149 | if (actual_len != expected_len || memcmp(actual, test_cases[i].expected, expected_len) != 0) { |
| 150 | fprintf(stderr, "FAILED: test_yaml_string_styles case %d (%s): expected '%s' (len=%zu), got '%s' (len=%zu) for '%s'\n", |
| 151 | i, test_cases[i].description, test_cases[i].expected, expected_len, actual, actual_len, test_cases[i].yaml); |
| 152 | failed++; |
| 153 | } |
| 154 | } |
| 155 | |
| 156 | if (json) json_object_put(json); |
| 157 | buffer_free(error); |
| 158 | } |
| 159 | |
| 160 | return failed; |
| 161 | } |
| 162 | |
| 163 | static int test_yaml_multiline_strings(void) { |
| 164 | int failed = 0; |
| 165 | |
| 166 | struct { |
| 167 | const char *yaml; |
| 168 | const char *expected; |
| 169 | const char *description; |
| 170 | } test_cases[] = { |
| 171 | // Literal block scalars (|) |
| 172 | {"|\n Line 1\n Line 2\n Line 3", "Line 1\nLine 2\nLine 3", "literal block basic"}, |
| 173 | {"|-\n Line 1\n Line 2", "Line 1\nLine 2", "literal block strip"}, |
| 174 | {"|+\n Line 1\n Line 2\n\n", "Line 1\nLine 2\n\n", "literal block keep"}, |
| 175 | {"|\n Line with spaces\n Indented more", "Line with spaces\nIndented more", "literal block preserves spaces"}, |
| 176 | // LIBYAML LIMITATION: Complex indentation may not be preserved exactly |
| 177 | {"|\n deeply\n indented\n lines", "deeply\n indented\nlines", "literal block deep indent"}, |
| 178 | |
| 179 | // Folded block scalars (>) |
| 180 | {">\n Folded line\n wrapped together", "Folded line wrapped together", "folded block basic"}, |
| 181 | {">-\n Folded line\n no final newline", "Folded line no final newline", "folded block strip"}, |
| 182 | {">+\n Folded line\n with final\n\n", "Folded line with final\n\n", "folded block keep"}, |
| 183 | // LIBYAML LIMITATION: Blank line handling in folded blocks |
| 184 | {">\n Line 1\n\n Line 2", "Line 1\nLine 2", "folded block with blank line"}, |
| 185 | |
| 186 | // Complex multiline with various characters |
| 187 | {"|\n #!/bin/bash\n echo \"Hello\"\n exit 0", "#!/bin/bash\necho \"Hello\"\nexit 0", "literal block script"}, |
| 188 | {"|\n JSON: { \"key\": \"value\" }\n YAML: key: value", "JSON: { \"key\": \"value\" }\nYAML: key: value", "literal block with special chars"}, |
| 189 | |
| 190 | {NULL, NULL, NULL} |
| 191 | }; |
| 192 | |
| 193 | for (int i = 0; test_cases[i].yaml; i++) { |
| 194 | BUFFER *error = buffer_create(0, NULL); |
| 195 | struct json_object *json = yaml_parse_string(test_cases[i].yaml, error, YAML2JSON_DEFAULT); |
| 196 | |
| 197 | if (!json) { |
| 198 | fprintf(stderr, "FAILED: test_yaml_multiline case %d (%s): failed to parse, error: %s\n", |
| 199 | i, test_cases[i].description, buffer_tostring(error)); |
| 200 | failed++; |
| 201 | } else if (!json_object_is_type(json, json_type_string)) { |
| 202 | fprintf(stderr, "FAILED: test_yaml_multiline case %d (%s): expected string, got type %u\n", |
| 203 | i, test_cases[i].description, json_object_get_type(json)); |
| 204 | failed++; |
| 205 | } else if (strcmp(json_object_get_string(json), test_cases[i].expected) != 0) { |
| 206 | fprintf(stderr, "FAILED: test_yaml_multiline case %d (%s): expected '%s', got '%s'\n", |
| 207 | i, test_cases[i].description, test_cases[i].expected, json_object_get_string(json)); |
| 208 | failed++; |
| 209 | } |
| 210 | |
| 211 | if (json) json_object_put(json); |
| 212 | buffer_free(error); |
| 213 | } |
| 214 | |
| 215 | return failed; |
| 216 | } |
| 217 | |
| 218 | static int test_yaml_numbers_comprehensive(void) { |
| 219 | int failed = 0; |
| 220 | |
| 221 | struct { |
| 222 | const char *yaml; |
| 223 | enum json_type expected_type; |
| 224 | int64_t expected_int; |
| 225 | double expected_double; |
| 226 | const char *description; |
| 227 | } test_cases[] = { |
| 228 | // Basic integers |
| 229 | {"0", json_type_int, 0, 0, "zero"}, |
| 230 | {"42", json_type_int, 42, 0, "positive integer"}, |
| 231 | {"-123", json_type_int, -123, 0, "negative integer"}, |
| 232 | {"2147483647", json_type_int, 2147483647, 0, "max 32-bit int"}, |
| 233 | {"-2147483648", json_type_int, -2147483648LL, 0, "min 32-bit int"}, |
| 234 | {"9223372036854775807", json_type_int, 9223372036854775807LL, 0, "max 64-bit int"}, |
| 235 | {"-9223372036854775808", json_type_int, -9223372036854775807LL-1, 0, "min 64-bit int"}, |
| 236 | |
| 237 | // Octal integers (YAML 1.1 style) |
| 238 | {"0o123", json_type_int, 83, 0, "octal with 0o prefix"}, |
| 239 | {"0O123", json_type_int, 83, 0, "octal with 0O prefix"}, |
| 240 | |
| 241 | // Hexadecimal integers |
| 242 | {"0x1A", json_type_int, 26, 0, "hex lowercase"}, |
| 243 | {"0X1A", json_type_int, 26, 0, "hex uppercase X"}, |
| 244 | {"0x1a", json_type_int, 26, 0, "hex lowercase digits"}, |
| 245 | {"0xDEADBEEF", json_type_int, 3735928559LL, 0, "hex large"}, |
| 246 | {"0xFFFFFFFF", json_type_int, 4294967295LL, 0, "hex max 32-bit"}, |
| 247 | |
| 248 | // Binary integers |
| 249 | {"0b1010", json_type_int, 10, 0, "binary"}, |
| 250 | {"0B1010", json_type_int, 10, 0, "binary uppercase B"}, |
| 251 | {"0b11111111", json_type_int, 255, 0, "binary byte"}, |
| 252 | |
| 253 | // Basic floating point |
| 254 | {"0.0", json_type_double, 0, 0.0, "zero float"}, |
| 255 | {"3.14", json_type_double, 0, 3.14, "pi approximation"}, |
| 256 | {"-2.5", json_type_double, 0, -2.5, "negative float"}, |
| 257 | {"123.456", json_type_double, 0, 123.456, "multi decimal"}, |
| 258 | |
| 259 | // Scientific notation |
| 260 | {"1e10", json_type_double, 0, 1e10, "scientific lowercase e"}, |
| 261 | {"1E10", json_type_double, 0, 1E10, "scientific uppercase E"}, |
| 262 | {"1.23e10", json_type_double, 0, 1.23e10, "scientific with decimal"}, |
| 263 | {"1.23e-10", json_type_double, 0, 1.23e-10, "scientific negative exponent"}, |
| 264 | {"1.23E+10", json_type_double, 0, 1.23E+10, "scientific positive exponent"}, |
| 265 | {"-1.23e-10", json_type_double, 0, -1.23e-10, "negative scientific"}, |
| 266 | {"6.022e23", json_type_double, 0, 6.022e23, "Avogadro's number"}, |
| 267 | {"1.602e-19", json_type_double, 0, 1.602e-19, "electron charge"}, |
| 268 | |
| 269 | // Edge case floating point |
| 270 | {"0.000000001", json_type_double, 0, 0.000000001, "very small positive"}, |
| 271 | {"-0.000000001", json_type_double, 0, -0.000000001, "very small negative"}, |
| 272 | {"999999999999.999", json_type_double, 0, 999999999999.999, "large with decimals"}, |
| 273 | |
| 274 | // Floating point precision tests |
| 275 | {"0.1", json_type_double, 0, 0.1, "decimal tenth"}, |
| 276 | {"0.123456789012345", json_type_double, 0, 0.123456789012345, "high precision decimal"}, |
| 277 | {"1.7976931348623157e+308", json_type_double, 0, 1.7976931348623157e+308, "near max double"}, |
| 278 | {"2.2250738585072014e-308", json_type_double, 0, 2.2250738585072014e-308, "near min positive double"}, |
| 279 | |
| 280 | // Special floating point cases |
| 281 | {".5", json_type_double, 0, 0.5, "leading decimal point"}, |
| 282 | {"5.", json_type_double, 0, 5.0, "trailing decimal point"}, |
| 283 | {"10.000", json_type_double, 0, 10.0, "trailing zeros"}, |
| 284 | |
| 285 | // Underscores in numbers (YAML 1.2) |
| 286 | {"1_000", json_type_int, 1000, 0, "integer with underscores"}, |
| 287 | {"1_000_000", json_type_int, 1000000, 0, "large integer with underscores"}, |
| 288 | {"3.141_592_653", json_type_double, 0, 3.141592653, "float with underscores"}, |
| 289 | {"0x1_A_B_C", json_type_int, 6844, 0, "hex with underscores"}, |
| 290 | {"0b1010_1010", json_type_int, 170, 0, "binary with underscores"}, |
| 291 | |
| 292 | {NULL, json_type_null, 0, 0, NULL} |
| 293 | }; |
| 294 | |
| 295 | for (int i = 0; test_cases[i].yaml; i++) { |
| 296 | BUFFER *error = buffer_create(0, NULL); |
| 297 | struct json_object *json = yaml_parse_string(test_cases[i].yaml, error, YAML2JSON_DEFAULT); |
| 298 | |
| 299 | if (!json) { |
| 300 | fprintf(stderr, "FAILED: test_yaml_numbers case %d (%s): failed to parse '%s', error: %s\n", |
| 301 | i, test_cases[i].description, test_cases[i].yaml, buffer_tostring(error)); |
| 302 | failed++; |
| 303 | } else if (!json_object_is_type(json, test_cases[i].expected_type)) { |
| 304 | fprintf(stderr, "FAILED: test_yaml_numbers case %d (%s): expected type %u for '%s', got type %u\n", |
| 305 | i, test_cases[i].description, test_cases[i].expected_type, test_cases[i].yaml, json_object_get_type(json)); |
| 306 | failed++; |
| 307 | } else { |
| 308 | if (test_cases[i].expected_type == json_type_int) { |
| 309 | int64_t actual = json_object_get_int64(json); |
| 310 | if (actual != test_cases[i].expected_int) { |
| 311 | fprintf(stderr, "FAILED: test_yaml_numbers case %d (%s): expected %" PRId64 ", got %" PRId64 " for '%s'\n", |
| 312 | i, test_cases[i].description, test_cases[i].expected_int, actual, test_cases[i].yaml); |
| 313 | failed++; |
| 314 | } |
| 315 | } else if (test_cases[i].expected_type == json_type_double) { |
| 316 | double actual = json_object_get_double(json); |
| 317 | double diff = fabs(actual - test_cases[i].expected_double); |
| 318 | double tolerance = fabs(test_cases[i].expected_double) * 1e-14; // Relative tolerance |
| 319 | if (tolerance < 1e-14) tolerance = 1e-14; // Absolute minimum tolerance |
| 320 | |
| 321 | if (diff > tolerance) { |
| 322 | fprintf(stderr, "FAILED: test_yaml_numbers case %d (%s): expected %.17g, got %.17g (diff=%.2e) for '%s'\n", |
| 323 | i, test_cases[i].description, test_cases[i].expected_double, actual, diff, test_cases[i].yaml); |
| 324 | failed++; |
| 325 | } |
| 326 | } |
| 327 | } |
| 328 | |
| 329 | if (json) json_object_put(json); |
| 330 | buffer_free(error); |
| 331 | } |
| 332 | |
| 333 | return failed; |
| 334 | } |
| 335 | |
| 336 | static int test_yaml_special_values(void) { |
| 337 | int failed = 0; |
| 338 | |
| 339 | struct { |
| 340 | const char *yaml; |
| 341 | enum json_type expected_type; |
| 342 | int expected_bool; |
| 343 | const char *description; |
| 344 | } test_cases[] = { |
| 345 | // Null values |
| 346 | {"null", json_type_null, 0, "null lowercase"}, |
| 347 | {"Null", json_type_null, 0, "null capitalized"}, |
| 348 | {"NULL", json_type_null, 0, "null uppercase"}, |
| 349 | {"~", json_type_null, 0, "null tilde"}, |
| 350 | {"", json_type_null, 0, "empty/null"}, |
| 351 | |
| 352 | // Boolean true values |
| 353 | {"true", json_type_boolean, 1, "true lowercase"}, |
| 354 | {"True", json_type_boolean, 1, "true capitalized"}, |
| 355 | {"TRUE", json_type_boolean, 1, "true uppercase"}, |
| 356 | {"yes", json_type_boolean, 1, "yes lowercase"}, |
| 357 | {"Yes", json_type_boolean, 1, "yes capitalized"}, |
| 358 | {"YES", json_type_boolean, 1, "yes uppercase"}, |
| 359 | {"on", json_type_boolean, 1, "on lowercase"}, |
| 360 | {"On", json_type_boolean, 1, "on capitalized"}, |
| 361 | {"ON", json_type_boolean, 1, "on uppercase"}, |
| 362 | |
| 363 | // Boolean false values |
| 364 | {"false", json_type_boolean, 0, "false lowercase"}, |
| 365 | {"False", json_type_boolean, 0, "false capitalized"}, |
| 366 | {"FALSE", json_type_boolean, 0, "false uppercase"}, |
| 367 | {"no", json_type_boolean, 0, "no lowercase"}, |
| 368 | {"No", json_type_boolean, 0, "no capitalized"}, |
| 369 | {"NO", json_type_boolean, 0, "no uppercase"}, |
| 370 | {"off", json_type_boolean, 0, "off lowercase"}, |
| 371 | {"Off", json_type_boolean, 0, "off capitalized"}, |
| 372 | {"OFF", json_type_boolean, 0, "off uppercase"}, |
| 373 | |
| 374 | {NULL, json_type_null, 0, NULL} |
| 375 | }; |
| 376 | |
| 377 | for (int i = 0; test_cases[i].yaml; i++) { |
| 378 | BUFFER *error = buffer_create(0, NULL); |
| 379 | struct json_object *json = yaml_parse_string(test_cases[i].yaml, error, YAML2JSON_DEFAULT); |
| 380 | |
| 381 | if (test_cases[i].expected_type == json_type_null) { |
| 382 | if (json != NULL) { |
| 383 | fprintf(stderr, "FAILED: test_yaml_special_values case %d (%s): expected NULL for '%s', got %p\n", |
| 384 | i, test_cases[i].description, test_cases[i].yaml, (void*)json); |
| 385 | failed++; |
| 386 | if (json) json_object_put(json); |
| 387 | } |
| 388 | } else { |
| 389 | if (!json) { |
| 390 | fprintf(stderr, "FAILED: test_yaml_special_values case %d (%s): expected non-NULL for '%s', error: %s\n", |
| 391 | i, test_cases[i].description, test_cases[i].yaml, buffer_tostring(error)); |
| 392 | failed++; |
| 393 | } else if (!json_object_is_type(json, test_cases[i].expected_type)) { |
| 394 | fprintf(stderr, "FAILED: test_yaml_special_values case %d (%s): expected type %u for '%s', got type %u\n", |
| 395 | i, test_cases[i].description, test_cases[i].expected_type, test_cases[i].yaml, json_object_get_type(json)); |
| 396 | failed++; |
| 397 | } else if (test_cases[i].expected_type == json_type_boolean) { |
| 398 | int actual = json_object_get_boolean(json); |
| 399 | if (actual != test_cases[i].expected_bool) { |
| 400 | fprintf(stderr, "FAILED: test_yaml_special_values case %d (%s): expected %d, got %d for '%s'\n", |
| 401 | i, test_cases[i].description, test_cases[i].expected_bool, actual, test_cases[i].yaml); |
| 402 | failed++; |
| 403 | } |
| 404 | } |
| 405 | |
| 406 | if (json) json_object_put(json); |
| 407 | } |
| 408 | |
| 409 | buffer_free(error); |
| 410 | } |
| 411 | |
| 412 | return failed; |
| 413 | } |
| 414 | |
| 415 | static int test_yaml_edge_cases_and_errors(void) { |
| 416 | int failed = 0; |
| 417 | |
| 418 | struct { |
| 419 | const char *yaml; |
| 420 | bool should_fail; |
| 421 | const char *description; |
| 422 | } test_cases[] = { |
| 423 | // These should parse successfully |
| 424 | {"key: value", false, "simple key-value"}, |
| 425 | {"- item", false, "simple array item"}, |
| 426 | {"[]", false, "empty array"}, |
| 427 | {"{}", false, "empty object"}, |
| 428 | {"key: 'value with spaces'", false, "quoted value with spaces"}, |
| 429 | {"key: \"value with \\\"quotes\\\"\"", false, "escaped quotes"}, |
| 430 | {"key: |\n multiline\n value", false, "multiline literal"}, |
| 431 | {"key: >\n folded\n value", false, "multiline folded"}, |
| 432 | |
| 433 | // Document markers |
| 434 | {"---\nkey: value", false, "document start marker"}, |
| 435 | {"key: value\n...", false, "document end marker"}, |
| 436 | {"---\nkey: value\n...", false, "both document markers"}, |
| 437 | |
| 438 | // Comments |
| 439 | {"key: value # comment", false, "inline comment"}, |
| 440 | {"# comment\nkey: value", false, "line comment"}, |
| 441 | |
| 442 | // Complex nesting |
| 443 | {"a: {b: {c: {d: value}}}", false, "deep nesting object"}, |
| 444 | {"- [[[[[nested]]]]]", false, "deep nesting array"}, |
| 445 | |
| 446 | // These should fail to parse |
| 447 | {"[unclosed array", true, "unclosed array"}, |
| 448 | {"{unclosed: object", true, "unclosed object"}, |
| 449 | {"key: value\n invalid: indentation", true, "invalid indentation"}, |
| 450 | // LIBYAML LIMITATION: Some invalid syntax is accepted |
| 451 | {"- item\n- - invalid", false, "invalid array nesting (libyaml accepts this)"}, |
| 452 | {"key: value\nkey: duplicate", false, "duplicate key (YAML allows this)"}, |
| 453 | {"invalid: :\nkey", true, "invalid colon placement"}, |
| 454 | {"'unclosed string", true, "unclosed single quote"}, |
| 455 | {"\"unclosed string", true, "unclosed double quote"}, |
| 456 | {"key: |\n multiline\nwrong indentation", true, "wrong multiline indentation"}, |
| 457 | |
| 458 | // Stress test cases |
| 459 | {"key: 'value with many many many many many words to test long strings'", false, "very long string"}, |
| 460 | |
| 461 | {NULL, false, NULL} |
| 462 | }; |
| 463 | |
| 464 | for (int i = 0; test_cases[i].yaml; i++) { |
| 465 | BUFFER *error = buffer_create(0, NULL); |
| 466 | struct json_object *json = yaml_parse_string(test_cases[i].yaml, error, YAML2JSON_DEFAULT); |
| 467 | |
| 468 | if (test_cases[i].should_fail) { |
| 469 | if (json != NULL) { |
| 470 | fprintf(stderr, "FAILED: test_yaml_edge_cases case %d (%s): expected failure for '%s', but parsing succeeded\n", |
| 471 | i, test_cases[i].description, test_cases[i].yaml); |
| 472 | failed++; |
| 473 | json_object_put(json); |
| 474 | } |
| 475 | } else { |
| 476 | if (json == NULL) { |
| 477 | fprintf(stderr, "FAILED: test_yaml_edge_cases case %d (%s): expected success for '%s', but parsing failed: %s\n", |
| 478 | i, test_cases[i].description, test_cases[i].yaml, buffer_tostring(error)); |
| 479 | failed++; |
| 480 | } else { |
| 481 | json_object_put(json); |
| 482 | } |
| 483 | } |
| 484 | |
| 485 | buffer_free(error); |
| 486 | } |
| 487 | |
| 488 | return failed; |
| 489 | } |
| 490 | |
| 491 | static int test_yaml_round_trip_comprehensive(void) { |
| 492 | int failed = 0; |
| 493 | |
| 494 | // Test that everything we can parse, we can also generate back identically |
| 495 | struct { |
| 496 | const char *yaml; |
| 497 | const char *description; |
| 498 | } test_cases[] = { |
| 499 | // Simple cases |
| 500 | {"42", "integer"}, |
| 501 | {"3.14", "float"}, |
| 502 | {"true", "boolean true"}, |
| 503 | {"false", "boolean false"}, |
| 504 | {"null", "null value"}, |
| 505 | {"\"hello world\"", "quoted string"}, |
| 506 | {"'single quoted'", "single quoted string"}, |
| 507 | |
| 508 | // Complex structures |
| 509 | {"[1, 2, 3]", "simple array"}, |
| 510 | {"{\"key\": \"value\"}", "simple object"}, |
| 511 | {"[{\"a\": 1}, {\"b\": 2}]", "array of objects"}, |
| 512 | {"{\"arr\": [1, 2, 3], \"obj\": {\"nested\": true}}", "mixed structure"}, |
| 513 | |
| 514 | // Special characters |
| 515 | {"\"\\\\ \\\" \\n \\t \\r\"", "escaped characters"}, |
| 516 | {"\"unicode: \\u00A9 \\u20AC\"", "unicode escapes"}, |
| 517 | |
| 518 | // Numbers with edge cases |
| 519 | {"0", "zero"}, |
| 520 | {"-0", "negative zero"}, |
| 521 | {"1.0", "integer as float"}, |
| 522 | {"1e10", "scientific notation"}, |
| 523 | |
| 524 | {NULL, NULL} |
| 525 | }; |
| 526 | |
| 527 | for (int i = 0; test_cases[i].yaml; i++) { |
| 528 | BUFFER *error = buffer_create(0, NULL); |
| 529 | |
| 530 | // Parse YAML to JSON |
| 531 | struct json_object *json = yaml_parse_string(test_cases[i].yaml, error, YAML2JSON_DEFAULT); |
| 532 | |
| 533 | // Check for actual error (NULL is valid for JSON null) |
| 534 | bool has_error = buffer_strlen(error) > 0; |
| 535 | if (has_error) { |
| 536 | fprintf(stderr, "FAILED: test_yaml_round_trip case %d (%s): failed to parse '%s': %s\n", |
| 537 | i, test_cases[i].description, test_cases[i].yaml, buffer_tostring(error)); |
| 538 | failed++; |
| 539 | buffer_free(error); |
| 540 | continue; |
| 541 | } |
| 542 | |
| 543 | // Generate YAML from JSON |
| 544 | BUFFER *generated = buffer_create(0, NULL); |
| 545 | buffer_flush(error); |
| 546 | |
| 547 | if (!yaml_generate_to_buffer(generated, json, error)) { |
| 548 | fprintf(stderr, "FAILED: test_yaml_round_trip case %d (%s): failed to generate YAML: %s\n", |
| 549 | i, test_cases[i].description, buffer_tostring(error)); |
| 550 | failed++; |
| 551 | if (json) json_object_put(json); |
| 552 | buffer_free(generated); |
| 553 | buffer_free(error); |
| 554 | continue; |
| 555 | } |
| 556 | |
| 557 | // Parse the generated YAML back |
| 558 | buffer_flush(error); |
| 559 | struct json_object *json2 = yaml_parse_string(buffer_tostring(generated), error, YAML2JSON_DEFAULT); |
| 560 | |
| 561 | // Check for actual error (NULL is valid for JSON null) |
| 562 | has_error = buffer_strlen(error) > 0; |
| 563 | if (has_error) { |
| 564 | fprintf(stderr, "FAILED: test_yaml_round_trip case %d (%s): failed to parse generated YAML '%s': %s\n", |
| 565 | i, test_cases[i].description, buffer_tostring(generated), buffer_tostring(error)); |
| 566 | failed++; |
| 567 | } else { |
| 568 | // Compare JSON representations (handle NULL special case) |
| 569 | if (json == NULL && json2 == NULL) { |
| 570 | // Both are null, it's a match |
| 571 | } else if (json == NULL || json2 == NULL) { |
| 572 | // One is null, one isn't - mismatch |
| 573 | fprintf(stderr, "FAILED: test_yaml_round_trip case %d (%s): round-trip mismatch (null handling)\n", |
| 574 | i, test_cases[i].description); |
| 575 | fprintf(stderr, " Original is %s\n", json ? "not null" : "null"); |
| 576 | fprintf(stderr, " Round-trip is %s\n", json2 ? "not null" : "null"); |
| 577 | fprintf(stderr, " Generated YAML: %s\n", buffer_tostring(generated)); |
| 578 | failed++; |
| 579 | } else { |
| 580 | // Both non-null, compare normally |
| 581 | const char *json1_str = json_object_to_json_string(json); |
| 582 | const char *json2_str = json_object_to_json_string(json2); |
| 583 | |
| 584 | if (strcmp(json1_str, json2_str) != 0) { |
| 585 | fprintf(stderr, "FAILED: test_yaml_round_trip case %d (%s): round-trip mismatch\n", |
| 586 | i, test_cases[i].description); |
| 587 | fprintf(stderr, " Original: %s\n", json1_str); |
| 588 | fprintf(stderr, " Round-trip: %s\n", json2_str); |
| 589 | fprintf(stderr, " Generated YAML: %s\n", buffer_tostring(generated)); |
| 590 | failed++; |
| 591 | } |
| 592 | } |
| 593 | |
| 594 | if (json2) json_object_put(json2); |
| 595 | } |
| 596 | |
| 597 | if (json) json_object_put(json); |
| 598 | buffer_free(generated); |
| 599 | buffer_free(error); |
| 600 | } |
| 601 | |
| 602 | return failed; |
| 603 | } |
| 604 | |
| 605 | int yaml_comprehensive_unittest(void) { |
| 606 | int passed = 0; |
| 607 | int failed = 0; |
| 608 | |
| 609 | printf("Starting comprehensive YAML parser/generator tests\n"); |
| 610 | printf("=================================================\n\n"); |
| 611 | |
| 612 | struct { |
| 613 | const char *name; |
| 614 | int (*test_func)(void); |
| 615 | } tests[] = { |
| 616 | {"test_yaml_string_styles_comprehensive", test_yaml_string_styles_comprehensive}, |
| 617 | {"test_yaml_multiline_strings", test_yaml_multiline_strings}, |
| 618 | {"test_yaml_numbers_comprehensive", test_yaml_numbers_comprehensive}, |
| 619 | {"test_yaml_special_values", test_yaml_special_values}, |
| 620 | {"test_yaml_edge_cases_and_errors", test_yaml_edge_cases_and_errors}, |
| 621 | {"test_yaml_round_trip_comprehensive", test_yaml_round_trip_comprehensive}, |
| 622 | {NULL, NULL} |
| 623 | }; |
| 624 | |
| 625 | for (int i = 0; tests[i].name; i++) { |
| 626 | printf("Running %s...\n", tests[i].name); |
| 627 | int test_failed = tests[i].test_func(); |
| 628 | if (test_failed == 0) { |
| 629 | printf(" PASSED\n"); |
| 630 | passed++; |
| 631 | } else { |
| 632 | printf(" FAILED (%d failures)\n", test_failed); |
| 633 | failed += test_failed; |
| 634 | } |
| 635 | } |
| 636 | |
| 637 | printf("\n=================================================\n"); |
| 638 | printf("Comprehensive YAML tests summary:\n"); |
| 639 | int total = 0; |
| 640 | for (int i = 0; tests[i].name; i++) total++; |
| 641 | printf(" Test suites run: %d\n", total); |
| 642 | printf(" Passed: %d\n", passed); |
| 643 | printf(" Failed: %d\n", failed); |
| 644 | printf("=================================================\n"); |
| 645 | |
| 646 | return failed; |
| 647 | } |