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 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 string[] exclude; 284 string[] include; 285 ReFilter fileFilter; 286 287 /// Data produced by the generatore intented to be written to specified file. 288 FileData[] file_data; 289 290 TestDoubleIncludes td_includes; 291 } 292 293 static auto makeVariant(ref RawConfiguration args) { 294 // dfmt off 295 auto variant = new CTestDoubleVariant( 296 MainFileName(args.mainFileName), Path(args.out_), 297 regex(args.stripInclude)) 298 .argPrefix(args.prefix) 299 .argMainName(args.mainName) 300 .argLocationAsComment(args.locationAsComment) 301 .argGmock(args.gmock) 302 .argPreInclude(args.generatePreInclude) 303 .argPostInclude(args.genPostInclude) 304 .argForceTestDoubleIncludes(args.testDoubleInclude) 305 .argFileExclude(args.fileExclude) 306 .argFileInclude(args.fileInclude) 307 .argCustomHeader(args.header, args.headerFile) 308 .argGenerateZeroGlobals(args.generateZeroGlobals) 309 .argXmlConfig(args.xmlConfig) 310 .systemCompiler(args.systemCompiler); 311 // dfmt on 312 313 return variant; 314 } 315 316 /** Design of c'tor. 317 * 318 * The c'tor has as paramters all the required configuration data. 319 * Assignment of members are used for optional configuration. 320 * 321 * Follows the design pattern "correct by construction". 322 * 323 * TODO document the parameters. 324 */ 325 this(MainFileName main_fname, Path output_dir, Regex!char strip_incl) { 326 this.output_dir = output_dir; 327 this.td_includes = TestDoubleIncludes(strip_incl); 328 329 import std.path : baseName, buildPath, stripExtension; 330 331 string base_filename = cast(string) main_fname; 332 333 this.main_file_hdr = Path(buildPath(cast(string) output_dir, base_filename ~ hdrExt)); 334 this.main_file_impl = Path(buildPath(cast(string) output_dir, base_filename ~ implExt)); 335 this.main_file_globals = Path(buildPath(cast(string) output_dir, 336 base_filename ~ "_global" ~ implExt)); 337 this.gmock_file = Path(buildPath(cast(string) output_dir, base_filename ~ "_gmock" ~ hdrExt)); 338 this.pre_incl_file = Path(buildPath(cast(string) output_dir, 339 base_filename ~ "_pre_includes" ~ hdrExt)); 340 this.post_incl_file = Path(buildPath(cast(string) output_dir, 341 base_filename ~ "_post_includes" ~ hdrExt)); 342 this.config_file = Path(buildPath(output_dir, base_filename ~ "_config" ~ xmlExt)); 343 this.log_file = Path(buildPath(output_dir, base_filename ~ "_log" ~ xmlExt)); 344 } 345 346 auto argFileExclude(string[] a) { 347 this.exclude = a; 348 fileFilter = ReFilter(include, exclude); 349 return this; 350 } 351 352 auto argFileInclude(string[] a) { 353 this.include = a; 354 fileFilter = ReFilter(include, exclude); 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, 0); 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 auto systemCompiler(string a) { 480 this.system_compiler = a; 481 return this; 482 } 483 484 // -- Controller -- 485 486 bool doFile(in string filename, in string info) { 487 return fileFilter.match(filename, (string s, string type) { 488 logger.tracef("matcher --file-%s removed %s. Skipping", s, type); 489 }); 490 } 491 492 bool doSymbol(string symbol) { 493 // fast path, assuming no symbol filter is the most common 494 if (!restrict_symbols.hasSymbols && !exclude_symbols.hasSymbols) { 495 return true; 496 } 497 498 if (restrict_symbols.hasSymbols && exclude_symbols.hasSymbols) { 499 return restrict_symbols.contains(symbol) && !exclude_symbols.contains(symbol); 500 } 501 502 if (restrict_symbols.hasSymbols) { 503 return restrict_symbols.contains(symbol); 504 } 505 506 if (exclude_symbols.hasSymbols) { 507 return !exclude_symbols.contains(symbol); 508 } 509 510 return true; 511 } 512 513 bool doGoogleMock() { 514 return gmock; 515 } 516 517 bool doPreIncludes() { 518 import std.file : exists; 519 520 return pre_incl && !exists(cast(string) pre_incl_file); 521 } 522 523 bool doIncludeOfPreIncludes() { 524 return pre_incl; 525 } 526 527 bool doPostIncludes() { 528 import std.file : exists; 529 530 return post_incl && !exists(cast(string) post_incl_file); 531 } 532 533 bool doIncludeOfPostIncludes() { 534 return post_incl; 535 } 536 537 bool doLocationAsComment() { 538 return loc_as_comment; 539 } 540 541 // -- Parameters -- 542 543 Path[] getIncludes() { 544 return td_includes.includes.map!(a => Path(a)).array(); 545 } 546 547 Path getOutputDirectory() { 548 return output_dir; 549 } 550 551 Parameters.Files getFiles() { 552 return Parameters.Files(main_file_hdr, main_file_impl, 553 main_file_globals, gmock_file, pre_incl_file, post_incl_file); 554 } 555 556 MainName getMainName() { 557 return main_name; 558 } 559 560 MainNs getMainNs() { 561 return main_ns; 562 } 563 564 MainInterface getMainInterface() { 565 return main_if; 566 } 567 568 StubPrefix getFilePrefix() { 569 return StubPrefix(""); 570 } 571 572 StubPrefix getArtifactPrefix() { 573 return prefix; 574 } 575 576 DextoolVersion getToolVersion() { 577 import dextool.utility : dextoolVersion; 578 579 return dextoolVersion; 580 } 581 582 CustomHeader getCustomHeader() { 583 return custom_hdr; 584 } 585 586 Flag!"generateZeroGlobals" generateZeroGlobals() { 587 return generate_zero_globals; 588 } 589 590 Compiler getSystemCompiler() const { 591 return Compiler(system_compiler); 592 } 593 594 Compiler getMissingFileCompiler() const { 595 if (system_compiler.empty) 596 return Compiler("/usr/bin/cc"); 597 return getSystemCompiler(); 598 } 599 600 // -- Products -- 601 602 void putFile(Path fname, CppHModule hdr_data) { 603 file_data ~= FileData(fname, hdr_data.render()); 604 } 605 606 void putFile(Path fname, CppModule impl_data) { 607 file_data ~= FileData(fname, impl_data.render()); 608 } 609 610 void putLocation(Path fname, LocationType type) { 611 td_includes.put(fname, type); 612 } 613 } 614 615 /// TODO refactor, doing too many things. 616 ExitStatusType genCstub(CTestDoubleVariant variant, string[] userCflags, 617 CompileCommandDB compile_db, Path[] inFiles) { 618 import std.typecons : Yes; 619 620 import libclang_ast.context : ClangContext; 621 import dextool.clang : reduceMissingFiles; 622 import dextool.compilation_db : limitOrAllRange, parse, prependFlags, 623 addCompiler, replaceCompiler, addSystemIncludes, fileRange; 624 import dextool.io : writeFileData; 625 import dextool.plugin.ctestdouble.backend.cvariant : CVisitor, Generator; 626 import dextool.utility : prependDefaultFlags, PreferLang, analyzeFile; 627 628 auto visitor = new CVisitor(variant, variant); 629 auto ctx = ClangContext(Yes.useInternalHeaders, Yes.prependParamSyntaxOnly); 630 auto generator = Generator(variant, variant, variant); 631 632 auto compDbRange() { 633 if (compile_db.empty) { 634 return fileRange(inFiles, variant.getMissingFileCompiler); 635 } 636 return compile_db.fileRange; 637 } 638 639 auto fixedDb = compDbRange.parse(variant.getCompileCommandFilter) 640 .addCompiler(variant.getMissingFileCompiler).replaceCompiler( 641 variant.getSystemCompiler).addSystemIncludes.prependFlags( 642 prependDefaultFlags(userCflags, PreferLang.c)).array; 643 644 auto limitRange = limitOrAllRange(fixedDb, inFiles.map!(a => cast(string) a).array) 645 .reduceMissingFiles(fixedDb); 646 647 if (!compile_db.empty && !limitRange.isMissingFilesEmpty) { 648 foreach (a; limitRange.missingFiles) { 649 logger.error("Unable to find any compiler flags for ", a); 650 } 651 return ExitStatusType.Errors; 652 } 653 654 foreach (pdata; limitRange.range) { 655 if (analyzeFile(pdata.cmd.absoluteFile, pdata.flags.completeFlags, 656 visitor, ctx) == ExitStatusType.Errors) { 657 return ExitStatusType.Errors; 658 } 659 660 generator.aggregate(visitor.root, visitor.container); 661 visitor.clearRoot; 662 variant.processIncludes; 663 } 664 665 variant.finalizeIncludes; 666 667 // Analyse and generate test double 668 generator.process(visitor.container); 669 670 debug { 671 logger.trace(visitor); 672 } 673 674 return writeFileData(variant.getProducedFiles); 675 }