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