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 logger = std.experimental.logger; 13 import std.algorithm : find, map; 14 import std.array : array, empty; 15 import std.range : drop; 16 import std.typecons : Nullable; 17 18 import cpptooling.type; 19 import dextool.compilation_db; 20 import dextool.type; 21 22 import dextool.plugin.types; 23 import dextool.plugin.ctestdouble.backend.cvariant : Controller, Parameters, Products; 24 import dextool.plugin.ctestdouble.frontend.types; 25 import dextool.plugin.ctestdouble.frontend.xml; 26 27 // workaround for ldc-1.1.0 and dmd-2.071.2 28 auto workaround_linker_error() { 29 import cpptooling.testdouble.header_filter : TestDoubleIncludes, 30 GenericTestDoubleIncludes, DummyPayload; 31 32 return typeid(GenericTestDoubleIncludes!DummyPayload).toString(); 33 } 34 35 struct RawConfiguration { 36 Nullable!XmlConfig xmlConfig; 37 38 string[] fileExclude; 39 string[] fileRestrict; 40 string[] testDoubleInclude; 41 Path[] inFiles; 42 string[] cflags; 43 string[] compileDb; 44 string header; 45 string headerFile; 46 string mainName = "TestDouble"; 47 string mainFileName = "test_double"; 48 string prefix = "Test_"; 49 string stripInclude; 50 string out_; 51 string config; 52 string systemCompiler = "/usr/bin/cc"; 53 bool help; 54 bool shortPluginHelp; 55 bool gmock; 56 bool generatePreInclude; 57 bool genPostInclude; 58 bool locationAsComment; 59 bool generateZeroGlobals; 60 bool invalidXmlConfig; 61 62 string[] originalFlags; 63 64 void parse(string[] args) { 65 import std.getopt; 66 67 originalFlags = args.dup; 68 string[] input; 69 70 try { 71 bool no_zero_globals; 72 // dfmt off 73 getopt(args, std.getopt.config.keepEndOfOptions, "h|help", &help, 74 "compile-db", &compileDb, 75 "config", &config, 76 "file-exclude", &fileExclude, 77 "file-restrict", &fileRestrict, 78 "gen-post-incl", &genPostInclude, 79 "gen-pre-incl", &generatePreInclude, 80 "gmock", &gmock, 81 "header", &header, 82 "header-file", &headerFile, 83 "in", &input, 84 "loc-as-comment", &locationAsComment, 85 "main", &mainName, 86 "main-fname", &mainFileName, 87 "no-zeroglobals", &no_zero_globals, 88 "out", &out_, 89 "prefix", &prefix, 90 "short-plugin-help", &shortPluginHelp, 91 "strip-incl", &stripInclude, 92 "system-compiler", "Derive the system include paths from this compiler [default /usr/bin/cc]", &systemCompiler, 93 "td-include", &testDoubleInclude); 94 // dfmt on 95 generateZeroGlobals = !no_zero_globals; 96 } catch (std.getopt.GetOptException ex) { 97 logger.error(ex.msg); 98 help = true; 99 } 100 101 // default arguments 102 if (stripInclude.length == 0) { 103 stripInclude = r".*/(.*)"; 104 logger.trace("--strip-incl: using default regex to strip include path (basename)"); 105 } 106 107 if (config.length != 0) { 108 xmlConfig = readRawConfig(Path(config)); 109 if (xmlConfig.isNull) { 110 invalidXmlConfig = true; 111 } 112 } 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 string system_compiler; 277 278 Nullable!XmlConfig xmlConfig; 279 CompileCommandFilter compiler_flag_filter; 280 FilterSymbol restrict_symbols; 281 FilterSymbol exclude_symbols; 282 283 Regex!char[] exclude; 284 Regex!char[] restrict; 285 286 /// Data produced by the generatore intented to be written to specified file. 287 FileData[] file_data; 288 289 TestDoubleIncludes td_includes; 290 } 291 292 static auto makeVariant(ref RawConfiguration args) { 293 // dfmt off 294 auto variant = new CTestDoubleVariant( 295 MainFileName(args.mainFileName), Path(args.out_), 296 regex(args.stripInclude)) 297 .argPrefix(args.prefix) 298 .argMainName(args.mainName) 299 .argLocationAsComment(args.locationAsComment) 300 .argGmock(args.gmock) 301 .argPreInclude(args.generatePreInclude) 302 .argPostInclude(args.genPostInclude) 303 .argForceTestDoubleIncludes(args.testDoubleInclude) 304 .argFileExclude(args.fileExclude) 305 .argFileRestrict(args.fileRestrict) 306 .argCustomHeader(args.header, args.headerFile) 307 .argGenerateZeroGlobals(args.generateZeroGlobals) 308 .argXmlConfig(args.xmlConfig) 309 .systemCompiler(args.systemCompiler); 310 // dfmt on 311 312 return variant; 313 } 314 315 /** Design of c'tor. 316 * 317 * The c'tor has as paramters all the required configuration data. 318 * Assignment of members are used for optional configuration. 319 * 320 * Follows the design pattern "correct by construction". 321 * 322 * TODO document the parameters. 323 */ 324 this(MainFileName main_fname, Path output_dir, Regex!char strip_incl) { 325 this.output_dir = output_dir; 326 this.td_includes = TestDoubleIncludes(strip_incl); 327 328 import std.path : baseName, buildPath, stripExtension; 329 330 string base_filename = cast(string) main_fname; 331 332 this.main_file_hdr = Path(buildPath(cast(string) output_dir, base_filename ~ hdrExt)); 333 this.main_file_impl = Path(buildPath(cast(string) output_dir, base_filename ~ implExt)); 334 this.main_file_globals = Path(buildPath(cast(string) output_dir, 335 base_filename ~ "_global" ~ implExt)); 336 this.gmock_file = Path(buildPath(cast(string) output_dir, base_filename ~ "_gmock" ~ hdrExt)); 337 this.pre_incl_file = Path(buildPath(cast(string) output_dir, 338 base_filename ~ "_pre_includes" ~ hdrExt)); 339 this.post_incl_file = Path(buildPath(cast(string) output_dir, 340 base_filename ~ "_post_includes" ~ hdrExt)); 341 this.config_file = Path(buildPath(output_dir, base_filename ~ "_config" ~ xmlExt)); 342 this.log_file = Path(buildPath(output_dir, base_filename ~ "_log" ~ xmlExt)); 343 } 344 345 auto argFileExclude(string[] a) { 346 this.exclude = a.map!(a => regex(a)).array(); 347 return this; 348 } 349 350 auto argFileRestrict(string[] a) { 351 this.restrict = a.map!(a => regex(a)).array(); 352 return this; 353 } 354 355 auto argPrefix(string s) { 356 this.prefix = StubPrefix(s); 357 return this; 358 } 359 360 auto argMainName(string s) { 361 this.main_name = MainName(s); 362 this.main_ns = MainNs(s); 363 this.main_if = MainInterface("I_" ~ s); 364 return this; 365 } 366 367 /// Force the includes to be those supplied by the user. 368 auto argForceTestDoubleIncludes(string[] a) { 369 if (a.length != 0) { 370 td_includes.forceIncludes(a); 371 } 372 return this; 373 } 374 375 auto argCustomHeader(string header, string header_file) { 376 if (header.length != 0) { 377 this.custom_hdr = CustomHeader(header); 378 } else if (header_file.length != 0) { 379 import std.file : readText; 380 381 string content = readText(header_file); 382 this.custom_hdr = CustomHeader(content); 383 } 384 385 return this; 386 } 387 388 auto argGmock(bool a) { 389 this.gmock = cast(Flag!"Gmock") a; 390 return this; 391 } 392 393 auto argPreInclude(bool a) { 394 this.pre_incl = cast(Flag!"PreInclude") a; 395 return this; 396 } 397 398 auto argPostInclude(bool a) { 399 this.post_incl = cast(Flag!"PostInclude") a; 400 return this; 401 } 402 403 auto argLocationAsComment(bool a) { 404 this.loc_as_comment = cast(Flag!"locationAsComment") a; 405 return this; 406 } 407 408 auto argGenerateZeroGlobals(bool value) { 409 this.generate_zero_globals = cast(Flag!"generateZeroGlobals") value; 410 return this; 411 } 412 413 /** Ensure that the relevant information from the xml file is extracted. 414 * 415 * May overwrite information from the command line. 416 * TODO or should the command line have priority over the xml file? 417 */ 418 auto argXmlConfig(Nullable!XmlConfig conf) { 419 import dextool.compilation_db : defaultCompilerFlagFilter; 420 421 if (conf.isNull) { 422 compiler_flag_filter = CompileCommandFilter(defaultCompilerFlagFilter, 0); 423 return this; 424 } 425 426 xmlConfig = conf; 427 compiler_flag_filter = CompileCommandFilter(conf.get.filterClangFlags, 428 conf.get.skipCompilerArgs); 429 restrict_symbols = conf.get.restrictSymbols; 430 exclude_symbols = conf.get.excludeSymbols; 431 432 return this; 433 } 434 435 void processIncludes() { 436 td_includes.process(); 437 } 438 439 void finalizeIncludes() { 440 td_includes.finalize(); 441 } 442 443 /// Destination of the configuration file containing how the test double was generated. 444 Path getXmlConfigFile() { 445 return config_file; 446 } 447 448 /** Destination of the xml log for how dextool was ran when generatinng the 449 * test double. 450 */ 451 Path getXmlLog() { 452 return log_file; 453 } 454 455 ref FilterSymbol getRestrictSymbols() { 456 return restrict_symbols; 457 } 458 459 ref FilterSymbol getExcludeSymbols() { 460 return exclude_symbols; 461 } 462 463 ref CompileCommandFilter getCompileCommandFilter() { 464 return compiler_flag_filter; 465 } 466 467 /// Data produced by the generatore intented to be written to specified file. 468 ref FileData[] getProducedFiles() { 469 return file_data; 470 } 471 472 void putFile(Path fname, string data) { 473 file_data ~= FileData(fname, data); 474 } 475 476 auto systemCompiler(string a) { 477 this.system_compiler = a; 478 return this; 479 } 480 481 // -- Controller -- 482 483 bool doFile(in string filename, in string info) { 484 import dextool.plugin.regex_matchers : matchAny; 485 486 bool restrict_pass = true; 487 bool exclude_pass = true; 488 489 if (restrict.length > 0) { 490 restrict_pass = matchAny(filename, restrict); 491 debug { 492 logger.tracef(!restrict_pass, "--file-restrict skipping %s", info); 493 } 494 } 495 496 if (exclude.length > 0) { 497 exclude_pass = !matchAny(filename, exclude); 498 debug { 499 logger.tracef(!exclude_pass, "--file-exclude skipping %s", info); 500 } 501 } 502 503 return restrict_pass && exclude_pass; 504 } 505 506 bool doSymbol(string symbol) { 507 // fast path, assuming no symbol filter is the most common 508 if (!restrict_symbols.hasSymbols && !exclude_symbols.hasSymbols) { 509 return true; 510 } 511 512 if (restrict_symbols.hasSymbols && exclude_symbols.hasSymbols) { 513 return restrict_symbols.contains(symbol) && !exclude_symbols.contains(symbol); 514 } 515 516 if (restrict_symbols.hasSymbols) { 517 return restrict_symbols.contains(symbol); 518 } 519 520 if (exclude_symbols.hasSymbols) { 521 return !exclude_symbols.contains(symbol); 522 } 523 524 return true; 525 } 526 527 bool doGoogleMock() { 528 return gmock; 529 } 530 531 bool doPreIncludes() { 532 import std.file : exists; 533 534 return pre_incl && !exists(cast(string) pre_incl_file); 535 } 536 537 bool doIncludeOfPreIncludes() { 538 return pre_incl; 539 } 540 541 bool doPostIncludes() { 542 import std.file : exists; 543 544 return post_incl && !exists(cast(string) post_incl_file); 545 } 546 547 bool doIncludeOfPostIncludes() { 548 return post_incl; 549 } 550 551 bool doLocationAsComment() { 552 return loc_as_comment; 553 } 554 555 // -- Parameters -- 556 557 Path[] getIncludes() { 558 return td_includes.includes.map!(a => Path(a)).array(); 559 } 560 561 Path getOutputDirectory() { 562 return output_dir; 563 } 564 565 Parameters.Files getFiles() { 566 return Parameters.Files(main_file_hdr, main_file_impl, 567 main_file_globals, gmock_file, pre_incl_file, post_incl_file); 568 } 569 570 MainName getMainName() { 571 return main_name; 572 } 573 574 MainNs getMainNs() { 575 return main_ns; 576 } 577 578 MainInterface getMainInterface() { 579 return main_if; 580 } 581 582 StubPrefix getFilePrefix() { 583 return StubPrefix(""); 584 } 585 586 StubPrefix getArtifactPrefix() { 587 return prefix; 588 } 589 590 DextoolVersion getToolVersion() { 591 import dextool.utility : dextoolVersion; 592 593 return dextoolVersion; 594 } 595 596 CustomHeader getCustomHeader() { 597 return custom_hdr; 598 } 599 600 Flag!"generateZeroGlobals" generateZeroGlobals() { 601 return generate_zero_globals; 602 } 603 604 Compiler getSystemCompiler() const { 605 return Compiler(system_compiler); 606 } 607 608 Compiler getMissingFileCompiler() const { 609 if (system_compiler.empty) 610 return Compiler("/usr/bin/cc"); 611 return getSystemCompiler(); 612 } 613 614 // -- Products -- 615 616 void putFile(Path fname, CppHModule hdr_data) { 617 file_data ~= FileData(fname, hdr_data.render()); 618 } 619 620 void putFile(Path fname, CppModule impl_data) { 621 file_data ~= FileData(fname, impl_data.render()); 622 } 623 624 void putLocation(Path fname, LocationType type) { 625 td_includes.put(fname, type); 626 } 627 } 628 629 /// TODO refactor, doing too many things. 630 ExitStatusType genCstub(CTestDoubleVariant variant, string[] userCflags, 631 CompileCommandDB compile_db, Path[] inFiles) { 632 import std.typecons : Yes; 633 634 import cpptooling.analyzer.clang.context : ClangContext; 635 import dextool.clang : reduceMissingFiles; 636 import dextool.compilation_db : limitOrAllRange, parse, prependFlags, 637 addCompiler, replaceCompiler, addSystemIncludes, fileRange; 638 import dextool.io : writeFileData; 639 import dextool.plugin.ctestdouble.backend.cvariant : CVisitor, Generator; 640 import dextool.utility : prependDefaultFlags, PreferLang, analyzeFile; 641 642 auto visitor = new CVisitor(variant, variant); 643 auto ctx = ClangContext(Yes.useInternalHeaders, Yes.prependParamSyntaxOnly); 644 auto generator = Generator(variant, variant, variant); 645 646 auto compDbRange() { 647 if (compile_db.empty) { 648 return fileRange(inFiles, variant.getMissingFileCompiler); 649 } 650 return compile_db.fileRange; 651 } 652 653 auto fixedDb = compDbRange.parse(variant.getCompileCommandFilter) 654 .addCompiler(variant.getMissingFileCompiler).replaceCompiler( 655 variant.getSystemCompiler).addSystemIncludes.prependFlags( 656 prependDefaultFlags(userCflags, PreferLang.c)).array; 657 658 auto limitRange = limitOrAllRange(fixedDb, inFiles.map!(a => cast(string) a).array) 659 .reduceMissingFiles(fixedDb); 660 661 if (!compile_db.empty && !limitRange.isMissingFilesEmpty) { 662 foreach (a; limitRange.missingFiles) { 663 logger.error("Unable to find any compiler flags for .", a); 664 } 665 return ExitStatusType.Errors; 666 } 667 668 foreach (pdata; limitRange.range) { 669 if (analyzeFile(pdata.cmd.absoluteFile, pdata.flags.completeFlags, 670 visitor, ctx) == ExitStatusType.Errors) { 671 return ExitStatusType.Errors; 672 } 673 674 generator.aggregate(visitor.root, visitor.container); 675 visitor.clearRoot; 676 variant.processIncludes; 677 } 678 679 variant.finalizeIncludes; 680 681 // Analyse and generate test double 682 generator.process(visitor.container); 683 684 debug { 685 logger.trace(visitor); 686 } 687 688 return writeFileData(variant.getProducedFiles); 689 }