1 /* 2 __ 3 / _| 4 __ _ _ _ _ __ ___ _ __ __ _ | |_ ___ ___ ___ 5 / _` | | | | '__/ _ \| '__/ _` | | _/ _ \/ __/ __| 6 | (_| | |_| | | | (_) | | | (_| | | || (_) \__ \__ \ 7 \__,_|\__,_|_| \___/|_| \__,_| |_| \___/|___/___/ 8 9 Copyright (C) 2018-2020 Aurora Free Open Source Software. 10 Copyright (C) 2018-2020 Luís Ferreira <luis@aurorafoss.org> 11 12 This file is part of the Aurora Free Open Source Software. This 13 organization promote free and open source software that you can 14 redistribute and/or modify under the terms of the GNU Lesser General 15 Public License Version 3 as published by the Free Software Foundation or 16 (at your option) any later version approved by the Aurora Free Open Source 17 Software Organization. The license is available in the package root path 18 as 'LICENSE' file. Please review the following information to ensure the 19 GNU Lesser General Public License version 3 requirements will be met: 20 https://www.gnu.org/licenses/lgpl.html . 21 22 Alternatively, this file may be used under the terms of the GNU General 23 Public License version 3 or later as published by the Free Software 24 Foundation. Please review the following information to ensure the GNU 25 General Public License requirements will be met: 26 http://www.gnu.org/licenses/gpl-3.0.html. 27 28 NOTE: All products, services or anything associated to trademarks and 29 service marks used or referenced on this file are the property of their 30 respective companies/owners or its subsidiaries. Other names and brands 31 may be claimed as the property of others. 32 33 For more info about intellectual property visit: aurorafoss.org or 34 directly send an email to: contact (at) aurorafoss.org . 35 */ 36 37 /++ 38 LST Parser 39 40 This file defines the LST format Parser 41 42 Authors: Luís Ferreira <luis@aurorafoss.org> 43 Copyright: All rights reserved, Aurora Free Open Source Software 44 License: GNU Lesser General Public License (Version 3, 29 June 2007) 45 Date: 2020 46 +/ 47 module liblstparse.parser; 48 49 import std.algorithm; 50 import std.array; 51 import std.ascii; 52 import std.conv : to; 53 import std.exception; 54 import std.file; 55 import std.format; 56 import std.range.primitives; 57 import std.string; 58 import std.typecons; 59 import std.typecons; 60 61 /// 62 class LSTFileParseException : Exception 63 { 64 /// 65 mixin basicExceptionCtors; 66 } 67 68 /// 69 class LSTFileMergeException : Exception 70 { 71 /// 72 mixin basicExceptionCtors; 73 } 74 75 /** LST File struct 76 * 77 * This defines an LST File model with all associated covered lines. 78 */ 79 @safe public struct LSTFile 80 { 81 /** LSTFile file content constructor 82 * 83 * This constructs a LSTFile using directly the content of the lst file 84 * 85 * Examples: 86 * -------------------- 87 * auto text = readText("tuna.lst"); 88 * LSTFile lst = LSTFile(text); 89 * -------------------- 90 * 91 * Params: 92 * text = content of the lst file 93 */ 94 @safe public this(string text) 95 { 96 import std.conv : to; 97 98 auto buf = text.chomp.splitLines; 99 100 if (buf.empty) // at the time, an empty file is generated from any empty .d file 101 return; 102 103 enforce!LSTFileParseException(buf.length >= 2, 104 "Minimum number of lines is 2. Probably not parsing .lst file"); 105 106 foreach (i, ref line; buf[0 .. $ - 1]) 107 { 108 immutable auto splittedLine = line.split("|"); 109 // check if the line is from a LST file 110 enforce!LSTFileParseException(splittedLine.length >= 2, 111 "'|' separator not found. Probably not parsing .lst file"); 112 113 immutable auto covered = splittedLine.front.strip; 114 115 _lines ~= Line( 116 (covered.empty) ? Nullable!(uint).init : nullable!uint(covered.to!uint), 117 splittedLine[1 .. $].join); 118 } 119 120 auto finalLine = buf.back; 121 122 if (!finalLine.endsWith(" has no code")) 123 { 124 auto s = finalLine.split("% covered"); 125 // check if it actually splits 126 enforce!LSTFileParseException(s.length >= 2, 127 "The last line is not well formatted: missing '% covered'"); 128 129 auto splitted = s.front.split(" "); 130 _totalCoverage = splitted.back.to!ubyte; 131 132 // check if lst is well formatted (has 'is' in splitted) 133 enforce!LSTFileParseException(splitted[$ - 2] == "is", 134 "The last line is not well formatted: missing ' is '"); 135 // remove ' is ' from 'filename.d is x% covered' 136 _filename = splitted[0 .. $ - 2].join(" "); 137 } 138 else 139 { 140 _filename = finalLine.split(" has no code").front; 141 } 142 } 143 144 /** 145 * LSTFile constructor 146 * 147 * This constructs the LSTFile from customizable parameters 148 * 149 * Params: 150 * filename = file name of the covered .d file 151 * lines = list of Line representing the coverage and line content 152 * totalCoverage = total reported coverage in percentage 153 */ 154 @safe public this(string filename, Line[] lines, Nullable!ubyte totalCoverage = Nullable!(ubyte).init) 155 { 156 _filename = filename; 157 _lines = lines; 158 _totalCoverage = totalCoverage; 159 } 160 161 /** LSTFile direntry constructor 162 * 163 * This constructs a LSTFile using a DirEntry as a file 164 * 165 * Examples: 166 * -------------------- 167 * LSTFile lst = LSTFile(DirEntry("tuna.lst")); 168 * -------------------- 169 * 170 * Params: 171 * file = file path 172 */ 173 @trusted public this(DirEntry file) 174 in (file.isFile, "You should pass a file, not a directory!") 175 { 176 this(readText(file.name)); 177 } 178 179 /** 180 * Constructs an LSTFile object from a given 181 * file path 182 * 183 * Params: 184 * filepath = file path of the lst file 185 * Returns: constructed LSTFile object 186 */ 187 public static LSTFile fromFilePath(string filepath) 188 { 189 return LSTFile(DirEntry(filepath)); 190 } 191 192 /** 193 * Generate the corresponding lst text file to this object 194 * 195 * Returns: generated lst file content 196 */ 197 public string generateLST() 198 { 199 auto ret = appender!string(); 200 if (_lines.empty) // at the time this is the behavior when generating lst file 201 // from an empty .d file 202 return ""; 203 204 // no need to calculate totalCoverage if there's no coverable lines and 205 // _totalCoverage is null 206 bool nocov = true; 207 208 foreach (l; _lines) 209 { 210 auto genline = format!"|%s\n"(l.content); 211 string prefixCov; 212 if (l.coverage.isNull) 213 { 214 prefixCov = format!"%7s"(" "); // fill with spaces 215 } 216 else 217 { 218 nocov = false; 219 if (l.coverage.get() == 0) 220 prefixCov = format!"%07d"(0); // fill with 0s 221 else 222 prefixCov = format!"%7d"(l.coverage.get()); 223 } 224 225 ret ~= prefixCov ~ genline; 226 } 227 228 if (nocov) 229 ret ~= format!"%s has no code"(_filename); 230 else 231 ret ~= format!"%s is %d%% covered"(_filename, totalCoverage); 232 233 ret ~= newline; 234 235 return ret[]; 236 } 237 238 /** 239 * Merge this LSTFile object with a given one 240 * 241 * Params: 242 * lstfile = lstfile object to merge with 243 * Returns: merged lstfile object 244 */ 245 public LSTFile merge(LSTFile lstfile) const 246 in 247 { 248 // check if the coverage report is from the exact same 249 // source, otherwise fail 250 enforce!LSTFileMergeException(lstfile._filename == _filename, 251 "should merge with the same file"); 252 enforce!LSTFileMergeException(lstfile._lines.length == _lines.length, 253 "lines length mismatch"); 254 255 } 256 do 257 { 258 T getOr(T)(Nullable!T nullable, T t) const 259 { 260 if (!nullable.isNull) 261 return nullable.get(); 262 return t; 263 } 264 265 // Can't use an appender here due to deprecation warning issue 266 // See: https://issues.dlang.org/show_bug.cgi?id=20552 267 Line[] lines; 268 //auto lines = appender!(Line[]); 269 270 foreach (idx, l; _lines) 271 { 272 enforce!LSTFileMergeException(l.content == lstfile._lines[idx].content, 273 format!"content mismatch at line %s"(idx + 1)); 274 Nullable!uint cov; 275 276 if (!(l.coverage.isNull && lstfile._lines[idx].coverage.isNull)) 277 cov = getOr(lstfile._lines[idx].coverage, 0) + getOr(l.coverage, 0); 278 279 lines ~= Line(cov, l.content); 280 } 281 282 return LSTFile(lstfile._filename, lines[]); 283 } 284 285 /** 286 * Merge two given LSTFile objects 287 * 288 * Params: 289 * lfile1 = first LSTFile object to merge 290 * lfile2 = second LSTFile object to merge 291 * Returns: a merged LSTFIle object 292 */ 293 public static LSTFile merge(LSTFile lfile1, LSTFile lfile2) 294 { 295 return lfile1.merge(lfile2); 296 } 297 298 /** 299 * Returns: Path of the covered filename 300 */ 301 @safe pure public string filename() const @property 302 { 303 return _filename; 304 } 305 306 /** 307 * Returns: Total coverage percentage 308 */ 309 @safe pure public ubyte totalCoverage() 310 { 311 if (_totalCoverage.isNull) 312 { 313 auto assocArr = linesCovered(); 314 if (assocArr.empty) 315 { 316 _totalCoverage = nullable!ubyte(0); 317 return 0; 318 } 319 320 size_t ret; 321 foreach (k, v; assocArr) 322 if (v > 0) 323 ret++; 324 325 // its fine to do this operation as it won't devide by 0 and its 326 // also fine to cast this because it won't be greater than 100, 327 // mathematically 328 return (_totalCoverage = nullable!ubyte( 329 cast(ubyte)((ret / cast(float) assocArr.length) * 100) 330 )).get(); 331 332 } 333 return _totalCoverage.get(); 334 } 335 336 /** 337 * Returns: Associative array of coverable lines 338 */ 339 @safe pure public const(uint[size_t]) linesCovered() const @property 340 { 341 uint[size_t] ret; 342 foreach (i, l; _lines) 343 if (l.coverage.isNull) 344 continue; 345 else 346 ret[i] = l.coverage.get(); 347 348 return ret; 349 } 350 351 /** 352 * Returns: Array of covered lines 353 */ 354 @safe pure public const(Line[]) lines() const @property 355 { 356 return _lines.dup; 357 } 358 359 /** 360 * Returns: Coverage value of the covered line 361 */ 362 @safe pure public Line opIndex(size_t i) 363 { 364 return _lines[i]; 365 } 366 367 /** 368 * Coverable Line 369 * 370 * This struct defines a coverable line in the lst file. 371 */ 372 struct Line 373 { 374 /// coverage of that line (if appliable) 375 Nullable!uint coverage; 376 377 /// content of the line 378 string content; 379 } 380 381 private string _filename; 382 private Line[] _lines; 383 private Nullable!ubyte _totalCoverage; 384 }