1 module cobertura;
2 
3 import std.typecons : Nullable, nullable;
4 import std.traits : isSomeString, isCallable;
5 import std.range : isInputRange, ElementType;
6 
7 struct LineCoverage {
8 	size_t index;
9 	size_t count;
10 }
11 
12 struct FileCoverage {
13 	string path;
14 	LineCoverage[] lines;
15 	size_t linesCovered;
16 }
17 
18 FileCoverage parseLstFile(string path) {
19 	import std.stdio : File;
20 	return parseLstFile(File(path).byLine, path);
21 }
22 
23 template tryTo(T) {
24 	Nullable!T tryTo(R)(R r) {
25 		import std.conv : to;
26 		try {
27 			return nullable(r.to!T);
28 		} catch (Exception e) {
29 			return Nullable!T.init;
30 		}
31 	}
32 }
33 
34 private template andThen(alias fun) {
35     auto andThen(T)(Nullable!T t) {
36         alias RT = typeof(fun(T.init));
37         static if (is(RT == void)) {
38             if (!t.isNull) {
39                 fun(t.get);
40 			}
41         } else {
42             alias Result = Nullable!RT;
43             if (t.isNull) {
44                 return Result.init;
45 			}
46             return Result(fun(t.get));
47         }
48     }
49 }
50 
51 FileCoverage parseLstFile(Lines)(Lines r, string path) if (isInputRange!Lines &&
52 													   isSomeString!(ElementType!Lines)) {
53 	import std.range : enumerate, tee;
54 	import std.algorithm : map, stripLeft, until, filter;
55 	import std.array : array;
56 	size_t linesCovered = 0;
57 	auto lines = r.enumerate.map!((line){
58 			return line
59 				.value
60 				.stripLeft(' ')
61 				.until('|')
62 				.tryTo!size_t
63 				.andThen!(v => LineCoverage(line.index, v));
64 		})
65 	    .filter!(a => !a.isNull).map!(a => a.get)
66 		.tee!(line => linesCovered += (line.count > 0))
67 		.array();
68 	return FileCoverage(path, lines, linesCovered);
69  }
70 
71 unittest {
72 	import unit_threaded;
73 	import std..string;
74 	testLst
75 		.splitLines
76 		.parseLstFile("file").should == FileCoverage("file", [LineCoverage(9, 1), LineCoverage(13, 0), LineCoverage(14, 0), LineCoverage(15, 0), LineCoverage(19, 0)], 1);
77 }
78 
79 auto generateXmlFile(FileCoverage[] files) {
80 	import core.stdc.time: time;
81 	import std.format: format;
82 	import std.path: buildPath;
83 
84 	import std.algorithm.iteration: splitter, map, sum;
85 
86 	size_t linesValid = files.map!(file => file.lines.length).sum();
87 	size_t linesCovered = files.map!(file => file.linesCovered).sum();
88 
89 	double lineRate = cast(double)linesCovered / linesValid;
90 
91 	string res = `<?xml version="1.0"?>
92 <coverage version="5.3"
93 	timestamp="%s"
94 	lines-valid="%s"
95 	lines-covered="%s"
96 	line-rate="%s"
97 	branches-covered="0"
98 	branches-valid="0"
99 	branch-rate="0"
100 	complexity="0"
101 >
102 `.format(time(null), linesValid, linesCovered, lineRate);
103 
104 	res ~= "\t<sources>\n\t\t<source>./</source>\n\t</sources>\n";
105 	res ~=
106 		`	<packages>
107 `;
108 	foreach(file; files) {
109 		lineRate = cast(double)file.linesCovered / file.lines.length;
110 
111 		string fpath = buildPath(file.path[0 .. $-4].splitter('-')) ~ ".d";
112 
113 		res ~=
114 			`		<package name="covered" line-rate="%1$s" branch-rate="0" complexity="0">
115 			<classes>
116 				<class name="%2$s" filename="%2$s" complexity="0" line-rate="%1$s" branch-rate="0"><methods></methods>
117 `.format(lineRate, fpath);
118 
119 		res ~= "\t\t\t\t\t<lines>\n";
120 		foreach(line; file.lines) {
121 			res ~= "\t\t\t\t\t\t<line number=\"%s\" hits=\"%s\"/>\n".format(line.index+1, line.count);
122 		}
123 		res ~= "\t\t\t\t\t</lines>\n\t\t\t\t</class>\n\t\t\t</classes>\n\t\t</package>\n\t";
124 	}
125 	res ~= "</packages>\n</coverage>";
126 	return res;
127 }
128 
129 unittest {
130 	import unit_threaded;
131 	import std..string;
132 	import std.regex;
133 	auto files = [testLst
134 				  .splitLines
135 				  .parseLstFile("file")];
136 	files.generateXmlFile()
137 		.replace(regex("timestamp=\"[0-9]+\""), "timestamp=\"filtered\"")
138 		.should == `<?xml version="1.0"?>
139 <coverage version="5.3"
140 	timestamp="filtered"
141 	lines-valid="5"
142 	lines-covered="1"
143 	line-rate="0.2"
144 	branches-covered="0"
145 	branches-valid="0"
146 	branch-rate="0"
147 	complexity="0"
148 >
149 	<sources>
150 		<source>./</source>
151 	</sources>
152 	<packages>
153 		<package name="covered" line-rate="0.2" branch-rate="0" complexity="0">
154 			<classes>
155 				<class name=".d" filename=".d" complexity="0" line-rate="0.2" branch-rate="0"><methods></methods>
156 					<lines>
157 						<line number="10" hits="1"/>
158 						<line number="14" hits="0"/>
159 						<line number="15" hits="0"/>
160 						<line number="16" hits="0"/>
161 						<line number="20" hits="0"/>
162 					</lines>
163 				</class>
164 			</classes>
165 		</package>
166 	</packages>
167 </coverage>`;
168 }
169 
170 version(unittest) enum testLst = `       |module foobar;
171        |
172        |struct FooBar(T, string memberName) {
173        |    import core.sync.mutex : Mutex;
174        |
175        |    private static shared T _val__;
176        |    private static shared Mutex _m__;
177        |
178        |    shared static this() {
179       1|        _m__ = new shared Mutex();
180        |    }
181        |
182        |    static typeof(this)opCall() {
183 0000000|        typeof(this)tmp;
184 0000000|        tmp._m__.lock_nothrow();
185 0000000|        return tmp;
186        |    }
187        |
188        |    ~this() {
189 0000000|        _m__.unlock_nothrow();
190        |    }
191        |
192        |
193        |    this(this) @disable;
194        |}`;