1 /** 2 * A language-agnostic C-like preprocessor. 3 * Only UTF-8 text is supported. 4 * 5 * Authors: 6 * Mike Bierlee, m.bierlee@lostmoment.com 7 * Copyright: 2023 Mike Bierlee 8 * License: 9 * This software is licensed under the terms of the MIT license. 10 * The full terms of the license can be found in the LICENSE.txt file. 11 */ 12 13 module preprocessor; 14 15 public import preprocessor.artifacts; 16 17 import preprocessor.processing : processFile; 18 19 import std.datetime.systime : SysTime, Clock; 20 import std.conv : to; 21 import std.string : capitalize, rightJustify; 22 23 /** 24 * Preprocess the sources contained in the given build context. 25 * Params: 26 * buildCtx = Context used in the pre-processing run. 27 * Returns: A procesing result containing all processed (main) sources. 28 */ 29 ProcessingResult preprocess(const ref BuildContext buildCtx) { 30 ProcessingResult result; 31 result.date = createDateString(); 32 result.time = createTimeString(); 33 result.timestamp = createTimestampString(); 34 35 const MacroMap builtInMacros = [ 36 DateMacro: result.date, 37 TimeMacro: result.time, 38 TimestampMacro: result.timestamp 39 ]; 40 41 const(SourceMap) sources = buildCtx.mainSources.length > 0 ? buildCtx.mainSources 42 : buildCtx.sources; 43 44 MacroMap macros = createInitialMacroMap(builtInMacros, buildCtx); 45 foreach (string name, string source; sources) { 46 string resultSource; 47 string[] guardedInclusions; 48 processFile(name, source, buildCtx, macros, guardedInclusions, resultSource); 49 result.sources[name] = resultSource; 50 } 51 52 return result; 53 } 54 55 private MacroMap createInitialMacroMap(const MacroMap builtInMacros, const ref BuildContext buildCtx) { 56 MacroMap macros = cast(MacroMap) buildCtx.macros.dup; 57 foreach (string macroName, string macroValue; builtInMacros) { 58 macros[macroName] = macroValue; 59 } 60 61 return macros; 62 } 63 64 private string createDateString() { 65 SysTime currentTime = Clock.currTime(); 66 auto month = currentTime.month.to!string.capitalize; 67 auto day = currentTime.day.to!string.rightJustify(2, '0'); 68 auto year = currentTime.year.to!string; 69 return month ~ " " ~ day ~ " " ~ year; 70 } 71 72 private string createTimeString() { 73 SysTime currentTime = Clock.currTime(); 74 auto hour = currentTime.hour.to!string.rightJustify(2, '0'); 75 auto minute = currentTime.minute.to!string.rightJustify(2, '0'); 76 auto second = currentTime.second.to!string.rightJustify(2, '0'); 77 return hour ~ ":" ~ minute ~ ":" ~ second; 78 } 79 80 private string createTimestampString() { 81 SysTime currentTime = Clock.currTime(); 82 auto dayOfWeek = currentTime.dayOfWeek.to!string.capitalize; 83 auto month = currentTime.month.to!string.capitalize; 84 auto day = currentTime.day.to!string.rightJustify(2, '0'); 85 auto time = createTimeString(); 86 auto year = currentTime.year.to!string; 87 return dayOfWeek ~ " " ~ month ~ " " ~ day ~ " " ~ time ~ " " ~ year; 88 } 89 90 version (unittest) { 91 import preprocessor.debugging; 92 93 import std.exception : assertThrown; 94 import std.string : strip; 95 import std.array : replace; 96 import std.conv : to; 97 98 string stripAllWhiteSpace(string input) { 99 return input.replace(' ', "").replace('\n', ""); 100 } 101 102 void assertThrownMsg(ExceptionT : Throwable = Exception, ExpressionT)( 103 string expectedMessage, lazy ExpressionT expression) { 104 try { 105 expression; 106 assert(false, "No exception was thrown. Expected: " ~ typeid(ExceptionT).to!string); 107 } catch (ExceptionT e) { 108 assert(e.message == expectedMessage, "Exception message was different. Expected: \"" ~ expectedMessage ~ 109 "\", actual: \"" ~ e.message ~ "\""); 110 } catch (Exception e) { 111 //dfmt off 112 assert(false, "Different type of exception was thrown. Expected: " ~ 113 typeid(ExceptionT).to!string ~ ", actual: " ~ typeid(typeof(e)).to!string); 114 //dfmt on 115 } 116 } 117 } 118 119 // Generic tests 120 version (unittest) { 121 @("Ignore unknown directive") 122 unittest { 123 auto main = "#banana rama"; 124 auto context = BuildContext(["main.txt": main]); 125 126 auto result = preprocess(context).sources["main.txt"]; 127 assert(result == main); 128 } 129 130 @("Only process specified set of main sources") 131 unittest { 132 auto main = "#include <libby>"; 133 auto libby = "#include <roses>"; 134 auto roses = "Roses"; 135 136 BuildContext context; 137 context.sources = [ 138 "libby": libby, 139 "roses": roses 140 ]; 141 context.mainSources = [ 142 "main": main 143 ]; 144 145 auto result = preprocess(context).sources; 146 assert(result["main"] == roses); 147 } 148 149 @("Ignore #include directive when disabled") 150 unittest { 151 auto main = "#include <libby>"; 152 auto context = BuildContext(["main": main]); 153 context.enableIncludeDirectives = false; 154 155 auto result = preprocess(context).sources; 156 assert(result["main"] == main); 157 } 158 159 @("Ignore conditional directives when disabled") 160 unittest { 161 auto main = " 162 #if LALA 163 lele 164 #endif 165 #ifdef LULU 166 LOLO 167 #endif 168 #else 169 #elif 170 "; 171 auto context = BuildContext(["main": main]); 172 context.enableConditionalDirectives = false; 173 174 auto result = preprocess(context).sources; 175 assert(result["main"] == main); 176 } 177 178 @("Ignore #define/#undef directives when disabled") 179 unittest { 180 auto main = " 181 #define LALA 182 #undef LALA 183 "; 184 auto context = BuildContext(["main": main]); 185 context.enableMacroDefineDirectives = false; 186 context.enableMacroUndefineDirectives = false; 187 188 auto result = preprocess(context).sources; 189 assert(result["main"] == main); 190 } 191 192 @("Ignore #error directives when disabled") 193 unittest { 194 auto main = "#error \"I am error\""; 195 auto context = BuildContext(["main": main]); 196 context.enableErrorDirectives = false; 197 198 auto result = preprocess(context).sources; 199 assert(result["main"] == main); 200 } 201 202 @("Ignore #pragma directives when disabled") 203 unittest { 204 auto main = "#pragma twice, or three times?"; 205 auto context = BuildContext(["main": main]); 206 context.enablePragmaDirectives = false; 207 208 auto result = preprocess(context).sources; 209 assert(result["main"] == main); 210 } 211 212 @("Ignore all directives when disabled") 213 unittest { 214 auto main = " 215 #include 216 #if 217 #elif 218 #else 219 #endif 220 #define 221 #undef 222 #dunno 223 #pragma 224 #ifdef 225 #ifndef 226 #error 227 "; 228 229 auto context = BuildContext(["main": main]); 230 context.disableAllDirectives(); 231 232 auto result = preprocess(context).sources; 233 assert(result["main"] == main); 234 } 235 236 @("Ignore macro expansion when disabled") 237 unittest { 238 auto main = " 239 __THE_BIG_FOX_JUMPS_OVER_THE_HUNGRY_DOG__ 240 "; 241 242 auto context = BuildContext(["main": main]); 243 context.enableMacroExpansion = false; 244 245 auto result = preprocess(context).sources; 246 assert(result["main"] == main); 247 } 248 } 249 250 // Includes tests 251 version (unittest) { 252 @("Resolve includes") 253 unittest { 254 auto hi = "Hi!"; 255 auto main = "#include <hi.txt>"; 256 auto context = BuildContext([ 257 "hi.txt": hi, 258 "main.txt": main 259 ]); 260 261 auto result = preprocess(context); 262 263 assert(result.sources["hi.txt"] == hi); 264 assert(result.sources["main.txt"] == hi); 265 } 266 267 @("Resolve multiple includes") 268 unittest { 269 auto hi = "Hi!"; 270 auto howAreYou = "How are you?"; 271 auto main = " 272 #include <hi.txt> 273 #include <howAreYou.txt> 274 "; 275 276 auto context = BuildContext([ 277 "hi.txt": hi, 278 "howAreYou.txt": howAreYou, 279 "main.txt": main 280 ]); 281 282 auto result = preprocess(context); 283 284 auto expectedResult = " 285 Hi! 286 How are you? 287 "; 288 289 assert(result.sources["hi.txt"] == hi); 290 assert(result.sources["howAreYou.txt"] == howAreYou); 291 assert(result.sources["main.txt"] == expectedResult); 292 } 293 294 @("Resolve includes in includes") 295 unittest { 296 auto hi = "Hi!"; 297 auto secondary = "#include <hi.txt>"; 298 auto main = "#include <secondary.txt>"; 299 300 auto context = BuildContext([ 301 "hi.txt": hi, 302 "secondary.txt": secondary, 303 "main.txt": main 304 ]); 305 306 auto result = preprocess(context); 307 308 assert(result.sources["hi.txt"] == hi); 309 assert(result.sources["secondary.txt"] == hi); 310 assert(result.sources["main.txt"] == hi); 311 } 312 313 @("Fail to include when filename is on other line") 314 unittest { 315 auto main = " 316 #include 317 <other.txt> 318 "; 319 320 auto context = BuildContext([ 321 "main.txt": main 322 ]); 323 324 assertThrownMsg!ParseException( 325 "Error processing main.txt(2,1): Parse error: Failed to parse include directive: Expected \" or <.", 326 preprocess(context) 327 ); 328 } 329 330 @("Fail to include when filename does not start with quote or <") 331 unittest { 332 auto main = "#include 'coolfile.c'"; 333 auto context = BuildContext([ 334 "main.txt": main 335 ]); 336 337 assertThrownMsg!ParseException( 338 "Error processing main.txt(0,9): Parse error: Failed to parse include directive: Expected \" or <.", 339 preprocess(context) 340 ); 341 } 342 343 @("Fail to include when included source is not in build context") 344 unittest { 345 auto main = "#include <notfound.404>"; 346 auto context = BuildContext([ 347 "main.txt": main 348 ]); 349 350 assertThrownMsg!PreprocessException( 351 "Error processing main.txt(0,0): Failed to include 'notfound.404': It does not exist.", 352 preprocess(context) 353 ); 354 } 355 356 @("Prevent endless inclusion cycle") 357 unittest { 358 auto main = "#include \"main.md\""; 359 auto context = BuildContext([ 360 "main.md": main 361 ]); 362 context.inclusionLimit = 5; 363 364 assertThrownMsg!PreprocessException( 365 "Error processing main.md(0,9): Inclusions has exceeded the limit of 5. Adjust BuildContext.inclusionLimit to increase.", 366 preprocess(context) 367 ); 368 } 369 370 @("Inclusions using quotes are directory-aware and relative") 371 unittest { 372 auto main = "#include \"secondary.txt\""; 373 auto secondary = "Heey"; 374 auto context = BuildContext([ 375 "cool/main.txt": main, 376 "cool/secondary.txt": secondary 377 ]); 378 379 auto result = preprocess(context); 380 381 assert(result.sources["cool/main.txt"] == secondary); 382 assert(result.sources["cool/secondary.txt"] == secondary); 383 } 384 } 385 386 // Conditional tests 387 version (unittest) { 388 @("Fail if a rogue #endif is found") 389 unittest { 390 auto main = "#endif"; 391 auto context = BuildContext(["main": main]); 392 393 assertThrownMsg!ParseException( 394 "Error processing main(0,6): Parse error: #endif directive found without accompanying starting conditional (#if/#ifdef)", 395 preprocess(context) 396 ); 397 } 398 399 @("Fail if a rogue #else is found") 400 unittest { 401 auto main = "#else"; 402 auto context = BuildContext(["main": main]); 403 404 assertThrownMsg!ParseException( 405 "Error processing main(0,5): Parse error: #else directive found without accompanying starting conditional (#if/#ifdef)", 406 preprocess(context) 407 ); 408 } 409 410 @("Fail if a rogue #elif is found") 411 unittest { 412 auto main = "#elif"; 413 auto context = BuildContext(["main": main]); 414 415 assertThrownMsg!ParseException( 416 "Error processing main(0,5): Parse error: #elif directive found without accompanying starting conditional (#if/#ifdef)", 417 preprocess(context) 418 ); 419 } 420 421 @("Not fail if a rogue #endif is found and ignored") 422 unittest { 423 auto main = "#endif"; 424 auto context = BuildContext(["main": main]); 425 context.ignoreUnmatchedConditionalDirectives = true; 426 427 auto result = preprocess(context).sources; 428 assert(result["main"].strip == "#endif"); 429 } 430 431 @("Not fail if a rogue #else is found and ignored") 432 unittest { 433 auto main = "#else"; 434 auto context = BuildContext(["main": main]); 435 context.ignoreUnmatchedConditionalDirectives = true; 436 437 auto result = preprocess(context).sources; 438 assert(result["main"].strip == "#else"); 439 } 440 441 @("Not fail if a rogue #elif is found and ignored") 442 unittest { 443 auto main = "#elif"; 444 auto context = BuildContext(["main": main]); 445 context.ignoreUnmatchedConditionalDirectives = true; 446 447 auto result = preprocess(context).sources; 448 assert(result["main"].strip == "#elif"); 449 } 450 451 @("Include body if token is defined") 452 unittest { 453 auto main = " 454 #ifdef I_AM_GROOT 455 Groot! 456 #endif 457 "; 458 459 auto context = BuildContext(["main": main]); 460 context.macros = [ 461 "I_AM_GROOT": "very" 462 ]; 463 464 auto result = preprocess(context).sources; 465 assert(result["main"].strip == "Groot!"); 466 } 467 468 @("Not include body if token is not defined") 469 unittest { 470 auto main = " 471 #ifdef I_AM_NOT_GROOT 472 Groot! 473 #endif 474 "; 475 476 auto context = BuildContext(["main": main]); 477 context.macros = [ 478 "I_AM_GROOT": "very" 479 ]; 480 481 auto result = preprocess(context).sources; 482 assert(result["main"].strip == ""); 483 } 484 485 @("Include else body if token is not defined") 486 unittest { 487 auto main = " 488 #ifdef I_AM_NOT_GROOT 489 Groot! 490 #else 491 Not Groot! 492 #endif 493 "; 494 495 auto context = BuildContext(["main": main]); 496 context.macros = [ 497 "I_AM_GROOT": "very" 498 ]; 499 500 auto result = preprocess(context).sources; 501 assert(result["main"].strip == "Not Groot!"); 502 } 503 504 @("Not include else body if token is defined") 505 unittest { 506 auto main = " 507 #ifdef I_AM_GROOT 508 Tree! 509 #else 510 Not Tree! 511 #endif 512 "; 513 514 auto context = BuildContext(["main": main]); 515 context.macros = [ 516 "I_AM_GROOT": "very" 517 ]; 518 519 auto result = preprocess(context).sources; 520 assert(result["main"].strip == "Tree!"); 521 } 522 523 @("Fail when else is defined multiple times") 524 unittest { 525 auto main = " 526 #ifdef I_AM_NOT_GROOT 527 Groot! 528 #else 529 Not Groot! 530 #else 531 Still not Groot! 532 #endif 533 "; 534 535 auto context = BuildContext(["main": main]); 536 assertThrownMsg!ParseException( 537 "Error processing main(3,1): Parse error: #else directive defined multiple times. Only one #else block is allowed.", 538 preprocess(context) 539 ); 540 } 541 542 @("Fail when end of file is reached before conditional terminator") 543 unittest { 544 auto main = " 545 #ifdef I_AM_GROOT 546 Groot! 547 "; 548 549 auto context = BuildContext(["main": main]); 550 assertThrownMsg!ParseException( 551 "Error processing main(3,9): Parse error: Unexpected end of file while processing directive.", 552 preprocess(context) 553 ); 554 } 555 556 @("Include body if token is not defined in ifndef") 557 unittest { 558 auto main = " 559 #ifndef I_AM_NOT_GROOT 560 Groot not here! 561 #endif 562 "; 563 564 auto context = BuildContext(["main": main]); 565 566 auto result = preprocess(context).sources; 567 assert(result["main"].strip == "Groot not here!"); 568 } 569 570 @("Not include body if token is defined in ifndef") 571 unittest { 572 auto main = " 573 #ifndef I_AM_NOT_GROOT 574 Groot not here! 575 #endif 576 "; 577 578 auto context = BuildContext(["main": main]); 579 context.macros = ["I_AM_NOT_GROOT": "ok man!"]; 580 581 auto result = preprocess(context).sources; 582 assert(result["main"].strip == ""); 583 } 584 585 @("Include else body if token is defined in ifndef") 586 unittest { 587 auto main = " 588 #ifndef I_AM_NOT_GROOT 589 Groot not here! 590 #else 591 Big tree thing is here! 592 #endif 593 "; 594 595 auto context = BuildContext(["main": main]); 596 context.macros = ["I_AM_NOT_GROOT": "ok man!"]; 597 598 auto result = preprocess(context).sources; 599 assert(result["main"].strip == "Big tree thing is here!"); 600 } 601 602 @("Not include else body if token is not defined in ifndef") 603 unittest { 604 auto main = " 605 #ifndef I_AM_NOT_GROOT 606 Groot not here! 607 #else 608 Big tree thing is here! 609 #endif 610 "; 611 612 auto context = BuildContext(["main": main]); 613 614 auto result = preprocess(context).sources; 615 assert(result["main"].strip == "Groot not here!"); 616 } 617 618 @("#Include works in conditional body") 619 unittest { 620 auto one = "One"; 621 auto eins = "EINS"; 622 auto two = "Two"; 623 auto zwei = "ZWEI"; 624 auto three = "Three"; 625 auto drei = "DREI"; 626 auto four = "Four"; 627 auto vier = "VIER"; 628 629 auto main = " 630 #ifdef ONE 631 #include <one> 632 #else 633 #include <eins> 634 #endif 635 #ifdef ZWEI 636 #include <zwei> 637 #else 638 #include <two> 639 #endif 640 #ifndef DREI 641 #include <three> 642 #else 643 #include <drei> 644 #endif 645 #ifndef FOUR 646 #include <vier> 647 #else 648 #include <four> 649 #endif 650 "; 651 652 BuildContext context; 653 context.macros = [ 654 "ONE": "", 655 "FOUR": "", 656 ]; 657 context.sources = [ 658 "one": one, 659 "eins": eins, 660 "two": two, 661 "zwei": zwei, 662 "three": three, 663 "drei": drei, 664 "four": four, 665 "vier": vier 666 ]; 667 context.mainSources = ["main": main]; 668 669 auto result = preprocess(context).sources; 670 assert(result["main"].stripAllWhiteSpace == "OneTwoThreeFour"); 671 } 672 673 @("Conditionals inside of conditional is not supported") 674 unittest { 675 auto main = " 676 #ifdef HI 677 #ifdef REALLY_HI 678 Hi! 679 #endif 680 #endif 681 "; 682 683 auto context = BuildContext(["main": main]); 684 685 assertThrownMsg!ParseException( 686 "Error processing main(2,1): Parse error: #endif directive found without accompanying starting conditional (#if/#ifdef)", 687 preprocess(context) 688 ); 689 } 690 691 @("Conditionals inside of included code is supported") 692 unittest { 693 auto main = " 694 #ifdef HI 695 #include <include> 696 #endif 697 "; 698 699 auto include = " 700 #ifdef REALLY_HI 701 Hi! 702 #endif 703 "; 704 705 BuildContext context; 706 context.sources = [ 707 "include": include 708 ]; 709 context.mainSources = [ 710 "main": main 711 ]; 712 context.macros = [ 713 "HI": null, 714 "REALLY_HI": null 715 ]; 716 717 auto result = preprocess(context).sources; 718 assert(result["main"].strip == "Hi!"); 719 } 720 721 @("Include body in if block") 722 unittest { 723 auto main = " 724 #if HOUSE_ON_FIRE 725 oh no! 726 #endif 727 #if FIREMAN_IN_SIGHT 728 yay saved! 729 #endif 730 #if WATER_BUCKET_IN_HAND 731 Quick use it! 732 #endif 733 #if LAKE_NEARBY 734 Throw house in it! 735 #endif 736 #if CAR_NEARBY 737 Book it! 738 #endif 739 #if SCREAM 740 AAAAAAAH! 741 #endif 742 "; 743 744 auto context = BuildContext(["main": main]); 745 context.macros = [ 746 "HOUSE_ON_FIRE": "true", 747 "WATER_BUCKET_IN_HAND": "0", 748 "LAKE_NEARBY": "FALSE", 749 "CAR_NEARBY": null, 750 "SCREAM": "AAAAAAAAAAAAH!" 751 ]; 752 753 auto result = preprocess(context).sources; 754 assert(result["main"].stripAllWhiteSpace == "ohno!AAAAAAAH!"); 755 } 756 757 @("Include else body in if block if false") 758 unittest { 759 auto main = " 760 #if MOON 761 It's a moon 762 #else 763 That's no moon, it's a space station! 764 #endif 765 "; 766 767 auto context = BuildContext(["main": main]); 768 context.macros = [ 769 "MOON": "false", 770 ]; 771 772 auto result = preprocess(context).sources; 773 assert(result["main"].strip == "That's no moon, it's a space station!"); 774 } 775 776 @("Include elif body in if block if else if is true") 777 unittest { 778 auto main = " 779 #if MOON 780 It's a moon 781 #elif EARTH 782 Oh it's just earth. 783 #elif FIRE 784 We're doing captain planet stuff now? 785 #else 786 That's no moon, it's a space station! 787 #endif 788 "; 789 790 auto context = BuildContext(["main": main]); 791 context.macros = [ 792 "MOON": "false", 793 "EARTH": "probably", 794 "FIRE": "true" 795 ]; 796 797 auto result = preprocess(context).sources; 798 assert(result["main"].strip == "Oh it's just earth."); 799 } 800 801 @("Include if body only in if block if it is true") 802 unittest { 803 auto main = " 804 #if JA 805 Ja! 806 #elif JA 807 Ja! 808 #else 809 Nee! 810 #endif 811 "; 812 813 auto context = BuildContext(["main": main]); 814 context.macros = [ 815 "JA": "ja!", 816 ]; 817 818 auto result = preprocess(context).sources; 819 assert(result["main"].strip == "Ja!"); 820 } 821 } 822 823 // Macros tests 824 version (unittest) { 825 @("Undefined macro fails to expand") 826 unittest { 827 auto main = " 828 __MOTOR__ 829 "; 830 831 auto context = BuildContext(["main.c": main]); 832 assertThrownMsg!ParseException( 833 "Error processing main.c(1,22): Parse error: Cannot expand macro __MOTOR__, it is undefined.", 834 preprocess(context) 835 ); 836 } 837 838 @("Expand custom pre-defined macro") 839 unittest { 840 auto main = " 841 #ifdef HI 842 __HI__ 843 #endif 844 #ifdef __THERE__ 845 __THERE__ 846 #endif 847 "; 848 849 auto context = BuildContext(["main": main]); 850 context.macros = [ 851 "HI": "Hi", 852 "THERE": "There" 853 ]; 854 855 auto result = preprocess(context).sources; 856 assert(result["main"].stripAllWhiteSpace == "HiThere"); 857 } 858 859 @("Built-in macro __FILE__ is defined") 860 unittest { 861 auto main = " 862 #ifdef __FILE__ 863 __FILE__ 864 #endif 865 "; 866 867 auto context = BuildContext(["main.c": main]); 868 auto result = preprocess(context).sources; 869 assert(result["main.c"].strip == "main.c"); 870 } 871 872 @("Built-in macro __LINE__ is defined") 873 unittest { 874 auto main = " 875 #ifdef __LINE__ 876 __LINE__ 877 #endif 878 "; 879 880 auto context = BuildContext(["main.c": main]); 881 auto result = preprocess(context).sources; 882 assert(result["main.c"].strip == "1"); // Code re-writing messes line numbers all up.... It truely is like a C-compiler! 883 } 884 885 @("Built-in macro __DATE__ is defined") 886 unittest { 887 auto main = " 888 #ifdef __DATE__ 889 __DATE__ 890 #endif 891 "; 892 893 auto context = BuildContext(["main.c": main]); 894 auto result = preprocess(context); 895 assert(result.sources["main.c"].strip == result.date); 896 } 897 898 @("Built-in macro __TIME__ is defined") 899 unittest { 900 auto main = " 901 #ifdef __TIME__ 902 __TIME__ 903 #endif 904 "; 905 906 auto context = BuildContext(["main.c": main]); 907 auto result = preprocess(context); 908 assert(result.sources["main.c"].strip == result.time); 909 } 910 911 @("Built-in macro __TIMESTAMP__ is defined") 912 unittest { 913 auto main = " 914 #ifdef __TIMESTAMP__ 915 __TIMESTAMP__ 916 #endif 917 "; 918 919 auto context = BuildContext(["main.c": main]); 920 auto result = preprocess(context); 921 assert(result.sources["main.c"].strip == result.timestamp); 922 } 923 924 @("Ignore detached second underscore as part of possible macro") 925 unittest { 926 auto main = "IM_AM_NOT_A_MACRO"; 927 928 auto context = BuildContext(["main": main]); 929 auto result = preprocess(context).sources; 930 assert(result["main"] == "IM_AM_NOT_A_MACRO"); 931 } 932 933 @("Define an empty macro") 934 unittest { 935 auto main = " 936 #define RTX_ON 937 #ifdef RTX_ON 938 It's on! 939 #endif 940 "; 941 942 auto context = BuildContext(["main": main]); 943 auto result = preprocess(context).sources; 944 assert(result["main"].strip == "It's on!"); 945 } 946 947 @("Define macro with value") 948 unittest { 949 auto main = " 950 #define RTX_ON \"true\" 951 #if RTX_ON 952 It's awwwn! 953 #endif 954 "; 955 956 auto context = BuildContext(["main": main]); 957 auto result = preprocess(context).sources; 958 assert(result["main"].strip == "It's awwwn!"); 959 } 960 961 @("Fail when defining a macro but the name is missing") 962 unittest { 963 auto main = " 964 #define 965 Fail! 966 "; 967 968 auto context = BuildContext(["main": main]); 969 assertThrownMsg!ParseException( 970 "Error processing main(2,2): Parse error: #define directive is missing name of macro.", 971 preprocess(context) 972 ); 973 } 974 975 @("Undefine macro") 976 unittest { 977 auto main = " 978 #define RTX_ON 979 #undef RTX_ON 980 #ifdef RTX_ON 981 It's on! 982 #else 983 It's all the way off. 984 #endif 985 "; 986 987 auto context = BuildContext(["main": main]); 988 auto result = preprocess(context).sources; 989 assert(result["main"].strip == "It's all the way off."); 990 } 991 992 @("Undefine pre-defined macro") 993 unittest { 994 auto main = " 995 #undef RTX_ON 996 #ifndef RTX_ON 997 It's all the way off. 998 #endif 999 "; 1000 1001 auto context = BuildContext(["main": main]); 1002 context.macros = ["RTX_ON": "true"]; 1003 1004 auto result = preprocess(context).sources; 1005 assert(result["main"].strip == "It's all the way off."); 1006 } 1007 1008 @("Fail when undefining a macro but the name is missing") 1009 unittest { 1010 auto main = " 1011 #undef 1012 Fail! 1013 "; 1014 1015 auto context = BuildContext(["main": main]); 1016 assertThrownMsg!ParseException( 1017 "Error processing main(2,2): Parse error: #undef directive is missing name of macro.", 1018 preprocess(context) 1019 ); 1020 } 1021 1022 @("Macro defined in include is available after include") 1023 unittest { 1024 auto sub = " 1025 #define subby 1026 "; 1027 1028 auto main = " 1029 #ifdef subby 1030 Should not be here! 1031 #endif 1032 1033 #include <sub> 1034 1035 #ifdef subby 1036 Should be here! 1037 #endif 1038 "; 1039 1040 BuildContext context; 1041 context.mainSources = ["main": main]; 1042 context.sources = ["sub": sub]; 1043 1044 auto result = preprocess(context).sources; 1045 assert(result["main"].strip == "Should be here!"); 1046 } 1047 1048 @("Macro defined in main is available in include") 1049 unittest { 1050 auto sub = " 1051 __DOG__ 1052 "; 1053 1054 auto main = " 1055 #define DOG \"dog\" 1056 #include <sub> 1057 "; 1058 1059 BuildContext context; 1060 context.mainSources = ["main": main]; 1061 context.sources = ["sub": sub]; 1062 1063 auto result = preprocess(context).sources; 1064 assert(result["main"].strip == "dog"); 1065 } 1066 1067 @("Includes can redefine macros") 1068 unittest { 1069 auto sub = " 1070 __DOG__ 1071 #define DOG \"cat\" 1072 "; 1073 1074 auto main = " 1075 #define DOG \"dog\" 1076 #include <sub> 1077 __DOG__ 1078 "; 1079 1080 BuildContext context; 1081 context.mainSources = ["main": main]; 1082 context.sources = ["sub": sub]; 1083 1084 auto result = preprocess(context).sources; 1085 assert(result["main"].stripAllWhiteSpace == "dogcat"); 1086 } 1087 1088 @("Filename macro used in include is properly expanded") 1089 unittest { 1090 auto sub = " 1091 __FILE__ 1092 "; 1093 1094 auto main = " 1095 #include <sub> 1096 __FILE__ 1097 "; 1098 1099 BuildContext context; 1100 context.mainSources = ["main": main]; 1101 context.sources = ["sub": sub]; 1102 1103 auto result = preprocess(context).sources; 1104 assert(result["main"].stripAllWhiteSpace == "submain"); 1105 } 1106 1107 @("Prevent definition of built-in macros") 1108 unittest { 1109 auto main = " 1110 #define FILE anotherfile.c 1111 "; 1112 1113 auto context = BuildContext(["main": main]); 1114 assertThrownMsg!PreprocessException( 1115 "Error processing main(1,26): Cannot use macro name 'FILE', it is a built-in macro.", 1116 preprocess(context) 1117 ); 1118 } 1119 1120 @("Prevent undefinition of built-in macros") 1121 unittest { 1122 auto main = " 1123 #undef FILE 1124 "; 1125 1126 auto context = BuildContext(["main": main]); 1127 assertThrownMsg!PreprocessException( 1128 "Error processing main(2,1): Cannot use macro name 'FILE', it is a built-in macro.", 1129 preprocess(context) 1130 ); 1131 } 1132 1133 @("Macros defined without quotes are also possible") 1134 unittest { 1135 auto main = " 1136 #define FILE_SIZE 1024 1137 __FILE_SIZE__ 1138 1139 #define SHOW_UNIT true 1140 #if SHOW_UNIT 1141 kB 1142 #endif 1143 "; 1144 1145 auto context = BuildContext(["main": main]); 1146 auto result = preprocess(context).sources; 1147 assert(result["main"].stripAllWhiteSpace == "1024kB"); 1148 } 1149 } 1150 1151 // Error tests 1152 version (unittest) { 1153 @("Error directive is thrown") 1154 unittest { 1155 auto main = " 1156 #error \"This unit test should fail?\" 1157 "; 1158 1159 auto context = BuildContext(["main": main]); 1160 assertThrownMsg!PreprocessException( 1161 "Error processing main(1,49): This unit test should fail?", 1162 preprocess(context) 1163 ); 1164 } 1165 1166 @("Error directive is not thrown when skipped in conditional") 1167 unittest { 1168 auto main = " 1169 #ifdef __WINDOWS__ 1170 #error \"We don't support windows here!\" 1171 #endif 1172 1173 Zen 1174 "; 1175 1176 auto context = BuildContext(["main": main]); 1177 auto result = preprocess(context).sources; 1178 assert(result["main"].strip == "Zen"); 1179 } 1180 1181 @("Error directive in include is thrown from include name, not main") 1182 unittest { 1183 auto include = " 1184 #error \"Should say include.h\" 1185 "; 1186 1187 auto main = " 1188 #include <include.h> 1189 "; 1190 1191 BuildContext context; 1192 context.mainSources = ["main.c": main]; 1193 context.sources = ["include.h": include]; 1194 1195 assertThrownMsg!PreprocessException( 1196 "Error processing include.h(1,42): Should say include.h", 1197 preprocess(context) 1198 ); 1199 } 1200 } 1201 1202 // Pragma tests 1203 version (unittest) { 1204 @("Pragma once guards against multiple inclusions") 1205 unittest { 1206 auto once = " 1207 #pragma once 1208 One time one! 1209 "; 1210 1211 auto main = " 1212 #include <once.d> 1213 #include <once.d> 1214 "; 1215 1216 BuildContext context; 1217 context.sources = ["once.d": once]; 1218 context.mainSources = ["main.d": main]; 1219 1220 auto result = preprocess(context).sources; 1221 assert(result["main.d"].strip == "One time one!"); 1222 } 1223 1224 @("Throw on unsupported pragma extension") 1225 unittest { 1226 auto main = " 1227 #pragma pizza 1228 "; 1229 1230 auto context = BuildContext(["main": main]); 1231 assertThrownMsg!PreprocessException( 1232 "Error processing main(2,1): Pragma extension 'pizza' is unsupported.", 1233 preprocess(context) 1234 ); 1235 } 1236 } 1237 1238 // Advanced tests 1239 version (unittest) { 1240 @("Inclusion guards") 1241 unittest { 1242 auto lib = " 1243 #ifndef CAKE_PHP 1244 #define CAKE_PHP 1245 Cake! 1246 #endif 1247 "; 1248 1249 auto main = " 1250 #include <cake.php> 1251 #include <cake.php> 1252 "; 1253 1254 BuildContext context; 1255 context.mainSources = ["main.php": main]; 1256 context.sources = ["cake.php": lib]; 1257 1258 auto result = preprocess(context).sources; 1259 assert(result["main.php"].strip == "Cake!"); 1260 } 1261 } 1262 1263 //TODO: conditionals in conditionals?