1 module fluentasserts.core.expect; 2 3 import fluentasserts.core.lifecycle; 4 import fluentasserts.core.evaluation; 5 import fluentasserts.core.results; 6 7 import fluentasserts.core.serializers; 8 9 import std.traits; 10 import std..string; 11 import std.uni; 12 import std.conv; 13 14 /// 15 @safe struct Expect { 16 17 private { 18 Evaluation evaluation; 19 int refCount; 20 } 21 22 this(ValueEvaluation value, const string fileName, const size_t line, string prependText = null) @trusted { 23 this.evaluation = new Evaluation(); 24 25 evaluation.id = Lifecycle.instance.beginEvaluation(value); 26 evaluation.currentValue = value; 27 evaluation.message = new MessageResult(); 28 evaluation.source = new SourceResult(fileName, line); 29 30 try { 31 auto sourceValue = evaluation.source.getValue; 32 33 if(sourceValue == "") { 34 evaluation.message.startWith(evaluation.currentValue.niceValue); 35 } else { 36 evaluation.message.startWith(sourceValue); 37 } 38 } catch(Exception) { 39 evaluation.message.startWith(evaluation.currentValue.strValue); 40 } 41 42 evaluation.message.addText(" should"); 43 44 if(prependText) { 45 evaluation.message.addText(prependText); 46 } 47 } 48 49 this(ref return scope Expect another) { 50 this.evaluation = another.evaluation; 51 this.refCount = another.refCount + 1; 52 } 53 54 ~this() { 55 refCount--; 56 57 if(refCount < 0) { 58 evaluation.message.addText(" "); 59 evaluation.message.addText(evaluation.operationName.toNiceOperation); 60 61 if(evaluation.expectedValue.niceValue) { 62 evaluation.message.addText(" "); 63 evaluation.message.addValue(evaluation.expectedValue.niceValue); 64 } else if(evaluation.expectedValue.strValue) { 65 evaluation.message.addText(" "); 66 evaluation.message.addValue(evaluation.expectedValue.strValue); 67 } 68 69 Lifecycle.instance.endEvaluation(evaluation); 70 } 71 } 72 73 string msg(const size_t line = __LINE__, const string file = __FILE__) @trusted { 74 if(this.thrown is null) { 75 throw new Exception("There were no thrown exceptions", file, line); 76 } 77 78 return this.thrown.message.to!string; 79 } 80 81 Expect withMessage(const size_t line = __LINE__, const string file = __FILE__) { 82 addOperationName("withMessage"); 83 return this; 84 } 85 86 Expect withMessage(string message, const size_t line = __LINE__, const string file = __FILE__) { 87 addOperationName("withMessage"); 88 return this.equal(message); 89 } 90 91 Throwable thrown() { 92 Lifecycle.instance.endEvaluation(evaluation); 93 return evaluation.throwable; 94 } 95 96 /// 97 Expect to() { 98 return this; 99 } 100 101 /// 102 Expect be () { 103 evaluation.message.addText(" be"); 104 return this; 105 } 106 107 /// 108 Expect not() { 109 evaluation.isNegated = !evaluation.isNegated; 110 evaluation.message.addText(" not"); 111 112 return this; 113 } 114 115 /// 116 auto throwAnyException() { 117 return opDispatch!"throwAnyException"; 118 } 119 120 /// 121 Expect throwException(Type)() { 122 this.evaluation.expectedValue.meta["exceptionType"] = fullyQualifiedName!Type; 123 this.evaluation.expectedValue.meta["throwableType"] = fullyQualifiedName!Type; 124 125 return opDispatch!"throwException"(fullyQualifiedName!Type); 126 } 127 128 auto because(string reason) { 129 evaluation.message.prependText("Because " ~ reason ~ ", "); 130 return this; 131 } 132 133 /// 134 auto equal(T)(T value) { 135 return opDispatch!"equal"(value); 136 } 137 138 /// 139 auto contain(T)(T value) { 140 return opDispatch!"contain"(value); 141 } 142 143 /// 144 auto greaterThan(T)(T value) { 145 return opDispatch!"greaterThan"(value); 146 } 147 148 /// 149 auto greaterOrEqualTo(T)(T value) { 150 return opDispatch!"greaterOrEqualTo"(value); 151 } 152 153 /// 154 auto above(T)(T value) { 155 return opDispatch!"above"(value); 156 } 157 /// 158 auto lessThan(T)(T value) { 159 return opDispatch!"lessThan"(value); 160 } 161 162 /// 163 auto lessOrEqualTo(T)(T value) { 164 return opDispatch!"lessOrEqualTo"(value); 165 } 166 167 /// 168 auto below(T)(T value) { 169 return opDispatch!"below"(value); 170 } 171 172 /// 173 auto startWith(T)(T value) { 174 return opDispatch!"startWith"(value); 175 } 176 177 /// 178 auto endWith(T)(T value) { 179 return opDispatch!"endWith"(value); 180 } 181 182 auto containOnly(T)(T value) { 183 return opDispatch!"containOnly"(value); 184 } 185 186 auto beNull() { 187 return opDispatch!"beNull"; 188 } 189 190 auto instanceOf(Type)() { 191 return opDispatch!"instanceOf"(fullyQualifiedName!Type); 192 } 193 194 auto approximately(T, U)(T value, U range) { 195 return opDispatch!"approximately"(value, range); 196 } 197 198 auto between(T, U)(T value, U range) { 199 return opDispatch!"between"(value, range); 200 } 201 202 auto within(T, U)(T value, U range) { 203 return opDispatch!"within"(value, range); 204 } 205 206 void inhibit() { 207 this.refCount = int.max; 208 } 209 210 auto haveExecutionTime() { 211 this.inhibit; 212 213 auto result = expect(evaluation.currentValue.duration, evaluation.source.file, evaluation.source.line, " have execution time"); 214 215 return result; 216 } 217 218 void addOperationName(string value) { 219 220 if(this.evaluation.operationName) { 221 this.evaluation.operationName ~= "."; 222 } 223 224 this.evaluation.operationName ~= value; 225 } 226 227 /// 228 Expect opDispatch(string methodName)() { 229 addOperationName(methodName); 230 231 return this; 232 } 233 234 /// 235 Expect opDispatch(string methodName, Params...)(Params params) if(Params.length > 0) { 236 addOperationName(methodName); 237 238 static if(Params.length > 0) { 239 auto expectedValue = params[0].evaluate.evaluation; 240 241 foreach(key, value; evaluation.expectedValue.meta) { 242 expectedValue.meta[key] = value; 243 } 244 245 evaluation.expectedValue = expectedValue; 246 } 247 248 static if(Params.length >= 1) { 249 static foreach (i, Param; Params) { 250 () @trusted { evaluation.expectedValue.meta[i.to!string] = SerializerRegistry.instance.serialize(params[i]); } (); 251 } 252 } 253 254 return this; 255 } 256 } 257 258 /// 259 Expect expect(void delegate() callable, const string file = __FILE__, const size_t line = __LINE__, string prependText = null) @trusted { 260 ValueEvaluation value; 261 value.typeNames = [ "callable" ]; 262 263 try { 264 if(callable !is null) { 265 callable(); 266 } else { 267 value.typeNames = ["null"]; 268 } 269 } catch(Exception e) { 270 value.throwable = e; 271 value.meta["Exception"] = "yes"; 272 } catch(Throwable t) { 273 value.throwable = t; 274 value.meta["Throwable"] = "yes"; 275 } 276 277 return Expect(value, file, line, prependText); 278 } 279 280 /// 281 Expect expect(T)(lazy T testedValue, const string file = __FILE__, const size_t line = __LINE__, string prependText = null) @trusted { 282 return Expect(testedValue.evaluate.evaluation, file, line, prependText); 283 } 284 285 /// 286 string toNiceOperation(string value) @safe nothrow { 287 string newValue; 288 289 foreach(index, ch; value) { 290 if(index == 0) { 291 newValue ~= ch.toLower; 292 continue; 293 } 294 295 if(ch == '.') { 296 newValue ~= ' '; 297 continue; 298 } 299 300 if(ch.isUpper && value[index - 1].isLower) { 301 newValue ~= ' '; 302 newValue ~= ch.toLower; 303 continue; 304 } 305 306 newValue ~= ch; 307 } 308 309 return newValue; 310 } 311 312 /// toNiceOperation converts to a nice and readable string 313 unittest { 314 expect("".toNiceOperation).to.equal(""); 315 expect("a.b".toNiceOperation).to.equal("a b"); 316 expect("aB".toNiceOperation).to.equal("a b"); 317 }