From e5ae31b36c9ffc910f1a9459a3f452c5f995ec22 Mon Sep 17 00:00:00 2001 From: Szabo Bogdan Date: Wed, 6 Oct 2021 18:39:45 +0200 Subject: [PATCH] Enable arrayEqual asserts for objects --- source/fluentasserts/core/evaluation.d | 69 ++++++++--- source/fluentasserts/core/lifecycle.d | 1 + .../core/operations/arrayEqual.d | 17 ++- .../fluentasserts/core/operations/registry.d | 112 +++++++++++++++--- source/fluentasserts/core/serializers.d | 3 +- test/operations/approximately.d | 4 +- test/operations/arrayEqual.d | 83 +++++++++++++ test/operations/equal.d | 49 ++++++++ 8 files changed, 305 insertions(+), 33 deletions(-) diff --git a/source/fluentasserts/core/evaluation.d b/source/fluentasserts/core/evaluation.d index df4fa55..e258645 100644 --- a/source/fluentasserts/core/evaluation.d +++ b/source/fluentasserts/core/evaluation.d @@ -6,7 +6,7 @@ import std.traits; import std.conv; import std.range; import std.array; -import std.algorithm : map; +import std.algorithm : map, sort; import fluentasserts.core.serializers; import fluentasserts.core.results; @@ -191,7 +191,7 @@ unittest { auto result = extractTypes!(T[]); - assert(result[0] == "fluentasserts.core.evaluation.__unittest_L188_C1.T[]", `Expected: ` ~ result[0] ); + assert(result[0] == "fluentasserts.core.evaluation.__unittest_L188_C1.T[]", `Expected: ` ~ result[0]); assert(result[1] == "object.Object[]", `Expected: ` ~ result[1] ); assert(result[2] == "fluentasserts.core.evaluation.__unittest_L188_C1.I[]", `Expected: ` ~ result[2] ); } @@ -203,17 +203,18 @@ interface EquableValue { EquableValue[] toArray(); string toString(); EquableValue generalize(); + string getSerialized(); } /// Wraps a value into equable value EquableValue equableValue(T)(T value, string serialized) { - static if(isArray!T) { - return null; - } else static if(isInputRange!T) { - return null; + static if(isArray!T && !isSomeString!T) { + return new ArrayEquable!T(value, serialized); + } else static if(isInputRange!T && !isSomeString!T) { + return new ArrayEquable!T(value.array, serialized); } else static if(isAssociativeArray!T) { - return null; + return new AssocArrayEquable!T(value, serialized); } else { return new ObjectEquable!T(value, serialized); } @@ -250,12 +251,16 @@ class ObjectEquable(T) : EquableValue { } } - return false; + return serialized == otherEquable.getSerialized; } catch(Exception) { return false; } } + string getSerialized() { + return serialized; + } + EquableValue generalize() { static if(is(T == class)) { auto obj = cast(Object) value; @@ -273,7 +278,11 @@ class ObjectEquable(T) : EquableValue { } override string toString() { - return serialized; + return "Equable." ~ serialized; + } + + override int opCmp (Object o) { + return -1; } } @@ -285,14 +294,14 @@ class ArrayEquable(U: T[], T) : EquableValue { string serialized; } - this(T[] value, string serialized) { - this.value = values; + this(T[] values, string serialized) { + this.values = values; this.serialized = serialized; } @safe nothrow: bool isEqualTo(EquableValue otherEquable) { - auto other = cast(ArrayEquable!T) otherEquable; + auto other = cast(ArrayEquable!U) otherEquable; if(other is null) { return false; @@ -301,11 +310,43 @@ class ArrayEquable(U: T[], T) : EquableValue { return serialized == other.serialized; } - EquableValue[] toArray() { - return values.map!(a => new ArrayEquable!U(a)).array; + string getSerialized() { + return serialized; + } + + @trusted EquableValue[] toArray() { + static if(is(T == void)) { + return []; + } else { + try { + auto newList = values.map!(a => equableValue(a, SerializerRegistry.instance.niceValue(a))).array; + + return cast(EquableValue[]) newList; + } catch(Exception) { + return []; + } + } + } + + EquableValue generalize() { + return this; } override string toString() { return serialized; } } + + +/// +class AssocArrayEquable(U: T[V], T, V) : ArrayEquable!(string[], string) { + this(T[V] values, string serialized) { + auto sortedKeys = values.keys.sort; + + auto sortedValues = sortedKeys + .map!(a => SerializerRegistry.instance.niceValue(a) ~ `: ` ~ SerializerRegistry.instance.niceValue(values[a])) + .array; + + super(sortedValues, serialized); + } +} diff --git a/source/fluentasserts/core/lifecycle.d b/source/fluentasserts/core/lifecycle.d index 21f5d2b..58e24b9 100644 --- a/source/fluentasserts/core/lifecycle.d +++ b/source/fluentasserts/core/lifecycle.d @@ -128,6 +128,7 @@ static this() { } Registry.instance.register("*[]", "*[]", "equal", &arrayEqual); + Registry.instance.register("*[*]", "*[*]", "equal", &arrayEqual); Registry.instance.register("*[][]", "*[][]", "equal", &arrayEqual); Registry.instance.register("*", "*", "equal", &equal); diff --git a/source/fluentasserts/core/operations/arrayEqual.d b/source/fluentasserts/core/operations/arrayEqual.d index 8ed836e..916f038 100644 --- a/source/fluentasserts/core/operations/arrayEqual.d +++ b/source/fluentasserts/core/operations/arrayEqual.d @@ -12,8 +12,21 @@ version(unittest) { /// IResult[] arrayEqual(ref Evaluation evaluation) @safe nothrow { evaluation.message.addText("."); - - auto result = evaluation.currentValue.strValue == evaluation.expectedValue.strValue; + bool result = true; + + EquableValue[] expectedPieces = evaluation.expectedValue.proxyValue.toArray; + EquableValue[] testData = evaluation.currentValue.proxyValue.toArray; + + if(testData.length == expectedPieces.length) { + foreach(index, testedValue; testData) { + if(testedValue !is null && !testedValue.isEqualTo(expectedPieces[index])) { + result = false; + break; + } + } + } else { + result = false; + } if(evaluation.isNegated) { result = !result; diff --git a/source/fluentasserts/core/operations/registry.d b/source/fluentasserts/core/operations/registry.d index 4d779fa..7fadbc4 100644 --- a/source/fluentasserts/core/operations/registry.d +++ b/source/fluentasserts/core/operations/registry.d @@ -57,17 +57,19 @@ class Registry { assert(valueType != "", "The value type is not set!"); assert(name != "", "The operation name is not set!"); - string key = valueType ~ "." ~ expectedValueType ~ "." ~ name; - - if(key !in operations) { - auto genericKey = generalizeKey(valueType, expectedValueType, name); - - assert(key in operations || genericKey in operations, "There is no `" ~ key ~ "` or `" ~ genericKey ~ "` registered to the assert operations."); + auto genericKeys = [valueType ~ "." ~ expectedValueType ~ "." ~ name] ~ generalizeKey(valueType, expectedValueType, name); + string matchedKey; - key = genericKey; + foreach(key; genericKeys) { + if(key in operations) { + matchedKey = key; + break; + } } - return operations[key]; + assert(matchedKey != "", "There are no matching assert operations. Register any of `" ~ genericKeys.join("`, `") ~ "` to perform this assert."); + + return operations[matchedKey]; } /// @@ -81,15 +83,97 @@ class Registry { } } -string generalizeKey(string valueType, string expectedValueType, string name) @safe nothrow { - return generalizeType(valueType) ~ "." ~ generalizeType(expectedValueType) ~ "." ~ name; +string[] generalizeKey(string valueType, string expectedValueType, string name) @safe nothrow { + string[] results; + + foreach (string generalizedValueType; generalizeType(valueType)) { + foreach (string generalizedExpectedValueType; generalizeType(expectedValueType)) { + results ~= generalizedValueType ~ "." ~ generalizedExpectedValueType ~ "." ~ name; + } + } + + return results; } -string generalizeType(string typeName) @safe nothrow { +string[] generalizeType(string typeName) @safe nothrow { auto pos = typeName.indexOf("["); if(pos == -1) { - return "*"; + return ["*"]; } - return "*" ~ typeName[pos..$]; -} \ No newline at end of file + string[] results = []; + + const pieces = typeName.split("["); + + string arrayType; + bool isHashMap; + int index = 0; + int diff = 0; + + foreach (ch; typeName[pos..$]) { + diff++; + if(ch == '[') { + index++; + } + + if(ch == ']') { + index--; + } + + if(index == 0 && diff == 2) { + arrayType ~= "[]"; + } + + if(index == 0 && diff != 2) { + arrayType ~= "[*]"; + isHashMap = true; + } + + if(index == 0) { + diff = 0; + } + } + + if(isHashMap) { + results ~= "*" ~ typeName[pos..$]; + results ~= pieces[0] ~ arrayType; + } + + results ~= "*" ~ arrayType; + + return results; +} + +version(unittest) { + import fluentasserts.core.base; +} + +/// It can generalize an int +unittest { + generalizeType("int").should.equal(["*"]); +} + +/// It can generalize a list +unittest { + generalizeType("int[]").should.equal(["*[]"]); +} + +/// It can generalize a list of lists +unittest { + generalizeType("int[][]").should.equal(["*[][]"]); +} + +/// It can generalize an assoc array +unittest { + generalizeType("int[int]").should.equal(["*[int]", "int[*]", "*[*]"]); +} + +/// It can generalize a combination of assoc arrays and lists +unittest { + generalizeType("int[int][][string][]").should.equal(["*[int][][string][]", "int[*][][*][]", "*[*][][*][]"]); +} + +/// It can generalize an assoc array with a key list +unittest { + generalizeType("int[int[]]").should.equal(["*[int[]]", "int[*]", "*[*]"]); +} diff --git a/source/fluentasserts/core/serializers.d b/source/fluentasserts/core/serializers.d index 3e5a993..efaf1b7 100644 --- a/source/fluentasserts/core/serializers.d +++ b/source/fluentasserts/core/serializers.d @@ -97,7 +97,8 @@ class SerializerRegistry { if(value is null) { result = "null"; } else { - result = T.stringof ~ "(" ~ (cast() value).toHash.to!string ~ ")"; + auto v = (cast() value); + result = T.stringof ~ "(" ~ v.toHash.to!string ~ ")"; } } else static if(is(Unqual!T == Duration)) { result = value.total!"nsecs".to!string; diff --git a/test/operations/approximately.d b/test/operations/approximately.d index b6166c9..f8f2e8d 100644 --- a/test/operations/approximately.d +++ b/test/operations/approximately.d @@ -35,12 +35,12 @@ alias s = Spec!({ [testValue].should.not.be.approximately([3], 0.24); }); - it("should not compare a string with a ", { + it("should not compare a string with a number", { auto msg = ({ "".should.be.approximately(3, 0.34); }).should.throwSomething.msg; - msg.split("\n")[0].should.equal("There is no `string.int.approximately` or `*.*.approximately` registered to the assert operations."); + msg.split("\n")[0].should.equal("There are no matching assert operations. Register any of `string.int.approximately`, `*.*.approximately` to perform this assert."); }); }); diff --git a/test/operations/arrayEqual.d b/test/operations/arrayEqual.d index f207535..55e258c 100644 --- a/test/operations/arrayEqual.d +++ b/test/operations/arrayEqual.d @@ -2,6 +2,7 @@ module test.operations.arrayEqual; import fluentasserts.core.expect; import fluent.asserts; +import fluentasserts.core.serializers; import trial.discovery.spec; @@ -216,4 +217,86 @@ alias s = Spec!({ }); }); } + + describe("using an array of objects with opEquals", { + Thing[] aList; + Thing[] anotherList; + Thing[] aListInOtherOrder; + + string strAList; + string strAnotherList; + string strAListInOtherOrder; + + before({ + aList = [ new Thing(1), new Thing(2), new Thing(3) ]; + aListInOtherOrder = [ new Thing(3), new Thing(2), new Thing(1) ]; + anotherList = [ new Thing(2), new Thing(3) ]; + + strAList = SerializerRegistry.instance.niceValue(aList); + strAnotherList = SerializerRegistry.instance.niceValue(anotherList); + strAListInOtherOrder = SerializerRegistry.instance.niceValue(aListInOtherOrder); + }); + + it("should compare two exact arrays", { + expect(aList).to.equal(aList); + }); + + it("should be able to compare that two arrays are not equal", { + expect(aList).to.not.equal(aListInOtherOrder); + expect(aList).to.not.equal(anotherList); + expect(anotherList).to.not.equal(aList); + }); + + it("should throw a detailed error message when the two arrays are not equal", { + auto msg = ({ + expect(aList).to.equal(anotherList); + }).should.throwException!TestException.msg.split("\n"); + + msg[0].strip.should.equal(strAList.to!string ~ " should equal " ~ strAnotherList.to!string ~ "."); + msg[1].strip.should.equal("Diff:"); + msg[4].strip.should.equal("Expected:" ~ strAnotherList.to!string); + msg[5].strip.should.equal("Actual:" ~ strAList.to!string); + }); + + it("should throw an error when the arrays have the same values in a different order", { + auto msg = ({ + expect(aList).to.equal(aListInOtherOrder); + }).should.throwException!TestException.msg.split("\n"); + + msg[0].strip.should.equal(strAList.to!string ~ " should equal " ~ strAListInOtherOrder ~ "."); + msg[1].strip.should.equal("Diff:"); + msg[4].strip.should.equal("Expected:" ~ strAListInOtherOrder); + msg[5].strip.should.equal("Actual:" ~ strAList.to!string); + }); + + it("should throw an error when the arrays should not be equal", { + auto msg = ({ + expect(aList).not.to.equal(aList); + }).should.throwException!TestException.msg.split("\n"); + + msg[0].strip.should.startWith(strAList.to!string ~ " should not equal " ~ strAList.to!string ~ "."); + msg[2].strip.should.equal("Expected:not " ~ strAList); + msg[3].strip.should.equal("Actual:" ~ strAList); + }); + }); }); + + +version(unittest) : +class Thing { + int x; + + this(int x) { this.x = x; } + + override bool opEquals(Object o) { + if(typeid(this) != typeid(o)) return false; + alias a = this; + auto b = cast(typeof(this)) o; + + return a.x == b.x; + } + + override string toString() { + return x.to!string; + } +} \ No newline at end of file diff --git a/test/operations/equal.d b/test/operations/equal.d index fdeb951..944bb0f 100644 --- a/test/operations/equal.d +++ b/test/operations/equal.d @@ -238,6 +238,55 @@ alias s = Spec!({ msg.split("\n")[0].should.equal(niceTestValue.to!string ~ ` should not equal ` ~ niceTestValue.to!string ~ `. ` ~ niceTestValue.to!string ~ ` is equal to ` ~ niceTestValue.to!string ~ `.`); }); }); + + describe("using assoc arrays", { + string[string] testValue; + string[string] sameTestValue; + string[string] otherTestValue; + + string niceTestValue; + string niceSameTestValue; + string niceOtherTestValue; + + before({ + testValue = ["b": "2", "a": "1", "c": "3"]; + sameTestValue = ["a": "1", "b": "2", "c": "3"]; + otherTestValue = ["a": "3", "b": "2", "c": "1"]; + + niceTestValue = SerializerRegistry.instance.niceValue(testValue); + niceSameTestValue = SerializerRegistry.instance.niceValue(sameTestValue); + niceOtherTestValue = SerializerRegistry.instance.niceValue(otherTestValue); + }); + + it("should be able to compare two exact values", { + expect(testValue).to.equal(testValue); + }); + + + it("should be able to compare two objects with the same fields", { + expect(testValue).to.equal(sameTestValue); + }); + + it("should be able to check if two values are not equal", { + expect(testValue).to.not.equal(otherTestValue); + }); + + it("should throw an exception with a detailed message when the strings are not equal", { + auto msg = ({ + expect(testValue).to.equal(otherTestValue); + }).should.throwException!TestException.msg; + + msg.split("\n")[0].should.equal(niceTestValue.to!string ~ ` should equal ` ~ niceOtherTestValue.to!string ~ `.`); + }); + + it("should throw an exception with a detailed message when the strings should not be equal", { + auto msg = ({ + expect(testValue).to.not.equal(testValue); + }).should.throwException!TestException.msg; + + msg.split("\n")[0].should.equal(niceTestValue.to!string ~ ` should not equal ` ~ niceTestValue.to!string ~ `.`); + }); + }); }); version(unittest) :