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 }