View raw

1 let test_version () = Alcotest.(check string) "version" "0.1.0" Org.version 2 3 (* Prescan tests *) 4 5 let test_prescan_default () = 6 let config = Org.Private.Prescan.scan "" in 7 let kws = Org.Config.all_todo_keywords config in 8 Alcotest.(check (list string)) "default keywords" [ "TODO"; "DONE" ] kws 9 10 let test_prescan_custom_todo () = 11 let input = "#+TODO: NEXT WAITING | DONE CANCELLED\n" in 12 let config = Org.Private.Prescan.scan input in 13 let kws = Org.Config.all_todo_keywords config in 14 Alcotest.(check (list string)) 15 "custom keywords" 16 [ "NEXT"; "WAITING"; "DONE"; "CANCELLED" ] 17 kws 18 19 let test_prescan_multiple_sequences () = 20 let input = "#+TODO: TODO | DONE\n#+TODO: OPEN | CLOSED\n" in 21 let config = Org.Private.Prescan.scan input in 22 let kws = Org.Config.all_todo_keywords config in 23 Alcotest.(check (list string)) 24 "multiple sequences" 25 [ "TODO"; "DONE"; "OPEN"; "CLOSED" ] 26 kws 27 28 let test_prescan_seq_todo () = 29 let input = "#+SEQ_TODO: NEXT ACTIVE | DONE\n" in 30 let config = Org.Private.Prescan.scan input in 31 Alcotest.(check bool) 32 "NEXT is todo" true 33 (Org.Config.is_todo_keyword config "NEXT"); 34 Alcotest.(check bool) 35 "DONE is todo" true 36 (Org.Config.is_todo_keyword config "DONE"); 37 Alcotest.(check bool) 38 "UNKNOWN is not todo" false 39 (Org.Config.is_todo_keyword config "UNKNOWN") 40 41 let test_prescan_case_insensitive_key () = 42 let input = "#+todo: BUG FIX | RESOLVED\n" in 43 let config = Org.Private.Prescan.scan input in 44 let kws = Org.Config.all_todo_keywords config in 45 Alcotest.(check (list string)) 46 "case insensitive key" 47 [ "BUG"; "FIX"; "RESOLVED" ] 48 kws 49 50 let test_prescan_no_bar () = 51 let input = "#+TODO: TODO DOING DONE\n" in 52 let config = Org.Private.Prescan.scan input in 53 let seqs = config.todo_sequences in 54 let seq = List.hd seqs in 55 Alcotest.(check (list string)) 56 "all active" 57 [ "TODO"; "DOING"; "DONE" ] 58 seq.active; 59 Alcotest.(check (list string)) "no done" [] seq.done_ 60 61 (* Lexer tests *) 62 module L = Org.Private.Lexer 63 module T = Org.Private.Parser 64 65 let config = Org.Config.default 66 let lex input = L.tokenize ~config input |> List.map fst 67 68 let tok_to_string = function 69 | T.BLANK -> "Blank" 70 | T.HEADING (n, s) -> Printf.sprintf "Heading(%d, %S)" n s 71 | T.KEYWORD (k, v) -> Printf.sprintf "Keyword(%S, %S)" k v 72 | T.AFFILIATED_KEYWORD (n, _, v) -> Printf.sprintf "Affiliated(%S, %S)" n v 73 | T.BEGIN_BLOCK (n, p) -> 74 Printf.sprintf "Begin_block(%S, %s)" n 75 (match p with Some s -> Printf.sprintf "%S" s | None -> "None") 76 | T.END_BLOCK n -> Printf.sprintf "End_block(%S)" n 77 | T.DRAWER_BEGIN n -> Printf.sprintf "Drawer_begin(%S)" n 78 | T.DRAWER_END -> "Drawer_end" 79 | T.PROPERTY (n, v) -> 80 Printf.sprintf "Property(%S, %s)" n 81 (match v with Some s -> Printf.sprintf "%S" s | None -> "None") 82 | T.PLANNING _ -> "Planning" 83 | T.LIST_ITEM d -> 84 Printf.sprintf "List_item(indent=%d, bullet=%S)" d.lid_indent d.lid_bullet 85 | T.TABLE_ROW _ -> "Table_row" 86 | T.FIXED_WIDTH s -> Printf.sprintf "Fixed_width(%S)" s 87 | T.COMMENT_LINE s -> Printf.sprintf "Comment_line(%S)" s 88 | T.HORIZONTAL_RULE -> "Horizontal_rule" 89 | T.TEXT_LINE s -> Printf.sprintf "Text_line(%S)" s 90 | T.EOF -> "Eof" 91 92 let check_tok msg expected actual = 93 Alcotest.(check string) msg expected (tok_to_string actual) 94 95 let test_lex_blank () = 96 let toks = lex "" in 97 check_tok "empty is blank+eof" "Blank" (List.nth toks 0); 98 check_tok "eof" "Eof" (List.nth toks 1) 99 100 let test_lex_heading () = 101 let toks = lex "* Hello world" in 102 check_tok "heading" "Heading(1, \"Hello world\")" (List.nth toks 0) 103 104 let test_lex_heading_level () = 105 let toks = lex "*** Deep heading" in 106 check_tok "heading level 3" "Heading(3, \"Deep heading\")" (List.nth toks 0) 107 108 let test_lex_keyword () = 109 let toks = lex "#+TITLE: My Document" in 110 check_tok "keyword" "Keyword(\"TITLE\", \"My Document\")" (List.nth toks 0) 111 112 let test_lex_affiliated () = 113 let toks = lex "#+NAME: my-table" in 114 check_tok "affiliated" "Affiliated(\"NAME\", \"my-table\")" (List.nth toks 0) 115 116 let test_lex_begin_block () = 117 let toks = lex "#+begin_src ocaml" in 118 check_tok "begin src" "Begin_block(\"src\", \"ocaml\")" (List.nth toks 0) 119 120 let test_lex_end_block () = 121 let toks = lex "#+end_src" in 122 check_tok "end src" "End_block(\"src\")" (List.nth toks 0) 123 124 let test_lex_drawer () = 125 let toks = lex ":PROPERTIES:" in 126 check_tok "drawer begin" "Drawer_begin(\"PROPERTIES\")" (List.nth toks 0) 127 128 let test_lex_drawer_end () = 129 let toks = lex ":END:" in 130 check_tok "drawer end" "Drawer_end" (List.nth toks 0) 131 132 let test_lex_list_unordered () = 133 let toks = lex "- item one" in 134 check_tok "unordered list" "List_item(indent=0, bullet=\"- \")" 135 (List.nth toks 0) 136 137 let test_lex_list_ordered () = 138 let toks = lex "1. first item" in 139 check_tok "ordered list" "List_item(indent=0, bullet=\"1. \")" 140 (List.nth toks 0) 141 142 let test_lex_list_indented () = 143 let toks = lex " - nested" in 144 check_tok "indented list" "List_item(indent=2, bullet=\"- \")" 145 (List.nth toks 0) 146 147 let test_lex_table () = 148 let toks = lex "| a | b | c |" in 149 check_tok "table row" "Table_row" (List.nth toks 0) 150 151 let test_lex_comment () = 152 let toks = lex "# a comment" in 153 check_tok "comment" "Comment_line(\"a comment\")" (List.nth toks 0) 154 155 let test_lex_fixed_width () = 156 let toks = lex ": fixed text" in 157 check_tok "fixed width" "Fixed_width(\"fixed text\")" (List.nth toks 0) 158 159 let test_lex_hrule () = 160 let toks = lex "-----" in 161 check_tok "horizontal rule" "Horizontal_rule" (List.nth toks 0) 162 163 let test_lex_text () = 164 let toks = lex "Just some text" in 165 check_tok "text line" "Text_line(\"Just some text\")" (List.nth toks 0) 166 167 let test_lex_planning () = 168 let toks = lex "DEADLINE: <2024-01-15 Mon>" in 169 check_tok "planning" "Planning" (List.nth toks 0) 170 171 let test_lex_property () = 172 let toks = lex " :CUSTOM_ID: my-id" in 173 check_tok "property" "Property(\"CUSTOM_ID\", \"my-id\")" (List.nth toks 0) 174 175 (* Parser tests *) 176 module R = Org.Private.Raw_ast 177 module A = Org.Ast 178 179 let parse input = Org.Private.Parse.document ~config input 180 let parse_inline input = Org.Private.Parse.inline input 181 182 let test_parse_empty () = 183 let doc = parse "" in 184 Alcotest.(check int) "no headings" 0 (List.length doc.R.rd_headings) 185 186 let test_parse_single_heading () = 187 let doc = parse "* Hello" in 188 Alcotest.(check int) "one heading" 1 (List.length doc.R.rd_headings); 189 let h = List.hd doc.R.rd_headings in 190 Alcotest.(check int) "level 1" 1 h.R.rh_level; 191 Alcotest.(check string) "title" "Hello" h.R.rh_raw_title 192 193 let test_parse_heading_with_body () = 194 let input = "* Heading\nSome text\nMore text" in 195 let doc = parse input in 196 Alcotest.(check int) "one heading" 1 (List.length doc.R.rd_headings); 197 let h = List.hd doc.R.rd_headings in 198 let has_paragraph = 199 List.exists 200 (function R.Raw_paragraph ls -> List.length ls > 0 | _ -> false) 201 h.R.rh_body 202 in 203 Alcotest.(check bool) "has paragraph body" true has_paragraph 204 205 let test_parse_preamble () = 206 let input = "Some preamble text\n\n* Heading" in 207 let doc = parse input in 208 let non_blank = 209 List.filter 210 (function R.Raw_paragraph [] -> false | _ -> true) 211 doc.R.rd_preamble 212 in 213 Alcotest.(check bool) "has preamble" true (List.length non_blank > 0); 214 Alcotest.(check int) "one heading" 1 (List.length doc.R.rd_headings) 215 216 let test_parse_keyword () = 217 let input = "#+TITLE: My Doc\n* Heading" in 218 let doc = parse input in 219 let has_kw = 220 List.exists 221 (function R.Raw_keyword ("TITLE", _) -> true | _ -> false) 222 doc.R.rd_preamble 223 in 224 Alcotest.(check bool) "has title keyword" true has_kw 225 226 let test_parse_block () = 227 let input = "#+begin_src ocaml\nlet x = 42\n#+end_src" in 228 let doc = parse input in 229 let has_block = 230 List.exists 231 (function R.Raw_block _ -> true | _ -> false) 232 doc.R.rd_preamble 233 in 234 Alcotest.(check bool) "has block" true has_block 235 236 let test_parse_list () = 237 let input = "- item one\n- item two" in 238 let doc = parse input in 239 let has_list = 240 List.exists 241 (function R.Raw_list_items _ -> true | _ -> false) 242 doc.R.rd_preamble 243 in 244 Alcotest.(check bool) "has list" true has_list 245 246 (* Inline parser tests *) 247 let test_inline_plain () = 248 let result = parse_inline "hello world" in 249 match result with 250 | [ A.Plain s ] -> Alcotest.(check string) "plain" "hello world" s 251 | _ -> Alcotest.fail "expected single Plain" 252 253 let test_inline_bold () = 254 let result = parse_inline "*bold*" in 255 match result with 256 | [ A.Bold [ A.Plain s ] ] -> Alcotest.(check string) "bold content" "bold" s 257 | _ -> Alcotest.fail "expected Bold" 258 259 let test_inline_italic () = 260 let result = parse_inline "/italic/" in 261 match result with 262 | [ A.Italic [ A.Plain s ] ] -> 263 Alcotest.(check string) "italic content" "italic" s 264 | _ -> Alcotest.fail "expected Italic" 265 266 let test_inline_code () = 267 let result = parse_inline "~code~" in 268 match result with 269 | [ A.Code s ] -> Alcotest.(check string) "code content" "code" s 270 | _ -> Alcotest.fail "expected Code" 271 272 let test_inline_verbatim () = 273 let result = parse_inline "=verb=" in 274 match result with 275 | [ A.Verbatim s ] -> Alcotest.(check string) "verbatim content" "verb" s 276 | _ -> Alcotest.fail "expected Verbatim" 277 278 let test_inline_link () = 279 let result = parse_inline "[[https://org.org][Org]]" in 280 match result with 281 | [ A.Link lnk ] -> ( 282 (match lnk.target with 283 | A.Url u -> Alcotest.(check string) "url" "https://org.org" u 284 | _ -> Alcotest.fail "expected Url target"); 285 match lnk.description with 286 | Some [ A.Plain d ] -> Alcotest.(check string) "desc" "Org" d 287 | _ -> Alcotest.fail "expected description") 288 | _ -> Alcotest.fail "expected Link" 289 290 let test_inline_link_no_desc () = 291 let result = parse_inline "[[file:readme.org]]" in 292 match result with 293 | [ A.Link lnk ] -> 294 (match lnk.target with 295 | A.File f -> Alcotest.(check string) "file" "readme.org" f 296 | _ -> Alcotest.fail "expected File target"); 297 Alcotest.(check bool) "no description" true (lnk.description = None) 298 | _ -> Alcotest.fail "expected Link" 299 300 let test_inline_timestamp () = 301 let result = parse_inline "<2024-01-15 Mon>" in 302 match result with 303 | [ A.Timestamp (A.Active (d, _)) ] -> 304 Alcotest.(check int) "year" 2024 d.year; 305 Alcotest.(check int) "month" 1 d.month; 306 Alcotest.(check int) "day" 15 d.day 307 | _ -> Alcotest.fail "expected active timestamp" 308 309 let test_inline_plain_link () = 310 let result = parse_inline "see https://example.com for info" in 311 let has_link = 312 List.exists (function A.Link _ -> true | _ -> false) result 313 in 314 Alcotest.(check bool) "has plain link" true has_link 315 316 let test_inline_mixed () = 317 let result = parse_inline "Hello *bold* and ~code~" in 318 let has_bold = 319 List.exists (function A.Bold _ -> true | _ -> false) result 320 in 321 let has_code = 322 List.exists (function A.Code _ -> true | _ -> false) result 323 in 324 Alcotest.(check bool) "has bold" true has_bold; 325 Alcotest.(check bool) "has code" true has_code 326 327 (* ================================================================== *) 328 (* Integration tests *) 329 (* ================================================================== *) 330 331 let full_fixture = 332 {|#+TITLE: Integration Test Document 333 #+AUTHOR: Test Author 334 #+TODO: TODO NEXT WAITING | DONE CANCELLED 335 336 This is the preamble paragraph with *bold*, /italic/, and ~code~. 337 338 See https://orgmode.org for more information. 339 340 * TODO [#A] First Heading :project:important: 341 SCHEDULED: <2024-06-15 Sat> 342 :PROPERTIES: 343 :CUSTOM_ID: first-heading 344 :EFFORT: 2h 345 :END: 346 347 A paragraph with [[https://example.com][a link]] and =verbatim text=. 348 349 ** DONE Sub-heading 350 CLOSED: [2024-06-10 Mon 14:30] 351 352 Another paragraph with +strikethrough+ and _underline_. 353 354 ** NEXT Another sub-heading :review: 355 356 *** Deep heading 357 358 Content in a deeply nested heading. 359 360 * Heading without TODO 361 362 ** Child heading 363 364 Paragraph text. 365 366 * Lists and Blocks 367 368 ** Unordered list 369 370 - First item 371 - Second item with *bold* 372 - Nested item 373 - Another nested item 374 - Third item 375 376 ** Ordered list 377 378 1. First 379 2. Second 380 3. Third 381 382 ** Descriptive list 383 384 - Term one :: Description of term one 385 - Term two :: Description of term two 386 387 ** Source block 388 389 #+begin_src ocaml 390 let greeting = "Hello, Org!" 391 let () = print_endline greeting 392 #+end_src 393 394 ** Quote block 395 396 #+begin_quote 397 This is a quoted paragraph. 398 It spans multiple lines. 399 #+end_quote 400 401 ** Example block 402 403 #+begin_example 404 This is example text. 405 Preserved verbatim. 406 #+end_example 407 408 * Tables 409 410 | Name | Age | City | 411 |-------+-----+----------| 412 | Alice | 30 | New York | 413 | Bob | 25 | London | 414 415 * Miscellaneous 416 417 A fixed-width section: 418 419 : This is fixed width 420 : Another fixed line 421 422 A comment: 423 424 # This is a comment 425 # Another comment line 426 427 ----- 428 429 Text after horizontal rule. 430 431 :NOTES: 432 This is content inside a drawer. 433 :END: 434 435 * Timestamps 436 437 An active timestamp: <2024-01-15 Mon 10:00>. 438 439 An inactive timestamp: [2024-03-20 Wed]. 440 441 A range: <2024-01-15 Mon>--<2024-01-20 Sat>. 442 |} 443 444 let full_doc = Org.parse full_fixture 445 446 let test_integration_structure () = 447 (* Directives *) 448 let dirs = full_doc.A.directives in 449 Alcotest.(check bool) "has directives" true (List.length dirs >= 2); 450 let title_dir = List.find_opt (fun d -> d.A.dir_key = "TITLE") dirs in 451 Alcotest.(check bool) "has TITLE" true (Option.is_some title_dir); 452 Alcotest.(check string) 453 "TITLE value" "Integration Test Document" (Option.get title_dir).A.dir_value; 454 let author_dir = List.find_opt (fun d -> d.A.dir_key = "AUTHOR") dirs in 455 Alcotest.(check bool) "has AUTHOR" true (Option.is_some author_dir); 456 Alcotest.(check string) 457 "AUTHOR value" "Test Author" (Option.get author_dir).A.dir_value; 458 (* Preamble *) 459 Alcotest.(check bool) "has preamble" true (List.length full_doc.A.preamble > 0); 460 (* Top-level headings *) 461 Alcotest.(check int) 462 "6 top-level headings" 6 463 (List.length full_doc.A.headings) 464 465 let test_integration_headings () = 466 let h1 = List.nth full_doc.A.headings 0 in 467 (* First heading: TODO, priority, tags *) 468 Alcotest.(check (option string)) "h1 todo" (Some "TODO") h1.A.todo; 469 Alcotest.(check (option char)) "h1 priority" (Some 'A') h1.A.priority; 470 Alcotest.(check (list string)) "h1 tags" [ "project"; "important" ] h1.A.tags; 471 Alcotest.(check int) "h1 level" 1 h1.A.level; 472 (* Planning *) 473 Alcotest.(check bool) "h1 has planning" true (Option.is_some h1.A.planning); 474 let planning = Option.get h1.A.planning in 475 Alcotest.(check bool) 476 "h1 has scheduled" true 477 (Option.is_some planning.A.scheduled); 478 (* Properties *) 479 Alcotest.(check bool) 480 "h1 has properties" true 481 (List.length h1.A.properties >= 2); 482 let custom_id = 483 List.find_opt (fun p -> p.A.prop_name = "CUSTOM_ID") h1.A.properties 484 in 485 Alcotest.(check bool) "has CUSTOM_ID" true (Option.is_some custom_id); 486 Alcotest.(check (option string)) 487 "CUSTOM_ID value" (Some "first-heading") (Option.get custom_id).A.prop_value; 488 let effort = 489 List.find_opt (fun p -> p.A.prop_name = "EFFORT") h1.A.properties 490 in 491 Alcotest.(check bool) "has EFFORT" true (Option.is_some effort); 492 Alcotest.(check (option string)) 493 "EFFORT value" (Some "2h") (Option.get effort).A.prop_value; 494 (* Children of first heading *) 495 Alcotest.(check int) "h1 has 2 children" 2 (List.length h1.A.children); 496 let child1 = List.nth h1.A.children 0 in 497 Alcotest.(check (option string)) "child1 todo" (Some "DONE") child1.A.todo; 498 (* DONE sub-heading has CLOSED planning *) 499 Alcotest.(check bool) 500 "child1 has planning" true 501 (Option.is_some child1.A.planning); 502 let child1_planning = Option.get child1.A.planning in 503 Alcotest.(check bool) 504 "child1 has closed" true 505 (Option.is_some child1_planning.A.closed); 506 let child2 = List.nth h1.A.children 1 in 507 Alcotest.(check (option string)) "child2 todo" (Some "NEXT") child2.A.todo; 508 Alcotest.(check (list string)) "child2 tags" [ "review" ] child2.A.tags; 509 (* Deep heading is a child of child2 *) 510 Alcotest.(check int) "child2 has 1 child" 1 (List.length child2.A.children); 511 let deep = List.nth child2.A.children 0 in 512 Alcotest.(check int) "deep level" 3 deep.A.level; 513 (* Heading without TODO *) 514 let h2 = List.nth full_doc.A.headings 1 in 515 Alcotest.(check (option string)) "h2 no todo" None h2.A.todo; 516 Alcotest.(check int) "h2 has 1 child" 1 (List.length h2.A.children) 517 518 let test_integration_elements () = 519 (* Lists and Blocks heading is at index 2 *) 520 let h3 = List.nth full_doc.A.headings 2 in 521 (* h3 children: Unordered list, Ordered list, Descriptive list, 522 Source block, Quote block, Example block *) 523 Alcotest.(check bool) 524 "Lists and Blocks has children" true 525 (List.length h3.A.children >= 6); 526 (* Unordered list *) 527 let ul_heading = List.nth h3.A.children 0 in 528 let ul_elems = ul_heading.A.contents in 529 let has_unordered = 530 List.exists 531 (function 532 | A.Plain_list (A.Unordered, items) -> List.length items >= 3 533 | _ -> false) 534 ul_elems 535 in 536 Alcotest.(check bool) "has unordered list" true has_unordered; 537 (* Ordered list *) 538 let ol_heading = List.nth h3.A.children 1 in 539 let ol_elems = ol_heading.A.contents in 540 let has_ordered = 541 List.exists 542 (function 543 | A.Plain_list (A.Ordered, items) -> List.length items >= 3 | _ -> false) 544 ol_elems 545 in 546 Alcotest.(check bool) "has ordered list" true has_ordered; 547 (* Descriptive list *) 548 let dl_heading = List.nth h3.A.children 2 in 549 let dl_elems = dl_heading.A.contents in 550 let has_descriptive = 551 List.exists 552 (function 553 | A.Plain_list (A.Descriptive, items) -> List.length items >= 2 554 | _ -> false) 555 dl_elems 556 in 557 Alcotest.(check bool) "has descriptive list" true has_descriptive; 558 (* Source block *) 559 let src_heading = List.nth h3.A.children 3 in 560 let src_elems = src_heading.A.contents in 561 let has_src = 562 List.exists 563 (function 564 | A.Block (A.Src_block sb) -> 565 sb.A.src_language = Some "ocaml" && String.length sb.A.src_value > 0 566 | _ -> false) 567 src_elems 568 in 569 Alcotest.(check bool) "has src block with ocaml" true has_src; 570 (* Quote block *) 571 let qt_heading = List.nth h3.A.children 4 in 572 let qt_elems = qt_heading.A.contents in 573 let has_quote = 574 List.exists 575 (function A.Block (A.Quote_block _) -> true | _ -> false) 576 qt_elems 577 in 578 Alcotest.(check bool) "has quote block" true has_quote; 579 (* Example block *) 580 let ex_heading = List.nth h3.A.children 5 in 581 let ex_elems = ex_heading.A.contents in 582 let has_example = 583 List.exists 584 (function A.Block (A.Example_block _) -> true | _ -> false) 585 ex_elems 586 in 587 Alcotest.(check bool) "has example block" true has_example; 588 (* Tables heading is at index 3 *) 589 let h4 = List.nth full_doc.A.headings 3 in 590 let table_elems = h4.A.contents in 591 let has_table = 592 List.exists 593 (function 594 | A.Table t -> 595 let has_standard = 596 List.exists 597 (function A.Table_row_standard _ -> true | _ -> false) 598 t.A.rows 599 in 600 let has_rule = 601 List.exists 602 (function A.Table_row_rule -> true | _ -> false) 603 t.A.rows 604 in 605 has_standard && has_rule 606 | _ -> false) 607 table_elems 608 in 609 Alcotest.(check bool) "has table with rows and rules" true has_table; 610 (* Miscellaneous heading is at index 4 *) 611 let h5 = List.nth full_doc.A.headings 4 in 612 let misc_elems = h5.A.contents in 613 let has_fixed_width = 614 List.exists 615 (function A.Fixed_width lines -> List.length lines >= 2 | _ -> false) 616 misc_elems 617 in 618 Alcotest.(check bool) "has fixed-width" true has_fixed_width; 619 let has_comment = 620 List.exists 621 (function A.Comment lines -> List.length lines >= 2 | _ -> false) 622 misc_elems 623 in 624 Alcotest.(check bool) "has comment" true has_comment; 625 let has_hrule = 626 List.exists (function A.Horizontal_rule -> true | _ -> false) misc_elems 627 in 628 Alcotest.(check bool) "has horizontal rule" true has_hrule; 629 let has_drawer = 630 List.exists 631 (function A.Drawer ("NOTES", _) -> true | _ -> false) 632 misc_elems 633 in 634 Alcotest.(check bool) "has drawer" true has_drawer 635 636 let test_integration_inline () = 637 (* Preamble paragraph should have inline markup *) 638 let preamble_para = 639 List.find_opt 640 (function A.Paragraph _ -> true | _ -> false) 641 full_doc.A.preamble 642 in 643 Alcotest.(check bool) 644 "preamble has paragraph" true 645 (Option.is_some preamble_para); 646 let inlines = 647 match preamble_para with Some (A.Paragraph (il, _)) -> il | _ -> [] 648 in 649 let has_bold = 650 List.exists (function A.Bold _ -> true | _ -> false) inlines 651 in 652 let has_italic = 653 List.exists (function A.Italic _ -> true | _ -> false) inlines 654 in 655 let has_code = 656 List.exists (function A.Code _ -> true | _ -> false) inlines 657 in 658 Alcotest.(check bool) "preamble has bold" true has_bold; 659 Alcotest.(check bool) "preamble has italic" true has_italic; 660 Alcotest.(check bool) "preamble has code" true has_code; 661 (* Second preamble paragraph should have a plain link *) 662 let preamble_paras = 663 List.filter 664 (function A.Paragraph _ -> true | _ -> false) 665 full_doc.A.preamble 666 in 667 Alcotest.(check bool) 668 "preamble has 2+ paragraphs" true 669 (List.length preamble_paras >= 2); 670 let para2_inlines = 671 match List.nth preamble_paras 1 with A.Paragraph (il, _) -> il | _ -> [] 672 in 673 let has_link = 674 List.exists (function A.Link _ -> true | _ -> false) para2_inlines 675 in 676 Alcotest.(check bool) "preamble para2 has link" true has_link; 677 (* Timestamps heading (index 5) *) 678 let h6 = List.nth full_doc.A.headings 5 in 679 let ts_elems = h6.A.contents in 680 let all_inlines = 681 List.concat_map (function A.Paragraph (il, _) -> il | _ -> []) ts_elems 682 in 683 let has_active_ts = 684 List.exists 685 (function A.Timestamp (A.Active _) -> true | _ -> false) 686 all_inlines 687 in 688 let has_inactive_ts = 689 List.exists 690 (function A.Timestamp (A.Inactive _) -> true | _ -> false) 691 all_inlines 692 in 693 let has_range = 694 List.exists 695 (function A.Timestamp (A.Active_range _) -> true | _ -> false) 696 all_inlines 697 in 698 Alcotest.(check bool) "has active timestamp" true has_active_ts; 699 Alcotest.(check bool) "has inactive timestamp" true has_inactive_ts; 700 Alcotest.(check bool) "has timestamp range" true has_range 701 702 let () = 703 Alcotest.run "org" 704 [ 705 ("basic", [ Alcotest.test_case "version" `Quick test_version ]); 706 ( "prescan", 707 [ 708 Alcotest.test_case "default config" `Quick test_prescan_default; 709 Alcotest.test_case "custom TODO" `Quick test_prescan_custom_todo; 710 Alcotest.test_case "multiple sequences" `Quick 711 test_prescan_multiple_sequences; 712 Alcotest.test_case "SEQ_TODO" `Quick test_prescan_seq_todo; 713 Alcotest.test_case "case insensitive key" `Quick 714 test_prescan_case_insensitive_key; 715 Alcotest.test_case "no bar separator" `Quick test_prescan_no_bar; 716 ] ); 717 ( "lexer", 718 [ 719 Alcotest.test_case "blank" `Quick test_lex_blank; 720 Alcotest.test_case "heading" `Quick test_lex_heading; 721 Alcotest.test_case "heading level" `Quick test_lex_heading_level; 722 Alcotest.test_case "keyword" `Quick test_lex_keyword; 723 Alcotest.test_case "affiliated keyword" `Quick test_lex_affiliated; 724 Alcotest.test_case "begin block" `Quick test_lex_begin_block; 725 Alcotest.test_case "end block" `Quick test_lex_end_block; 726 Alcotest.test_case "drawer begin" `Quick test_lex_drawer; 727 Alcotest.test_case "drawer end" `Quick test_lex_drawer_end; 728 Alcotest.test_case "unordered list" `Quick test_lex_list_unordered; 729 Alcotest.test_case "ordered list" `Quick test_lex_list_ordered; 730 Alcotest.test_case "indented list" `Quick test_lex_list_indented; 731 Alcotest.test_case "table row" `Quick test_lex_table; 732 Alcotest.test_case "comment" `Quick test_lex_comment; 733 Alcotest.test_case "fixed width" `Quick test_lex_fixed_width; 734 Alcotest.test_case "horizontal rule" `Quick test_lex_hrule; 735 Alcotest.test_case "text line" `Quick test_lex_text; 736 Alcotest.test_case "planning" `Quick test_lex_planning; 737 Alcotest.test_case "property" `Quick test_lex_property; 738 ] ); 739 ( "parser", 740 [ 741 Alcotest.test_case "empty document" `Quick test_parse_empty; 742 Alcotest.test_case "single heading" `Quick test_parse_single_heading; 743 Alcotest.test_case "heading with body" `Quick 744 test_parse_heading_with_body; 745 Alcotest.test_case "preamble" `Quick test_parse_preamble; 746 Alcotest.test_case "keyword" `Quick test_parse_keyword; 747 Alcotest.test_case "block" `Quick test_parse_block; 748 Alcotest.test_case "list" `Quick test_parse_list; 749 ] ); 750 ( "inline", 751 [ 752 Alcotest.test_case "plain text" `Quick test_inline_plain; 753 Alcotest.test_case "bold" `Quick test_inline_bold; 754 Alcotest.test_case "italic" `Quick test_inline_italic; 755 Alcotest.test_case "code" `Quick test_inline_code; 756 Alcotest.test_case "verbatim" `Quick test_inline_verbatim; 757 Alcotest.test_case "link with desc" `Quick test_inline_link; 758 Alcotest.test_case "link no desc" `Quick test_inline_link_no_desc; 759 Alcotest.test_case "timestamp" `Quick test_inline_timestamp; 760 Alcotest.test_case "plain link" `Quick test_inline_plain_link; 761 Alcotest.test_case "mixed inline" `Quick test_inline_mixed; 762 ] ); 763 ( "integration", 764 [ 765 Alcotest.test_case "structure" `Quick test_integration_structure; 766 Alcotest.test_case "headings" `Quick test_integration_headings; 767 Alcotest.test_case "elements" `Quick test_integration_elements; 768 Alcotest.test_case "inline" `Quick test_integration_inline; 769 ] ); 770 ] 771