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