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 }