1 /** 2 This tool aggregates D code coverage files to a common directory. 3 4 D code coverage files are written to the directory where the test was initiated. When 5 multiple tests are run from different directories, multiple output files are produced. 6 This tool moves these files to a common directory, aggregating coverage files for the 7 same source code file along the way. 8 9 Copyright (c) 2017-2020, eBay Inc. 10 Initially written by Jon Degenhardt 11 12 License: Boost Licence 1.0 (http://boost.org/LICENSE_1_0.txt) 13 14 **/ 15 module buildtools.aggregate_codecov; 16 17 import std.file : exists, isDir, isFile, remove, rename; 18 import std.path : baseName, buildPath, stripExtension; 19 import std.stdio; 20 21 int main(string[] cmdArgs) 22 { 23 auto programName = (cmdArgs.length > 0) ? cmdArgs[0].stripExtension.baseName : "Unknown_program_name"; 24 25 if (cmdArgs.length < 3) 26 { 27 writefln("Synopsis: %s target-dir coverage-file [coverage-file...]", programName); 28 return 1; 29 } 30 31 auto targetDir = cmdArgs[1]; 32 auto coverageFiles = cmdArgs[2..$]; 33 34 if (!targetDir.exists || !targetDir.isDir) 35 { 36 writefln("%s is not a directory", targetDir); 37 return 1; 38 } 39 40 foreach (cf; coverageFiles) 41 { 42 if (!cf.exists || !cf.isFile) 43 { 44 writefln("%s is not a file", cf); 45 return 1; 46 } 47 } 48 49 foreach (cf; coverageFiles) 50 { 51 auto targetFile = buildPath(targetDir, cf.baseName); 52 if (!targetFile.exists) cf.rename(targetFile); 53 else mergeCoverageFiles(cf, targetFile); 54 } 55 56 return 0; 57 } 58 59 void mergeCoverageFiles(string fromFile, string toFile) 60 { 61 import std.algorithm : find, findSplit, max; 62 import std.array : appender; 63 import std.conv : to; 64 import std.format : format; 65 import std.math : log10; 66 import std.range : empty, lockstep, StoppingPolicy; 67 68 struct LineCounter 69 { 70 long count; 71 string line; 72 } 73 74 auto lines = appender!(LineCounter[])(); 75 string lastLine = ""; 76 long maxCounter = -1; 77 78 { // Scope for file opens 79 auto toInput = toFile.File; 80 auto fromInput = fromFile.File; 81 82 foreach (lineNum, f1, f2; lockstep(toInput.byLine, fromInput.byLine, StoppingPolicy.requireSameLength)) 83 { 84 if (!lastLine.empty) 85 throw new Exception(format("Unexpected file input. File: %s; File: %s; Line: %d", 86 fromFile, toFile, lineNum)); 87 88 auto f1Split = f1.findSplit("|"); 89 auto f2Split = f2.findSplit("|"); 90 91 if (f1Split[0].empty) 92 throw new Exception(format("Unexpected input. File: %s, %d", toFile, lineNum)); 93 if (f2Split[0].empty) 94 throw new Exception(format("Unexpected input. File: %s, %d", fromFile, lineNum)); 95 96 if ((f1Split[2].empty && !f2Split[2].empty) || 97 (!f1Split[2].empty && f2Split[2].empty) || 98 (!f1Split[2].empty && !f2Split[2].empty && f1Split[2] != f2Split[2])) 99 { 100 throw new Exception(format("Inconsistent file code line. File: %s; File: %s; Line: %d", 101 fromFile, toFile, lineNum)); 102 } 103 104 if (f1Split[1].empty) 105 { 106 lastLine = f1.to!string; 107 continue; 108 } 109 auto f1CounterStr = f1Split[0].find!(c => c != ' '); 110 auto f2CounterStr = f2Split[0].find!(c => c != ' '); 111 112 long f1Counter = f1CounterStr.empty ? -1 : f1CounterStr.to!long; 113 long f2Counter = f2CounterStr.empty ? -1 : f2CounterStr.to!long; 114 long counter = 115 (f1Counter == -1) ? f2Counter : 116 (f2Counter == -1) ? f1Counter : 117 f1Counter + f2Counter; 118 119 auto lc = LineCounter(counter, f1Split[2].to!string); 120 lines ~= lc; 121 if (counter > maxCounter) maxCounter = counter; 122 } 123 } 124 125 auto toBackup = toFile ~ ".backup"; 126 toFile.rename(toBackup); 127 128 size_t minDigits = max(7, (maxCounter <= 0) ? 1 : log10(maxCounter).to!long + 1); 129 string blanks; 130 string zeros; 131 foreach (i; 0 .. minDigits) 132 { 133 blanks ~= ' '; 134 zeros ~= '0'; 135 } 136 137 size_t codeLines = 0; 138 size_t coveredCodeLines = 0; 139 auto ofile = toFile.File("w"); 140 foreach (lc; lines.data) 141 { 142 if (lc.count >= 0) codeLines++; 143 if (lc.count > 0) coveredCodeLines++; 144 ofile.writeln( 145 (lc.count < 0) ? blanks : 146 (lc.count == 0) ? zeros : 147 format("%*d", minDigits, lc.count), 148 '|', lc.line); 149 } 150 auto lastLineSplit = lastLine.findSplit(" "); 151 ofile.write(lastLineSplit[0]); 152 if (codeLines == 0) ofile.writeln(" has no code"); 153 else ofile.writefln( 154 " is %d%% covered", 155 ((coveredCodeLines.to!double / codeLines.to!double) * 100.0).to!size_t); 156 toBackup.remove; 157 fromFile.remove; 158 }