1 module tagion.behaviour.Behaviour; 2 3 import core.exception : AssertError; 4 import std.algorithm.searching : all, any; 5 import std.array : join; 6 import std.exception : assumeWontThrow; 7 import std.format; 8 import std.meta : AliasSeq; 9 import std.range : only; 10 import std.traits; 11 import tagion.basic.Types : FileExtension; 12 import tagion.basic.basic : isOneOf; 13 import tagion.behaviour.BehaviourException; 14 public import tagion.behaviour.BehaviourFeature; 15 import tagion.behaviour.BehaviourReporter; 16 import tagion.behaviour.BehaviourResult; 17 import tagion.hibon.Document; 18 import tagion.hibon.HiBONRecord; 19 20 /** 21 Runs the scenario in Given, When, Then, But order 22 Returns: 23 The ScenarioGroup including the result of each action 24 */ 25 @trusted 26 ScenarioGroup run(T)(T scenario) if (isScenario!T) { 27 ScenarioGroup scenario_group = getScenarioGroup!T; 28 import std.stdio; 29 30 debug (bdd) 31 writefln("Feature: %s", scenario_group.info.property.description); 32 33 try { 34 // Mixin code to produce the action Given, When, Then, But 35 alias memberCode = format!(q{ 36 // Scenario group %1$s 37 // Action propery info %2$s 38 // Info index (i) %3$d 39 // Test scenario %4$s 40 // Test member %5$s 41 // $ Given: some scenario scenario descriotion 42 debug(bdd) writeln("%2$s: ", %1$s.%2$s.infos[%3$d].property.description); 43 try { 44 // Example. 45 // scenario_group.when.info[i].result = scenario.member_function; 46 %1$s.%2$s.infos[%3$d].result = %4$s.%5$s; 47 } 48 catch (Exception e) { 49 // In case of an exception error the result is set to a BehaviourError 50 // Example. 51 // scemario_group.when.info[i].result = BehaviourError(e).toDoc; 52 %1$s.%2$s.infos[%3$d].result= BehaviourError(e).toDoc; 53 } 54 }, string, string, size_t, string, string); 55 import std.uni : toLower; 56 57 58 59 .check(scenario !is null, 60 format("The constructor must be called for %s before it's runned", T.stringof)); 61 static foreach (_Property; ActionProperties) { 62 { 63 alias all_actions = getActions!(T, _Property); 64 static if (is(all_actions == void)) { 65 static assert(!isOneOf!(_Property, MandatoryActionProperties), 66 format("%s is missing a @%s action", T.stringof, _Property.stringof)); 67 } 68 else { 69 // Traverse all the actions the scenario 70 static foreach (i, behaviour; all_actions) { 71 { 72 // This action_name is the action of the scenario 73 // The action is the lower case of Action type (ex. Given is given) 74 // See the definition of ScenarioGroup 75 enum action_name = __traits(identifier, 76 typeof(getProperty!(behaviour))).toLower; 77 enum code = memberCode( 78 scenario_group.stringof, action_name, i, 79 scenario.stringof, __traits(identifier, behaviour)); 80 // The memberCode is used here 81 mixin(code); 82 } 83 } 84 } 85 } 86 } 87 scenario_group.info.result = result_ok; 88 } 89 catch (Exception e) { 90 stderr.writefln("BDD Caught Exception:\n\n%s", e); 91 scenario_group.info.result = BehaviourError(e).toDoc; 92 } 93 // We want to be able to report asserts as well 94 catch (AssertError e) { 95 stderr.writefln("BDD Caught AssertError:\n\n%s", e); 96 scenario_group.info.result = BehaviourError(e).toDoc; 97 } 98 return scenario_group; 99 } 100 101 ///Examples: How use the rub fuction on a feature 102 @safe 103 unittest { 104 import std.algorithm.comparison : equal; 105 import std.algorithm.iteration : map; 106 import std.array; 107 import tagion.behaviour.BehaviourUnittest; 108 109 auto awesome = new Some_awesome_feature; 110 const runner_result = run(awesome); 111 auto expected = only( 112 "tagion.behaviour.BehaviourUnittest.Some_awesome_feature.is_valid", 113 "tagion.behaviour.BehaviourUnittest.Some_awesome_feature.in_credit", 114 "tagion.behaviour.BehaviourUnittest.Some_awesome_feature.contains_cash", 115 "tagion.behaviour.BehaviourUnittest.Some_awesome_feature.request_cash", 116 "tagion.behaviour.BehaviourUnittest.Some_awesome_feature.is_debited", 117 "tagion.behaviour.BehaviourUnittest.Some_awesome_feature.is_dispensed", 118 "tagion.behaviour.BehaviourUnittest.Some_awesome_feature.swollow_the_card", 119 ) 120 .map!(a => result(a)); 121 assert(awesome.count == 7); 122 Document[] results; 123 results ~= runner_result.given.infos 124 .map!(info => info.result) 125 .array; 126 results ~= runner_result.when.infos 127 .map!(info => info.result) 128 .array; 129 results ~= runner_result.then.infos 130 .map!(info => info.result) 131 .array; 132 results ~= runner_result.but.infos 133 .map!(info => info.result) 134 .array; 135 assert(equal(results, expected)); 136 } 137 138 @safe 139 ScenarioGroup getScenarioGroup(T)() if (isScenario!T) { 140 ScenarioGroup scenario_group; 141 scenario_group.info.property = getScenario!T; 142 scenario_group.info.name = T.stringof; 143 static foreach (_Property; ActionProperties) { 144 { 145 alias behaviours = getActions!(T, _Property); 146 static if (!is(behaviours == void)) { 147 import std.uni : toLower; 148 149 enum group_name = _Property.stringof.toLower; 150 auto group = &__traits(getMember, scenario_group, group_name); 151 group.infos.length = behaviours.length; 152 static foreach (i, behaviour; behaviours) { 153 { 154 Info!_Property info; 155 info.property = getProperty!behaviour; 156 info.name = __traits(identifier, behaviour); 157 group.infos[i] = info; 158 } 159 } 160 } 161 } 162 } 163 return scenario_group; 164 } 165 166 @safe 167 FeatureGroup getFeature(alias M)() if (isFeature!M) { 168 FeatureGroup result; 169 result.info.property = obtainFeature!M; 170 result.info.name = moduleName!M; 171 alias ScenariosSeq = Scenarios!M; 172 result.scenarios.length = ScenariosSeq.length; 173 static foreach (i, _Scenario; ScenariosSeq) { 174 result.scenarios[i] = getScenarioGroup!_Scenario; 175 } 176 return result; 177 } 178 179 ///Examples: How to use getFeature on a feature 180 181 @safe 182 unittest { // 183 import core.demangle : mangle; 184 import std.path; 185 import tagion.basic.basic : unitfile; 186 import Module = tagion.behaviour.BehaviourUnittest; 187 import tagion.hibon.HiBONFile : fread; 188 189 enum filename = mangle!(FunctionTypeOf!(getFeature!Module))("unittest") 190 .unitfile 191 .setExtension(FileExtension.hibon); 192 const feature = getFeature!(Module); 193 const expected = filename.fread!FeatureGroup; 194 assert(feature.toDoc == expected.toDoc); 195 } 196 197 @safe 198 auto automation(alias M)() if (isFeature!M) { 199 import std.algorithm.searching : any; 200 import std.typecons; 201 202 mixin(format(q{import %s;}, moduleName!M)); 203 204 @safe 205 static struct FeatureFactory { 206 string alternative; 207 FeatureContext context; 208 void opDispatch(string scenario_name, Args...)(Args args) { 209 import std.algorithm.searching : countUntil; 210 211 enum tuple_index = [FeatureContext.fieldNames] 212 .countUntil(scenario_name); 213 static assert(tuple_index >= 0, 214 format("Scenarion '%s' does not exists. Possible scenarions is\n%s", 215 scenario_name, [FeatureContext.fieldNames[0 .. $ - 1]].join(",\n"))); 216 alias _Scenario = FeatureContext.Types[tuple_index]; 217 context[tuple_index] = new _Scenario(args); 218 } 219 220 bool create(Args...)(string regex_text, Args args) { 221 const index = find_scenario(regex_text); 222 import std.stdio; 223 224 switch (index) { 225 static foreach (tuple_index; 0 .. FeatureContext.Types.length - 1) { 226 { 227 alias _Scenario = FeatureContext.Types[tuple_index]; 228 enum scenario_property = getScenario!_Scenario; 229 enum compiles = __traits(compiles, new _Scenario(args)); 230 case tuple_index: 231 static if (compiles) { 232 context[tuple_index] = new _Scenario(args); 233 return true; 234 } 235 else { 236 check(false, 237 format("Arguments %s does not match construct of %s", 238 Args.stringof, _Scenario.stringof)); 239 } 240 // return true; 241 } 242 } 243 default: 244 return false; 245 246 } 247 return false; 248 } 249 250 static int find_scenario(string regex_text) { 251 import std.regex; 252 253 const search_regex = regex(regex_text); 254 255 static foreach (tuple_index; 0 .. FeatureContext.Types.length - 1) { 256 { 257 alias _Scenario = FeatureContext.Types[tuple_index]; 258 enum scenario_property = getScenario!_Scenario; 259 // enum compiles = __traits(compiles, new _Scenario(args)); 260 if (!scenario_property.description.matchFirst(search_regex).empty || 261 scenario_property.comments.any!(c => !c.matchFirst(search_regex).empty)) { 262 return tuple_index; 263 } 264 } 265 } 266 return -1; 267 } 268 269 version (none) auto find(string regex_text)() { 270 import std.regex; 271 272 enum tuple_index = find_scenario(regex_text); 273 static assert(tuple_index >= 0, format("Scenario description with '%s' not found in %s", regex_text, FeatureContext 274 .stringof)); 275 return FeatureContext.Types[tuple_index]; 276 } 277 278 @safe 279 FeatureContext run() nothrow { 280 if (reporter !is null) { 281 auto raw_feature_group = getFeature!M; 282 raw_feature_group.alternative = alternative; 283 reporter.before(&raw_feature_group); 284 } 285 scope (exit) { 286 if (reporter !is null) { 287 context.result.alternative = alternative; 288 reporter.after(context.result); 289 } 290 291 } 292 uint error_count; 293 context.result = new FeatureGroup; 294 context.result.info.property = obtainFeature!M; 295 context.result.info.name = moduleName!M; 296 context.result.scenarios.length = FeatureContext.Types.length - 1; //ScenariosSeq.length; 297 static foreach (i, _Scenario; FeatureContext.Types[0 .. $ - 1]) { 298 try { 299 static if (__traits(compiles, new _Scenario())) { 300 if (context[i] is null) { 301 context[i] = new _Scenario(); 302 } 303 } 304 else { 305 check(context[i]!is null, 306 format("Scenario '%s' must be constructed before can be executed in '%s' feature", 307 FeatureContext.fieldNames[i], 308 moduleName!M)); 309 } 310 context.result.scenarios[i] = .run(context[i]); 311 } 312 catch (Exception e) { 313 error_count++; 314 import std.exception : assumeWontThrow; 315 316 context.result.scenarios[i].info.result = assumeWontThrow(BehaviourError(e) 317 .toDoc); 318 } 319 } 320 if (error_count == 0) { 321 context.result.info.result = result_ok; 322 } 323 return context; 324 } 325 } 326 327 FeatureFactory result; 328 return result; 329 } 330 331 /** 332 Returns: 333 true if one of more scenarios in the Feature has failed 334 */ 335 @safe 336 bool hasErrors(ref const FeatureGroup feature_group) nothrow { 337 if (BehaviourError.isRecord(feature_group.info.result)) { 338 return true; 339 } 340 return feature_group.scenarios.any!(scenario => scenario.hasErrors); 341 } 342 343 @safe 344 bool hasErrors(const(FeatureGroup*) feature_group) nothrow { 345 return hasErrors(*feature_group); 346 } 347 348 /** 349 Returns: 350 true if one of more actions in the Scenario has failed 351 */ 352 @safe 353 bool hasErrors(ref const ScenarioGroup scenario_group) nothrow { 354 static foreach (i, Type; Fields!ScenarioGroup) { 355 static if (isActionGroup!Type) { 356 if (scenario_group.tupleof[i].infos.any!(info => info.result.isRecord!BehaviourError)) { 357 return true; 358 } 359 } 360 else static if (isInfo!Type) { 361 if (scenario_group.tupleof[i].result.isRecord!BehaviourError) { 362 return true; 363 } 364 } 365 } 366 return false; 367 } 368 369 /* import std.algorithm.iteration: filter, each; */ 370 const(BehaviourError)[] getBDDErrors(const(ScenarioGroup) scenarioGroup) { 371 const(BehaviourError)[] errors; 372 // How do i statically iteratate over each actionGroup member of scenarioGroup 373 foreach (info; scenarioGroup.given.infos) { 374 if (info.result.isRecord!BehaviourError) { 375 const result = BehaviourError(info.result); 376 errors ~= result; 377 } 378 } 379 foreach (info; scenarioGroup.when.infos) { 380 if (info.result.isRecord!BehaviourError) { 381 const result = BehaviourError(info.result); 382 errors ~= result; 383 } 384 } 385 foreach (info; scenarioGroup.then.infos) { 386 if (info.result.isRecord!BehaviourError) { 387 const result = BehaviourError(info.result); 388 errors ~= result; 389 } 390 } 391 foreach (info; scenarioGroup.but.infos) { 392 if (info.result.isRecord!BehaviourError) { 393 const result = BehaviourError(info.result); 394 errors ~= result; 395 } 396 } 397 return errors; 398 } 399 400 ///Examples: Show how to use the automation function and the hasError on a feature group 401 @safe 402 unittest { 403 import WithCtor = tagion.behaviour.BehaviourUnittestWithCtor; 404 405 auto feature_with_ctor = automation!(WithCtor)(); 406 407 { // No constructor has been called for the scenarios, this means that scenarios and the feature will have errors 408 const feature_context = feature_with_ctor.run; 409 assert(feature_context.result.scenarios[0].hasErrors); 410 assert(feature_context.result.scenarios[1].hasErrors); 411 assert(feature_context.result.hasErrors); 412 version (behaviour_unitdata) 413 "/tmp/bdd_which_has_feature_errors.hibon".fwrite(feature_context.result); 414 } 415 416 { // Fails in second scenario because the constructor has not been called 417 // Calls the construction for the Some_awesome_feature scenario 418 feature_with_ctor.Some_awesome_feature(42, "with_ctor"); 419 const feature_context = feature_with_ctor.run; 420 assert(!feature_context.result.scenarios[0].hasErrors); 421 assert(feature_context.result.scenarios[1].hasErrors); 422 assert(feature_context.result.hasErrors); 423 version (behaviour_unitdata) 424 "/tmp/bdd_which_has_scenario_errors.hibon".fwrite(feature_context.result); 425 } 426 427 { // The constructor of both scenarios has been called, this means that no errors is reported 428 // Calls the construction for the Some_awesome_feature scenario 429 feature_with_ctor.Some_awesome_feature(42, "with_ctor"); 430 feature_with_ctor.Some_awesome_feature_bad_format_double_property(17); 431 const feature_context = feature_with_ctor.run; 432 assert(!feature_context.result.scenarios[0].hasErrors); 433 assert(!feature_context.result.scenarios[1].hasErrors); 434 assert(!feature_context.result.hasErrors); 435 version (behaviour_unitdata) 436 "/tmp/bdd_which_has_no_errors.hibon".fwrite(feature_context.result); 437 } 438 } 439 440 /** 441 Checks if a feature has passed all tests 442 Returns: 443 true if all scenarios in a Feature has passed all tests 444 */ 445 @safe 446 bool hasPassed(ref const FeatureGroup feature_group) nothrow { 447 return feature_group.info.result.isRecord!Result && 448 feature_group.scenarios.all!(scenario => scenario.hasPassed); 449 } 450 451 @safe 452 bool hasPassed(const(FeatureGroup*) feature_group) nothrow { 453 return hasPassed(*feature_group); 454 } 455 456 /** 457 Used to checks if a scenario has passed all tests 458 Params: 459 scenario_group = The scenario which been runned 460 Returns: true if the scenario has passed all tests 461 */ 462 @safe 463 bool hasPassed(ref const ScenarioGroup scenario_group) nothrow { 464 static foreach (i, Type; Fields!ScenarioGroup) { 465 static if (isActionGroup!Type) { 466 if (scenario_group 467 .tupleof[i].infos 468 .any!(info => !info 469 .result 470 .isRecord!Result)) { 471 return false; 472 } 473 } 474 else static if (isInfo!Type) { 475 if (!scenario_group 476 .tupleof[i] 477 .result 478 .isRecord!Result) { 479 return false; 480 } 481 } 482 } 483 return true; 484 } 485 486 @safe 487 bool hasStarted(ref const ScenarioGroup scenario_group) nothrow { 488 static foreach (i, Type; Fields!ScenarioGroup) { 489 static if (isActionGroup!Type) { 490 if (!scenario_group 491 .tupleof[i].infos 492 .any!(info => !info 493 .result 494 .empty)) { 495 return true; 496 } 497 } 498 } 499 return false; 500 } 501 502 @safe 503 bool hasStarted(ref const FeatureGroup feature_group) nothrow { 504 return feature_group.scenarios.any!(scenario => scenario.hasStarted); 505 } 506 507 enum TestCode { 508 none, 509 passed, 510 error, 511 started, 512 } 513 514 @safe 515 TestCode testCode(Group)(Group group) nothrow 516 if (is(Group : const(ScenarioGroup)) || is(Group : const(FeatureGroup))) { 517 TestCode result; 518 if (hasPassed(group)) { 519 result = TestCode.passed; 520 } 521 else if (hasErrors(group)) { 522 result = TestCode.error; 523 } 524 else if (hasStarted(group)) { 525 result = TestCode.started; 526 } 527 return result; 528 } 529 530 @safe 531 string testColor(const TestCode code) nothrow pure { 532 import tagion.utils.Term; 533 534 with (TestCode) { 535 final switch (code) { 536 case none: 537 return BLUE; 538 case passed: 539 return GREEN; 540 case error: 541 return RED; 542 case started: 543 return YELLOW; 544 } 545 } 546 } 547 548 @safe 549 unittest { 550 551 import WithoutCtor = tagion.behaviour.BehaviourUnittestWithoutCtor; 552 553 auto feature_without_ctor = automation!(WithoutCtor)(); 554 { // None of the scenario passes 555 const feature_context = feature_without_ctor.run; 556 assert(!feature_context.result.scenarios[0].hasPassed); 557 assert(!feature_context.result.scenarios[1].hasPassed); 558 assert(!feature_context.result.hasPassed); 559 } 560 } 561 562 ///Examples: Shows how to use a automation on scenarios with constructor and the hasParssed 563 @safe 564 unittest { 565 // Test of hasPassed function on Scenarios and Feature 566 import WithCtor = tagion.behaviour.BehaviourUnittestWithCtor; 567 568 auto feature_with_ctor = automation!(WithCtor)(); 569 feature_with_ctor.Some_awesome_feature(42, "with_ctor"); 570 feature_with_ctor.Some_awesome_feature_bad_format_double_property(17); 571 572 { // None of the scenario passes 573 const feature_context = feature_with_ctor.run; 574 version (behaviour_unitdata) 575 "/tmp/bdd_sample_has_failed.hibon".fwrite(feature_context.result); 576 assert(!feature_context.result.scenarios[0].hasPassed); 577 assert(!feature_context.result.scenarios[1].hasPassed); 578 assert(!feature_context.result.hasPassed); 579 } 580 581 { // One of the scenario passed 582 WithCtor.pass_one = true; 583 const feature_context = feature_with_ctor.run; 584 version (behaviour_unitdata) 585 "/tmp/bdd_sample_one_has_passed.hibon".fwrite(feature_context.result); 586 assert(!feature_context.result.scenarios[0].hasPassed); 587 assert(feature_context.result.scenarios[1].hasPassed); 588 assert(!feature_context.result.hasPassed); 589 } 590 591 { // Some actions passed passes 592 WithCtor.pass_some = true; 593 WithCtor.pass_one = false; 594 const feature_context = feature_with_ctor.run; 595 version (behaviour_unitdata) 596 "/tmp/bdd_sample_some_actions_has_passed.hibon".fwrite(feature_context.result); 597 assert(!feature_context.result.scenarios[0].hasPassed); 598 assert(!feature_context.result.scenarios[1].hasPassed); 599 assert(!feature_context.result.hasPassed); 600 } 601 602 { // All of the scenario passes 603 WithCtor.pass = true; /// Pass all tests! 604 WithCtor.pass_some = false; 605 606 const feature_context = feature_with_ctor.run; 607 version (behaviour_unitdata) 608 "/tmp/bdd_sample_has_passed.hibon".fwrite(feature_context.result); 609 assert(feature_context.result.scenarios[0].hasPassed); 610 assert(feature_context.result.scenarios[1].hasPassed); 611 } 612 } 613 614 @safe 615 unittest { 616 import WithCtor = tagion 617 .behaviour 618 .BehaviourUnittestWithCtor; 619 620 auto feature_with_ctor = automation!(WithCtor)(); 621 assert(feature_with_ctor.create("bankster", 17)); 622 assertThrown!BehaviourException(feature_with_ctor.create("bankster", "wrong argument")); 623 assert(!feature_with_ctor.create("this-text-does-not-exists", 17)); 624 } 625 626 version (unittest) { 627 import std.exception; 628 import tagion.hibon.Document; 629 import tagion.hibon.HiBONJSON; 630 import tagion.hibon.HiBONRecord; 631 }