102030405060708090100110120130140150160170180190200210220230240250260270280290300310320330340350360370380390400410420430440450460470480490500510520530540550560570580590600610620639649659669679680690709719720739749759769770780790800810820830840850860870880890900910920930940950960970980990100010101020103010401050106010701080109011001110112011301140115011601170118011901200121012201230124012501260127012801290130013101320133013401350136013701380139014001410142014301440145014601470148014901500151015201530154015501560157015801590160016101620163016401650166016701680169017001710172017301740175017601770178017901800181018201837184018571860187018855618901900191141920193019401955561960197556198556199020055620102020203020456020502060207020802090210021102125532130214021502162815217021802190220281522102220223022472250226022702280229023002310232023302340235023602370238023902400241024202430244024502460247024802490250025102520253025402550256025702580259026002610262026302640265026602670268026902700271027202730274027502760277027802790280028102820283028402850286028702880289029002910292029302940295029602970298029903000301030203030304030503060307030803090310031103120313031403150316031703180319032003210322032303240325032603270328032903300331033203330334033503360337033803390340034103420343034403450346034703480349035003510352035303540355035603570358035903600361036203630364036503660367036803690370037103720373037403750376037703780379038003810382038303840385038603870388038943904391439203930394039503960397039803990400840104020403040484050406040704080409041004110412041304140415041604170418041904200421042204230424042504260427042804290430043104320433043404350436043704380439044004410442044304440445044604470448044904500451045204530454045504560457045804590460046104620463046404650466046704680469047004710472047304740475047604770478047904800481048204830484048504860487048804890490049104920493049404950496849704980499050005010502050305040505050605070508050905100511051205130514051505160517051805190520052105220523052405250526052705280529053005310532053305340535053605370538053905400541054205430544054505460547054805490550055105520553055405550556055705580559056005610562056305640565056605670568056905700571057205730574057505760577057805790580058105820583058405850586058705880589059005910592059305940595059605970598059906000601060206030604060506060607060806090610061106120613061406150616061706180619062006210622062306240625062606270628062906300631063206330634063506360637063806390640064106420643064406450646064706480649065006512652065326542655065606570658065946604661466246630664466546660667066886694670067106724673067406750676067706780679068006810682068306840685068606870688068906900691554692069317944694564269574869606976922698069907000701554702070307040705558706558707558708558709071055871155871255871307146697990715167435871607171674358718289037192890372007210722167435872328816724072507261674358727884872807290730167435873112762673235497333549734073507363373373755873855873907405587410742074307440745558746074707480749075007510752075307540755075607570758075907600761076207630764076507660767076807690770077107720773077407750776077707780779078007810782078307840785078607870788078907905587915587920793079416522795399179607973991798108799080008013991802108803080408054549806550807550808080908103541811881208130814081555081608173415948188526181908208526182163882208230824852618256388260827638828550829550830083108320833083455083508360837083808390840084108420843084408450846084708480849085008510852085308540855085608570858085908600861086208630864086508660867086808690870087108720873087408750876087708780879088008810882088308840885088608871123888112388908901123891112389208932233389470708957070896089770708981398990900090170709021289030904090570709061190709080909705991087591109120913618491450991509160917113059186091909200921561592254392309240925507292609270928092950729306469310932093362399340935093609370938507293909400941094209430944094509460947094809490950095109520953095409550956095709580959096009610962096309640965096609670968096909700971097209730974097509760977097809790980098109820983098409850986098709880989099009910992099309940995099609970998099901000210010100214310030100401005010062100721008010090101001011010120101301014010150101601017010180101901020010210102201023010240102501026010270102801029010300103101032010330103401035010360103701038010390104001041010420104301044010450104601047010480104901050010510105201053010540105501056010570105801059010600106101062010630106401065010660106701068010690107001071010720107301074010750107601077010780107901080010810108201083010840108501086010870108801089010900109101092010930109401095010960109701098010990110001101556110201103885911104284511050110601107556110821109011100111155411120111301114011150111601117011180111901120011210112201123011240112501126011270112801129011300113101132011330113401135011360113701138011390114001141011420114301144011455581146558114755811480114955811500115101152011530115401155558115655811570115855811595581160011615581162011630116401165011660116755811682711692711700117101172011730117455611755561176556117701178556117955611800118155611825561183556118401185556118655411870118855411890119001191211920119321194011950119601197011980119901200212010120201203012040120541206412070120841209012100121101212412134121441215012166621217144121801219528122050122128122201223412240122501226012271441228261229012300123114412324612330123401235288123601237144123801239144124014412411441242012432621244121245012460124701248412490125001251012520125301254012550125601257012580125901260012610126201263012640126501266012670126801269012700127101272012730127401275012760127701278012790128001281012820128301284012850128601287012880128901290012910129201293012940129501296012970129801299013000130101302013030130401305013060130701308013090131001311013120131301314013150131601317013180131901320013210132201323013240132501326013270132801329013300133101332013330133401335013360133701338013390134001341013420134301344013450134601347761348381349381350013513813520135301354013557613560135738135838135938136038136101362381363381364013651253781366013670136801369013700137101372013730137401375013760137701378013790138001381013820138301384013850138601387013880138901390013910139201393013940139501396013970139801399014000140101402014030140401405014060140701408014090141001411014120141301414014150141601417014180141901420014210142201423014240142501426014270142801429014300143101432014330143401435014360143701438014390144001441014420144301444014450144601447014480144901450014510145201453014540145501456014570145801459014600146101462014630146401465014660146701468014690147001471014720147301474014750147601477014780147901480014810148201483014840148501486201777148767232148801489672321490594211491014927811149378111494014957742714961799814971799814981799814990150001501015020150301504015050150601507015080150901510015110151201513015140151501516015170151801519015200152101522015230152401525015260152701528015290153001531015320153301534015350153601537015380153901540015410154201543015440154501546015470154801549015500155101552015530155401555015560155701558015590156001561015620156301564015650156601567015680156901570015710157201573015740157501576015770157801579015800158101582015830158401585015860158701588015890159001591015920159301594015950159601597015980159901600016010160201603016040160501606016070160801609016100161101612016130161401615016160161701618016190162001621016220162301624016250162601627016280162901630016310163201633016340163501636016370163801639016400164101642016430 module fluentasserts.core.results; import std.stdio; import std.file; import std.algorithm; import std.conv; import std.range; import std.string; import std.exception; import std.typecons; import dparse.lexer; import dparse.parser; @safe: /// Glyphs used to display special chars in the results struct ResultGlyphs { static { /// Glyph for the tab char string tab; /// Glyph for the \r char string carriageReturn; /// Glyph for the \n char string newline; /// Glyph for the space char string space; /// Glyph for the \0 char string nullChar; /// Glyph that indicates the error line string sourceIndicator; /// Glyph that sepparates the line number string sourceLineSeparator; /// Glyph for the diff begin indicator string diffBegin; /// Glyph for the diff end indicator string diffEnd; /// Glyph that marks an inserted text in diff string diffInsert; /// Glyph that marks deleted text in diff string diffDelete; } /// Set the default values. The values are static resetDefaults() { version(windows) { ResultGlyphs.tab = `\t`; ResultGlyphs.carriageReturn = `\r`; ResultGlyphs.newline = `\n`; ResultGlyphs.space = ` `; ResultGlyphs.nullChar = `␀`; } else { ResultGlyphs.tab = `¤`; ResultGlyphs.carriageReturn = `←`; ResultGlyphs.newline = `↲`; ResultGlyphs.space = `᛫`; ResultGlyphs.nullChar = `\0`; } ResultGlyphs.sourceIndicator = ">"; ResultGlyphs.sourceLineSeparator = ":"; ResultGlyphs.diffBegin = "["; ResultGlyphs.diffEnd = "]"; ResultGlyphs.diffInsert = "+"; ResultGlyphs.diffDelete = "-"; } } /// interface ResultPrinter { void primary(string); void info(string); void danger(string); void success(string); void dangerReverse(string); void successReverse(string); } version(unittest) { class MockPrinter : ResultPrinter { string buffer; void primary(string val) { buffer ~= "[primary:" ~ val ~ "]"; } void info(string val) { buffer ~= "[info:" ~ val ~ "]"; } void danger(string val) { buffer ~= "[danger:" ~ val ~ "]"; } void success(string val) { buffer ~= "[success:" ~ val ~ "]"; } void dangerReverse(string val) { buffer ~= "[dangerReverse:" ~ val ~ "]"; } void successReverse(string val) { buffer ~= "[successReverse:" ~ val ~ "]"; } } } struct WhiteIntervals { size_t left; size_t right; } WhiteIntervals getWhiteIntervals(string text) { auto stripText = text.strip; if(stripText == "") { return WhiteIntervals(0, 0); } return WhiteIntervals(text.indexOf(stripText[0]), text.lastIndexOf(stripText[stripText.length - 1])); } /// This is the most simple implementation of a ResultPrinter. /// All the plain data is printed to stdout class DefaultResultPrinter : ResultPrinter { void primary(string text) { write(text); } void info(string text) { write(text); } void danger(string text) { write(text); } void success(string text) { write(text); } void dangerReverse(string text) { write(text); } void successReverse(string text) { write(text); } } interface IResult { string toString(); void print(ResultPrinter); } /// A result that prints a simple message to the user class MessageResult : IResult { private { struct Message { bool isValue; string text; } Message[] messages; } this(string message) nothrow { add(false, message); } this() nothrow { } override string toString() { return messages.map!"a.text".join.to!string; } void startWith(string message) @safe nothrow { Message[] newMessages; newMessages ~= Message(false, message); newMessages ~= this.messages; this.messages = newMessages; } void add(bool isValue, string message) nothrow { this.messages ~= Message(isValue, message .replace("\r", ResultGlyphs.carriageReturn) .replace("\n", ResultGlyphs.newline) .replace("\0", ResultGlyphs.nullChar) .replace("\t", ResultGlyphs.tab)); } void addValue(string text) @safe nothrow { add(true, text); } void addText(string text) @safe nothrow { if(text == "throwAnyException") { text = "throw any exception"; } this.messages ~= Message(false, text); } void prependText(string text) @safe nothrow { this.messages = Message(false, text) ~ this.messages; } void prependValue(string text) @safe nothrow { this.messages = Message(true, text) ~ this.messages; } void print(ResultPrinter printer) { foreach(message; messages) { if(message.isValue) { printer.info(message.text); } else { printer.primary(message.text); } } } } version (unittest) { import fluentasserts.core.base; } @("Message result should return the message") unittest { auto result = new MessageResult("Message"); result.toString.should.equal("Message"); } @("Message result should replace the special chars") unittest { auto result = new MessageResult("\t \r\n"); result.toString.should.equal(`¤ ←↲`); } @("Message result should replace the special chars with the custom glyphs") unittest { scope(exit) { ResultGlyphs.resetDefaults; } ResultGlyphs.tab = `\t`; ResultGlyphs.carriageReturn = `\r`; ResultGlyphs.newline = `\n`; auto result = new MessageResult("\t \r\n"); result.toString.should.equal(`\t \r\n`); } @("Message result should return values as string") unittest { auto result = new MessageResult("text"); result.addValue("value"); result.addText("text"); result.toString.should.equal(`textvaluetext`); } @("Message result should print a string as primary") unittest { auto result = new MessageResult("\t \r\n"); auto printer = new MockPrinter; result.print(printer); printer.buffer.should.equal(`[primary:¤ ←↲]`); } @("Message result should print values as info") unittest { auto result = new MessageResult("text"); result.addValue("value"); result.addText("text"); auto printer = new MockPrinter; result.print(printer); printer.buffer.should.equal(`[primary:text][info:value][primary:text]`); } class DiffResult : IResult { import ddmp.diff; protected { string expected; string actual; } this(string expected, string actual) { this.expected = expected.replace("\0", ResultGlyphs.nullChar); this.actual = actual.replace("\0", ResultGlyphs.nullChar); } private string getResult(const Diff d) { final switch(d.operation) { case Operation.DELETE: return ResultGlyphs.diffBegin ~ ResultGlyphs.diffDelete ~ d.text ~ ResultGlyphs.diffEnd; case Operation.INSERT: return ResultGlyphs.diffBegin ~ ResultGlyphs.diffInsert ~ d.text ~ ResultGlyphs.diffEnd; case Operation.EQUAL: return d.text; } } override string toString() @trusted { return "Diff:\n" ~ diff_main(expected, actual).map!(a => getResult(a)).join; } void print(ResultPrinter printer) @trusted { auto result = diff_main(expected, actual); printer.info("Diff:"); foreach(diff; result) { if(diff.operation == Operation.EQUAL) { printer.primary(diff.text); } if(diff.operation == Operation.INSERT) { printer.successReverse(diff.text); } if(diff.operation == Operation.DELETE) { printer.dangerReverse(diff.text); } } printer.primary("\n"); } } /// DiffResult should find the differences unittest { auto diff = new DiffResult("abc", "asc"); diff.toString.should.equal("Diff:\na[-b][+s]c"); } /// DiffResult should use the custom glyphs unittest { scope(exit) { ResultGlyphs.resetDefaults; } ResultGlyphs.diffBegin = "{"; ResultGlyphs.diffEnd = "}"; ResultGlyphs.diffInsert = "!"; ResultGlyphs.diffDelete = "?"; auto diff = new DiffResult("abc", "asc"); diff.toString.should.equal("Diff:\na{?b}{!s}c"); } class KeyResult(string key) : IResult { private immutable { string value; size_t indent; } this(string value, size_t indent = 10) { this.value = value.replace("\0", ResultGlyphs.nullChar); this.indent = indent; } bool hasValue() { return value != ""; } override string toString() { if(value == "") { return ""; } return rightJustify(key ~ ":", indent, ' ') ~ printableValue; } void print(ResultPrinter printer) { if(value == "") { return; } printer.info(rightJustify(key ~ ":", indent, ' ')); auto lines = value.split("\n"); auto spaces = rightJustify(":", indent, ' '); int index; foreach(line; lines) { if(index > 0) { printer.info(ResultGlyphs.newline); printer.primary("\n"); printer.info(spaces); } printLine(line, printer); index++; } } private { struct Message { bool isSpecial; string text; } void printLine(string line, ResultPrinter printer) { Message[] messages; auto whiteIntervals = line.getWhiteIntervals; foreach(size_t index, ch; line) { bool showSpaces = index < whiteIntervals.left || index >= whiteIntervals.right; auto special = isSpecial(ch, showSpaces); if(messages.length == 0 || messages[messages.length - 1].isSpecial != special) { messages ~= Message(special, ""); } messages[messages.length - 1].text ~= toVisible(ch, showSpaces); } foreach(message; messages) { if(message.isSpecial) { printer.info(message.text); } else { printer.primary(message.text); } } } bool isSpecial(T)(T ch, bool showSpaces) { if(ch == ' ' && showSpaces) { return true; } if(ch == '\r' || ch == '\t') { return true; } return false; } string toVisible(T)(T ch, bool showSpaces) { if(ch == ' ' && showSpaces) { return ResultGlyphs.space; } if(ch == '\r') { return ResultGlyphs.carriageReturn; } if(ch == '\t') { return ResultGlyphs.tab; } return ch.to!string; } pure string printableValue() { return value.split("\n").join("\\n\n" ~ rightJustify(":", indent, ' ')); } } } /// KeyResult should not dispaly spaces between words with special chars unittest { auto result = new KeyResult!"key"(" row1 row2 "); auto printer = new MockPrinter(); result.print(printer); printer.buffer.should.equal(`[info: key:][info:᛫][primary:row1 row2][info:᛫]`); } /// KeyResult should dispaly spaces with special chars on space lines unittest { auto result = new KeyResult!"key"(" "); auto printer = new MockPrinter(); result.print(printer); printer.buffer.should.equal(`[info: key:][info:᛫᛫᛫]`); } /// KeyResult should display no char for empty lines unittest { auto result = new KeyResult!"key"(""); auto printer = new MockPrinter(); result.print(printer); printer.buffer.should.equal(``); } /// KeyResult should display special characters with different contexts unittest { auto result = new KeyResult!"key"("row1\n \trow2"); auto printer = new MockPrinter(); result.print(printer); printer.buffer.should.equal(`[info: key:][primary:row1][info:↲][primary:` ~ "\n" ~ `][info: :][info:᛫¤][primary:row2]`); } /// KeyResult should display custom glyphs with different contexts unittest { scope(exit) { ResultGlyphs.resetDefaults; } ResultGlyphs.newline = `\n`; ResultGlyphs.tab = `\t`; ResultGlyphs.space = ` `; auto result = new KeyResult!"key"("row1\n \trow2"); auto printer = new MockPrinter(); result.print(printer); printer.buffer.should.equal(`[info: key:][primary:row1][info:\n][primary:` ~ "\n" ~ `][info: :][info: \t][primary:row2]`); } /// class ExpectedActualResult : IResult { protected { string title; KeyResult!"Expected" expected; KeyResult!"Actual" actual; } this(string title, string expected, string actual) nothrow @safe { this.title = title; this(expected, actual); } this(string expected, string actual) nothrow @safe { this.expected = new KeyResult!"Expected"(expected); this.actual = new KeyResult!"Actual"(actual); } override string toString() { auto line1 = expected.toString; auto line2 = actual.toString; string glue; string prefix; if(line1 != "" && line2 != "") { glue = "\n"; } if(line1 != "" || line2 != "") { prefix = title == "" ? "\n" : ("\n" ~ title ~ "\n"); } return prefix ~ line1 ~ glue ~ line2; } void print(ResultPrinter printer) { auto line1 = expected.toString; auto line2 = actual.toString; if(actual.hasValue || expected.hasValue) { printer.info(title == "" ? "\n" : ("\n" ~ title ~ "\n")); } expected.print(printer); if(actual.hasValue && expected.hasValue) { printer.primary("\n"); } actual.print(printer); } } @("ExpectedActual result should be empty when no data is provided") unittest { auto result = new ExpectedActualResult("", ""); result.toString.should.equal(""); } @("ExpectedActual result should be empty when null data is provided") unittest { auto result = new ExpectedActualResult(null, null); result.toString.should.equal(""); } @("ExpectedActual result should show one line of the expected and actual data") unittest { auto result = new ExpectedActualResult("data", "data"); result.toString.should.equal(` Expected:data Actual:data`); } @("ExpectedActual result should show one line of the expected and actual data") unittest { auto result = new ExpectedActualResult("data\ndata", "data\ndata"); result.toString.should.equal(` Expected:data\n :data Actual:data\n :data`); } /// A result that displays differences between ranges class ExtraMissingResult : IResult { protected { KeyResult!"Extra" extra; KeyResult!"Missing" missing; } this(string extra, string missing) { this.extra = new KeyResult!"Extra"(extra); this.missing = new KeyResult!"Missing"(missing); } override string toString() { auto line1 = extra.toString; auto line2 = missing.toString; string glue; string prefix; if(line1 != "" || line2 != "") { prefix = "\n"; } if(line1 != "" && line2 != "") { glue = "\n"; } return prefix ~ line1 ~ glue ~ line2; } void print(ResultPrinter printer) { if(extra.hasValue || missing.hasValue) { printer.primary("\n"); } extra.print(printer); if(extra.hasValue && missing.hasValue) { printer.primary("\n"); } missing.print(printer); } } string toString(const(Token)[] tokens) { string result; foreach(token; tokens.filter!(a => str(a.type) != "comment")) { if(str(token.type) == "whitespace" && token.text == "") { result ~= "\n"; } else { result ~= token.text == "" ? str(token.type) : token.text; } } return result; } auto getScope(const(Token)[] tokens, size_t line) nothrow { bool foundScope; bool foundAssert; size_t beginToken; size_t endToken = tokens.length; int paranthesisCount = 0; int scopeLevel; size_t[size_t] paranthesisLevels; foreach(i, token; tokens) { string type = str(token.type); if(type == "{") { paranthesisLevels[paranthesisCount] = i; paranthesisCount++; } if(type == "}") { paranthesisCount--; } if(line == token.line) { foundScope = true; } if(foundScope) { if(token.text == "should" || token.text == "Assert" || type == "assert" || type == ";") { foundAssert = true; scopeLevel = paranthesisCount; } if(type == "}" && paranthesisCount <= scopeLevel) { beginToken = paranthesisLevels[paranthesisCount]; endToken = i + 1; break; } } } return const Tuple!(size_t, "begin", size_t, "end")(beginToken, endToken); } /// Get the spec function and scope that contains a lambda unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/values.d"), tokens); auto result = getScope(tokens, 101); auto identifierStart = getPreviousIdentifier(tokens, result.begin); tokens[identifierStart .. result.end].toString.strip.should.equal("it(\"should throw an exception if we request 2 android devices\", { ({ auto result = [ device1.idup, device2.idup ].filterBy(RunOptions(\"\", \"android\", 2)).array; }).should.throwException!DeviceException.withMessage.equal(\"You requested 2 `androdid` devices, but there is only 1 healthy.\"); }"); } /// Get the a method scope and signature unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/class.d"), tokens); auto result = getScope(tokens, 10); auto identifierStart = getPreviousIdentifier(tokens, result.begin); tokens[identifierStart .. result.end].toString.strip.should.equal("void bar() { assert(false); }"); } /// Get the a method scope without assert unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/class.d"), tokens); auto result = getScope(tokens, 14); auto identifierStart = getPreviousIdentifier(tokens, result.begin); tokens[identifierStart .. result.end].toString.strip.should.equal("void bar2() { enforce(false); }"); } size_t getFunctionEnd(const(Token)[] tokens, size_t start) { int paranthesisCount; size_t result = start; // iterate the parameters foreach(i, token; tokens[start .. $]) { string type = str(token.type); if(type == "(") { paranthesisCount++; } if(type == ")") { paranthesisCount--; } if(type == "{" && paranthesisCount == 0) { result = start + i; break; } if(type == ";" && paranthesisCount == 0) { return start + i; } } paranthesisCount = 0; // iterate the scope foreach(i, token; tokens[result .. $]) { string type = str(token.type); if(type == "{") { paranthesisCount++; } if(type == "}") { paranthesisCount--; if(paranthesisCount == 0) { result = result + i; break; } } } return result; } /// Get the end of a spec function with a lambda unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/values.d"), tokens); auto result = getScope(tokens, 101); auto identifierStart = getPreviousIdentifier(tokens, result.begin); auto functionEnd = getFunctionEnd(tokens, identifierStart); tokens[identifierStart .. functionEnd].toString.strip.should.equal("it(\"should throw an exception if we request 2 android devices\", { ({ auto result = [ device1.idup, device2.idup ].filterBy(RunOptions(\"\", \"android\", 2)).array; }).should.throwException!DeviceException.withMessage.equal(\"You requested 2 `androdid` devices, but there is only 1 healthy.\"); })"); } /// Get the end of an unittest function with a lambda unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/values.d"), tokens); auto result = getScope(tokens, 81); auto identifierStart = getPreviousIdentifier(tokens, result.begin); auto functionEnd = getFunctionEnd(tokens, identifierStart) + 1; tokens[identifierStart .. functionEnd].toString.strip.should.equal("unittest { ({ ({ }).should.beNull; }).should.throwException!TestException.msg; }"); } /// Get tokens from a scope that contains a lambda unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/values.d"), tokens); auto result = getScope(tokens, 81); tokens[result.begin .. result.end].toString.strip.should.equal(`{ ({ ({ }).should.beNull; }).should.throwException!TestException.msg; }`); } size_t getPreviousIdentifier(const(Token)[] tokens, size_t startIndex) { enforce(startIndex > 0); enforce(startIndex < tokens.length); int paranthesisCount; bool foundIdentifier; foreach(i; 0..startIndex) { auto index = startIndex - i - 1; auto type = str(tokens[index].type); if(type == "(") { paranthesisCount--; } if(type == ")") { paranthesisCount++; } if(paranthesisCount < 0) { return getPreviousIdentifier(tokens, index - 1); } if(paranthesisCount != 0) { continue; } if(type == "unittest") { return index; } if(type == "{" || type == "}") { return index + 1; } if(type == ";") { return index + 1; } if(type == "=") { return index + 1; } if(type == ".") { foundIdentifier = false; } if(type == "identifier" && foundIdentifier) { foundIdentifier = true; continue; } if(foundIdentifier) { return index; } } return 0; } /// Get the the previous unittest identifier from a list of tokens unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/values.d"), tokens); auto scopeResult = getScope(tokens, 81); auto result = getPreviousIdentifier(tokens, scopeResult.begin); tokens[result .. scopeResult.begin].toString.strip.should.equal(`unittest`); } /// Get the the previous paranthesis identifier from a list of tokens unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/values.d"), tokens); auto scopeResult = getScope(tokens, 63); auto end = scopeResult.end - 11; auto result = getPreviousIdentifier(tokens, end); tokens[result .. end].toString.strip.should.equal(`(5, (11))`); } /// Get the the previous function call identifier from a list of tokens unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/values.d"), tokens); auto scopeResult = getScope(tokens, 75); auto end = scopeResult.end - 11; auto result = getPreviousIdentifier(tokens, end); tokens[result .. end].toString.strip.should.equal(`found(4)`); } /// Get the the previous map!"" identifier from a list of tokens unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/values.d"), tokens); auto scopeResult = getScope(tokens, 85); auto end = scopeResult.end - 12; auto result = getPreviousIdentifier(tokens, end); tokens[result .. end].toString.strip.should.equal(`[1, 2, 3].map!"a"`); } size_t getAssertIndex(const(Token)[] tokens, size_t startLine) { auto assertTokens = tokens .enumerate .filter!(a => a[1].text == "Assert") .filter!(a => a[1].line <= startLine) .array; if(assertTokens.length == 0) { return 0; } return assertTokens[assertTokens.length - 1].index; } /// Get the index of the Assert structure identifier from a list of tokens unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/values.d"), tokens); auto result = getAssertIndex(tokens, 55); tokens[result .. result + 4].toString.strip.should.equal(`Assert.equal(`); } auto getParameter(const(Token)[] tokens, size_t startToken) { size_t paranthesisCount; foreach(i; startToken..tokens.length) { string type = str(tokens[i].type); if(type == "(" || type == "[") { paranthesisCount++; } if(type == ")" || type == "]") { if(paranthesisCount == 0) { return i; } paranthesisCount--; } if(paranthesisCount > 0) { continue; } if(type == ",") { return i; } } return 0; } /// Get the first parameter from a list of tokens unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/values.d"), tokens); auto begin = getAssertIndex(tokens, 57) + 4; auto end = getParameter(tokens, begin); tokens[begin .. end].toString.strip.should.equal(`(5, (11))`); } /// Get the first list parameter from a list of tokens unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/values.d"), tokens); auto begin = getAssertIndex(tokens, 89) + 4; auto end = getParameter(tokens, begin); tokens[begin .. end].toString.strip.should.equal(`[ new Value(1), new Value(2) ]`); } /// Get the previous array identifier from a list of tokens unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/values.d"), tokens); auto scopeResult = getScope(tokens, 4); auto end = scopeResult.end - 13; auto result = getPreviousIdentifier(tokens, end); tokens[result .. end].toString.strip.should.equal(`[1, 2, 3]`); } /// Get the previous array of instances identifier from a list of tokens unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/values.d"), tokens); auto scopeResult = getScope(tokens, 90); auto end = scopeResult.end - 16; auto result = getPreviousIdentifier(tokens, end); tokens[result .. end].toString.strip.should.equal(`[ new Value(1), new Value(2) ]`); } size_t getShouldIndex(const(Token)[] tokens, size_t startLine) { auto shouldTokens = tokens .enumerate .filter!(a => a[1].text == "should") .filter!(a => a[1].line <= startLine) .array; if(shouldTokens.length == 0) { return 0; } return shouldTokens[shouldTokens.length - 1].index; } /// Get the index of the should call unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/values.d"), tokens); auto result = getShouldIndex(tokens, 4); auto token = tokens[result]; token.line.should.equal(3); token.text.should.equal(`should`); str(token.type).text.should.equal(`identifier`); } /// An alternative to SourceResult that uses // DParse to get the source code class SourceResult : IResult { static private { const(Token)[][string] fileTokens; } immutable { string file; size_t line; } private const { Token[] tokens; } this(string fileName = __FILE__, size_t line = __LINE__, size_t range = 6) nothrow @trusted { this.file = fileName; this.line = line; if (!fileName.exists) { return; } try { updateFileTokens(fileName); auto result = getScope(fileTokens[fileName], line); auto begin = getPreviousIdentifier(fileTokens[fileName], result.begin); auto end = getFunctionEnd(fileTokens[fileName], begin) + 1; this.tokens = fileTokens[fileName][begin .. end]; } catch (Throwable t) { } } static void updateFileTokens(string fileName) { if(fileName !in fileTokens) { fileTokens[fileName] = []; splitMultilinetokens(fileToDTokens(fileName), fileTokens[fileName]); } } string getValue() { size_t startIndex = 0; size_t possibleStartIndex = 0; size_t endIndex = 0; size_t lastStartIndex = 0; size_t lastEndIndex = 0; int paranthesisCount = 0; size_t begin; size_t end = getShouldIndex(tokens, line); if(end != 0) { begin = tokens.getPreviousIdentifier(end - 1); return tokens[begin .. end - 1].toString.strip; } auto beginAssert = getAssertIndex(tokens, line); if(beginAssert > 0) { begin = beginAssert + 4; end = getParameter(tokens, begin); return tokens[begin .. end].toString.strip; } return ""; } override string toString() nothrow { auto separator = leftJustify("", 20, '-'); string result = "\n" ~ separator ~ "\n" ~ file ~ ":" ~ line.to!string ~ "\n" ~ separator; if(tokens.length == 0) { return result ~ "\n"; } size_t line = tokens[0].line - 1; size_t column = 1; bool afterErrorLine = false; foreach(token; this.tokens.filter!(token => token != tok!"whitespace")) { string prefix = ""; foreach(lineNumber; line..token.line) { if(lineNumber < this.line -1 || afterErrorLine) { prefix ~= "\n" ~ rightJustify((lineNumber+1).to!string, 6, ' ') ~ ": "; } else { prefix ~= "\n>" ~ rightJustify((lineNumber+1).to!string, 5, ' ') ~ ": "; } } if(token.line != line) { column = 1; } if(token.column > column) { prefix ~= ' '.repeat.take(token.column - column).array; } auto stringRepresentation = token.text == "" ? str(token.type) : token.text; auto lines = stringRepresentation.split("\n"); result ~= prefix ~ lines[0]; line = token.line; column = token.column + stringRepresentation.length; if(token.line >= this.line && str(token.type) == ";") { afterErrorLine = true; } } return result; } void print(ResultPrinter printer) { if(tokens.length == 0) { return; } printer.primary("\n"); printer.info(file ~ ":" ~ line.to!string); size_t line = tokens[0].line - 1; size_t column = 1; bool afterErrorLine = false; foreach(token; this.tokens.filter!(token => token != tok!"whitespace")) { foreach(lineNumber; line..token.line) { printer.primary("\n"); if(lineNumber < this.line -1 || afterErrorLine) { printer.primary(rightJustify((lineNumber+1).to!string, 6, ' ') ~ ":"); } else { printer.dangerReverse(">" ~ rightJustify((lineNumber+1).to!string, 5, ' ') ~ ":"); } } if(token.line != line) { column = 1; } if(token.column > column) { printer.primary(' '.repeat.take(token.column - column).array); } auto stringRepresentation = token.text == "" ? str(token.type) : token.text; if(token.text == "" && str(token.type) != "whitespace") { printer.info(str(token.type)); } else if(str(token.type).indexOf("Literal") != -1) { printer.success(token.text); } else { printer.primary(token.text); } line = token.line; column = token.column + stringRepresentation.length; if(token.line >= this.line && str(token.type) == ";") { afterErrorLine = true; } } printer.primary("\n"); } } @("TestException should read the code from the file") unittest { auto result = new SourceResult("test/values.d", 26); auto msg = result.toString; msg.should.equal("\n--------------------\ntest/values.d:26\n--------------------\n" ~ " 23: unittest {\n" ~ " 24: /++/\n" ~ " 25: \n" ~ "> 26: [1, 2, 3]\n" ~ "> 27: .should\n" ~ "> 28: .contain(4);\n" ~ " 29: }"); } @("TestException should print the lines before multiline tokens") unittest { auto result = new SourceResult("test/values.d", 45); auto msg = result.toString; msg.should.equal("\n--------------------\ntest/values.d:45\n--------------------\n" ~ " 40: unittest {\n" ~ " 41: /*\n" ~ " 42: Multi line comment\n" ~ " 43: */\n" ~ " 44: \n" ~ "> 45: `multi\n" ~ "> 46: line\n" ~ "> 47: string`\n" ~ "> 48: .should\n" ~ "> 49: .contain(`multi\n" ~ "> 50: line\n" ~ "> 51: string`);\n" ~ " 52: }"); } /// Converts a file to D tokens provided by libDParse. /// All the whitespaces are ignored const(Token)[] fileToDTokens(string fileName) nothrow @trusted { try { auto f = File(fileName); immutable auto fileSize = f.size(); ubyte[] fileBytes = new ubyte[](fileSize.to!size_t); if(f.rawRead(fileBytes).length != fileSize) { return []; } StringCache cache = StringCache(StringCache.defaultBucketCount); LexerConfig config; config.stringBehavior = StringBehavior.source; config.fileName = fileName; config.commentBehavior = CommentBehavior.intern; auto lexer = DLexer(fileBytes, config, &cache); const(Token)[] tokens = lexer.array; return tokens.map!(token => const Token(token.type, token.text.idup, token.line, token.column, token.index)).array; } catch(Throwable) { return []; } } @("TestException should ignore missing files") unittest { auto result = new SourceResult("test/missing.txt", 10); auto msg = result.toString; msg.should.equal("\n" ~ `-------------------- test/missing.txt:10 --------------------` ~ "\n"); } @("Source reporter should find the tested value on scope start") unittest { auto result = new SourceResult("test/values.d", 4); result.getValue.should.equal("[1, 2, 3]"); } @("Source reporter should find the tested value after a statment") unittest { auto result = new SourceResult("test/values.d", 12); result.getValue.should.equal("[1, 2, 3]"); } @("Source reporter should find the tested value after a */ comment") unittest { auto result = new SourceResult("test/values.d", 20); result.getValue.should.equal("[1, 2, 3]"); } @("Source reporter should find the tested value after a +/ comment") unittest { auto result = new SourceResult("test/values.d", 28); result.getValue.should.equal("[1, 2, 3]"); } @("Source reporter should find the tested value after a // comment") unittest { auto result = new SourceResult("test/values.d", 36); result.getValue.should.equal("[1, 2, 3]"); } @("Source reporter should find the tested value from an assert utility") unittest { auto result = new SourceResult("test/values.d", 55); result.getValue.should.equal("5"); result = new SourceResult("test/values.d", 56); result.getValue.should.equal("(5+1)"); result = new SourceResult("test/values.d", 57); result.getValue.should.equal("(5, (11))"); } @("Source reporter should get the value from multiple should asserts") unittest { auto result = new SourceResult("test/values.d", 61); result.getValue.should.equal("5"); result = new SourceResult("test/values.d", 62); result.getValue.should.equal("(5+1)"); result = new SourceResult("test/values.d", 63); result.getValue.should.equal("(5, (11))"); } @("Source reporter should get the value after a scope") unittest { auto result = new SourceResult("test/values.d", 71); result.getValue.should.equal("found"); } @("Source reporter should get a function call value") unittest { auto result = new SourceResult("test/values.d", 75); result.getValue.should.equal("found(4)"); } @("Source reporter should parse nested lambdas") unittest { auto result = new SourceResult("test/values.d", 81); result.getValue.should.equal("({ ({ }).should.beNull; })"); } /// Source reporter should print the source code unittest { auto result = new SourceResult("test/values.d", 36); auto printer = new MockPrinter(); result.print(printer); auto lines = printer.buffer.split("[primary:\n]"); lines[1].should.equal(`[info:test/values.d:36]`); lines[2].should.equal(`[primary: 31:][info:unittest][primary: ][info:{]`); lines[7].should.equal(`[dangerReverse:> 36:][primary: ][info:.][primary:contain][info:(][success:4][info:)][info:;]`); } /// split multiline tokens in multiple single line tokens with the same type void splitMultilinetokens(const(Token)[] tokens, ref const(Token)[] result) nothrow @trusted { try { foreach(token; tokens) { auto pieces = token.text.idup.split("\n"); if(pieces.length <= 1) { result ~= const Token(token.type, token.text.dup, token.line, token.column, token.index); } else { size_t line = token.line; size_t column = token.column; foreach(textPiece; pieces) { result ~= const Token(token.type, textPiece, line, column, token.index); line++; column = 1; } } } } catch(Throwable) {} } /// A new line sepparator class SeparatorResult : IResult { override string toString() { return "\n"; } void print(ResultPrinter printer) { printer.primary("\n"); } } class ListInfoResult : IResult { private { struct Item { string singular; string plural; string[] valueList; string key() { return valueList.length > 1 ? plural : singular; } MessageResult toMessage(size_t indentation = 0) { auto printableKey = rightJustify(key ~ ":", indentation, ' '); auto result = new MessageResult(printableKey); string glue; foreach(value; valueList) { result.addText(glue); result.addValue(value); glue = ","; } return result; } } Item[] items; } void add(string key, string value) { items ~= Item(key, "", [value]); } void add(string singular, string plural, string[] valueList) { items ~= Item(singular, plural, valueList); } private size_t indentation() { auto elements = items.filter!"a.valueList.length > 0"; if(elements.empty) { return 0; } return elements.map!"a.key".map!"a.length".maxElement + 2; } override string toString() { auto indent = indentation; auto elements = items.filter!"a.valueList.length > 0"; if(elements.empty) { return ""; } return "\n" ~ elements.map!(a => a.toMessage(indent)).map!"a.toString".join("\n"); } void print(ResultPrinter printer) { auto indent = indentation; auto elements = items.filter!"a.valueList.length > 0"; if(elements.empty) { return; } foreach(item; elements) { printer.primary("\n"); item.toMessage(indent).print(printer); } } } /// convert to string the added data to ListInfoResult unittest { auto result = new ListInfoResult(); result.add("a", "1"); result.add("ab", "2"); result.add("abc", "3"); result.toString.should.equal(` a:1 ab:2 abc:3`); } /// print the added data to ListInfoResult unittest { auto printer = new MockPrinter(); auto result = new ListInfoResult(); result.add("a", "1"); result.add("ab", "2"); result.add("abc", "3"); result.print(printer); printer.buffer.should.equal(`[primary: ][primary: a:][primary:][info:1][primary: ][primary: ab:][primary:][info:2][primary: ][primary: abc:][primary:][info:3]`); } /// convert to string the added data lists to ListInfoResult unittest { auto result = new ListInfoResult(); result.add("a", "as", ["1", "2","3"]); result.add("ab", "abs", ["2", "3"]); result.add("abc", "abcs", ["3"]); result.add("abcd", "abcds", []); result.toString.should.equal(` as:1,2,3 abs:2,3 abc:3`); } IResult[] toResults(Exception e) nothrow @trusted { try { return [ new MessageResult(e.message.to!string) ]; } catch(Exception) { return [ new MessageResult("Unknown error!") ]; } }
module fluentasserts.core.results; import std.stdio; import std.file; import std.algorithm; import std.conv; import std.range; import std.string; import std.exception; import std.typecons; import dparse.lexer; import dparse.parser; @safe: /// Glyphs used to display special chars in the results struct ResultGlyphs { static { /// Glyph for the tab char string tab; /// Glyph for the \r char string carriageReturn; /// Glyph for the \n char string newline; /// Glyph for the space char string space; /// Glyph for the \0 char string nullChar; /// Glyph that indicates the error line string sourceIndicator; /// Glyph that sepparates the line number string sourceLineSeparator; /// Glyph for the diff begin indicator string diffBegin; /// Glyph for the diff end indicator string diffEnd; /// Glyph that marks an inserted text in diff string diffInsert; /// Glyph that marks deleted text in diff string diffDelete; } /// Set the default values. The values are static resetDefaults() { version(windows) { ResultGlyphs.tab = `\t`; ResultGlyphs.carriageReturn = `\r`; ResultGlyphs.newline = `\n`; ResultGlyphs.space = ` `; ResultGlyphs.nullChar = `␀`; } else { ResultGlyphs.tab = `¤`; ResultGlyphs.carriageReturn = `←`; ResultGlyphs.newline = `↲`; ResultGlyphs.space = `᛫`; ResultGlyphs.nullChar = `\0`; } ResultGlyphs.sourceIndicator = ">"; ResultGlyphs.sourceLineSeparator = ":"; ResultGlyphs.diffBegin = "["; ResultGlyphs.diffEnd = "]"; ResultGlyphs.diffInsert = "+"; ResultGlyphs.diffDelete = "-"; } } /// interface ResultPrinter { void primary(string); void info(string); void danger(string); void success(string); void dangerReverse(string); void successReverse(string); } version(unittest) { class MockPrinter : ResultPrinter { string buffer; void primary(string val) { buffer ~= "[primary:" ~ val ~ "]"; } void info(string val) { buffer ~= "[info:" ~ val ~ "]"; } void danger(string val) { buffer ~= "[danger:" ~ val ~ "]"; } void success(string val) { buffer ~= "[success:" ~ val ~ "]"; } void dangerReverse(string val) { buffer ~= "[dangerReverse:" ~ val ~ "]"; } void successReverse(string val) { buffer ~= "[successReverse:" ~ val ~ "]"; } } } struct WhiteIntervals { size_t left; size_t right; } WhiteIntervals getWhiteIntervals(string text) { auto stripText = text.strip; if(stripText == "") { return WhiteIntervals(0, 0); } return WhiteIntervals(text.indexOf(stripText[0]), text.lastIndexOf(stripText[stripText.length - 1])); } /// This is the most simple implementation of a ResultPrinter. /// All the plain data is printed to stdout class DefaultResultPrinter : ResultPrinter { void primary(string text) { write(text); } void info(string text) { write(text); } void danger(string text) { write(text); } void success(string text) { write(text); } void dangerReverse(string text) { write(text); } void successReverse(string text) { write(text); } } interface IResult { string toString(); void print(ResultPrinter); } /// A result that prints a simple message to the user class MessageResult : IResult { private { struct Message { bool isValue; string text; } Message[] messages; } this(string message) nothrow { add(false, message); } this() nothrow { } override string toString() { return messages.map!"a.text".join.to!string; } void startWith(string message) @safe nothrow { Message[] newMessages; newMessages ~= Message(false, message); newMessages ~= this.messages; this.messages = newMessages; } void add(bool isValue, string message) nothrow { this.messages ~= Message(isValue, message .replace("\r", ResultGlyphs.carriageReturn) .replace("\n", ResultGlyphs.newline) .replace("\0", ResultGlyphs.nullChar) .replace("\t", ResultGlyphs.tab)); } void addValue(string text) @safe nothrow { add(true, text); } void addText(string text) @safe nothrow { if(text == "throwAnyException") { text = "throw any exception"; } this.messages ~= Message(false, text); } void prependText(string text) @safe nothrow { this.messages = Message(false, text) ~ this.messages; } void prependValue(string text) @safe nothrow { this.messages = Message(true, text) ~ this.messages; } void print(ResultPrinter printer) { foreach(message; messages) { if(message.isValue) { printer.info(message.text); } else { printer.primary(message.text); } } } } version (unittest) { import fluentasserts.core.base; } @("Message result should return the message") unittest { auto result = new MessageResult("Message"); result.toString.should.equal("Message"); } @("Message result should replace the special chars") unittest { auto result = new MessageResult("\t \r\n"); result.toString.should.equal(`¤ ←↲`); } @("Message result should replace the special chars with the custom glyphs") unittest { scope(exit) { ResultGlyphs.resetDefaults; } ResultGlyphs.tab = `\t`; ResultGlyphs.carriageReturn = `\r`; ResultGlyphs.newline = `\n`; auto result = new MessageResult("\t \r\n"); result.toString.should.equal(`\t \r\n`); } @("Message result should return values as string") unittest { auto result = new MessageResult("text"); result.addValue("value"); result.addText("text"); result.toString.should.equal(`textvaluetext`); } @("Message result should print a string as primary") unittest { auto result = new MessageResult("\t \r\n"); auto printer = new MockPrinter; result.print(printer); printer.buffer.should.equal(`[primary:¤ ←↲]`); } @("Message result should print values as info") unittest { auto result = new MessageResult("text"); result.addValue("value"); result.addText("text"); auto printer = new MockPrinter; result.print(printer); printer.buffer.should.equal(`[primary:text][info:value][primary:text]`); } class DiffResult : IResult { import ddmp.diff; protected { string expected; string actual; } this(string expected, string actual) { this.expected = expected.replace("\0", ResultGlyphs.nullChar); this.actual = actual.replace("\0", ResultGlyphs.nullChar); } private string getResult(const Diff d) { final switch(d.operation) { case Operation.DELETE: return ResultGlyphs.diffBegin ~ ResultGlyphs.diffDelete ~ d.text ~ ResultGlyphs.diffEnd; case Operation.INSERT: return ResultGlyphs.diffBegin ~ ResultGlyphs.diffInsert ~ d.text ~ ResultGlyphs.diffEnd; case Operation.EQUAL: return d.text; } } override string toString() @trusted { return "Diff:\n" ~ diff_main(expected, actual).map!(a => getResult(a)).join; } void print(ResultPrinter printer) @trusted { auto result = diff_main(expected, actual); printer.info("Diff:"); foreach(diff; result) { if(diff.operation == Operation.EQUAL) { printer.primary(diff.text); } if(diff.operation == Operation.INSERT) { printer.successReverse(diff.text); } if(diff.operation == Operation.DELETE) { printer.dangerReverse(diff.text); } } printer.primary("\n"); } } /// DiffResult should find the differences unittest { auto diff = new DiffResult("abc", "asc"); diff.toString.should.equal("Diff:\na[-b][+s]c"); } /// DiffResult should use the custom glyphs unittest { scope(exit) { ResultGlyphs.resetDefaults; } ResultGlyphs.diffBegin = "{"; ResultGlyphs.diffEnd = "}"; ResultGlyphs.diffInsert = "!"; ResultGlyphs.diffDelete = "?"; auto diff = new DiffResult("abc", "asc"); diff.toString.should.equal("Diff:\na{?b}{!s}c"); } class KeyResult(string key) : IResult { private immutable { string value; size_t indent; } this(string value, size_t indent = 10) { this.value = value.replace("\0", ResultGlyphs.nullChar); this.indent = indent; } bool hasValue() { return value != ""; } override string toString() { if(value == "") { return ""; } return rightJustify(key ~ ":", indent, ' ') ~ printableValue; } void print(ResultPrinter printer) { if(value == "") { return; } printer.info(rightJustify(key ~ ":", indent, ' ')); auto lines = value.split("\n"); auto spaces = rightJustify(":", indent, ' '); int index; foreach(line; lines) { if(index > 0) { printer.info(ResultGlyphs.newline); printer.primary("\n"); printer.info(spaces); } printLine(line, printer); index++; } } private { struct Message { bool isSpecial; string text; } void printLine(string line, ResultPrinter printer) { Message[] messages; auto whiteIntervals = line.getWhiteIntervals; foreach(size_t index, ch; line) { bool showSpaces = index < whiteIntervals.left || index >= whiteIntervals.right; auto special = isSpecial(ch, showSpaces); if(messages.length == 0 || messages[messages.length - 1].isSpecial != special) { messages ~= Message(special, ""); } messages[messages.length - 1].text ~= toVisible(ch, showSpaces); } foreach(message; messages) { if(message.isSpecial) { printer.info(message.text); } else { printer.primary(message.text); } } } bool isSpecial(T)(T ch, bool showSpaces) { if(ch == ' ' && showSpaces) { return true; } if(ch == '\r' || ch == '\t') { return true; } return false; } string toVisible(T)(T ch, bool showSpaces) { if(ch == ' ' && showSpaces) { return ResultGlyphs.space; } if(ch == '\r') { return ResultGlyphs.carriageReturn; } if(ch == '\t') { return ResultGlyphs.tab; } return ch.to!string; } pure string printableValue() { return value.split("\n").join("\\n\n" ~ rightJustify(":", indent, ' ')); } } } /// KeyResult should not dispaly spaces between words with special chars unittest { auto result = new KeyResult!"key"(" row1 row2 "); auto printer = new MockPrinter(); result.print(printer); printer.buffer.should.equal(`[info: key:][info:᛫][primary:row1 row2][info:᛫]`); } /// KeyResult should dispaly spaces with special chars on space lines unittest { auto result = new KeyResult!"key"(" "); auto printer = new MockPrinter(); result.print(printer); printer.buffer.should.equal(`[info: key:][info:᛫᛫᛫]`); } /// KeyResult should display no char for empty lines unittest { auto result = new KeyResult!"key"(""); auto printer = new MockPrinter(); result.print(printer); printer.buffer.should.equal(``); } /// KeyResult should display special characters with different contexts unittest { auto result = new KeyResult!"key"("row1\n \trow2"); auto printer = new MockPrinter(); result.print(printer); printer.buffer.should.equal(`[info: key:][primary:row1][info:↲][primary:` ~ "\n" ~ `][info: :][info:᛫¤][primary:row2]`); } /// KeyResult should display custom glyphs with different contexts unittest { scope(exit) { ResultGlyphs.resetDefaults; } ResultGlyphs.newline = `\n`; ResultGlyphs.tab = `\t`; ResultGlyphs.space = ` `; auto result = new KeyResult!"key"("row1\n \trow2"); auto printer = new MockPrinter(); result.print(printer); printer.buffer.should.equal(`[info: key:][primary:row1][info:\n][primary:` ~ "\n" ~ `][info: :][info: \t][primary:row2]`); } /// class ExpectedActualResult : IResult { protected { string title; KeyResult!"Expected" expected; KeyResult!"Actual" actual; } this(string title, string expected, string actual) nothrow @safe { this.title = title; this(expected, actual); } this(string expected, string actual) nothrow @safe { this.expected = new KeyResult!"Expected"(expected); this.actual = new KeyResult!"Actual"(actual); } override string toString() { auto line1 = expected.toString; auto line2 = actual.toString; string glue; string prefix; if(line1 != "" && line2 != "") { glue = "\n"; } if(line1 != "" || line2 != "") { prefix = title == "" ? "\n" : ("\n" ~ title ~ "\n"); } return prefix ~ line1 ~ glue ~ line2; } void print(ResultPrinter printer) { auto line1 = expected.toString; auto line2 = actual.toString; if(actual.hasValue || expected.hasValue) { printer.info(title == "" ? "\n" : ("\n" ~ title ~ "\n")); } expected.print(printer); if(actual.hasValue && expected.hasValue) { printer.primary("\n"); } actual.print(printer); } } @("ExpectedActual result should be empty when no data is provided") unittest { auto result = new ExpectedActualResult("", ""); result.toString.should.equal(""); } @("ExpectedActual result should be empty when null data is provided") unittest { auto result = new ExpectedActualResult(null, null); result.toString.should.equal(""); } @("ExpectedActual result should show one line of the expected and actual data") unittest { auto result = new ExpectedActualResult("data", "data"); result.toString.should.equal(` Expected:data Actual:data`); } @("ExpectedActual result should show one line of the expected and actual data") unittest { auto result = new ExpectedActualResult("data\ndata", "data\ndata"); result.toString.should.equal(` Expected:data\n :data Actual:data\n :data`); } /// A result that displays differences between ranges class ExtraMissingResult : IResult { protected { KeyResult!"Extra" extra; KeyResult!"Missing" missing; } this(string extra, string missing) { this.extra = new KeyResult!"Extra"(extra); this.missing = new KeyResult!"Missing"(missing); } override string toString() { auto line1 = extra.toString; auto line2 = missing.toString; string glue; string prefix; if(line1 != "" || line2 != "") { prefix = "\n"; } if(line1 != "" && line2 != "") { glue = "\n"; } return prefix ~ line1 ~ glue ~ line2; } void print(ResultPrinter printer) { if(extra.hasValue || missing.hasValue) { printer.primary("\n"); } extra.print(printer); if(extra.hasValue && missing.hasValue) { printer.primary("\n"); } missing.print(printer); } } string toString(const(Token)[] tokens) { string result; foreach(token; tokens.filter!(a => str(a.type) != "comment")) { if(str(token.type) == "whitespace" && token.text == "") { result ~= "\n"; } else { result ~= token.text == "" ? str(token.type) : token.text; } } return result; } auto getScope(const(Token)[] tokens, size_t line) nothrow { bool foundScope; bool foundAssert; size_t beginToken; size_t endToken = tokens.length; int paranthesisCount = 0; int scopeLevel; size_t[size_t] paranthesisLevels; foreach(i, token; tokens) { string type = str(token.type); if(type == "{") { paranthesisLevels[paranthesisCount] = i; paranthesisCount++; } if(type == "}") { paranthesisCount--; } if(line == token.line) { foundScope = true; } if(foundScope) { if(token.text == "should" || token.text == "Assert" || type == "assert" || type == ";") { foundAssert = true; scopeLevel = paranthesisCount; } if(type == "}" && paranthesisCount <= scopeLevel) { beginToken = paranthesisLevels[paranthesisCount]; endToken = i + 1; break; } } } return const Tuple!(size_t, "begin", size_t, "end")(beginToken, endToken); } /// Get the spec function and scope that contains a lambda unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/values.d"), tokens); auto result = getScope(tokens, 101); auto identifierStart = getPreviousIdentifier(tokens, result.begin); tokens[identifierStart .. result.end].toString.strip.should.equal("it(\"should throw an exception if we request 2 android devices\", { ({ auto result = [ device1.idup, device2.idup ].filterBy(RunOptions(\"\", \"android\", 2)).array; }).should.throwException!DeviceException.withMessage.equal(\"You requested 2 `androdid` devices, but there is only 1 healthy.\"); }"); } /// Get the a method scope and signature unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/class.d"), tokens); auto result = getScope(tokens, 10); auto identifierStart = getPreviousIdentifier(tokens, result.begin); tokens[identifierStart .. result.end].toString.strip.should.equal("void bar() { assert(false); }"); } /// Get the a method scope without assert unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/class.d"), tokens); auto result = getScope(tokens, 14); auto identifierStart = getPreviousIdentifier(tokens, result.begin); tokens[identifierStart .. result.end].toString.strip.should.equal("void bar2() { enforce(false); }"); } size_t getFunctionEnd(const(Token)[] tokens, size_t start) { int paranthesisCount; size_t result = start; // iterate the parameters foreach(i, token; tokens[start .. $]) { string type = str(token.type); if(type == "(") { paranthesisCount++; } if(type == ")") { paranthesisCount--; } if(type == "{" && paranthesisCount == 0) { result = start + i; break; } if(type == ";" && paranthesisCount == 0) { return start + i; } } paranthesisCount = 0; // iterate the scope foreach(i, token; tokens[result .. $]) { string type = str(token.type); if(type == "{") { paranthesisCount++; } if(type == "}") { paranthesisCount--; if(paranthesisCount == 0) { result = result + i; break; } } } return result; } /// Get the end of a spec function with a lambda unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/values.d"), tokens); auto result = getScope(tokens, 101); auto identifierStart = getPreviousIdentifier(tokens, result.begin); auto functionEnd = getFunctionEnd(tokens, identifierStart); tokens[identifierStart .. functionEnd].toString.strip.should.equal("it(\"should throw an exception if we request 2 android devices\", { ({ auto result = [ device1.idup, device2.idup ].filterBy(RunOptions(\"\", \"android\", 2)).array; }).should.throwException!DeviceException.withMessage.equal(\"You requested 2 `androdid` devices, but there is only 1 healthy.\"); })"); } /// Get the end of an unittest function with a lambda unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/values.d"), tokens); auto result = getScope(tokens, 81); auto identifierStart = getPreviousIdentifier(tokens, result.begin); auto functionEnd = getFunctionEnd(tokens, identifierStart) + 1; tokens[identifierStart .. functionEnd].toString.strip.should.equal("unittest { ({ ({ }).should.beNull; }).should.throwException!TestException.msg; }"); } /// Get tokens from a scope that contains a lambda unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/values.d"), tokens); auto result = getScope(tokens, 81); tokens[result.begin .. result.end].toString.strip.should.equal(`{ ({ ({ }).should.beNull; }).should.throwException!TestException.msg; }`); } size_t getPreviousIdentifier(const(Token)[] tokens, size_t startIndex) { enforce(startIndex > 0); enforce(startIndex < tokens.length); int paranthesisCount; bool foundIdentifier; foreach(i; 0..startIndex) { auto index = startIndex - i - 1; auto type = str(tokens[index].type); if(type == "(") { paranthesisCount--; } if(type == ")") { paranthesisCount++; } if(paranthesisCount < 0) { return getPreviousIdentifier(tokens, index - 1); } if(paranthesisCount != 0) { continue; } if(type == "unittest") { return index; } if(type == "{" || type == "}") { return index + 1; } if(type == ";") { return index + 1; } if(type == "=") { return index + 1; } if(type == ".") { foundIdentifier = false; } if(type == "identifier" && foundIdentifier) { foundIdentifier = true; continue; } if(foundIdentifier) { return index; } } return 0; } /// Get the the previous unittest identifier from a list of tokens unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/values.d"), tokens); auto scopeResult = getScope(tokens, 81); auto result = getPreviousIdentifier(tokens, scopeResult.begin); tokens[result .. scopeResult.begin].toString.strip.should.equal(`unittest`); } /// Get the the previous paranthesis identifier from a list of tokens unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/values.d"), tokens); auto scopeResult = getScope(tokens, 63); auto end = scopeResult.end - 11; auto result = getPreviousIdentifier(tokens, end); tokens[result .. end].toString.strip.should.equal(`(5, (11))`); } /// Get the the previous function call identifier from a list of tokens unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/values.d"), tokens); auto scopeResult = getScope(tokens, 75); auto end = scopeResult.end - 11; auto result = getPreviousIdentifier(tokens, end); tokens[result .. end].toString.strip.should.equal(`found(4)`); } /// Get the the previous map!"" identifier from a list of tokens unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/values.d"), tokens); auto scopeResult = getScope(tokens, 85); auto end = scopeResult.end - 12; auto result = getPreviousIdentifier(tokens, end); tokens[result .. end].toString.strip.should.equal(`[1, 2, 3].map!"a"`); } size_t getAssertIndex(const(Token)[] tokens, size_t startLine) { auto assertTokens = tokens .enumerate .filter!(a => a[1].text == "Assert") .filter!(a => a[1].line <= startLine) .array; if(assertTokens.length == 0) { return 0; } return assertTokens[assertTokens.length - 1].index; } /// Get the index of the Assert structure identifier from a list of tokens unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/values.d"), tokens); auto result = getAssertIndex(tokens, 55); tokens[result .. result + 4].toString.strip.should.equal(`Assert.equal(`); } auto getParameter(const(Token)[] tokens, size_t startToken) { size_t paranthesisCount; foreach(i; startToken..tokens.length) { string type = str(tokens[i].type); if(type == "(" || type == "[") { paranthesisCount++; } if(type == ")" || type == "]") { if(paranthesisCount == 0) { return i; } paranthesisCount--; } if(paranthesisCount > 0) { continue; } if(type == ",") { return i; } } return 0; } /// Get the first parameter from a list of tokens unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/values.d"), tokens); auto begin = getAssertIndex(tokens, 57) + 4; auto end = getParameter(tokens, begin); tokens[begin .. end].toString.strip.should.equal(`(5, (11))`); } /// Get the first list parameter from a list of tokens unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/values.d"), tokens); auto begin = getAssertIndex(tokens, 89) + 4; auto end = getParameter(tokens, begin); tokens[begin .. end].toString.strip.should.equal(`[ new Value(1), new Value(2) ]`); } /// Get the previous array identifier from a list of tokens unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/values.d"), tokens); auto scopeResult = getScope(tokens, 4); auto end = scopeResult.end - 13; auto result = getPreviousIdentifier(tokens, end); tokens[result .. end].toString.strip.should.equal(`[1, 2, 3]`); } /// Get the previous array of instances identifier from a list of tokens unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/values.d"), tokens); auto scopeResult = getScope(tokens, 90); auto end = scopeResult.end - 16; auto result = getPreviousIdentifier(tokens, end); tokens[result .. end].toString.strip.should.equal(`[ new Value(1), new Value(2) ]`); } size_t getShouldIndex(const(Token)[] tokens, size_t startLine) { auto shouldTokens = tokens .enumerate .filter!(a => a[1].text == "should") .filter!(a => a[1].line <= startLine) .array; if(shouldTokens.length == 0) { return 0; } return shouldTokens[shouldTokens.length - 1].index; } /// Get the index of the should call unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/values.d"), tokens); auto result = getShouldIndex(tokens, 4); auto token = tokens[result]; token.line.should.equal(3); token.text.should.equal(`should`); str(token.type).text.should.equal(`identifier`); } /// An alternative to SourceResult that uses // DParse to get the source code class SourceResult : IResult { static private { const(Token)[][string] fileTokens; } immutable { string file; size_t line; } private const { Token[] tokens; } this(string fileName = __FILE__, size_t line = __LINE__, size_t range = 6) nothrow @trusted { this.file = fileName; this.line = line; if (!fileName.exists) { return; } try { updateFileTokens(fileName); auto result = getScope(fileTokens[fileName], line); auto begin = getPreviousIdentifier(fileTokens[fileName], result.begin); auto end = getFunctionEnd(fileTokens[fileName], begin) + 1; this.tokens = fileTokens[fileName][begin .. end]; } catch (Throwable t) { } } static void updateFileTokens(string fileName) { if(fileName !in fileTokens) { fileTokens[fileName] = []; splitMultilinetokens(fileToDTokens(fileName), fileTokens[fileName]); } } string getValue() { size_t startIndex = 0; size_t possibleStartIndex = 0; size_t endIndex = 0; size_t lastStartIndex = 0; size_t lastEndIndex = 0; int paranthesisCount = 0; size_t begin; size_t end = getShouldIndex(tokens, line); if(end != 0) { begin = tokens.getPreviousIdentifier(end - 1); return tokens[begin .. end - 1].toString.strip; } auto beginAssert = getAssertIndex(tokens, line); if(beginAssert > 0) { begin = beginAssert + 4; end = getParameter(tokens, begin); return tokens[begin .. end].toString.strip; } return ""; } override string toString() nothrow { auto separator = leftJustify("", 20, '-'); string result = "\n" ~ separator ~ "\n" ~ file ~ ":" ~ line.to!string ~ "\n" ~ separator; if(tokens.length == 0) { return result ~ "\n"; } size_t line = tokens[0].line - 1; size_t column = 1; bool afterErrorLine = false; foreach(token; this.tokens.filter!(token => token != tok!"whitespace")) { string prefix = ""; foreach(lineNumber; line..token.line) { if(lineNumber < this.line -1 || afterErrorLine) { prefix ~= "\n" ~ rightJustify((lineNumber+1).to!string, 6, ' ') ~ ": "; } else { prefix ~= "\n>" ~ rightJustify((lineNumber+1).to!string, 5, ' ') ~ ": "; } } if(token.line != line) { column = 1; } if(token.column > column) { prefix ~= ' '.repeat.take(token.column - column).array; } auto stringRepresentation = token.text == "" ? str(token.type) : token.text; auto lines = stringRepresentation.split("\n"); result ~= prefix ~ lines[0]; line = token.line; column = token.column + stringRepresentation.length; if(token.line >= this.line && str(token.type) == ";") { afterErrorLine = true; } } return result; } void print(ResultPrinter printer) { if(tokens.length == 0) { return; } printer.primary("\n"); printer.info(file ~ ":" ~ line.to!string); size_t line = tokens[0].line - 1; size_t column = 1; bool afterErrorLine = false; foreach(token; this.tokens.filter!(token => token != tok!"whitespace")) { foreach(lineNumber; line..token.line) { printer.primary("\n"); if(lineNumber < this.line -1 || afterErrorLine) { printer.primary(rightJustify((lineNumber+1).to!string, 6, ' ') ~ ":"); } else { printer.dangerReverse(">" ~ rightJustify((lineNumber+1).to!string, 5, ' ') ~ ":"); } } if(token.line != line) { column = 1; } if(token.column > column) { printer.primary(' '.repeat.take(token.column - column).array); } auto stringRepresentation = token.text == "" ? str(token.type) : token.text; if(token.text == "" && str(token.type) != "whitespace") { printer.info(str(token.type)); } else if(str(token.type).indexOf("Literal") != -1) { printer.success(token.text); } else { printer.primary(token.text); } line = token.line; column = token.column + stringRepresentation.length; if(token.line >= this.line && str(token.type) == ";") { afterErrorLine = true; } } printer.primary("\n"); } } @("TestException should read the code from the file") unittest { auto result = new SourceResult("test/values.d", 26); auto msg = result.toString; msg.should.equal("\n--------------------\ntest/values.d:26\n--------------------\n" ~ " 23: unittest {\n" ~ " 24: /++/\n" ~ " 25: \n" ~ "> 26: [1, 2, 3]\n" ~ "> 27: .should\n" ~ "> 28: .contain(4);\n" ~ " 29: }"); } @("TestException should print the lines before multiline tokens") unittest { auto result = new SourceResult("test/values.d", 45); auto msg = result.toString; msg.should.equal("\n--------------------\ntest/values.d:45\n--------------------\n" ~ " 40: unittest {\n" ~ " 41: /*\n" ~ " 42: Multi line comment\n" ~ " 43: */\n" ~ " 44: \n" ~ "> 45: `multi\n" ~ "> 46: line\n" ~ "> 47: string`\n" ~ "> 48: .should\n" ~ "> 49: .contain(`multi\n" ~ "> 50: line\n" ~ "> 51: string`);\n" ~ " 52: }"); } /// Converts a file to D tokens provided by libDParse. /// All the whitespaces are ignored const(Token)[] fileToDTokens(string fileName) nothrow @trusted { try { auto f = File(fileName); immutable auto fileSize = f.size(); ubyte[] fileBytes = new ubyte[](fileSize.to!size_t); if(f.rawRead(fileBytes).length != fileSize) { return []; } StringCache cache = StringCache(StringCache.defaultBucketCount); LexerConfig config; config.stringBehavior = StringBehavior.source; config.fileName = fileName; config.commentBehavior = CommentBehavior.intern; auto lexer = DLexer(fileBytes, config, &cache); const(Token)[] tokens = lexer.array; return tokens.map!(token => const Token(token.type, token.text.idup, token.line, token.column, token.index)).array; } catch(Throwable) { return []; } } @("TestException should ignore missing files") unittest { auto result = new SourceResult("test/missing.txt", 10); auto msg = result.toString; msg.should.equal("\n" ~ `-------------------- test/missing.txt:10 --------------------` ~ "\n"); } @("Source reporter should find the tested value on scope start") unittest { auto result = new SourceResult("test/values.d", 4); result.getValue.should.equal("[1, 2, 3]"); } @("Source reporter should find the tested value after a statment") unittest { auto result = new SourceResult("test/values.d", 12); result.getValue.should.equal("[1, 2, 3]"); } @("Source reporter should find the tested value after a */ comment") unittest { auto result = new SourceResult("test/values.d", 20); result.getValue.should.equal("[1, 2, 3]"); } @("Source reporter should find the tested value after a +/ comment") unittest { auto result = new SourceResult("test/values.d", 28); result.getValue.should.equal("[1, 2, 3]"); } @("Source reporter should find the tested value after a // comment") unittest { auto result = new SourceResult("test/values.d", 36); result.getValue.should.equal("[1, 2, 3]"); } @("Source reporter should find the tested value from an assert utility") unittest { auto result = new SourceResult("test/values.d", 55); result.getValue.should.equal("5"); result = new SourceResult("test/values.d", 56); result.getValue.should.equal("(5+1)"); result = new SourceResult("test/values.d", 57); result.getValue.should.equal("(5, (11))"); } @("Source reporter should get the value from multiple should asserts") unittest { auto result = new SourceResult("test/values.d", 61); result.getValue.should.equal("5"); result = new SourceResult("test/values.d", 62); result.getValue.should.equal("(5+1)"); result = new SourceResult("test/values.d", 63); result.getValue.should.equal("(5, (11))"); } @("Source reporter should get the value after a scope") unittest { auto result = new SourceResult("test/values.d", 71); result.getValue.should.equal("found"); } @("Source reporter should get a function call value") unittest { auto result = new SourceResult("test/values.d", 75); result.getValue.should.equal("found(4)"); } @("Source reporter should parse nested lambdas") unittest { auto result = new SourceResult("test/values.d", 81); result.getValue.should.equal("({ ({ }).should.beNull; })"); } /// Source reporter should print the source code unittest { auto result = new SourceResult("test/values.d", 36); auto printer = new MockPrinter(); result.print(printer); auto lines = printer.buffer.split("[primary:\n]"); lines[1].should.equal(`[info:test/values.d:36]`); lines[2].should.equal(`[primary: 31:][info:unittest][primary: ][info:{]`); lines[7].should.equal(`[dangerReverse:> 36:][primary: ][info:.][primary:contain][info:(][success:4][info:)][info:;]`); } /// split multiline tokens in multiple single line tokens with the same type void splitMultilinetokens(const(Token)[] tokens, ref const(Token)[] result) nothrow @trusted { try { foreach(token; tokens) { auto pieces = token.text.idup.split("\n"); if(pieces.length <= 1) { result ~= const Token(token.type, token.text.dup, token.line, token.column, token.index); } else { size_t line = token.line; size_t column = token.column; foreach(textPiece; pieces) { result ~= const Token(token.type, textPiece, line, column, token.index); line++; column = 1; } } } } catch(Throwable) {} } /// A new line sepparator class SeparatorResult : IResult { override string toString() { return "\n"; } void print(ResultPrinter printer) { printer.primary("\n"); } } class ListInfoResult : IResult { private { struct Item { string singular; string plural; string[] valueList; string key() { return valueList.length > 1 ? plural : singular; } MessageResult toMessage(size_t indentation = 0) { auto printableKey = rightJustify(key ~ ":", indentation, ' '); auto result = new MessageResult(printableKey); string glue; foreach(value; valueList) { result.addText(glue); result.addValue(value); glue = ","; } return result; } } Item[] items; } void add(string key, string value) { items ~= Item(key, "", [value]); } void add(string singular, string plural, string[] valueList) { items ~= Item(singular, plural, valueList); } private size_t indentation() { auto elements = items.filter!"a.valueList.length > 0"; if(elements.empty) { return 0; } return elements.map!"a.key".map!"a.length".maxElement + 2; } override string toString() { auto indent = indentation; auto elements = items.filter!"a.valueList.length > 0"; if(elements.empty) { return ""; } return "\n" ~ elements.map!(a => a.toMessage(indent)).map!"a.toString".join("\n"); } void print(ResultPrinter printer) { auto indent = indentation; auto elements = items.filter!"a.valueList.length > 0"; if(elements.empty) { return; } foreach(item; elements) { printer.primary("\n"); item.toMessage(indent).print(printer); } } } /// convert to string the added data to ListInfoResult unittest { auto result = new ListInfoResult(); result.add("a", "1"); result.add("ab", "2"); result.add("abc", "3"); result.toString.should.equal(` a:1 ab:2 abc:3`); } /// print the added data to ListInfoResult unittest { auto printer = new MockPrinter(); auto result = new ListInfoResult(); result.add("a", "1"); result.add("ab", "2"); result.add("abc", "3"); result.print(printer); printer.buffer.should.equal(`[primary: ][primary: a:][primary:][info:1][primary: ][primary: ab:][primary:][info:2][primary: ][primary: abc:][primary:][info:3]`); } /// convert to string the added data lists to ListInfoResult unittest { auto result = new ListInfoResult(); result.add("a", "as", ["1", "2","3"]); result.add("ab", "abs", ["2", "3"]); result.add("abc", "abcs", ["3"]); result.add("abcd", "abcds", []); result.toString.should.equal(` as:1,2,3 abs:2,3 abc:3`); } IResult[] toResults(Exception e) nothrow @trusted { try { return [ new MessageResult(e.message.to!string) ]; } catch(Exception) { return [ new MessageResult("Unknown error!") ]; } }