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?