1 /** 2 Copyright: Copyright (c) 2015-2017, Joakim Brännström. All rights reserved. 3 License: MPL-2 4 Author: Joakim Brännström (joakim.brannstrom@gmx.com) 5 6 This Source Code Form is subject to the terms of the Mozilla Public License, 7 v.2.0. If a copy of the MPL was not distributed with this file, You can obtain 8 one at http://mozilla.org/MPL/2.0/. 9 */ 10 module dextool.plugin.ctestdouble.frontend.ctestdouble; 11 12 import std.typecons : Nullable; 13 14 import logger = std.experimental.logger; 15 16 import cpptooling.type; 17 import dextool.compilation_db; 18 import dextool.type; 19 20 import dextool.plugin.types; 21 import dextool.plugin.ctestdouble.backend.cvariant : Controller, Parameters, Products; 22 import dextool.plugin.ctestdouble.frontend.types; 23 import dextool.plugin.ctestdouble.frontend.xml; 24 25 // workaround for ldc-1.1.0 and dmd-2.071.2 26 auto workaround_linker_error() { 27 import cpptooling.testdouble.header_filter : TestDoubleIncludes, 28 GenericTestDoubleIncludes, DummyPayload; 29 30 return typeid(GenericTestDoubleIncludes!DummyPayload).toString(); 31 } 32 33 struct RawConfiguration { 34 Nullable!XmlConfig xmlConfig; 35 36 string[] fileExclude; 37 string[] fileRestrict; 38 string[] testDoubleInclude; 39 Path[] inFiles; 40 string[] cflags; 41 string[] compileDb; 42 string header; 43 string headerFile; 44 string mainName = "TestDouble"; 45 string mainFileName = "test_double"; 46 string prefix = "Test_"; 47 string stripInclude; 48 string out_; 49 string config; 50 bool help; 51 bool shortPluginHelp; 52 bool gmock; 53 bool generatePreInclude; 54 bool genPostInclude; 55 bool locationAsComment; 56 bool generateZeroGlobals; 57 bool invalidXmlConfig; 58 59 string[] originalFlags; 60 61 void parse(string[] args) { 62 import std.getopt; 63 64 originalFlags = args.dup; 65 string[] input; 66 67 try { 68 bool no_zero_globals; 69 // dfmt off 70 getopt(args, std.getopt.config.keepEndOfOptions, "h|help", &help, 71 "compile-db", &compileDb, 72 "config", &config, 73 "file-exclude", &fileExclude, 74 "file-restrict", &fileRestrict, 75 "gen-post-incl", &genPostInclude, 76 "gen-pre-incl", &generatePreInclude, 77 "gmock", &gmock, 78 "header", &header, 79 "header-file", &headerFile, 80 "in", &input, 81 "loc-as-comment", &locationAsComment, 82 "main", &mainName, 83 "main-fname", &mainFileName, 84 "no-zeroglobals", &no_zero_globals, 85 "out", &out_, 86 "prefix", &prefix, 87 "short-plugin-help", &shortPluginHelp, 88 "strip-incl", &stripInclude, 89 "td-include", &testDoubleInclude); 90 // dfmt on 91 generateZeroGlobals = !no_zero_globals; 92 } catch (std.getopt.GetOptException ex) { 93 logger.error(ex.msg); 94 help = true; 95 } 96 97 // default arguments 98 if (stripInclude.length == 0) { 99 stripInclude = r".*/(.*)"; 100 logger.trace("--strip-incl: using default regex to strip include path (basename)"); 101 } 102 103 if (config.length != 0) { 104 xmlConfig = readRawConfig(Path(config)); 105 if (xmlConfig.isNull) { 106 invalidXmlConfig = true; 107 } 108 } 109 110 import std.algorithm : find, map; 111 import std.array : array; 112 import std.range : drop; 113 114 inFiles = input.map!(a => Path(a)).array; 115 116 // at this point args contain "what is left". What is interesting then is those after "--". 117 cflags = args.find("--").drop(1).array(); 118 } 119 120 void printHelp() { 121 import std.stdio : writefln; 122 123 writefln("%s\n\n%s\n%s", ctestdouble_opt.usage, 124 ctestdouble_opt.optional, ctestdouble_opt.others); 125 } 126 127 void dump() { 128 // TODO remove this 129 logger.tracef("args: 130 --header :%s 131 --header-file :%s 132 --file-restrict :%s 133 --prefix :%s 134 --gmock :%s 135 --out :%s 136 --file-exclude :%s 137 --main :%s 138 --strip-incl :%s 139 --main-fname :%s 140 --in :%s 141 --compile-db :%s 142 --gen-post-incl :%s 143 --gen-pre-incl :%s 144 --help :%s 145 --loc-as-comment :%s 146 --td-include :%s 147 --no-zeroglobals :%s 148 --config :%s 149 CFLAGS :%s 150 151 xmlConfig :%s", header, headerFile, fileRestrict, prefix, gmock, 152 out_, fileExclude, mainName, stripInclude, 153 mainFileName, inFiles, compileDb, genPostInclude, generatePreInclude, help, locationAsComment, 154 testDoubleInclude, !generateZeroGlobals, config, cflags, xmlConfig); 155 } 156 } 157 158 // dfmt off 159 static auto ctestdouble_opt = CliOptionParts( 160 "usage: 161 dextool ctestdouble [options] [--in=] [-- CFLAGS]", 162 // ------------- 163 " --main=name Used as part of interface, namespace etc [default: TestDouble] 164 --main-fname=n Used as part of filename for generated files [default: test_double] 165 --prefix=p Prefix used when generating test artifacts [default: Test_] 166 --strip-incl=r A regexp used to strip the include paths 167 --gmock Generate a gmock implementation of test double interface 168 --gen-pre-incl Generate a pre include header file if it doesn't exist and use it 169 --gen-post-incl Generate a post include header file if it doesn't exist and use it 170 --loc-as-comment Generate a comment containing the location the symbol was derived from. 171 Makes it easier to correctly define excludes/restricts 172 --header=s Prepend generated files with the string 173 --header-file=f Prepend generated files with the header read from the file 174 --no-zeroglobals Turn off generation of the default implementation that zeroes globals 175 --config=path Use configuration file", 176 // ------------- 177 "others: 178 --in= Input file to parse 179 --out=dir directory for generated files [default: ./] 180 --compile-db= Retrieve compilation parameters from the file 181 --file-exclude= Exclude files from generation matching the regex 182 --file-restrict= Restrict the scope of the test double to those files 183 matching the regex 184 --td-include= User supplied includes used instead of those found 185 186 REGEX 187 The regex syntax is found at http://dlang.org/phobos/std_regex.html 188 189 Information about --strip-incl. 190 Default regexp is: .*/(.*) 191 192 To allow the user to selectively extract parts of the include path dextool 193 applies the regex and then concatenates all the matcher groups found. It is 194 turned into the replacement include path. 195 196 Important to remember then is that this approach requires that at least one 197 matcher group exists. 198 199 Information about --file-exclude. 200 The regex must fully match the filename the AST node is located in. 201 If it matches all data from the file is excluded from the generated code. 202 203 Information about --file-restrict. 204 The regex must fully match the filename the AST node is located in. 205 Only symbols from files matching the restrict affect the generated test double. 206 207 EXAMPLES 208 209 Generate a simple C test double. 210 dextool ctestdouble functions.h 211 212 Analyze and generate a test double for function prototypes and extern variables. 213 Both those found in functions.h and outside, aka via includes. 214 215 The test double is written to ./test_double.hpp/.cpp. 216 The name of the interface is Test_Double. 217 218 Generate a C test double excluding data from specified files. 219 dextool ctestdouble --file-exclude=/foo.h --file-exclude='functions.[h,c]' --out=outdata/ functions.h -- -DBAR -I/some/path 220 221 The code analyzer (Clang) will be passed the compiler flags -DBAR and -I/some/path. 222 During generation declarations found in foo.h or functions.h will be excluded. 223 224 The file holding the test double is written to directory outdata. 225 " 226 ); 227 // dfmt on 228 229 struct FileData { 230 import dextool.io : WriteStrategy; 231 import dextool.type : Path; 232 233 Path filename; 234 string data; 235 WriteStrategy strategy; 236 } 237 238 /** Test double generation of C code. 239 * 240 * TODO Describe the options. 241 */ 242 class CTestDoubleVariant : Controller, Parameters, Products { 243 import std.regex : regex, Regex; 244 import std.typecons : Flag; 245 import dextool.compilation_db : CompileCommandFilter; 246 import cpptooling.testdouble.header_filter : TestDoubleIncludes, LocationType; 247 import dsrcgen.cpp : CppModule, CppHModule; 248 249 private { 250 static const hdrExt = ".hpp"; 251 static const implExt = ".cpp"; 252 static const xmlExt = ".xml"; 253 254 StubPrefix prefix; 255 256 Path output_dir; 257 Path main_file_hdr; 258 Path main_file_impl; 259 Path main_file_globals; 260 Path gmock_file; 261 Path pre_incl_file; 262 Path post_incl_file; 263 Path config_file; 264 Path log_file; 265 CustomHeader custom_hdr; 266 267 MainName main_name; 268 MainNs main_ns; 269 MainInterface main_if; 270 Flag!"Gmock" gmock; 271 Flag!"PreInclude" pre_incl; 272 Flag!"PostInclude" post_incl; 273 Flag!"locationAsComment" loc_as_comment; 274 Flag!"generateZeroGlobals" generate_zero_globals; 275 276 Nullable!XmlConfig xmlConfig; 277 CompileCommandFilter compiler_flag_filter; 278 FilterSymbol restrict_symbols; 279 FilterSymbol exclude_symbols; 280 281 Regex!char[] exclude; 282 Regex!char[] restrict; 283 284 /// Data produced by the generatore intented to be written to specified file. 285 FileData[] file_data; 286 287 TestDoubleIncludes td_includes; 288 } 289 290 static auto makeVariant(ref RawConfiguration args) { 291 // dfmt off 292 auto variant = new CTestDoubleVariant( 293 MainFileName(args.mainFileName), Path(args.out_), 294 regex(args.stripInclude)) 295 .argPrefix(args.prefix) 296 .argMainName(args.mainName) 297 .argLocationAsComment(args.locationAsComment) 298 .argGmock(args.gmock) 299 .argPreInclude(args.generatePreInclude) 300 .argPostInclude(args.genPostInclude) 301 .argForceTestDoubleIncludes(args.testDoubleInclude) 302 .argFileExclude(args.fileExclude) 303 .argFileRestrict(args.fileRestrict) 304 .argCustomHeader(args.header, args.headerFile) 305 .argGenerateZeroGlobals(args.generateZeroGlobals) 306 .argXmlConfig(args.xmlConfig); 307 // dfmt on 308 309 return variant; 310 } 311 312 /** Design of c'tor. 313 * 314 * The c'tor has as paramters all the required configuration data. 315 * Assignment of members are used for optional configuration. 316 * 317 * Follows the design pattern "correct by construction". 318 * 319 * TODO document the parameters. 320 */ 321 this(MainFileName main_fname, Path output_dir, Regex!char strip_incl) { 322 this.output_dir = output_dir; 323 this.td_includes = TestDoubleIncludes(strip_incl); 324 325 import std.path : baseName, buildPath, stripExtension; 326 327 string base_filename = cast(string) main_fname; 328 329 this.main_file_hdr = Path(buildPath(cast(string) output_dir, base_filename ~ hdrExt)); 330 this.main_file_impl = Path(buildPath(cast(string) output_dir, base_filename ~ implExt)); 331 this.main_file_globals = Path(buildPath(cast(string) output_dir, 332 base_filename ~ "_global" ~ implExt)); 333 this.gmock_file = Path(buildPath(cast(string) output_dir, base_filename ~ "_gmock" ~ hdrExt)); 334 this.pre_incl_file = Path(buildPath(cast(string) output_dir, 335 base_filename ~ "_pre_includes" ~ hdrExt)); 336 this.post_incl_file = Path(buildPath(cast(string) output_dir, 337 base_filename ~ "_post_includes" ~ hdrExt)); 338 this.config_file = Path(buildPath(output_dir, base_filename ~ "_config" ~ xmlExt)); 339 this.log_file = Path(buildPath(output_dir, base_filename ~ "_log" ~ xmlExt)); 340 } 341 342 auto argFileExclude(string[] a) { 343 import std.array : array; 344 import std.algorithm : map; 345 346 this.exclude = a.map!(a => regex(a)).array(); 347 return this; 348 } 349 350 auto argFileRestrict(string[] a) { 351 import std.array : array; 352 import std.algorithm : map; 353 354 this.restrict = a.map!(a => regex(a)).array(); 355 return this; 356 } 357 358 auto argPrefix(string s) { 359 this.prefix = StubPrefix(s); 360 return this; 361 } 362 363 auto argMainName(string s) { 364 this.main_name = MainName(s); 365 this.main_ns = MainNs(s); 366 this.main_if = MainInterface("I_" ~ s); 367 return this; 368 } 369 370 /// Force the includes to be those supplied by the user. 371 auto argForceTestDoubleIncludes(string[] a) { 372 if (a.length != 0) { 373 td_includes.forceIncludes(a); 374 } 375 return this; 376 } 377 378 auto argCustomHeader(string header, string header_file) { 379 if (header.length != 0) { 380 this.custom_hdr = CustomHeader(header); 381 } else if (header_file.length != 0) { 382 import std.file : readText; 383 384 string content = readText(header_file); 385 this.custom_hdr = CustomHeader(content); 386 } 387 388 return this; 389 } 390 391 auto argGmock(bool a) { 392 this.gmock = cast(Flag!"Gmock") a; 393 return this; 394 } 395 396 auto argPreInclude(bool a) { 397 this.pre_incl = cast(Flag!"PreInclude") a; 398 return this; 399 } 400 401 auto argPostInclude(bool a) { 402 this.post_incl = cast(Flag!"PostInclude") a; 403 return this; 404 } 405 406 auto argLocationAsComment(bool a) { 407 this.loc_as_comment = cast(Flag!"locationAsComment") a; 408 return this; 409 } 410 411 auto argGenerateZeroGlobals(bool value) { 412 this.generate_zero_globals = cast(Flag!"generateZeroGlobals") value; 413 return this; 414 } 415 416 /** Ensure that the relevant information from the xml file is extracted. 417 * 418 * May overwrite information from the command line. 419 * TODO or should the command line have priority over the xml file? 420 */ 421 auto argXmlConfig(Nullable!XmlConfig conf) { 422 import dextool.compilation_db : defaultCompilerFlagFilter; 423 424 if (conf.isNull) { 425 compiler_flag_filter = CompileCommandFilter(defaultCompilerFlagFilter, 1); 426 return this; 427 } 428 429 xmlConfig = conf; 430 compiler_flag_filter = CompileCommandFilter(conf.get.filterClangFlags, 431 conf.get.skipCompilerArgs); 432 restrict_symbols = conf.get.restrictSymbols; 433 exclude_symbols = conf.get.excludeSymbols; 434 435 return this; 436 } 437 438 void processIncludes() { 439 td_includes.process(); 440 } 441 442 void finalizeIncludes() { 443 td_includes.finalize(); 444 } 445 446 /// Destination of the configuration file containing how the test double was generated. 447 Path getXmlConfigFile() { 448 return config_file; 449 } 450 451 /** Destination of the xml log for how dextool was ran when generatinng the 452 * test double. 453 */ 454 Path getXmlLog() { 455 return log_file; 456 } 457 458 ref FilterSymbol getRestrictSymbols() { 459 return restrict_symbols; 460 } 461 462 ref FilterSymbol getExcludeSymbols() { 463 return exclude_symbols; 464 } 465 466 ref CompileCommandFilter getCompileCommandFilter() { 467 return compiler_flag_filter; 468 } 469 470 /// Data produced by the generatore intented to be written to specified file. 471 ref FileData[] getProducedFiles() { 472 return file_data; 473 } 474 475 void putFile(Path fname, string data) { 476 file_data ~= FileData(fname, data); 477 } 478 479 // -- Controller -- 480 481 bool doFile(in string filename, in string info) { 482 import dextool.plugin.regex_matchers : matchAny; 483 484 bool restrict_pass = true; 485 bool exclude_pass = true; 486 487 if (restrict.length > 0) { 488 restrict_pass = matchAny(filename, restrict); 489 debug { 490 logger.tracef(!restrict_pass, "--file-restrict skipping %s", info); 491 } 492 } 493 494 if (exclude.length > 0) { 495 exclude_pass = !matchAny(filename, exclude); 496 debug { 497 logger.tracef(!exclude_pass, "--file-exclude skipping %s", info); 498 } 499 } 500 501 return restrict_pass && exclude_pass; 502 } 503 504 bool doSymbol(string symbol) { 505 // fast path, assuming no symbol filter is the most common 506 if (!restrict_symbols.hasSymbols && !exclude_symbols.hasSymbols) { 507 return true; 508 } 509 510 if (restrict_symbols.hasSymbols && exclude_symbols.hasSymbols) { 511 return restrict_symbols.contains(symbol) && !exclude_symbols.contains(symbol); 512 } 513 514 if (restrict_symbols.hasSymbols) { 515 return restrict_symbols.contains(symbol); 516 } 517 518 if (exclude_symbols.hasSymbols) { 519 return !exclude_symbols.contains(symbol); 520 } 521 522 return true; 523 } 524 525 bool doGoogleMock() { 526 return gmock; 527 } 528 529 bool doPreIncludes() { 530 import std.file : exists; 531 532 return pre_incl && !exists(cast(string) pre_incl_file); 533 } 534 535 bool doIncludeOfPreIncludes() { 536 return pre_incl; 537 } 538 539 bool doPostIncludes() { 540 import std.file : exists; 541 542 return post_incl && !exists(cast(string) post_incl_file); 543 } 544 545 bool doIncludeOfPostIncludes() { 546 return post_incl; 547 } 548 549 bool doLocationAsComment() { 550 return loc_as_comment; 551 } 552 553 // -- Parameters -- 554 555 Path[] getIncludes() { 556 import std.algorithm : map; 557 import std.array : array; 558 559 return td_includes.includes.map!(a => Path(a)).array(); 560 } 561 562 Path getOutputDirectory() { 563 return output_dir; 564 } 565 566 Parameters.Files getFiles() { 567 return Parameters.Files(main_file_hdr, main_file_impl, 568 main_file_globals, gmock_file, pre_incl_file, post_incl_file); 569 } 570 571 MainName getMainName() { 572 return main_name; 573 } 574 575 MainNs getMainNs() { 576 return main_ns; 577 } 578 579 MainInterface getMainInterface() { 580 return main_if; 581 } 582 583 StubPrefix getFilePrefix() { 584 return StubPrefix(""); 585 } 586 587 StubPrefix getArtifactPrefix() { 588 return prefix; 589 } 590 591 DextoolVersion getToolVersion() { 592 import dextool.utility : dextoolVersion; 593 594 return dextoolVersion; 595 } 596 597 CustomHeader getCustomHeader() { 598 return custom_hdr; 599 } 600 601 Flag!"generateZeroGlobals" generateZeroGlobals() { 602 return generate_zero_globals; 603 } 604 605 // -- Products -- 606 607 void putFile(Path fname, CppHModule hdr_data) { 608 file_data ~= FileData(fname, hdr_data.render()); 609 } 610 611 void putFile(Path fname, CppModule impl_data) { 612 file_data ~= FileData(fname, impl_data.render()); 613 } 614 615 void putLocation(Path fname, LocationType type) { 616 td_includes.put(fname, type); 617 } 618 } 619 620 /// TODO refactor, doing too many things. 621 ExitStatusType genCstub(CTestDoubleVariant variant, in string[] in_cflags, 622 CompileCommandDB compile_db, Path[] in_files) { 623 import std.typecons : Yes; 624 625 import dextool.clang : findFlags; 626 import dextool.compilation_db : ParseData = SearchResult; 627 import cpptooling.analyzer.clang.context : ClangContext; 628 import dextool.io : writeFileData; 629 import dextool.plugin.ctestdouble.backend.cvariant : CVisitor, Generator; 630 import dextool.utility : prependDefaultFlags, PreferLang, analyzeFile; 631 632 const user_cflags = prependDefaultFlags(in_cflags, PreferLang.c); 633 const total_files = in_files.length; 634 auto visitor = new CVisitor(variant, variant); 635 auto ctx = ClangContext(Yes.useInternalHeaders, Yes.prependParamSyntaxOnly); 636 auto generator = Generator(variant, variant, variant); 637 638 foreach (idx, in_file; in_files) { 639 logger.infof("File %d/%d ", idx + 1, total_files); 640 ParseData pdata; 641 642 // TODO duplicate code in c, c++ and plantuml. Fix it. 643 if (compile_db.length > 0) { 644 auto tmp = compile_db.findFlags(Path(in_file), user_cflags, 645 variant.getCompileCommandFilter); 646 if (tmp.isNull) { 647 return ExitStatusType.Errors; 648 } 649 pdata = tmp.get; 650 } else { 651 pdata.flags.prependCflags(user_cflags.dup); 652 pdata.absoluteFile = AbsolutePath(Path(in_file)); 653 } 654 655 if (analyzeFile(pdata.absoluteFile, pdata.cflags, visitor, ctx) == ExitStatusType.Errors) { 656 return ExitStatusType.Errors; 657 } 658 659 generator.aggregate(visitor.root, visitor.container); 660 visitor.clearRoot; 661 variant.processIncludes; 662 } 663 664 variant.finalizeIncludes; 665 666 // Analyse and generate test double 667 generator.process(visitor.container); 668 669 debug { 670 logger.trace(visitor); 671 } 672 673 return writeFileData(variant.getProducedFiles); 674 }