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 237 // Includes tests 238 version (unittest) { 239 @("Resolve includes") 240 unittest { 241 auto hi = "Hi!"; 242 auto main = "#include <hi.txt>"; 243 auto context = BuildContext([ 244 "hi.txt": hi, 245 "main.txt": main 246 ]); 247 248 auto result = preprocess(context); 249 250 assert(result.sources["hi.txt"] == hi); 251 assert(result.sources["main.txt"] == hi); 252 } 253 254 @("Resolve multiple includes") 255 unittest { 256 auto hi = "Hi!"; 257 auto howAreYou = "How are you?"; 258 auto main = " 259 #include <hi.txt> 260 #include <howAreYou.txt> 261 "; 262 263 auto context = BuildContext([ 264 "hi.txt": hi, 265 "howAreYou.txt": howAreYou, 266 "main.txt": main 267 ]); 268 269 auto result = preprocess(context); 270 271 auto expectedResult = " 272 Hi! 273 How are you? 274 "; 275 276 assert(result.sources["hi.txt"] == hi); 277 assert(result.sources["howAreYou.txt"] == howAreYou); 278 assert(result.sources["main.txt"] == expectedResult); 279 } 280 281 @("Resolve includes in includes") 282 unittest { 283 auto hi = "Hi!"; 284 auto secondary = "#include <hi.txt>"; 285 auto main = "#include <secondary.txt>"; 286 287 auto context = BuildContext([ 288 "hi.txt": hi, 289 "secondary.txt": secondary, 290 "main.txt": main 291 ]); 292 293 auto result = preprocess(context); 294 295 assert(result.sources["hi.txt"] == hi); 296 assert(result.sources["secondary.txt"] == hi); 297 assert(result.sources["main.txt"] == hi); 298 } 299 300 @("Fail to include when filename is on other line") 301 unittest { 302 auto main = " 303 #include 304 <other.txt> 305 "; 306 307 auto context = BuildContext([ 308 "main.txt": main 309 ]); 310 311 assertThrownMsg!ParseException( 312 "Error processing main.txt(2,1): Parse error: Failed to parse include directive: Expected \" or <.", 313 preprocess(context) 314 ); 315 } 316 317 @("Fail to include when filename does not start with quote or <") 318 unittest { 319 auto main = "#include 'coolfile.c'"; 320 auto context = BuildContext([ 321 "main.txt": main 322 ]); 323 324 assertThrownMsg!ParseException( 325 "Error processing main.txt(0,9): Parse error: Failed to parse include directive: Expected \" or <.", 326 preprocess(context) 327 ); 328 } 329 330 @("Fail to include when included source is not in build context") 331 unittest { 332 auto main = "#include <notfound.404>"; 333 auto context = BuildContext([ 334 "main.txt": main 335 ]); 336 337 assertThrownMsg!PreprocessException( 338 "Error processing main.txt(0,0): Failed to include 'notfound.404': It does not exist.", 339 preprocess(context) 340 ); 341 } 342 343 @("Prevent endless inclusion cycle") 344 unittest { 345 auto main = "#include \"main.md\""; 346 auto context = BuildContext([ 347 "main.md": main 348 ]); 349 context.inclusionLimit = 5; 350 351 assertThrownMsg!PreprocessException( 352 "Error processing main.md(0,9): Inclusions has exceeded the limit of 5. Adjust BuildContext.inclusionLimit to increase.", 353 preprocess(context) 354 ); 355 } 356 357 @("Inclusions using quotes are directory-aware and relative") 358 unittest { 359 auto main = "#include \"secondary.txt\""; 360 auto secondary = "Heey"; 361 auto context = BuildContext([ 362 "cool/main.txt": main, 363 "cool/secondary.txt": secondary 364 ]); 365 366 auto result = preprocess(context); 367 368 assert(result.sources["cool/main.txt"] == secondary); 369 assert(result.sources["cool/secondary.txt"] == secondary); 370 } 371 } 372 373 // Conditional tests 374 version (unittest) { 375 @("Fail if a rogue #endif is found") 376 unittest { 377 auto main = "#endif"; 378 auto context = BuildContext(["main": main]); 379 380 assertThrownMsg!ParseException( 381 "Error processing main(0,6): Parse error: #endif directive found without accompanying starting conditional (#if/#ifdef)", 382 preprocess(context) 383 ); 384 } 385 386 @("Fail if a rogue #else is found") 387 unittest { 388 auto main = "#else"; 389 auto context = BuildContext(["main": main]); 390 391 assertThrownMsg!ParseException( 392 "Error processing main(0,5): Parse error: #else directive found without accompanying starting conditional (#if/#ifdef)", 393 preprocess(context) 394 ); 395 } 396 397 @("Fail if a rogue #elif is found") 398 unittest { 399 auto main = "#elif"; 400 auto context = BuildContext(["main": main]); 401 402 assertThrownMsg!ParseException( 403 "Error processing main(0,5): Parse error: #elif directive found without accompanying starting conditional (#if/#ifdef)", 404 preprocess(context) 405 ); 406 } 407 408 @("Not fail if a rogue #endif is found and ignored") 409 unittest { 410 auto main = "#endif"; 411 auto context = BuildContext(["main": main]); 412 context.ignoreUnmatchedConditionalDirectives = true; 413 414 auto result = preprocess(context).sources; 415 assert(result["main"].strip == "#endif"); 416 } 417 418 @("Not fail if a rogue #else is found and ignored") 419 unittest { 420 auto main = "#else"; 421 auto context = BuildContext(["main": main]); 422 context.ignoreUnmatchedConditionalDirectives = true; 423 424 auto result = preprocess(context).sources; 425 assert(result["main"].strip == "#else"); 426 } 427 428 @("Not fail if a rogue #elif is found and ignored") 429 unittest { 430 auto main = "#elif"; 431 auto context = BuildContext(["main": main]); 432 context.ignoreUnmatchedConditionalDirectives = true; 433 434 auto result = preprocess(context).sources; 435 assert(result["main"].strip == "#elif"); 436 } 437 438 @("Include body if token is defined") 439 unittest { 440 auto main = " 441 #ifdef I_AM_GROOT 442 Groot! 443 #endif 444 "; 445 446 auto context = BuildContext(["main": main]); 447 context.macros = [ 448 "I_AM_GROOT": "very" 449 ]; 450 451 auto result = preprocess(context).sources; 452 assert(result["main"].strip == "Groot!"); 453 } 454 455 @("Not include body if token is not defined") 456 unittest { 457 auto main = " 458 #ifdef I_AM_NOT_GROOT 459 Groot! 460 #endif 461 "; 462 463 auto context = BuildContext(["main": main]); 464 context.macros = [ 465 "I_AM_GROOT": "very" 466 ]; 467 468 auto result = preprocess(context).sources; 469 assert(result["main"].strip == ""); 470 } 471 472 @("Include else body if token is not defined") 473 unittest { 474 auto main = " 475 #ifdef I_AM_NOT_GROOT 476 Groot! 477 #else 478 Not Groot! 479 #endif 480 "; 481 482 auto context = BuildContext(["main": main]); 483 context.macros = [ 484 "I_AM_GROOT": "very" 485 ]; 486 487 auto result = preprocess(context).sources; 488 assert(result["main"].strip == "Not Groot!"); 489 } 490 491 @("Not include else body if token is defined") 492 unittest { 493 auto main = " 494 #ifdef I_AM_GROOT 495 Tree! 496 #else 497 Not Tree! 498 #endif 499 "; 500 501 auto context = BuildContext(["main": main]); 502 context.macros = [ 503 "I_AM_GROOT": "very" 504 ]; 505 506 auto result = preprocess(context).sources; 507 assert(result["main"].strip == "Tree!"); 508 } 509 510 @("Fail when else is defined multiple times") 511 unittest { 512 auto main = " 513 #ifdef I_AM_NOT_GROOT 514 Groot! 515 #else 516 Not Groot! 517 #else 518 Still not Groot! 519 #endif 520 "; 521 522 auto context = BuildContext(["main": main]); 523 assertThrownMsg!ParseException( 524 "Error processing main(3,1): Parse error: #else directive defined multiple times. Only one #else block is allowed.", 525 preprocess(context) 526 ); 527 } 528 529 @("Fail when end of file is reached before conditional terminator") 530 unittest { 531 auto main = " 532 #ifdef I_AM_GROOT 533 Groot! 534 "; 535 536 auto context = BuildContext(["main": main]); 537 assertThrownMsg!ParseException( 538 "Error processing main(3,9): Parse error: Unexpected end of file while processing directive.", 539 preprocess(context) 540 ); 541 } 542 543 @("Include body if token is not defined in ifndef") 544 unittest { 545 auto main = " 546 #ifndef I_AM_NOT_GROOT 547 Groot not here! 548 #endif 549 "; 550 551 auto context = BuildContext(["main": main]); 552 553 auto result = preprocess(context).sources; 554 assert(result["main"].strip == "Groot not here!"); 555 } 556 557 @("Not include body if token is defined in ifndef") 558 unittest { 559 auto main = " 560 #ifndef I_AM_NOT_GROOT 561 Groot not here! 562 #endif 563 "; 564 565 auto context = BuildContext(["main": main]); 566 context.macros = ["I_AM_NOT_GROOT": "ok man!"]; 567 568 auto result = preprocess(context).sources; 569 assert(result["main"].strip == ""); 570 } 571 572 @("Include else body if token is defined in ifndef") 573 unittest { 574 auto main = " 575 #ifndef I_AM_NOT_GROOT 576 Groot not here! 577 #else 578 Big tree thing is here! 579 #endif 580 "; 581 582 auto context = BuildContext(["main": main]); 583 context.macros = ["I_AM_NOT_GROOT": "ok man!"]; 584 585 auto result = preprocess(context).sources; 586 assert(result["main"].strip == "Big tree thing is here!"); 587 } 588 589 @("Not include else body if token is not defined in ifndef") 590 unittest { 591 auto main = " 592 #ifndef I_AM_NOT_GROOT 593 Groot not here! 594 #else 595 Big tree thing is here! 596 #endif 597 "; 598 599 auto context = BuildContext(["main": main]); 600 601 auto result = preprocess(context).sources; 602 assert(result["main"].strip == "Groot not here!"); 603 } 604 605 @("#Include works in conditional body") 606 unittest { 607 auto one = "One"; 608 auto eins = "EINS"; 609 auto two = "Two"; 610 auto zwei = "ZWEI"; 611 auto three = "Three"; 612 auto drei = "DREI"; 613 auto four = "Four"; 614 auto vier = "VIER"; 615 616 auto main = " 617 #ifdef ONE 618 #include <one> 619 #else 620 #include <eins> 621 #endif 622 #ifdef ZWEI 623 #include <zwei> 624 #else 625 #include <two> 626 #endif 627 #ifndef DREI 628 #include <three> 629 #else 630 #include <drei> 631 #endif 632 #ifndef FOUR 633 #include <vier> 634 #else 635 #include <four> 636 #endif 637 "; 638 639 BuildContext context; 640 context.macros = [ 641 "ONE": "", 642 "FOUR": "", 643 ]; 644 context.sources = [ 645 "one": one, 646 "eins": eins, 647 "two": two, 648 "zwei": zwei, 649 "three": three, 650 "drei": drei, 651 "four": four, 652 "vier": vier 653 ]; 654 context.mainSources = ["main": main]; 655 656 auto result = preprocess(context).sources; 657 assert(result["main"].stripAllWhiteSpace == "OneTwoThreeFour"); 658 } 659 660 @("Conditionals inside of conditional is not supported") 661 unittest { 662 auto main = " 663 #ifdef HI 664 #ifdef REALLY_HI 665 Hi! 666 #endif 667 #endif 668 "; 669 670 auto context = BuildContext(["main": main]); 671 672 assertThrownMsg!ParseException( 673 "Error processing main(2,1): Parse error: #endif directive found without accompanying starting conditional (#if/#ifdef)", 674 preprocess(context) 675 ); 676 } 677 678 @("Conditionals inside of included code is supported") 679 unittest { 680 auto main = " 681 #ifdef HI 682 #include <include> 683 #endif 684 "; 685 686 auto include = " 687 #ifdef REALLY_HI 688 Hi! 689 #endif 690 "; 691 692 BuildContext context; 693 context.sources = [ 694 "include": include 695 ]; 696 context.mainSources = [ 697 "main": main 698 ]; 699 context.macros = [ 700 "HI": null, 701 "REALLY_HI": null 702 ]; 703 704 auto result = preprocess(context).sources; 705 assert(result["main"].strip == "Hi!"); 706 } 707 708 @("Include body in if block") 709 unittest { 710 auto main = " 711 #if HOUSE_ON_FIRE 712 oh no! 713 #endif 714 #if FIREMAN_IN_SIGHT 715 yay saved! 716 #endif 717 #if WATER_BUCKET_IN_HAND 718 Quick use it! 719 #endif 720 #if LAKE_NEARBY 721 Throw house in it! 722 #endif 723 #if CAR_NEARBY 724 Book it! 725 #endif 726 #if SCREAM 727 AAAAAAAH! 728 #endif 729 "; 730 731 auto context = BuildContext(["main": main]); 732 context.macros = [ 733 "HOUSE_ON_FIRE": "true", 734 "WATER_BUCKET_IN_HAND": "0", 735 "LAKE_NEARBY": "FALSE", 736 "CAR_NEARBY": null, 737 "SCREAM": "AAAAAAAAAAAAH!" 738 ]; 739 740 auto result = preprocess(context).sources; 741 assert(result["main"].stripAllWhiteSpace == "ohno!AAAAAAAH!"); 742 } 743 744 @("Include else body in if block if false") 745 unittest { 746 auto main = " 747 #if MOON 748 It's a moon 749 #else 750 That's no moon, it's a space station! 751 #endif 752 "; 753 754 auto context = BuildContext(["main": main]); 755 context.macros = [ 756 "MOON": "false", 757 ]; 758 759 auto result = preprocess(context).sources; 760 assert(result["main"].strip == "That's no moon, it's a space station!"); 761 } 762 763 @("Include elif body in if block if else if is true") 764 unittest { 765 auto main = " 766 #if MOON 767 It's a moon 768 #elif EARTH 769 Oh it's just earth. 770 #elif FIRE 771 We're doing captain planet stuff now? 772 #else 773 That's no moon, it's a space station! 774 #endif 775 "; 776 777 auto context = BuildContext(["main": main]); 778 context.macros = [ 779 "MOON": "false", 780 "EARTH": "probably", 781 "FIRE": "true" 782 ]; 783 784 auto result = preprocess(context).sources; 785 assert(result["main"].strip == "Oh it's just earth."); 786 } 787 788 @("Include if body only in if block if it is true") 789 unittest { 790 auto main = " 791 #if JA 792 Ja! 793 #elif JA 794 Ja! 795 #else 796 Nee! 797 #endif 798 "; 799 800 auto context = BuildContext(["main": main]); 801 context.macros = [ 802 "JA": "ja!", 803 ]; 804 805 auto result = preprocess(context).sources; 806 assert(result["main"].strip == "Ja!"); 807 } 808 } 809 810 // Macros tests 811 version (unittest) { 812 @("Undefined macro fails to expand") 813 unittest { 814 auto main = " 815 __MOTOR__ 816 "; 817 818 auto context = BuildContext(["main.c": main]); 819 assertThrownMsg!ParseException( 820 "Error processing main.c(1,22): Parse error: Cannot expand macro __MOTOR__, it is undefined.", 821 preprocess(context) 822 ); 823 } 824 825 @("Expand custom pre-defined macro") 826 unittest { 827 auto main = " 828 #ifdef HI 829 __HI__ 830 #endif 831 #ifdef __THERE__ 832 __THERE__ 833 #endif 834 "; 835 836 auto context = BuildContext(["main": main]); 837 context.macros = [ 838 "HI": "Hi", 839 "THERE": "There" 840 ]; 841 842 auto result = preprocess(context).sources; 843 assert(result["main"].stripAllWhiteSpace == "HiThere"); 844 } 845 846 @("Built-in macro __FILE__ is defined") 847 unittest { 848 auto main = " 849 #ifdef __FILE__ 850 __FILE__ 851 #endif 852 "; 853 854 auto context = BuildContext(["main.c": main]); 855 auto result = preprocess(context).sources; 856 assert(result["main.c"].strip == "main.c"); 857 } 858 859 @("Built-in macro __LINE__ is defined") 860 unittest { 861 auto main = " 862 #ifdef __LINE__ 863 __LINE__ 864 #endif 865 "; 866 867 auto context = BuildContext(["main.c": main]); 868 auto result = preprocess(context).sources; 869 assert(result["main.c"].strip == "1"); // Code re-writing messes line numbers all up.... It truely is like a C-compiler! 870 } 871 872 @("Built-in macro __DATE__ is defined") 873 unittest { 874 auto main = " 875 #ifdef __DATE__ 876 __DATE__ 877 #endif 878 "; 879 880 auto context = BuildContext(["main.c": main]); 881 auto result = preprocess(context); 882 assert(result.sources["main.c"].strip == result.date); 883 } 884 885 @("Built-in macro __TIME__ is defined") 886 unittest { 887 auto main = " 888 #ifdef __TIME__ 889 __TIME__ 890 #endif 891 "; 892 893 auto context = BuildContext(["main.c": main]); 894 auto result = preprocess(context); 895 assert(result.sources["main.c"].strip == result.time); 896 } 897 898 @("Built-in macro __TIMESTAMP__ is defined") 899 unittest { 900 auto main = " 901 #ifdef __TIMESTAMP__ 902 __TIMESTAMP__ 903 #endif 904 "; 905 906 auto context = BuildContext(["main.c": main]); 907 auto result = preprocess(context); 908 assert(result.sources["main.c"].strip == result.timestamp); 909 } 910 911 @("Ignore detached second underscore as part of possible macro") 912 unittest { 913 auto main = "IM_AM_NOT_A_MACRO"; 914 915 auto context = BuildContext(["main": main]); 916 auto result = preprocess(context).sources; 917 assert(result["main"] == "IM_AM_NOT_A_MACRO"); 918 } 919 920 @("Define an empty macro") 921 unittest { 922 auto main = " 923 #define RTX_ON 924 #ifdef RTX_ON 925 It's on! 926 #endif 927 "; 928 929 auto context = BuildContext(["main": main]); 930 auto result = preprocess(context).sources; 931 assert(result["main"].strip == "It's on!"); 932 } 933 934 @("Define macro with value") 935 unittest { 936 auto main = " 937 #define RTX_ON \"true\" 938 #if RTX_ON 939 It's awwwn! 940 #endif 941 "; 942 943 auto context = BuildContext(["main": main]); 944 auto result = preprocess(context).sources; 945 assert(result["main"].strip == "It's awwwn!"); 946 } 947 948 @("Fail when defining a macro but the name is missing") 949 unittest { 950 auto main = " 951 #define 952 Fail! 953 "; 954 955 auto context = BuildContext(["main": main]); 956 assertThrownMsg!ParseException( 957 "Error processing main(2,2): Parse error: #define directive is missing name of macro.", 958 preprocess(context) 959 ); 960 } 961 962 @("Undefine macro") 963 unittest { 964 auto main = " 965 #define RTX_ON 966 #undef RTX_ON 967 #ifdef RTX_ON 968 It's on! 969 #else 970 It's all the way off. 971 #endif 972 "; 973 974 auto context = BuildContext(["main": main]); 975 auto result = preprocess(context).sources; 976 assert(result["main"].strip == "It's all the way off."); 977 } 978 979 @("Undefine pre-defined macro") 980 unittest { 981 auto main = " 982 #undef RTX_ON 983 #ifndef RTX_ON 984 It's all the way off. 985 #endif 986 "; 987 988 auto context = BuildContext(["main": main]); 989 context.macros = ["RTX_ON": "true"]; 990 991 auto result = preprocess(context).sources; 992 assert(result["main"].strip == "It's all the way off."); 993 } 994 995 @("Fail when undefining a macro but the name is missing") 996 unittest { 997 auto main = " 998 #undef 999 Fail! 1000 "; 1001 1002 auto context = BuildContext(["main": main]); 1003 assertThrownMsg!ParseException( 1004 "Error processing main(2,2): Parse error: #undef directive is missing name of macro.", 1005 preprocess(context) 1006 ); 1007 } 1008 1009 @("Macro defined in include is available after include") 1010 unittest { 1011 auto sub = " 1012 #define subby 1013 "; 1014 1015 auto main = " 1016 #ifdef subby 1017 Should not be here! 1018 #endif 1019 1020 #include <sub> 1021 1022 #ifdef subby 1023 Should be here! 1024 #endif 1025 "; 1026 1027 BuildContext context; 1028 context.mainSources = ["main": main]; 1029 context.sources = ["sub": sub]; 1030 1031 auto result = preprocess(context).sources; 1032 assert(result["main"].strip == "Should be here!"); 1033 } 1034 1035 @("Macro defined in main is available in include") 1036 unittest { 1037 auto sub = " 1038 __DOG__ 1039 "; 1040 1041 auto main = " 1042 #define DOG \"dog\" 1043 #include <sub> 1044 "; 1045 1046 BuildContext context; 1047 context.mainSources = ["main": main]; 1048 context.sources = ["sub": sub]; 1049 1050 auto result = preprocess(context).sources; 1051 assert(result["main"].strip == "dog"); 1052 } 1053 1054 @("Includes can redefine macros") 1055 unittest { 1056 auto sub = " 1057 __DOG__ 1058 #define DOG \"cat\" 1059 "; 1060 1061 auto main = " 1062 #define DOG \"dog\" 1063 #include <sub> 1064 __DOG__ 1065 "; 1066 1067 BuildContext context; 1068 context.mainSources = ["main": main]; 1069 context.sources = ["sub": sub]; 1070 1071 auto result = preprocess(context).sources; 1072 assert(result["main"].stripAllWhiteSpace == "dogcat"); 1073 } 1074 1075 @("Filename macro used in include is properly expanded") 1076 unittest { 1077 auto sub = " 1078 __FILE__ 1079 "; 1080 1081 auto main = " 1082 #include <sub> 1083 __FILE__ 1084 "; 1085 1086 BuildContext context; 1087 context.mainSources = ["main": main]; 1088 context.sources = ["sub": sub]; 1089 1090 auto result = preprocess(context).sources; 1091 assert(result["main"].stripAllWhiteSpace == "submain"); 1092 } 1093 1094 @("Prevent definition of built-in macros") 1095 unittest { 1096 auto main = " 1097 #define FILE anotherfile.c 1098 "; 1099 1100 auto context = BuildContext(["main": main]); 1101 assertThrownMsg!PreprocessException( 1102 "Error processing main(1,26): Cannot use macro name 'FILE', it is a built-in macro.", 1103 preprocess(context) 1104 ); 1105 } 1106 1107 @("Prevent undefinition of built-in macros") 1108 unittest { 1109 auto main = " 1110 #undef FILE 1111 "; 1112 1113 auto context = BuildContext(["main": main]); 1114 assertThrownMsg!PreprocessException( 1115 "Error processing main(2,1): Cannot use macro name 'FILE', it is a built-in macro.", 1116 preprocess(context) 1117 ); 1118 } 1119 1120 @("Macros defined without quotes are also possible") 1121 unittest { 1122 auto main = " 1123 #define FILE_SIZE 1024 1124 __FILE_SIZE__ 1125 1126 #define SHOW_UNIT true 1127 #if SHOW_UNIT 1128 kB 1129 #endif 1130 "; 1131 1132 auto context = BuildContext(["main": main]); 1133 auto result = preprocess(context).sources; 1134 assert(result["main"].stripAllWhiteSpace == "1024kB"); 1135 } 1136 } 1137 1138 // Error tests 1139 version (unittest) { 1140 @("Error directive is thrown") 1141 unittest { 1142 auto main = " 1143 #error \"This unit test should fail?\" 1144 "; 1145 1146 auto context = BuildContext(["main": main]); 1147 assertThrownMsg!PreprocessException( 1148 "Error processing main(1,49): This unit test should fail?", 1149 preprocess(context) 1150 ); 1151 } 1152 1153 @("Error directive is not thrown when skipped in conditional") 1154 unittest { 1155 auto main = " 1156 #ifdef __WINDOWS__ 1157 #error \"We don't support windows here!\" 1158 #endif 1159 1160 Zen 1161 "; 1162 1163 auto context = BuildContext(["main": main]); 1164 auto result = preprocess(context).sources; 1165 assert(result["main"].strip == "Zen"); 1166 } 1167 1168 @("Error directive in include is thrown from include name, not main") 1169 unittest { 1170 auto include = " 1171 #error \"Should say include.h\" 1172 "; 1173 1174 auto main = " 1175 #include <include.h> 1176 "; 1177 1178 BuildContext context; 1179 context.mainSources = ["main.c": main]; 1180 context.sources = ["include.h": include]; 1181 1182 assertThrownMsg!PreprocessException( 1183 "Error processing include.h(1,42): Should say include.h", 1184 preprocess(context) 1185 ); 1186 } 1187 } 1188 1189 // Pragma tests 1190 version (unittest) { 1191 @("Pragma once guards against multiple inclusions") 1192 unittest { 1193 auto once = " 1194 #pragma once 1195 One time one! 1196 "; 1197 1198 auto main = " 1199 #include <once.d> 1200 #include <once.d> 1201 "; 1202 1203 BuildContext context; 1204 context.sources = ["once.d": once]; 1205 context.mainSources = ["main.d": main]; 1206 1207 auto result = preprocess(context).sources; 1208 assert(result["main.d"].strip == "One time one!"); 1209 } 1210 1211 @("Throw on unsupported pragma extension") 1212 unittest { 1213 auto main = " 1214 #pragma pizza 1215 "; 1216 1217 auto context = BuildContext(["main": main]); 1218 assertThrownMsg!PreprocessException( 1219 "Error processing main(2,1): Pragma extension 'pizza' is unsupported.", 1220 preprocess(context) 1221 ); 1222 } 1223 } 1224 1225 // Advanced tests 1226 version (unittest) { 1227 @("Inclusion guards") 1228 unittest { 1229 auto lib = " 1230 #ifndef CAKE_PHP 1231 #define CAKE_PHP 1232 Cake! 1233 #endif 1234 "; 1235 1236 auto main = " 1237 #include <cake.php> 1238 #include <cake.php> 1239 "; 1240 1241 BuildContext context; 1242 context.mainSources = ["main.php": main]; 1243 context.sources = ["cake.php": lib]; 1244 1245 auto result = preprocess(context).sources; 1246 assert(result["main.php"].strip == "Cake!"); 1247 } 1248 } 1249 1250 //TODO: conditionals in conditionals?