1 module unit_threaded.behave;
2 
3 import std.algorithm;
4 import std.array;
5 import std.format;
6 import std.path: baseName;
7 import std.range;
8 import std.string;
9 import unit_threaded.runner.io;
10 import unit_threaded.runner.testcase;
11 
12 public:
13 
14 alias background = printBehaveLine!"Background:";
15 alias given = printBehaveLine!"Given";
16 alias when = printBehaveLine!"When";
17 alias then = printBehaveLine!"Then";
18 
19 private:
20 
21 template printBehaveLine(string mode) {
22     public void printBehaveLine(string file = __FILE__, size_t line = __LINE__, T...)(string message, T args) {
23         // to save space, only use the filename.
24         const stepLocation = format!"# %s:%s"(file.baseName, line);
25         output.setBehaveLine(mode, format(message, args).intenseQuotes, stepLocation);
26     }
27     public void printBehaveLine(string fmt, string file = __FILE__, size_t line = __LINE__, T...)(T args) {
28         // to save space, only use the filename.
29         const stepLocation = format!"# %s:%s"(file.baseName, line);
30         output.setBehaveLine(mode, format!fmt(args).intenseQuotes, stepLocation);
31     }
32 }
33 
34 string intenseQuotes(string text) {
35     if (!_useEscCodes)
36         // preserve `` in that case
37         return text;
38     // [a] => [a]
39     // [a, b] => [a, b.intense]
40     alias markIntense = pair => chain(pair.take(1), pair.drop(1).map!intense);
41     return text.splitter('`').chunks(2).map!markIntense.joiner.join;
42 }
43 
44 @("mark quotes with intense formatting")
45 unittest
46 {
47     auto result = "Given `Hello` and `World`".intenseQuotes;
48 
49     if (_useEscCodes) {
50         assert(result == "Given \033[1mHello\033[0m and \033[1mWorld\033[0m");
51     } else {
52         assert(result == "Given `Hello` and `World`");
53     }
54 }
55 
56 private size_t visibleLength(string ansiFormattedText) @safe {
57     return ansiFormattedText
58         .splitter("\033[")
59         .mapExceptFirst!(fragment => fragment.find("m").drop(1))
60         .map!"a.length"
61         .sum;
62 }
63 
64 @("visible length of string")
65 unittest {
66     assert(("Hello " ~ green("World")).visibleLength == "Hello World".length);
67 }
68 
69 private alias mapExceptFirst(alias pred) = range => chain(range.take(1), range.drop(1).map!pred);
70 
71 class BehaveOutput: Output {
72     this(Output next) {
73         _next = next;
74     }
75 
76     override void send(in string output) @safe {
77         removePartial;
78         _next.send(output);
79     }
80 
81     override void flush(bool success) @safe {
82         finishBehaveLine(success);
83         // for spacing
84         _next.send("\n");
85         _next.flush(success);
86     }
87 
88 package:
89 
90     void setBehaveLine(Output, Location)(string mode, Output output, Location location) @safe
91     {
92         // for spacing
93         if (_previousMode == "")
94             _next.send("\n");
95         finishBehaveLine(true);
96         if (mode == _previousMode)
97             mode = "And";
98         else _previousMode = mode;
99         _behaveLine = "\t" ~ mode.intense ~ " " ~ output;
100         _longestLine = max(_longestLine, _behaveLine.visibleLength);
101         _location = location;
102         if (_useEscCodes) {
103             // otherwise, we won't be able to erase it later
104             _next.send(fullLine!noColor);
105             _partialLine = true;
106         }
107     }
108 
109 private:
110 
111     void finishBehaveLine(bool success) @safe {
112         removePartial;
113         if (_behaveLine) {
114             _next.send((success ? fullLine!green : fullLine!red) ~ "\n");
115             _behaveLine = null;
116         }
117     }
118 
119     void removePartial() @safe {
120         if (_partialLine) {
121             assert(_useEscCodes);
122             // delete current line, carriage return.
123             _next.send("\033[2K\r");
124             _partialLine = false;
125         }
126     }
127 
128     final string fullLine(alias color)() @safe {
129         const spacing = ((_longestLine + 7) / 8) * 8 + 3 - _behaveLine.visibleLength;
130         return color(_behaveLine) ~ " ".repeat(spacing).join ~ _location;
131     }
132 
133     // So we know to write 'Given...' 'And...'
134     string _previousMode;
135     // So we can flush on error, success or manual write.
136     bool _partialLine;
137     // Longest previous behave line output part.
138     // Used for stable indenting of location.
139     size_t _longestLine;
140     string _behaveLine;
141     string _location;
142     Output _next;
143 }
144 
145 private alias noColor = s => s;
146 
147 // Return the current testcase's behave output.
148 // If the current output is not a behave output, make it one.
149 // TODO if there's another output wrapper like this, they'll
150 // compete for being the "outermost". Instead, provide a way to
151 // search the `next_` list.
152 BehaveOutput output() {
153     import unit_threaded.runner.testcase: TestCase;
154 
155     auto writer = TestCase.currentTest.getWriter;
156     if (auto behave = cast(BehaveOutput) writer)
157         return behave;
158     auto behave = new BehaveOutput(writer);
159     TestCase.currentTest._output = behave;
160     return behave;
161 }