1 /** 2 * This module loads and parses code coverage analysis results (*.lst files) 3 * Authors: Anton Fediushin 4 * License: MIT 5 * Copyright: Copyright © 2017, Anton Fediushin 6 */ 7 module covered.loader; 8 9 import std.stdio : File; 10 version(unittest) import fluent.asserts; 11 12 /** 13 * Creates CoverageLoader for each file and looks for such files in each directory 14 * Params: 15 * files = Input files 16 * dirs = Input dirs 17 * hidden = If `true`, looks for hidden files in directories 18 * recursive = If `true`, goes through directory recursively 19 * Returns: A range of `CoverageLoader`. 20 */ 21 @trusted auto openFilesAndDirs(string[] files, string[] dirs, bool hidden = false, bool recursive = false) { 22 import std.algorithm : map, filter, joiner; 23 import std.file : exists, dirEntries, SpanMode; 24 import std.range : chain; 25 return files 26 .chain(dirs 27 .map!(a => a.dirEntries( 28 hidden 29 ? "*.lst" 30 : "[!.]*.lst", 31 recursive 32 ? SpanMode.breadth 33 : SpanMode.shallow)) 34 .joiner) 35 .filter!(a => a.exists) 36 .map!(a => CoverageLoader(a)); 37 } 38 39 /** 40 * Handles *.lst files 41 */ 42 struct CoverageLoader { 43 private { 44 File m_file; 45 char[] m_buffer; 46 47 string m_sourcefile; 48 49 bool m_coverage_computed = false; 50 float m_coverage; 51 52 bool m_stats_available = 0; 53 54 size_t m_covered; 55 size_t m_total; 56 } 57 58 /// Opens file 59 this(string fname) { this(File(fname, "r")); } 60 /// ditto 61 this(File f) { 62 m_file = f; 63 m_file.seek(0); 64 } 65 66 /** 67 * Creates `ByEntryRange`, which lazily iterates over input file 68 */ 69 @safe ByEntryRange byEntry() { return ByEntryRange(m_file); } 70 71 @trusted private void getCoveredAndTotalLines() { 72 import std..string : indexOf, stripLeft; 73 74 m_file.seek(0); 75 m_buffer.reserve(4096); 76 77 m_covered = m_total = 0; 78 79 while(m_file.readln(m_buffer)) { 80 immutable bar = m_buffer.indexOf('|'); 81 82 if(bar == -1) { 83 break; 84 } else { 85 auto num = m_buffer[0..bar].stripLeft; 86 if(num.length) { 87 foreach(ref c; num) { 88 if(c != '0') { 89 ++m_covered; 90 break; 91 } 92 } 93 ++m_total; 94 } 95 } 96 } 97 98 m_stats_available = true; 99 } 100 101 /** 102 * This function reads file only once, so calling it many times doesn't result in any slowdown 103 * Returns: Number of executable lines 104 */ 105 @safe @property size_t getTotalCount() { 106 if(!m_stats_available) 107 this.getCoveredAndTotalLines(); 108 109 return m_total; 110 } 111 112 /** 113 * This function reads file only once, so calling it many times doesn't result in any slowdown 114 * Returns: Number of lines, executed at least 1 time 115 */ 116 @safe @property size_t getCoveredCount() { 117 if(!m_stats_available) 118 this.getCoveredAndTotalLines(); 119 120 return m_covered; 121 } 122 123 /** 124 * This function reads file only once, so calling it many times doesn't result in any slowdown 125 * Returns: Code coverage in % (covered / total * 100%) 126 */ 127 @safe @property float getCoverage() { 128 import std.conv : to; 129 130 if(!m_stats_available) 131 this.getCoveredAndTotalLines(); 132 133 if(!m_coverage_computed) { 134 if(m_covered == 0 && m_total == 0) { 135 m_coverage = float.infinity; 136 } else { 137 m_coverage = m_covered.to!float / m_total.to!float * 100.0f; 138 } 139 } 140 141 return m_coverage; 142 } 143 144 /** 145 * This function reads file only once, so calling it many times doesn't result in any slowdown 146 * Returns: Source file path 147 */ 148 @trusted @property string getSourceFile() { 149 if(!m_sourcefile.length) { 150 import std.algorithm : canFind; 151 import std.regex : matchFirst, regex; 152 153 m_file.seek(0); 154 m_buffer.reserve(4096); 155 156 while(m_file.readln(m_buffer)) { 157 if(m_buffer.canFind('|')) 158 continue; 159 160 auto m = m_buffer.matchFirst( 161 regex(r"(.+\.d) (?:(?:is \d+% covered)|(?:has no code))")); 162 163 if(m.empty) 164 continue; 165 166 m_sourcefile = m[1].dup; 167 break; 168 } 169 } 170 171 return m_sourcefile; 172 } 173 174 /** 175 * Returns: File name of code coverage result 176 */ 177 @safe @property pure nothrow string getFile() { return m_file.name; } 178 } 179 180 @("getCoveredCount(), getTotalCount() and getCoverage() produce expected results") 181 unittest { 182 auto c = CoverageLoader("sample/hello.lst"); 183 c.getCoveredCount.should.be.equal(1); 184 c.getTotalCount.should.be.equal(1); 185 186 c.getCoverage.should.be.equal(100.0f); 187 } 188 189 @("getSourceFile() returns correct file name") 190 unittest { 191 CoverageLoader("sample/hello.lst").getSourceFile.should.be.equal("hello.d"); 192 CoverageLoader("sample/hello.lst").getFile.should.be.equal("sample/hello.lst"); 193 } 194 195 struct Entry { 196 bool Used; 197 size_t Count; 198 string Source; 199 } 200 201 struct ByEntryRange { 202 private { 203 File m_file; 204 Entry m_last; 205 bool m_empty; 206 char[] m_buffer; 207 } 208 209 @trusted this(File f) { 210 m_buffer.reserve(4096); 211 m_file = f; 212 m_file.seek(0); 213 this.popFront; 214 } 215 216 @safe pure @nogc nothrow @property Entry front() { return m_last; } 217 @safe pure @nogc nothrow @property bool empty() { return m_empty; } 218 219 @trusted void popFront() { 220 import std..string : indexOf, stripLeft; 221 import std.conv : to; 222 immutable read = m_file.readln(m_buffer); 223 if(read == 0) { 224 m_empty = true; 225 return; 226 } else { 227 immutable bar = m_buffer[0..read].indexOf('|'); 228 229 if(bar == -1) { 230 m_empty = true; 231 return; 232 } else { 233 auto num = m_buffer[0..bar].stripLeft; 234 if(num.length) { 235 m_last.Used = true; 236 m_last.Count = num.to!size_t; 237 } else { 238 m_last.Used = false; 239 m_last.Count = 0; 240 } 241 242 m_last.Source = m_buffer[bar + 1 .. read].dup; 243 } 244 } 245 } 246 } 247 248 @("ByElementRange produces expected results") 249 unittest { 250 import std.array : array; 251 252 ByEntryRange(File("sample/hello.lst" ,"r")).array 253 .should.be.equal([ 254 Entry(false, 0, "import std.stdio;\n"), 255 Entry(false, 0, "\n"), 256 Entry(false, 0, "void main() {\n"), 257 Entry(true, 1, " writeln(\"Hello world!\");\n"), 258 Entry(false, 0, "}\n"), 259 ]); 260 }