1 /// Tool to generated behaviour driven code from markdown description 2 module tagion.tools.collider.collider; 3 4 /** 5 * @brief tool generate d files from bdd md files and vice versa 6 */ 7 8 import std.algorithm.iteration : each, filter, fold, joiner, map, splitter, uniq; 9 import std.algorithm.searching : canFind; 10 import std.algorithm.sorting : sort; 11 import std.array : array, join, split; 12 import std.file : SpanMode, dirEntries, exists, readText, fwrite = write; 13 import std.format; 14 import std.getopt; 15 import std.parallelism : parallel; 16 import std.path : buildPath, dirName, extension, setExtension; 17 import std.process : environment, execute; 18 import std.range; 19 import std.regex : matchFirst, regex; 20 import std.stdio; 21 import std.string : join, splitLines, strip; 22 import std.typecons : Tuple; 23 import tagion.basic.Types : DOT, FileExtension, hasExtension; 24 import tagion.behaviour.Behaviour : TestCode, getBDDErrors, testCode, testColor; 25 import tagion.behaviour.BehaviourFeature; 26 import tagion.behaviour.BehaviourIssue : Dlang, DlangT, Markdown; 27 import tagion.behaviour.BehaviourParser; 28 import tagion.behaviour.Emendation : emendation, suggestModuleName; 29 import tagion.hibon.HiBONFile : fread, fwrite; 30 import tagion.tools.Basic; 31 import tagion.tools.collider.BehaviourOptions; 32 import tagion.tools.collider.schedule; 33 import tagion.tools.revision : revision_text; 34 import tagion.utils.Term; 35 36 //import shitty=tagion.tools.collider.shitty; 37 38 alias ModuleInfo = Tuple!(string, "name", string, "file"); /// Holds the filename and the module name for a d-module 39 40 /** 41 * Used to check valid filename 42 * @param filename - filename to be checked 43 * @return true if the file is not a generated or markdown 44 */ 45 bool checkValidFile(string file_name) { 46 return !(canFind(file_name, ".gen") || !canFind(file_name, ".md")); 47 } 48 49 /** 50 * Parses markdown BDD and produce a new format markdown 51 * and a d-source skeleton 52 * @param opts - options for behaviour 53 * @return amount of erros in markdown files 54 */ 55 int parse_bdd(ref const(BehaviourOptions) opts) { 56 const regex_include = regex(opts.regex_inc); 57 const regex_exclude = regex(opts.regex_exc); 58 alias DlangFile = DlangT!File; 59 if (opts.importfile) { 60 DlangFile.preparations = opts.importfile.readText.splitLines; 61 } 62 auto bdd_files = opts.paths 63 .map!(path => dirEntries(path, SpanMode.depth)) 64 .joiner 65 .filter!(file => file.isFile) 66 .filter!(file => file.name.hasExtension(opts.bdd_ext)) 67 .filter!(file => (opts.regex_inc.length is 0) || !file.name.matchFirst(regex_include).empty) 68 .filter!(file => (opts.regex_exc.length is 0) || file.name.matchFirst(regex_exclude).empty) 69 .array; 70 string[] dfmt; 71 72 verbose("%-(BDD: %s\n%)", bdd_files); 73 if (opts.dfmt.length) { 74 dfmt = opts.dfmt.strip ~ opts.dfmt_flags.dup; 75 } 76 else { 77 dfmt = environment.get(DFMT_ENV, null).split.array.dup; 78 } 79 80 string[] iconv; /// Character convert used to remove illegal chars in files 81 if (opts.iconv.length) { 82 iconv = opts.iconv.strip ~ opts.iconv_flags.dup; 83 } 84 ModuleInfo[] list_of_modules; 85 86 /* Error counter */ 87 int result_errors; 88 foreach (file; bdd_files) { 89 if (!checkValidFile(file)) { 90 continue; 91 } 92 auto dsource = file.name.setExtension(FileExtension.dsrc); 93 const bdd_gen = dsource.setExtension(opts.bdd_gen_ext); 94 if (dsource.exists) { 95 dsource = dsource.setExtension(opts.d_ext); 96 } 97 try { 98 string[] errors; /// List of parse errors 99 100 auto feature = parser(file.name, errors); 101 feature.emendation(file.name.suggestModuleName(opts.paths)); 102 103 if (errors.length) { 104 writefln("Amount of erros in %s: %s", file.name, errors.length); 105 errors.join("\n").writeln; 106 result_errors++; 107 continue; 108 } 109 110 if (opts.enable_package && feature.info.name) { 111 list_of_modules ~= ModuleInfo(feature.info.name, file.setExtension( 112 FileExtension.dsrc)); 113 } 114 { // Generate d-source file 115 auto fout = File(dsource, "w"); 116 verbose("SRC: %s", dsource); 117 auto dlang = Dlang(fout); 118 dlang.issue(feature); 119 fout.close; 120 if (iconv.length) { 121 const exit_code = execute(iconv ~ dsource); 122 if (exit_code.status) { 123 writefln("Correction error %s", exit_code.output); 124 } 125 else { 126 dsource.fwrite(exit_code.output); 127 } 128 } 129 if (dfmt.length) { 130 const exit_code = execute(dfmt ~ dsource); 131 if (exit_code.status) { 132 writefln("Format error %s", exit_code.output); 133 } 134 } 135 } 136 { // Generate bdd-md file 137 auto fout = File(bdd_gen, "w"); 138 scope (exit) { 139 fout.close; 140 } 141 auto markdown = Markdown(fout); 142 markdown.issue(feature); 143 } 144 } 145 catch (Exception e) { 146 writeln(e); 147 result_errors++; 148 } 149 } 150 list_of_modules.generate_packages; 151 return result_errors; 152 } 153 154 enum package_filename = "package".setExtension(FileExtension.dsrc); 155 void generate_packages(const(ModuleInfo[]) list_of_modules) { 156 auto module_paths = list_of_modules 157 .map!(mod => mod.file.dirName) //.array 158 .uniq; 159 foreach (path; module_paths) { 160 auto modules_in_the_same_package = list_of_modules 161 .filter!(mod => mod.file.dirName == path); 162 const package_path = buildPath(path, package_filename); 163 auto fout = File(package_path, "w"); 164 scope (exit) { 165 fout.close; 166 } 167 auto module_split = modules_in_the_same_package.front.name 168 .splitter(DOT); 169 170 const count_without_module_mame = module_split.walkLength - 1; 171 172 fout.writefln(q{module %-(%s.%);}, 173 module_split.take(count_without_module_mame)); 174 175 fout.writeln; 176 177 modules_in_the_same_package 178 .map!(mod => mod.name.idup) 179 .array 180 .sort 181 .each!(module_name => fout.writefln(q{public import %s = %s;}, 182 module_name.split(DOT).tail(1).front, // Module identifier 183 module_name)); 184 } 185 } 186 187 int check_reports(string[] paths) { 188 189 bool show(const TestCode test_code) nothrow { 190 return verbose_switch || test_code == TestCode.error || test_code == TestCode.started; 191 } 192 193 void show_report(Args...)(const TestCode test_code, string fmt, Args args) { 194 static if (Args.length is 0) { 195 const text = fmt; 196 } 197 else { 198 const text = format(fmt, args); 199 } 200 writefln("%s%s%s", testColor(test_code), text, RESET); 201 } 202 203 void report(Args...)(const TestCode test_code, string fmt, Args args) { 204 if (show(test_code)) { 205 show_report(test_code, fmt, args); 206 } 207 } 208 209 struct TraceCount { 210 uint passed; 211 uint errors; 212 uint started; 213 uint total; 214 void update(const TestCode test_code) nothrow pure { 215 final switch (test_code) { 216 case TestCode.none: 217 break; 218 case TestCode.passed: 219 passed++; 220 break; 221 case TestCode.error: 222 errors++; 223 break; 224 case TestCode.started: 225 started++; 226 227 } 228 total++; 229 } 230 231 TestCode testCode() nothrow pure const { 232 if (passed == total) { 233 return TestCode.passed; 234 } 235 if (errors > 0) { 236 return TestCode.error; 237 } 238 if (started > 0) { 239 return TestCode.started; 240 } 241 return TestCode.none; 242 } 243 244 int result() nothrow pure const { 245 final switch (testCode) { 246 case TestCode.none: 247 return 1; 248 case TestCode.error: 249 return cast(int) errors; 250 case TestCode.started: 251 return -cast(int)(started); 252 case TestCode.passed: 253 return 0; 254 } 255 assert(0); 256 } 257 258 void report(string text) { 259 const test_code = testCode; 260 if (test_code == TestCode.passed) { 261 show_report(test_code, "%d test passed BDD-tests", total); 262 } 263 else { 264 writef("%s%s%s: ", BLUE, text, RESET); 265 show_report(test_code, " passed %2$s/%1$s, failed %3$s/%1$s, started %4$s/%1$s", 266 total, passed, errors, started); 267 } 268 } 269 270 } 271 272 TraceCount feature_count; 273 TraceCount scenario_count; 274 int result; 275 foreach (path; paths) { 276 foreach (string report_file; dirEntries(path, "*.hibon", SpanMode.breadth) 277 .filter!(f => f.isFile)) { 278 try { 279 const feature_group = report_file.fread!FeatureGroup; 280 const feature_test_code = testCode(feature_group); 281 feature_count.update(feature_test_code); 282 if (show(feature_test_code)) { 283 writefln("Trace file %s", report_file); 284 } 285 286 report(feature_test_code, feature_group.info.property.description); 287 const show_scenario = feature_test_code == TestCode.error 288 || feature_test_code == TestCode.started; 289 foreach (scenario_group; feature_group.scenarios) { 290 const scenario_test_code = testCode(scenario_group); 291 scenario_count.update(scenario_test_code); 292 if (show_scenario) { 293 report(scenario_test_code, "\t%s", scenario_group.info.property 294 .description); 295 foreach (err; getBDDErrors(scenario_group)) { 296 report(scenario_test_code, "\t\t%s", err.msg); 297 } 298 } 299 } 300 } 301 catch (Exception e) { 302 error("Error: %s in handling report %s", e.msg, report_file); 303 } 304 } 305 } 306 307 feature_count.report("Features "); 308 if (feature_count.testCode !is TestCode.passed) { 309 scenario_count.report("Scenarios"); 310 } 311 return feature_count.result; 312 } 313 314 SubTools sub_tools; 315 static this() { 316 import reporter = tagion.tools.collider.reporter; 317 318 sub_tools["reporter"] = &reporter._main; 319 } 320 321 int main(string[] args) { 322 BehaviourOptions options; 323 immutable program = args[0]; /** file for configurations */ 324 auto config_file = "collider".setExtension(FileExtension.json); /** flag for print current version of behaviour */ 325 bool version_switch; /** flag for overwrite config file */ 326 bool overwrite_switch; /** falg for to enable report checks */ 327 bool Check_reports_switch; 328 bool check_reports_switch; 329 // string[] stages; 330 options.schedule_file = "collider_schedule".setExtension(FileExtension.json); 331 332 string[] run_stages; 333 uint schedule_jobs = 0; 334 bool schedule_rewrite; 335 bool schedule_write_proto; 336 337 string testbench = "testbench"; 338 bool force_switch; 339 // int function(string[])[string] sub_tools; 340 // sub_tools["shitty"] = &shitty._main; 341 try { 342 if (config_file.exists) { 343 options.load(config_file); 344 } 345 else { 346 options.setDefault; 347 } 348 const Result result = subTool(sub_tools, args); 349 if (result.executed) { 350 return result.exit_code; 351 } 352 auto main_args = getopt(args, std.getopt.config.caseSensitive, 353 "version", "display the version", &version_switch, 354 "I", "Include directory", &options.paths, std.getopt.config.bundling, 355 "O", format("Write configure file '%s'", config_file), &overwrite_switch, 356 "R|regex_inc", format(`Include regex Default:"%s"`, options.regex_inc), &options.regex_inc, 357 "X|regex_exc", format(`Exclude regex Default:"%s"`, options.regex_exc), &options.regex_exc, 358 "i|import", format(`Set include file Default:"%s"`, options.importfile), &options 359 .importfile, 360 "p|package", "Generates D package to the source files", &options 361 .enable_package, 362 "c|check", "Check the bdd reports in give list of directories", &check_reports_switch, 363 "C", "Same as check but the program will return a nozero exit-code if the check fails", &Check_reports_switch, 364 "s|schedule", format( 365 "Execution schedule Default: '%s'", options.schedule_file), &options.schedule_file, 366 "r|run", "Runs the test in the schedule", &run_stages, 367 "S", "Rewrite the schedule file", &schedule_rewrite, 368 "j|jobs", format( 369 "Sets number jobs to run simultaneously (0 == max) Default: %d", schedule_jobs), &schedule_jobs, 370 "b|bin", format("Testbench program Default: '%s'", testbench), &testbench, 371 "P|proto", "Writes sample schedule file", &schedule_write_proto, 372 "f|force", "Force a symbolic link to be created", &force_switch, 373 "v|verbose", "Enable verbose print-out", &__verbose_switch, 374 "n|dry", "Shows the parameter for a schedule run (dry-run)", &__dry_switch, 375 "silent", "Don't show progess", &options.silent, 376 ); 377 if (version_switch) { 378 revision_text.writeln; 379 return 0; 380 } 381 382 if (overwrite_switch) { 383 if (args.length is ONE_ARGS_ONLY) { 384 config_file = args[1]; 385 } 386 options.save(config_file); 387 writefln("Configure file written to %s", config_file); 388 return 0; 389 } 390 391 if (main_args.helpWanted) { 392 defaultGetoptPrinter([ 393 revision_text, 394 "Documentation: https://tagion.org/", 395 "", 396 "Usage:", 397 format("%s [<option>...]", program), 398 "# Sub-tools", 399 format("%s %-(%s|%) [<options>...]", program, sub_tools.keys), 400 "", 401 "<option>:", 402 ].join("\n"), main_args.options); 403 return 0; 404 } 405 406 if (force_switch) { 407 forceSymLink(sub_tools); 408 } 409 410 if (schedule_write_proto) { 411 Schedule schedule; 412 auto run_unit = RunUnit(["example"], ["WORKDIR": "$(HOME)/work"], ["-f$WORKDIR"], 0.0); 413 414 schedule.units["collider_test"] = run_unit; 415 schedule.save(options.schedule_file); 416 return 0; 417 } 418 419 if (run_stages) { 420 import core.cpuid : coresPerCPU; 421 422 Schedule schedule; 423 schedule.load(options.schedule_file); 424 schedule_jobs = (schedule_jobs == 0) ? coresPerCPU : schedule_jobs; 425 const cov_enable = (environment.get("COV") !is null); 426 auto schedule_runner = ScheduleRunner(schedule, run_stages, schedule_jobs, options, cov_enable); 427 schedule_runner.run([testbench]); 428 if (schedule_rewrite) { 429 schedule.save(options.schedule_file); 430 } 431 // Check_reports_switch = true; 432 } 433 434 check_reports_switch = Check_reports_switch || check_reports_switch; 435 if (check_reports_switch) { 436 const ret = check_reports(args[1 .. $]); 437 if (ret) { 438 writeln("Test result failed!"); 439 } 440 else { 441 writeln("Test result success!"); 442 } 443 return (Check_reports_switch) ? ret : 0; 444 } 445 return parse_bdd(options); 446 } 447 catch (Exception e) { 448 error(e); 449 return 1; 450 } 451 return 0; 452 }