diff --git a/.gitignore b/.gitignore index c926ac8..b93ae7d 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,4 @@ test/unit-threaded/unit-threaded-example test/disabledDiffResult/disabled-diff-result-example test/disabledMessageResult/disabled-message-result-example test/disabledSourceResult/disabled-source-result-example +fluent-asserts-test-unittest diff --git a/dub.json b/dub.json index 6bf25fb..ecb0f18 100644 --- a/dub.json +++ b/dub.json @@ -4,11 +4,12 @@ "Szabo Bogdan" ], "description": "Fluent assertions done right", - "copyright": "Copyright © 2022, Szabo Bogdan", + "copyright": "Copyright © 2023, Szabo Bogdan", "license": "MIT", "homepage": "http://fluentasserts.szabobogdan.com/", "dependencies": { "libdparse": "~>0.20.0", + "either": "~>1.1.3", "ddmp": "~>0.0.1-0.dev.3", "unit-threaded": { "version": "*", @@ -26,11 +27,11 @@ ] }, { - "name": "trial", - "sourcePaths": [ - "source", - "test/operations" - ] + "name": "unittest", + "targetType": "library", + "dependencies": { + "silly": "~>1.2.0-dev.2" + } } ] } \ No newline at end of file diff --git a/source/fluentasserts/core/base.d b/source/fluentasserts/core/base.d index e7c305b..11327ef 100644 --- a/source/fluentasserts/core/base.d +++ b/source/fluentasserts/core/base.d @@ -71,11 +71,6 @@ struct Result { } } -struct Message { - bool isValue; - string text; -} - mixin template DisabledShouldThrowableCommons() { auto throwSomething(string file = __FILE__, size_t line = __LINE__) { static assert("`throwSomething` does not work for arrays and ranges"); @@ -491,13 +486,13 @@ auto should(T)(lazy T testData, const string file = __FILE__, const size_t line } } -@("because") +/// "because" adds a text before the assert message unittest { auto msg = ({ true.should.equal(false).because("of test reasons"); }).should.throwException!TestException.msg; - msg.split("\n")[0].should.equal("Because of test reasons, true should equal false."); + msg.split("\n")[0].should.equal("Because of test reasons, true should equal false. "); } struct Assert { diff --git a/source/fluentasserts/core/basetype.d b/source/fluentasserts/core/basetype.d index e669a55..82a02f8 100644 --- a/source/fluentasserts/core/basetype.d +++ b/source/fluentasserts/core/basetype.d @@ -45,16 +45,16 @@ unittest { 5.should.equal(6); }).should.throwException!TestException.msg; - msg.split("\n")[0].should.equal("5 should equal 6. 5 is not equal to 6."); + msg.split("\n")[0].should.equal("5 should equal 6. 5 is not equal to 6. "); msg = ({ 5.should.not.equal(5); }).should.throwException!TestException.msg; - msg.split("\n")[0].should.equal("5 should not equal 5. 5 is equal to 5."); + msg.split("\n")[0].should.equal("5 should not equal 5. 5 is equal to 5. "); } -@("bools equal") +/// bools equal unittest { ({ true.should.equal(true); @@ -65,7 +65,7 @@ unittest { true.should.equal(false); }).should.throwException!TestException.msg; - msg.split("\n")[0].should.equal("true should equal false."); + msg.split("\n")[0].should.equal("true should equal false. "); msg.split("\n")[2].strip.should.equal("Expected:false"); msg.split("\n")[3].strip.should.equal("Actual:true"); @@ -73,12 +73,12 @@ unittest { true.should.not.equal(true); }).should.throwException!TestException.msg; - msg.split("\n")[0].should.equal("true should not equal true."); + msg.split("\n")[0].should.equal("true should not equal true. "); msg.split("\n")[2].strip.should.equal("Expected:not true"); msg.split("\n")[3].strip.should.equal("Actual:true"); } -@("numbers greater than") +/// numbers greater than unittest { ({ 5.should.be.greaterThan(4); diff --git a/source/fluentasserts/core/message.d b/source/fluentasserts/core/message.d new file mode 100644 index 0000000..d225eeb --- /dev/null +++ b/source/fluentasserts/core/message.d @@ -0,0 +1,198 @@ +module fluentasserts.core.message; + +import std.string; +import ddmp.diff; +import fluentasserts.core.results; +import std.algorithm; +import std.conv; + +@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 = "-"; + } +} + +struct Message { + enum Type { + info, + value, + title, + category, + insert, + delete_ + } + + Type type; + string text; + + this(Type type, string text) nothrow { + this.type = type; + + if(type == Type.value || type == Type.insert || type == Type.delete_) { + this.text = text + .replace("\r", ResultGlyphs.carriageReturn) + .replace("\n", ResultGlyphs.newline) + .replace("\0", ResultGlyphs.nullChar) + .replace("\t", ResultGlyphs.tab); + } else { + this.text = text; + } + } + + string toString() nothrow inout { + switch(type) { + case Type.title: + return "\n\n" ~ text ~ "\n"; + case Type.insert: + return "[-" ~ text ~ "]"; + case Type.delete_: + return "[+" ~ text ~ "]"; + case Type.category: + return "\n" ~ text ~ ""; + default: + return text; + } + } +} + +IResult[] toException(ref EvaluationResult result) nothrow { + if(result.messages.length == 0) { + return []; + } + + return [ new EvaluationResultInstance(result) ]; +} + +struct EvaluationResult { + private { + immutable(Message)[] messages; + } + + void add(immutable(Message) message) nothrow { + messages ~= message; + } + + string toString() nothrow { + string result; + + foreach (message; messages) { + result ~= message.toString; + } + + return result; + } + + void print(ResultPrinter printer) nothrow { + foreach (message; messages) { + printer.print(message); + } + } +} + +static immutable actualTitle = Message(Message.Type.category, "Actual:"); + +void addResult(ref EvaluationResult result, string value) nothrow @trusted { + result.add(actualTitle); + + result.add(Message(Message.Type.value, value)); +} + + +static immutable expectedTitle = Message(Message.Type.category, "Expected:"); +static immutable expectedNot = Message(Message.Type.info, "not "); + +void addExpected(ref EvaluationResult result, bool isNegated, string value) nothrow @trusted { + result.add(expectedTitle); + + if(isNegated) { + result.add(expectedNot); + } + + result.add(Message(Message.Type.value, value)); +} + + +static immutable diffTitle = Message(Message.Type.title, "Diff:"); + +void addDiff(ref EvaluationResult result, string actual, string expected) nothrow @trusted { + result.add(diffTitle); + + try { + auto diffResult = diff_main(expected, actual); + + foreach(diff; diffResult) { + if(diff.operation == Operation.EQUAL) { + result.add(Message(Message.Type.info, diff.text)); + } + + if(diff.operation == Operation.INSERT) { + result.add(Message(Message.Type.insert, diff.text)); + } + + if(diff.operation == Operation.DELETE) { + result.add(Message(Message.Type.delete_, diff.text)); + } + } + } catch(Exception e) { + return; + } +} diff --git a/source/fluentasserts/core/operations/equal.d b/source/fluentasserts/core/operations/equal.d index 870c863..979092e 100644 --- a/source/fluentasserts/core/operations/equal.d +++ b/source/fluentasserts/core/operations/equal.d @@ -4,6 +4,7 @@ import fluentasserts.core.results; import fluentasserts.core.evaluation; import fluentasserts.core.lifecycle; +import fluentasserts.core.message; version(unittest) { import fluentasserts.core.expect; @@ -11,9 +12,15 @@ version(unittest) { static immutable equalDescription = "Asserts that the target is strictly == equal to the given val."; +static immutable isEqualTo = Message(Message.Type.info, " is equal to "); +static immutable isNotEqualTo = Message(Message.Type.info, " is not equal to "); +static immutable endSentence = Message(Message.Type.info, ". "); + /// IResult[] equal(ref Evaluation evaluation) @safe nothrow { - evaluation.message.addText("."); + EvaluationResult evaluationResult; + + evaluation.message.add(endSentence); bool result = evaluation.currentValue.strValue == evaluation.expectedValue.strValue; @@ -32,22 +39,23 @@ IResult[] equal(ref Evaluation evaluation) @safe nothrow { IResult[] results = []; if(evaluation.currentValue.typeName != "bool") { - evaluation.message.addText(" "); - evaluation.message.addValue(evaluation.currentValue.strValue); + evaluation.message.add(Message(Message.Type.value, evaluation.currentValue.strValue)); if(evaluation.isNegated) { - evaluation.message.addText(" is equal to "); + evaluation.message.add(isEqualTo); } else { - evaluation.message.addText(" is not equal to "); + evaluation.message.add(isNotEqualTo); } - evaluation.message.addValue(evaluation.expectedValue.strValue); - evaluation.message.addText("."); + evaluation.message.add(Message(Message.Type.value, evaluation.expectedValue.strValue)); + evaluation.message.add(endSentence); + evaluationResult.addDiff(evaluation.expectedValue.strValue, evaluation.currentValue.strValue); try results ~= new DiffResult(evaluation.expectedValue.strValue, evaluation.currentValue.strValue); catch(Exception) {} } - try results ~= new ExpectedActualResult((evaluation.isNegated ? "not " : "") ~ evaluation.expectedValue.strValue, evaluation.currentValue.strValue); catch(Exception) {} + evaluationResult.addExpected(evaluation.isNegated, evaluation.expectedValue.strValue); + evaluationResult.addResult(evaluation.currentValue.strValue); - return results; + return evaluationResult.toException; } diff --git a/source/fluentasserts/core/results.d b/source/fluentasserts/core/results.d index 6cc345d..bf39fdb 100644 --- a/source/fluentasserts/core/results.d +++ b/source/fluentasserts/core/results.d @@ -12,86 +12,36 @@ 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`; - } +public import fluentasserts.core.message; - ResultGlyphs.sourceIndicator = ">"; - ResultGlyphs.sourceLineSeparator = ":"; - - ResultGlyphs.diffBegin = "["; - ResultGlyphs.diffEnd = "]"; - ResultGlyphs.diffInsert = "+"; - ResultGlyphs.diffDelete = "-"; - } -} +@safe: /// interface ResultPrinter { - void primary(string); - void info(string); - void danger(string); - void success(string); + nothrow: + void print(Message); + void primary(string); + void info(string); + void danger(string); + void success(string); - void dangerReverse(string); - void successReverse(string); + void dangerReverse(string); + void successReverse(string); } version(unittest) { class MockPrinter : ResultPrinter { string buffer; + void print(Message message) { + import std.conv : to; + try { + buffer ~= "[" ~ message.type.to!string ~ ":" ~ message.text ~ "]"; + } catch(Exception) { + buffer ~= "ERROR"; + } + } + void primary(string val) { buffer ~= "[primary:" ~ val ~ "]"; } @@ -133,51 +83,74 @@ WhiteIntervals getWhiteIntervals(string text) { return WhiteIntervals(text.indexOf(stripText[0]), text.lastIndexOf(stripText[stripText.length - 1])); } +void writeNoThrow(T)(T text) nothrow { + try { + write(text); + } catch(Exception e) { + assert(true, "Can't write to stdout!"); + } +} + /// This is the most simple implementation of a ResultPrinter. /// All the plain data is printed to stdout class DefaultResultPrinter : ResultPrinter { + nothrow: + + void print(Message message) { + + } + void primary(string text) { - write(text); + writeNoThrow(text); } void info(string text) { - write(text); + writeNoThrow(text); } void danger(string text) { - write(text); + writeNoThrow(text); } void success(string text) { - write(text); + writeNoThrow(text); } void dangerReverse(string text) { - write(text); + writeNoThrow(text); } void successReverse(string text) { - write(text); + writeNoThrow(text); } } -interface IResult -{ +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; - } +class EvaluationResultInstance : IResult { - Message[] messages; + EvaluationResult result; + + this(EvaluationResult result) nothrow { + this.result = result; + } + + override string toString() nothrow { + return result.toString; + } + + void print(ResultPrinter printer) nothrow { + result.print(printer); + } +} + +/// A result that prints a simple message to the user +class MessageResult : IResult { + private { + immutable(Message)[] messages; } this(string message) nothrow @@ -192,22 +165,26 @@ class MessageResult : IResult } void startWith(string message) @safe nothrow { - Message[] newMessages; + immutable(Message)[] newMessages; - newMessages ~= Message(false, message); + newMessages ~= Message(Message.Type.info, message); newMessages ~= this.messages; this.messages = newMessages; } void add(bool isValue, string message) nothrow { - this.messages ~= Message(isValue, message + this.messages ~= Message(isValue ? Message.Type.value : Message.Type.info, message .replace("\r", ResultGlyphs.carriageReturn) .replace("\n", ResultGlyphs.newline) .replace("\0", ResultGlyphs.nullChar) .replace("\t", ResultGlyphs.tab)); } + void add(Message message) nothrow { + this.messages ~= message; + } + void addValue(string text) @safe nothrow { add(true, text); } @@ -217,21 +194,21 @@ class MessageResult : IResult text = "throw any exception"; } - this.messages ~= Message(false, text); + this.messages ~= Message(Message.Type.info, text); } void prependText(string text) @safe nothrow { - this.messages = Message(false, text) ~ this.messages; + this.messages = Message(Message.Type.info, text) ~ this.messages; } void prependValue(string text) @safe nothrow { - this.messages = Message(true, text) ~ this.messages; + this.messages = Message(Message.Type.value, text) ~ this.messages; } void print(ResultPrinter printer) { foreach(message; messages) { - if(message.isValue) { + if(message.type == Message.Type.value) { printer.info(message.text); } else { printer.primary(message.text); @@ -309,8 +286,7 @@ unittest class DiffResult : IResult { import ddmp.diff; - protected - { + protected { string expected; string actual; } diff --git a/source/fluentasserts/core/string.d b/source/fluentasserts/core/string.d index 86f4c52..f3c1731 100644 --- a/source/fluentasserts/core/string.d +++ b/source/fluentasserts/core/string.d @@ -221,15 +221,18 @@ unittest { "test string".should.equal("test"); }).should.throwException!TestException.msg; - msg.split("\n")[0].should.equal(`"test string" should equal "test". "test string" is not equal to "test".`); + msg.split("\n")[0].should.equal(`"test string" should equal "test". "test string" is not equal to "test". `); msg = ({ "test string".should.not.equal("test string"); }).should.throwException!TestException.msg; - msg.split("\n")[0].should.equal(`"test string" should not equal "test string". "test string" is equal to "test string".`); + msg.split("\n")[0].should.equal(`"test string" should not equal "test string". "test string" is equal to "test string". `); +} - msg = ({ +/// it shows null chars in the diff +unittest { + auto msg = ({ ubyte[] data = [115, 111, 109, 101, 32, 100, 97, 116, 97, 0, 0]; data.assumeUTF.to!string.should.equal("some data"); }).should.throwException!TestException.msg;