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 }