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 |}`;