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 dextool.compilation_db; 17 import dextool.type; 18 19 import dextool.plugin.types; 20 import dextool.plugin.ctestdouble.backend.cvariant : Controller, Parameters, 21 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 string[] 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 66 try { 67 bool no_zero_globals; 68 // dfmt off 69 getopt(args, std.getopt.config.keepEndOfOptions, "h|help", &help, 70 "short-plugin-help", &shortPluginHelp, 71 "main", &mainName, 72 "main-fname", &mainFileName, 73 "out", &out_, 74 "compile-db", &compileDb, 75 "no-zeroglobals", &no_zero_globals, 76 "prefix", &prefix, 77 "strip-incl", &stripInclude, 78 "header", &header, 79 "header-file", &headerFile, 80 "gmock", &gmock, 81 "gen-pre-incl", &generatePreInclude, 82 "gen-post-incl", &genPostInclude, 83 "loc-as-comment", &locationAsComment, 84 "td-include", &testDoubleInclude, 85 "file-exclude", &fileExclude, 86 "file-restrict", &fileRestrict, 87 "in", &inFiles, 88 "config", &config); 89 // dfmt on 90 generateZeroGlobals = !no_zero_globals; 91 } 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(FileName(config)); 105 if (xmlConfig.isNull) { 106 invalidXmlConfig = true; 107 } 108 } 109 110 import std.algorithm : find; 111 import std.array : array; 112 import std.range : drop; 113 114 // at this point args contain "what is left". What is interesting then is those after "--". 115 cflags = args.find("--").drop(1).array(); 116 } 117 118 void printHelp() { 119 import std.stdio : writefln; 120 121 writefln("%s\n\n%s\n%s", ctestdouble_opt.usage, 122 ctestdouble_opt.optional, ctestdouble_opt.others); 123 } 124 125 void dump() { 126 // TODO remove this 127 logger.tracef("args: 128 --header :%s 129 --header-file :%s 130 --file-restrict :%s 131 --prefix :%s 132 --gmock :%s 133 --out :%s 134 --file-exclude :%s 135 --main :%s 136 --strip-incl :%s 137 --main-fname :%s 138 --in :%s 139 --compile-db :%s 140 --gen-post-incl :%s 141 --gen-pre-incl :%s 142 --help :%s 143 --loc-as-comment :%s 144 --td-include :%s 145 --no-zeroglobals :%s 146 --config :%s 147 CFLAGS :%s 148 149 xmlConfig :%s", header, headerFile, fileRestrict, prefix, gmock, 150 out_, fileExclude, mainName, stripInclude, 151 mainFileName, inFiles, compileDb, genPostInclude, generatePreInclude, help, locationAsComment, 152 testDoubleInclude, !generateZeroGlobals, config, cflags, xmlConfig); 153 } 154 } 155 156 // dfmt off 157 static auto ctestdouble_opt = CliOptionParts( 158 "usage: 159 dextool ctestdouble [options] [--in=] [-- CFLAGS]", 160 // ------------- 161 " --main=name Used as part of interface, namespace etc [default: TestDouble] 162 --main-fname=n Used as part of filename for generated files [default: test_double] 163 --prefix=p Prefix used when generating test artifacts [default: Test_] 164 --strip-incl=r A regexp used to strip the include paths 165 --gmock Generate a gmock implementation of test double interface 166 --gen-pre-incl Generate a pre include header file if it doesn't exist and use it 167 --gen-post-incl Generate a post include header file if it doesn't exist and use it 168 --loc-as-comment Generate a comment containing the location the symbol was derived from. 169 Makes it easier to correctly define excludes/restricts 170 --header=s Prepend generated files with the string 171 --header-file=f Prepend generated files with the header read from the file 172 --no-zeroglobals Turn off generation of the default implementation that zeroes globals 173 --config=path Use configuration file", 174 // ------------- 175 "others: 176 --in= Input file to parse 177 --out=dir directory for generated files [default: ./] 178 --compile-db= Retrieve compilation parameters from the file 179 --file-exclude= Exclude files from generation matching the regex 180 --file-restrict= Restrict the scope of the test double to those files 181 matching the regex 182 --td-include= User supplied includes used instead of those found 183 184 REGEX 185 The regex syntax is found at http://dlang.org/phobos/std_regex.html 186 187 Information about --strip-incl. 188 Default regexp is: .*/(.*) 189 190 To allow the user to selectively extract parts of the include path dextool 191 applies the regex and then concatenates all the matcher groups found. It is 192 turned into the replacement include path. 193 194 Important to remember then is that this approach requires that at least one 195 matcher group exists. 196 197 Information about --file-exclude. 198 The regex must fully match the filename the AST node is located in. 199 If it matches all data from the file is excluded from the generated code. 200 201 Information about --file-restrict. 202 The regex must fully match the filename the AST node is located in. 203 Only symbols from files matching the restrict affect the generated test double. 204 205 EXAMPLES 206 207 Generate a simple C test double. 208 dextool ctestdouble functions.h 209 210 Analyze and generate a test double for function prototypes and extern variables. 211 Both those found in functions.h and outside, aka via includes. 212 213 The test double is written to ./test_double.hpp/.cpp. 214 The name of the interface is Test_Double. 215 216 Generate a C test double excluding data from specified files. 217 dextool ctestdouble --file-exclude=/foo.h --file-exclude='functions.[h,c]' --out=outdata/ functions.h -- -DBAR -I/some/path 218 219 The code analyzer (Clang) will be passed the compiler flags -DBAR and -I/some/path. 220 During generation declarations found in foo.h or functions.h will be excluded. 221 222 The file holding the test double is written to directory outdata. 223 " 224 ); 225 // dfmt on 226 227 struct FileData { 228 import dextool.type : FileName, WriteStrategy; 229 230 FileName filename; 231 string data; 232 WriteStrategy strategy; 233 } 234 235 /** Test double generation of C code. 236 * 237 * TODO Describe the options. 238 */ 239 class CTestDoubleVariant : Controller, Parameters, Products { 240 import std.regex : regex, Regex; 241 import std.typecons : Flag; 242 import dextool.compilation_db : CompileCommandFilter; 243 import dextool.type : StubPrefix, FileName, DirName; 244 import cpptooling.testdouble.header_filter : TestDoubleIncludes, 245 LocationType; 246 import dsrcgen.cpp : CppModule, CppHModule; 247 248 private { 249 static const hdrExt = ".hpp"; 250 static const implExt = ".cpp"; 251 static const xmlExt = ".xml"; 252 253 StubPrefix prefix; 254 255 DirName output_dir; 256 FileName main_file_hdr; 257 FileName main_file_impl; 258 FileName main_file_globals; 259 FileName gmock_file; 260 FileName pre_incl_file; 261 FileName post_incl_file; 262 FileName config_file; 263 FileName log_file; 264 CustomHeader custom_hdr; 265 266 MainName main_name; 267 MainNs main_ns; 268 MainInterface main_if; 269 Flag!"Gmock" gmock; 270 Flag!"PreInclude" pre_incl; 271 Flag!"PostInclude" post_incl; 272 Flag!"locationAsComment" loc_as_comment; 273 Flag!"generateZeroGlobals" generate_zero_globals; 274 275 Nullable!XmlConfig xmlConfig; 276 CompileCommandFilter compiler_flag_filter; 277 FilterSymbol restrict_symbols; 278 FilterSymbol exclude_symbols; 279 280 Regex!char[] exclude; 281 Regex!char[] restrict; 282 283 /// Data produced by the generatore intented to be written to specified file. 284 FileData[] file_data; 285 286 TestDoubleIncludes td_includes; 287 } 288 289 static auto makeVariant(ref RawConfiguration args) { 290 // dfmt off 291 auto variant = new CTestDoubleVariant( 292 MainFileName(args.mainFileName), DirName(args.out_), 293 regex(args.stripInclude)) 294 .argPrefix(args.prefix) 295 .argMainName(args.mainName) 296 .argLocationAsComment(args.locationAsComment) 297 .argGmock(args.gmock) 298 .argPreInclude(args.generatePreInclude) 299 .argPostInclude(args.genPostInclude) 300 .argForceTestDoubleIncludes(args.testDoubleInclude) 301 .argFileExclude(args.fileExclude) 302 .argFileRestrict(args.fileRestrict) 303 .argCustomHeader(args.header, args.headerFile) 304 .argGenerateZeroGlobals(args.generateZeroGlobals) 305 .argXmlConfig(args.xmlConfig); 306 // dfmt on 307 308 return variant; 309 } 310 311 /** Design of c'tor. 312 * 313 * The c'tor has as paramters all the required configuration data. 314 * Assignment of members are used for optional configuration. 315 * 316 * Follows the design pattern "correct by construction". 317 * 318 * TODO document the parameters. 319 */ 320 this(MainFileName main_fname, DirName output_dir, Regex!char strip_incl) { 321 this.output_dir = output_dir; 322 this.td_includes = TestDoubleIncludes(strip_incl); 323 324 import std.path : baseName, buildPath, stripExtension; 325 326 string base_filename = cast(string) main_fname; 327 328 this.main_file_hdr = FileName(buildPath(cast(string) output_dir, base_filename ~ hdrExt)); 329 this.main_file_impl = FileName(buildPath(cast(string) output_dir, base_filename ~ implExt)); 330 this.main_file_globals = FileName(buildPath(cast(string) output_dir, 331 base_filename ~ "_global" ~ implExt)); 332 this.gmock_file = FileName(buildPath(cast(string) output_dir, 333 base_filename ~ "_gmock" ~ hdrExt)); 334 this.pre_incl_file = FileName(buildPath(cast(string) output_dir, 335 base_filename ~ "_pre_includes" ~ hdrExt)); 336 this.post_incl_file = FileName(buildPath(cast(string) output_dir, 337 base_filename ~ "_post_includes" ~ hdrExt)); 338 this.config_file = FileName(buildPath(output_dir, base_filename ~ "_config" ~ xmlExt)); 339 this.log_file = FileName(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.filterClangFlags, conf.skipCompilerArgs); 431 restrict_symbols = conf.restrictSymbols; 432 exclude_symbols = conf.excludeSymbols; 433 434 return this; 435 } 436 437 void processIncludes() { 438 td_includes.process(); 439 } 440 441 void finalizeIncludes() { 442 td_includes.finalize(); 443 } 444 445 /// Destination of the configuration file containing how the test double was generated. 446 FileName getXmlConfigFile() { 447 return config_file; 448 } 449 450 /** Destination of the xml log for how dextool was ran when generatinng the 451 * test double. 452 */ 453 FileName getXmlLog() { 454 return log_file; 455 } 456 457 ref FilterSymbol getRestrictSymbols() { 458 return restrict_symbols; 459 } 460 461 ref FilterSymbol getExcludeSymbols() { 462 return exclude_symbols; 463 } 464 465 ref CompileCommandFilter getCompileCommandFilter() { 466 return compiler_flag_filter; 467 } 468 469 /// Data produced by the generatore intented to be written to specified file. 470 ref FileData[] getProducedFiles() { 471 return file_data; 472 } 473 474 void putFile(FileName fname, string data) { 475 file_data ~= FileData(fname, data); 476 } 477 478 // -- Controller -- 479 480 bool doFile(in string filename, in string info) { 481 import dextool.plugin.regex_matchers : matchAny; 482 483 bool restrict_pass = true; 484 bool exclude_pass = true; 485 486 if (restrict.length > 0) { 487 restrict_pass = matchAny(filename, restrict); 488 debug { 489 logger.tracef(!restrict_pass, "--file-restrict skipping %s", info); 490 } 491 } 492 493 if (exclude.length > 0) { 494 exclude_pass = !matchAny(filename, exclude); 495 debug { 496 logger.tracef(!exclude_pass, "--file-exclude skipping %s", info); 497 } 498 } 499 500 return restrict_pass && exclude_pass; 501 } 502 503 bool doSymbol(string symbol) { 504 // fast path, assuming no symbol filter is the most common 505 if (!restrict_symbols.hasSymbols && !exclude_symbols.hasSymbols) { 506 return true; 507 } 508 509 if (restrict_symbols.hasSymbols && exclude_symbols.hasSymbols) { 510 return restrict_symbols.contains(symbol) && !exclude_symbols.contains(symbol); 511 } 512 513 if (restrict_symbols.hasSymbols) { 514 return restrict_symbols.contains(symbol); 515 } 516 517 if (exclude_symbols.hasSymbols) { 518 return !exclude_symbols.contains(symbol); 519 } 520 521 return true; 522 } 523 524 bool doGoogleMock() { 525 return gmock; 526 } 527 528 bool doPreIncludes() { 529 import std.file : exists; 530 531 return pre_incl && !exists(cast(string) pre_incl_file); 532 } 533 534 bool doIncludeOfPreIncludes() { 535 return pre_incl; 536 } 537 538 bool doPostIncludes() { 539 import std.file : exists; 540 541 return post_incl && !exists(cast(string) post_incl_file); 542 } 543 544 bool doIncludeOfPostIncludes() { 545 return post_incl; 546 } 547 548 bool doLocationAsComment() { 549 return loc_as_comment; 550 } 551 552 // -- Parameters -- 553 554 FileName[] getIncludes() { 555 import std.algorithm : map; 556 import std.array : array; 557 558 return td_includes.includes.map!(a => FileName(a)).array(); 559 } 560 561 DirName 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 // -- Products -- 605 606 void putFile(FileName fname, CppHModule hdr_data) { 607 file_data ~= FileData(fname, hdr_data.render()); 608 } 609 610 void putFile(FileName fname, CppModule impl_data) { 611 file_data ~= FileData(fname, impl_data.render()); 612 } 613 614 void putLocation(FileName fname, LocationType type) { 615 td_includes.put(fname, type); 616 } 617 } 618 619 /// TODO refactor, doing too many things. 620 ExitStatusType genCstub(CTestDoubleVariant variant, in string[] in_cflags, 621 CompileCommandDB compile_db, InFiles in_files) { 622 import std.typecons : Yes; 623 624 import dextool.clang : findFlags; 625 import dextool.compilation_db : ParseData = SearchResult; 626 import cpptooling.analyzer.clang.context : ClangContext; 627 import dextool.io : writeFileData; 628 import dextool.plugin.ctestdouble.backend.cvariant : CVisitor, Generator; 629 import dextool.utility : prependDefaultFlags, PreferLang, analyzeFile; 630 631 const auto user_cflags = prependDefaultFlags(in_cflags, PreferLang.c); 632 const auto total_files = in_files.length; 633 auto visitor = new CVisitor(variant, variant); 634 auto ctx = ClangContext(Yes.useInternalHeaders, Yes.prependParamSyntaxOnly); 635 auto generator = Generator(variant, variant, variant); 636 637 foreach (idx, in_file; in_files) { 638 logger.infof("File %d/%d ", idx + 1, total_files); 639 ParseData pdata; 640 641 // TODO duplicate code in c, c++ and plantuml. Fix it. 642 if (compile_db.length > 0) { 643 auto tmp = compile_db.findFlags(FileName(in_file), user_cflags, 644 variant.getCompileCommandFilter); 645 if (tmp.isNull) { 646 return ExitStatusType.Errors; 647 } 648 pdata = tmp.get; 649 } else { 650 pdata.cflags = user_cflags.dup; 651 pdata.absoluteFile = AbsolutePath(FileName(in_file)); 652 } 653 654 if (analyzeFile(pdata.absoluteFile, pdata.cflags, visitor, ctx) == ExitStatusType.Errors) { 655 return ExitStatusType.Errors; 656 } 657 658 generator.aggregate(visitor.root, visitor.container); 659 visitor.clearRoot; 660 variant.processIncludes; 661 } 662 663 variant.finalizeIncludes; 664 665 // Analyse and generate test double 666 generator.process(visitor.container); 667 668 debug { 669 logger.trace(visitor); 670 } 671 672 return writeFileData(variant.getProducedFiles); 673 }