tnative.nim
1 # NimYAML - YAML implementation in Nim 2 # (c) Copyright 2015-2023 Felix Krause 3 # 4 # See the file "copying.txt", included in this 5 # distribution, for details about the copyright. 6 7 import "../yaml" 8 import unittest, strutils, tables, times, math, options 9 import commonTestUtils 10 11 type 12 MyTuple = tuple 13 str: string 14 i: int32 15 b: bool 16 17 TrafficLight = enum 18 tlGreen, tlYellow, tlRed 19 20 Person = object 21 firstnamechar: char 22 surname: string 23 age: int32 24 25 Node = object 26 value: string 27 next: ref Node 28 29 BetterInt = distinct int 30 31 AnimalKind = enum 32 akCat, akDog 33 34 Animal = object 35 name: string 36 case kind: AnimalKind 37 of akCat: 38 purringIntensity: float 39 of akDog: barkometer: int 40 41 DumbEnum = enum 42 deA, deB, deC 43 44 NonVariantWithTransient = object 45 a {.transient.}, b, c {.transient.}, d: string 46 47 VariantWithTransient = object 48 gStorable: string 49 gTemporary {.transient.}: string 50 case kind: DumbEnum 51 of deA: 52 cStorable: string 53 cTemporary {.transient.}: string 54 of deB: 55 alwaysThere: int 56 of deC: 57 neverThere {.transient.}: int 58 59 WithDefault = object 60 a, b {.defaultVal: "b".}, c, d {.defaultVal: "d".}: string 61 62 WithIgnoredField {.ignore: ["z"].} = object 63 x, y: int 64 65 Parent = object of RootObj 66 i*: int 67 Child = object of Parent 68 s*: string 69 70 AsFlow {.collection: csFlow.} = object 71 a* {.scalar: ssSingleQuoted.}: string 72 b* {.scalar: ssDoubleQuoted.}: string 73 c* {.scalar: ssPlain.}: string 74 75 OverrideColStyle = object 76 flowChild: AsFlow 77 blockChild {.collection: csBlock.}: AsFlow 78 79 proc `$`(v: BetterInt): string {.borrow.} 80 proc `==`(left, right: BetterInt): bool {.borrow.} 81 82 setTag(TrafficLight, Tag("!tl")) 83 setTag(Node, Tag("!example.net:Node")) 84 setTag(BetterInt, Tag("!test:BetterInt")) 85 86 const yamlDir = "---" 87 const yamlTagDirs = "%TAG !n! tag:nimyaml.org,2016:\n---" 88 89 proc representObject*( 90 ctx : var SerializationContext, 91 value: BetterInt, 92 tag : Tag 93 ) {.raises: [].} = 94 var 95 val = $value 96 i = val.len - 3 97 while i > 0: 98 val.insert("_", i) 99 i -= 3 100 ctx.put(scalarEvent(val, tag, yAnchorNone)) 101 102 proc constructObject*( 103 ctx : var ConstructionContext, 104 result: var BetterInt, 105 ) {.raises: [YamlConstructionError, YamlStreamError].} = 106 ctx.input.constructScalarItem(item, BetterInt): 107 result = BetterInt(parseBiggestInt(item.scalarContent) + 1) 108 109 template expectConstructionError(li, co: int, message: string, body: typed) = 110 try: 111 body 112 echo "Expected YamlConstructionError, but none was raised!" 113 fail() 114 except YamlConstructionError as e: 115 doAssert li == e.mark.line, "Expected error line " & $li & ", was " & $e.mark.line 116 doAssert co == e.mark.column, "Expected error column " & $co & ", was " & $e.mark.column 117 doAssert message == e.msg, "Expected error message \n" & escape(message) & 118 ", got \n" & escape(e.msg) 119 120 proc newNode(v: string): ref Node = 121 new(result) 122 result.value = v 123 result.next = nil 124 125 template dualTest(name: string, body: untyped) = 126 test name: 127 body 128 129 # fix for compiler problem (comptime doesn't work when included from tests.nim) 130 # possibly this issue: https://github.com/nim-lang/Nim/issues/18103 131 when isMainModule: 132 test name & " [comptime]": 133 static: body 134 135 suite "Serialization": 136 dualTest "Load integer without fixed length": 137 var input = "-4247" 138 var result: int 139 load(input, result) 140 assert result == -4247, "result is " & $result 141 142 input = $(int64(int32.high) + 1'i64) 143 var gotException = false 144 try: load(input, result) 145 except YamlConstructionError: gotException = true 146 assert gotException, "Expected exception, got none." 147 148 test "Dump integer without fixed length": 149 var input = -4247 150 var output = blockOnlyDumper().dump(input) 151 assertStringEqual "\"-4247\"\n", output 152 153 when sizeof(int) == sizeof(int64): 154 input = int(int32.high) + 1 155 var gotException = false 156 try: output = blockOnlyDumper().dump(input) 157 except YamlSerializationError: gotException = true 158 assert gotException, "Expected exception, got none." 159 160 dualTest "Load Hex byte (0xFF)": 161 let input = "0xFF" 162 var result: byte 163 load(input, result) 164 assert(result == 255) 165 166 dualTest "Load Hex byte (0xC)": 167 let input = "0xC" 168 var result: byte 169 load(input, result) 170 assert(result == 12) 171 172 dualTest "Load Octal byte (0o14)": 173 let input = "0o14" 174 var result: byte 175 load(input, result) 176 assert(result == 12) 177 178 dualTest "Load byte (14)": 179 let input = "14" 180 var result: byte 181 load(input, result) 182 assert(result == 14) 183 184 dualTest "Load Hex int (0xFF)": 185 let input = "0xFF" 186 var result: int 187 load(input, result) 188 assert(result == 255) 189 190 dualTest "Load Hex int (0xC)": 191 let input = "0xC" 192 var result: int 193 load(input, result) 194 assert(result == 12) 195 196 dualTest "Load Octal int (0o14)": 197 let input = "0o14" 198 var result: int 199 load(input, result) 200 assert(result == 12) 201 202 dualTest "Load int (14)": 203 let input = "14" 204 var result: int 205 load(input, result) 206 assert(result == 14) 207 208 dualTest "Load floats": 209 let input = "[6.8523015e+5, 685.230_15e+03, 685_230.15, -.inf, .NaN]" 210 var result: seq[float] 211 load(input, result) 212 for i in 0..2: 213 assert result[i] == 6.8523015e+5 214 assert result[3] == NegInf 215 assert classify(result[4]) == fcNan 216 217 test "Load timestamps": 218 let input = "[2001-12-15T02:59:43.1Z, 2001-12-14t21:59:43.10-05:00, 2001-12-14 21:59:43.10-5]" 219 var result: seq[Time] 220 load(input, result) 221 assert result.len() == 3 222 # currently, there is no good way of checking the result content, because 223 # the parsed Time may have any timezone offset. 224 225 dualTest "Load string sequence": 226 let input = " - a\n - b" 227 var result: seq[string] 228 load(input, result) 229 assert result.len == 2 230 assert result[0] == "a" 231 assert result[1] == "b" 232 233 test "Dump string sequence": 234 var input = @["a", "b"] 235 var output = blockOnlyDumper().dump(input) 236 assertStringEqual "- a\n- b\n", output 237 238 dualTest "Load char set": 239 let input = "- a\n- b" 240 var result: set[char] 241 load(input, result) 242 assert result.card == 2 243 assert 'a' in result 244 assert 'b' in result 245 246 test "Dump char set": 247 var input = {'a', 'b'} 248 var output = blockOnlyDumper().dump(input) 249 assertStringEqual "- a\n- b\n", output 250 251 dualTest "Load array": 252 let input = "- 23\n- 42\n- 47" 253 var result: array[0..2, int32] 254 load(input, result) 255 assert result[0] == 23 256 assert result[1] == 42 257 assert result[2] == 47 258 259 test "Dump array": 260 let input = [23'i32, 42'i32, 47'i32] 261 var output = blockOnlyDumper().dump(input) 262 assertStringEqual "- 23\n- 42\n- 47\n", output 263 264 dualTest "Load Option": 265 let input = "- Some\n- !!null ~" 266 var result: array[0..1, Option[string]] 267 load(input, result) 268 assert result[0].isSome 269 assert result[0].get() == "Some" 270 assert not result[1].isSome 271 272 test "Dump Option": 273 let input = [none(int32), some(42'i32), none(int32)] 274 let output = blockOnlyDumper().dump(input) 275 assertStringEqual "- !!null ~\n- 42\n- !!null ~\n", output 276 277 dualTest "Load Option of seq": 278 let input = "- !!null\n- [a, b]" 279 var result: array[0..1, Option[seq[string]]] 280 load(input, result) 281 assert not result[0].isSome 282 assert result[1].get()[0] == "a" 283 assert result[1].get()[1] == "b" 284 285 test "Dump Option of seq": 286 let input = [none(seq[string]), some(@["a", "b"])] 287 let output = Dumper().dump(input) 288 assertStringEqual "- !!null ~\n- [a, b]\n", output 289 290 dualTest "Load Table[int, string]": 291 let input = "23: dreiundzwanzig\n42: zweiundvierzig" 292 var result: Table[int32, string] 293 load(input, result) 294 assert result.len == 2 295 assert result[23] == "dreiundzwanzig" 296 assert result[42] == "zweiundvierzig" 297 298 test "Dump Table[int, string]": 299 var input = initTable[int32, string]() 300 input[23] = "dreiundzwanzig" 301 input[42] = "zweiundvierzig" 302 var output = blockOnlyDumper().dump(input) 303 assertStringEqual("23: dreiundzwanzig\n42: zweiundvierzig\n", 304 output) 305 306 dualTest "Load OrderedTable[tuple[int32, int32], string]": 307 let input = "- {a: 23, b: 42}: drzw\n- {a: 13, b: 47}: drsi" 308 var result: OrderedTable[tuple[a, b: int32], string] 309 load(input, result) 310 var i = 0 311 for key, value in result.pairs: 312 case i 313 of 0: 314 assert key == (a: 23'i32, b: 42'i32) 315 assert value == "drzw" 316 of 1: 317 assert key == (a: 13'i32, b: 47'i32) 318 assert value == "drsi" 319 else: assert false 320 i.inc() 321 322 test "Dump OrderedTable[tuple[int32, int32], string]": 323 var input = initOrderedTable[tuple[a, b: int32], string]() 324 input[(a: 23'i32, b: 42'i32)] = "dreiundzwanzigzweiundvierzig" 325 input[(a: 13'i32, b: 47'i32)] = "dreizehnsiebenundvierzig" 326 var dumper = blockOnlyDumper() 327 dumper.serialization.tagStyle = tsRootOnly 328 dumper.serialization.handles = initNimYamlTagHandle() 329 var output = dumper.dump(input) 330 assertStringEqual(yamlTagDirs & 331 " !n!tables:OrderedTable(tag:nimyaml.org;2016:tuple(tag:nimyaml.org;2016:system:int32;tag:nimyaml.org;2016:system:int32);tag:yaml.org;2002:str)\n" & 332 "- ? a: 23\n" & 333 " b: 42\n" & 334 " : dreiundzwanzigzweiundvierzig\n" & 335 "- ? a: 13\n" & 336 " b: 47\n" & 337 " : dreizehnsiebenundvierzig\n", output) 338 339 dualTest "Load Sequences in Sequence": 340 let input = " - [1, 2, 3]\n - [4, 5]\n - [6]" 341 var result: seq[seq[int32]] 342 load(input, result) 343 assert result.len == 3 344 assert result[0] == @[1.int32, 2.int32, 3.int32] 345 assert result[1] == @[4.int32, 5.int32] 346 assert result[2] == @[6.int32] 347 348 test "Dump Sequences in Sequence": 349 let input = @[@[1.int32, 2.int32, 3.int32], @[4.int32, 5.int32], @[6.int32]] 350 var output = Dumper().dump(input) 351 assertStringEqual "- [1, 2, 3]\n- [4, 5]\n- [6]\n", output 352 353 dualTest "Load Enum": 354 let input = 355 "!<tag:nimyaml.org,2016:system:seq(tl)>\n- !tl tlRed\n- tlGreen\n- tlYellow" 356 var result: seq[TrafficLight] 357 load(input, result) 358 assert result.len == 3 359 assert result[0] == tlRed 360 assert result[1] == tlGreen 361 assert result[2] == tlYellow 362 363 test "Dump Enum": 364 let input = @[tlRed, tlGreen, tlYellow] 365 var output = blockOnlyDumper().dump(input) 366 assertStringEqual "- tlRed\n- tlGreen\n- tlYellow\n", output 367 368 dualTest "Load Tuple": 369 let input = "str: value\ni: 42\nb: true" 370 var result: MyTuple 371 load(input, result) 372 assert result.str == "value" 373 assert result.i == 42 374 assert result.b == true 375 376 test "Dump Tuple": 377 let input = (str: "value", i: 42.int32, b: true) 378 var output = Dumper().dump(input) 379 assertStringEqual "str: value\ni: 42\nb: true\n", output 380 381 test "Load Tuple - unknown field": 382 let input = "str: value\nfoo: bar\ni: 42\nb: true" 383 var result: MyTuple 384 expectConstructionError(2, 1, "While constructing MyTuple: Unknown field: \"foo\""): 385 load(input, result) 386 387 test "Load Tuple - missing field": 388 let input = "str: value\nb: true" 389 var result: MyTuple 390 expectConstructionError(1, 1, "While constructing MyTuple: Missing field: \"i\""): 391 load(input, result) 392 393 test "Load Tuple - duplicate field": 394 let input = "str: value\ni: 42\nb: true\nb: true" 395 var result: MyTuple 396 expectConstructionError(4, 1, "While constructing MyTuple: Duplicate field: \"b\""): 397 load(input, result) 398 399 dualTest "Load Multiple Documents": 400 let input = "1\n---\n2" 401 var result: seq[int] 402 loadMultiDoc(input, result) 403 assert(result.len == 2) 404 assert result[0] == 1 405 assert result[1] == 2 406 407 dualTest "Load Multiple Documents (Single Doc)": 408 let input = "1" 409 var result: seq[int] 410 loadMultiDoc(input, result) 411 assert(result.len == 1) 412 assert result[0] == 1 413 414 dualTest "Load custom object": 415 let input = "firstnamechar: P\nsurname: Pan\nage: 12" 416 var result: Person 417 load(input, result) 418 assert result.firstnamechar == 'P' 419 assert result.surname == "Pan" 420 assert result.age == 12 421 422 test "Dump custom object": 423 let input = Person(firstnamechar: 'P', surname: "Pan", age: 12) 424 var output = blockOnlyDumper().dump(input) 425 assertStringEqual( 426 "firstnamechar: P\nsurname: Pan\nage: 12\n", output) 427 428 test "Load custom object - unknown field": 429 let input = " firstnamechar: P\n surname: Pan\n age: 12\n occupation: free" 430 var result: Person 431 expectConstructionError(4, 3, "While constructing Person: Unknown field: \"occupation\""): 432 load(input, result) 433 434 test "Load custom object - missing field": 435 let input = "surname: Pan\nage: 12\n " 436 var result: Person 437 expectConstructionError(1, 1, "While constructing Person: Missing field: \"firstnamechar\""): 438 load(input, result) 439 440 test "Load custom object - duplicate field": 441 let input = "firstnamechar: P\nsurname: Pan\nage: 12\nsurname: Pan" 442 var result: Person 443 expectConstructionError(4, 1, "While constructing Person: Duplicate field: \"surname\""): 444 load(input, result) 445 446 dualTest "Load sequence with explicit tags": 447 let input = yamlTagDirs & " !n!system:seq(" & 448 "tag:yaml.org;2002:str)\n- !!str one\n- !!str two" 449 var result: seq[string] 450 load(input, result) 451 assert result[0] == "one" 452 assert result[1] == "two" 453 454 test "Dump sequence with explicit tags": 455 let input = @["one", "two"] 456 var dumper = blockOnlyDumper() 457 dumper.serialization.tagStyle = tsAll 458 dumper.serialization.handles = initNimYamlTagHandle() 459 var output = dumper.dump(input) 460 assertStringEqual(yamlTagDirs & " !n!system:seq(" & 461 "tag:yaml.org;2002:str)\n- !!str one\n- !!str two\n", output) 462 463 dualTest "Load custom object with explicit root tag": 464 let input = 465 "--- !<tag:nimyaml.org,2016:custom:Person>\nfirstnamechar: P\nsurname: Pan\nage: 12" 466 var result: Person 467 load(input, result) 468 assert result.firstnamechar == 'P' 469 assert result.surname == "Pan" 470 assert result.age == 12 471 472 test "Dump custom object with explicit root tag": 473 let input = Person(firstnamechar: 'P', surname: "Pan", age: 12) 474 var dumper = blockOnlyDumper() 475 dumper.serialization.tagStyle = tsRootOnly 476 dumper.serialization.handles = initNimYamlTagHandle() 477 var output = dumper.dump(input) 478 assertStringEqual(yamlTagDirs & 479 " !n!custom:Person\nfirstnamechar: P\nsurname: Pan\nage: 12\n", output) 480 481 dualTest "Load object with inherited fields": 482 let input = 483 "i: 4\ns: hello" 484 var result: Child 485 load(input, result) 486 assert result.i == 4 487 assert result.s == "hello" 488 489 dualTest "Load custom variant object": 490 let input = 491 "---\n- - name: Bastet\n - kind: akCat\n - purringIntensity: 7.2\n" & 492 "- - name: Anubis\n - kind: akDog\n - barkometer: 13" 493 var result: seq[Animal] 494 load(input, result) 495 assert result.len == 2 496 assert result[0].name == "Bastet" 497 assert result[0].kind == akCat 498 assert abs(result[0].purringIntensity - 7.2) < 1E-7 499 assert result[1].name == "Anubis" 500 assert result[1].kind == akDog 501 assert result[1].barkometer == 13 502 503 test "Dump custom variant object": 504 let input = @[Animal(name: "Bastet", kind: akCat, purringIntensity: 7.2), 505 Animal(name: "Anubis", kind: akDog, barkometer: 13)] 506 var output = blockOnlyDumper().dump(input) 507 assertStringEqual "" & 508 "- - name: Bastet\n" & 509 " - kind: akCat\n" & 510 " - purringIntensity: 7.2\n" & 511 "- - name: Anubis\n" & 512 " - kind: akDog\n" & 513 " - barkometer: 13\n", output 514 515 test "Load custom variant object - missing field": 516 let input = "[{name: Bastet}, {kind: akCat}]" 517 var result: Animal 518 expectConstructionError(1, 1, "While constructing Animal: Missing field: \"purringIntensity\""): 519 load(input, result) 520 521 dualTest "Load non-variant object with transient fields": 522 let input = "{b: b, d: d}" 523 var result: NonVariantWithTransient 524 load(input, result) 525 assert result.a.len == 0 526 assert result.b == "b" 527 assert result.c.len == 0 528 assert result.d == "d" 529 530 test "Load non-variant object with transient fields - unknown field": 531 let input = "{b: b, c: c, d: d}" 532 var result: NonVariantWithTransient 533 expectConstructionError(1, 8, "While constructing NonVariantWithTransient: Field \"c\" is transient and may not occur in input"): 534 load(input, result) 535 536 test "Dump non-variant object with transient fields": 537 let input = NonVariantWithTransient(a: "a", b: "b", c: "c", d: "d") 538 let output = blockOnlyDumper().dump(input) 539 assertStringEqual "b: b\nd: d\n", output 540 541 dualTest "Load variant object with transient fields": 542 let input = "[[gStorable: gs, kind: deA, cStorable: cs], [gStorable: a, kind: deC]]" 543 var result: seq[VariantWithTransient] 544 load(input, result) 545 assert result.len == 2 546 assert result[0].kind == deA 547 assert result[0].gStorable == "gs" 548 assert result[0].cStorable == "cs" 549 assert result[1].kind == deC 550 assert result[1].gStorable == "a" 551 552 test "Load variant object with transient fields, error": 553 let input = "[gStorable: gc, kind: deC, neverThere: foo]" 554 var result: VariantWithTransient 555 expectConstructionError(1, 28, "While constructing VariantWithTransient: Field \"neverThere\" is transient and may not occur in input"): 556 load(input, result) 557 558 test "Dump variant object with transient fields": 559 let input = @[VariantWithTransient(kind: deA, gStorable: "gs", 560 gTemporary: "gt", cStorable: "cs", cTemporary: "ct"), 561 VariantWithTransient(kind: deC, gStorable: "a", gTemporary: "b", 562 neverThere: 42)] 563 let output = blockOnlyDumper().dump(input) 564 assertStringEqual "" & 565 "- - gStorable: gs\n" & 566 " - kind: deA\n" & 567 " - cStorable: cs\n" & 568 "- - gStorable: a\n" & 569 " - kind: deC\n", output 570 571 dualTest "Load object with ignored key": 572 let input = "[{x: 1, y: 2}, {x: 3, z: 4, y: 5}, {z: [1, 2, 3], x: 4, y: 5}]" 573 var result: seq[WithIgnoredField] 574 load(input, result) 575 assert result.len == 3 576 assert result[0].x == 1 577 assert result[0].y == 2 578 assert result[1].x == 3 579 assert result[1].y == 5 580 assert result[2].x == 4 581 assert result[2].y == 5 582 583 test "Load object with ignored key - unknown field": 584 let input = "{x: 1, y: 2, zz: 3}" 585 var result: WithIgnoredField 586 expectConstructionError(1, 14, "While constructing WithIgnoredField: Unknown field: \"zz\""): 587 load(input, result) 588 589 when not defined(JS): 590 test "Dump cyclic data structure": 591 var 592 a = newNode("a") 593 b = newNode("b") 594 c = newNode("c") 595 a.next = b 596 b.next = c 597 c.next = a 598 var dumper = blockOnlyDumper() 599 dumper.serialization.tagStyle = tsRootOnly 600 var output = dumper.dump(a) 601 assertStringEqual yamlDir & " !example.net:Node &a\n" & 602 "value: a\n" & 603 "next:\n" & 604 " value: b\n" & 605 " next:\n" & 606 " value: c\n" & 607 " next: *a\n", output 608 609 test "Load cyclic data structure": 610 let input = yamlTagDirs & """ !n!system:seq(example.net:Node) 611 - &a 612 value: a 613 next: &b 614 value: b 615 next: &c 616 value: c 617 next: *a 618 - *b 619 - *c 620 """ 621 var result: seq[ref Node] 622 try: load(input, result) 623 except YamlConstructionError: 624 let ex = (ref YamlConstructionError)(getCurrentException()) 625 echo "line ", ex.mark.line, ", column ", ex.mark.column, ": ", ex.msg 626 echo ex.lineContent 627 raise ex 628 629 assert(result.len == 3) 630 assert(result[0].value == "a") 631 assert(result[1].value == "b") 632 assert(result[2].value == "c") 633 assert(result[0].next == result[1]) 634 assert(result[1].next == result[2]) 635 assert(result[2].next == result[0]) 636 637 dualTest "Load object with default values": 638 let input = "a: abc\nc: dce" 639 var result: WithDefault 640 load(input, result) 641 assert result.a == "abc" 642 assert result.b == "b" 643 assert result.c == "dce" 644 assert result.d == "d" 645 646 dualTest "Load object with partly default values": 647 let input = "a: abc\nb: bcd\nc: cde" 648 var result: WithDefault 649 load(input, result) 650 assert result.a == "abc" 651 assert result.b == "bcd" 652 assert result.c == "cde" 653 assert result.d == "d" 654 655 dualTest "Custom constructObject": 656 let input = "- 1\n- !test:BetterInt 2" 657 var result: seq[BetterInt] 658 load(input, result) 659 assert(result.len == 2) 660 assert(result[0] == 2.BetterInt) 661 assert(result[1] == 3.BetterInt) 662 663 test "Custom representObject": 664 let input = @[1.BetterInt, 9998887.BetterInt, 98312.BetterInt] 665 var dumper = blockOnlyDumper() 666 dumper.serialization.tagStyle = tsAll 667 dumper.serialization.handles = initNimYamlTagHandle() 668 var output = dumper.dump(input) 669 assertStringEqual yamlTagDirs & " !n!system:seq(test:BetterInt)\n" & 670 "- !test:BetterInt 1\n" & 671 "- !test:BetterInt 9_998_887\n" & 672 "- !test:BetterInt 98_312\n", output 673 674 test "Representation pragmas": 675 let input = OverrideColStyle( 676 flowChild: AsFlow(a: "abc", b: "abc", c: "abc"), 677 blockChild: AsFlow(a: "a\nc", b: "abc", c: "ab:") 678 ) 679 var dumper = blockOnlyDumper() 680 dumper.presentation.maxLineLength = some(20) 681 var output = dumper.dump(input) 682 assertStringEqual "flowChild: {\n" & 683 " a: 'abc',\n" & 684 " b: \"abc\",\n" & 685 " c: abc\n" & 686 " }\n" & 687 "blockChild:\n" & 688 " a: \"a\\nc\"\n" & 689 " b: \"abc\"\n" & 690 " c: \"ab:\"\n", output 691