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 }