1 /// Tool to create report files from collider hibon files
2 module tagion.tools.collider.reporter;
3 
4 /**
5  * @brief tool generate d files from bdd md files and vice versa
6  */
7 import std.algorithm.iteration;
8 import std.algorithm.sorting;
9 import std.array;
10 import std.file;
11 import std.format;
12 import std.getopt;
13 import std.path;
14 import std.process : environment;
15 import std.stdio;
16 import std.string;
17 import std.traits;
18 import tagion.behaviour.BehaviourFeature;
19 import tagion.behaviour.BehaviourResult;
20 import tagion.behaviour.Behaviour;
21 import tagion.hibon.Document : Document;
22 import tagion.hibon.HiBONFile : fread, fwrite;
23 import tagion.hibon.HiBONJSON;
24 import tagion.hibon.HiBONRecord : isRecord;
25 import tagion.tools.Basic : Main;
26 import tagion.tools.revision : revision_text;
27 
28 mixin Main!(_main);
29 
30 enum OutputFormat {
31     github = "github",
32     markdown = "markdown",
33 }
34 
35 int _main(string[] args) {
36     immutable program = args[0];
37     string output;
38     OutputFormat format_style = OutputFormat.markdown;
39 
40     auto main_args = getopt(args,
41             "o|output", "output file", &output,
42             "f|format", format("Format style, one of %s", [EnumMembers!OutputFormat]), &format_style,
43     );
44 
45     if (main_args.helpWanted) {
46         defaultGetoptPrinter([
47             revision_text,
48             "Documentation: https://tagion.org/",
49             "",
50             "Usage:",
51             format("%s [<option>...]", program),
52             "",
53             "<option>:",
54         ].join("\n"), main_args.options);
55         return 0;
56     }
57 
58     string log_dir = ".";
59     if (args.length == 2) {
60         log_dir = args[1];
61     }
62     if (!output) {
63         output = "/dev/stdout";
64     }
65 
66     if (!log_dir.exists || !log_dir.isDir) {
67         stderr.writeln("Log directory does not exist");
68         return 1;
69     }
70 
71     auto result_files = dirEntries(log_dir, SpanMode.depth).filter!(dirEntry => dirEntry.name.endsWith(".hibon"))
72         .map!(dirEntry => dirEntry.name)
73         .array
74         .sort;
75 
76     if (result_files.length == 0) {
77         stderr.writeln("No hibon result files found");
78     }
79 
80     FeatureGroup[] featuregroups;
81     foreach (result; result_files) {
82         featuregroups ~= fread!FeatureGroup(result);
83     }
84 
85     auto outstring = appender!string;
86     if (format_style == OutputFormat.github) {
87         const REPOROOT = environment.get("REPOROOT", getcwd());
88         foreach (fg; featuregroups) {
89             foreach (scenario; fg.scenarios) {
90                 void printErrors(Info)(Info info) {
91                     if (info.result.isRecord!BehaviourError) {
92                         auto bdd_err = BehaviourError(info.result);
93                         outstring.put(
94                                 "::error file=%s,line=%s,title=BDD error::%s\n"
95                                 .format(
96                                     bdd_err.file.relativePath(REPOROOT),
97                                     bdd_err.line,
98                                     bdd_err.msg,
99                         )
100                         );
101                         outstring.put("::group::BDD error\n");
102                         foreach (l; bdd_err.trace) {
103                             outstring.put(l ~ '\n');
104                         }
105                         outstring.put("::endgroup::\n");
106                     }
107                 }
108 
109                 foreach (info; scenario.given.infos) {
110                     printErrors(info);
111                 }
112                 foreach (info; scenario.when.infos) {
113                     printErrors(info);
114                 }
115                 foreach (info; scenario.then.infos) {
116                     printErrors(info);
117                 }
118                 foreach (info; scenario.but.infos) {
119                     printErrors(info);
120                 }
121             }
122         }
123     }
124     else {
125         foreach (fg; featuregroups) {
126             outstring.put(fg.toMd);
127         }
128     }
129 
130     File(output, "w").write(outstring.data);
131 
132     return 0;
133 }
134 
135 alias MdString = string;
136 
137 /// Gh flavor markdown
138 MdString toMd(FeatureGroup fg) {
139     auto result_md = appender!string;
140     string result_type() {
141         if (fg.hasErrors) {
142             return "❌";
143         }
144         else if (fg.info.result.isRecord!Result) {
145             return "✔️ ";
146         }
147         else {
148             return "❓";
149         }
150     }
151 
152     uint successful = 0;
153     foreach (scenario; fg.scenarios) {
154         if (scenario.info.result.isRecord!Result) {
155             successful += 1;
156         }
157     }
158     const summary = format("<summary> %s (%s/%s) %s </summary>\n\n", result_type, successful, fg.scenarios.length, fg
159             .info.name);
160 
161     // result_md.put(format("%s\n\n", result_type));
162 
163     result_md.put("<details>");
164     result_md.put(summary);
165     result_md.put("```json\n");
166     result_md.put(format("%s\n", fg.toPretty));
167     result_md.put("```\n\n");
168     result_md.put("</details><br>\n\n");
169     return result_md[];
170 }