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?