1 /** 2 * Implementation of code coverage analyzer. 3 * 4 * Copyright: Copyright Digital Mars 1995 - 2015. 5 * License: Distributed under the 6 * $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0). 7 * (See accompanying file LICENSE) 8 * Authors: Walter Bright, Sean Kelly 9 * Source: $(DRUNTIMESRC rt/_cover.d) 10 */ 11 12 module rt.cover; 13 14 import core.internal.util.math : min, max; 15 16 private 17 { 18 version (Windows) 19 { 20 import core.sys.windows.basetsd /+: HANDLE+/; 21 import core.sys.windows.winbase /+: LOCKFILE_EXCLUSIVE_LOCK, LockFileEx, OVERLAPPED, SetEndOfFile+/; 22 } 23 else version (Posix) 24 { 25 import core.sys.posix.fcntl; 26 import core.sys.posix.unistd; 27 } 28 import core.stdc.config : c_long; 29 import core.stdc.stdio; 30 import core.stdc.stdlib; 31 import core.internal.utf; 32 33 struct BitArray 34 { 35 size_t len; 36 size_t* ptr; 37 38 bool opIndex( size_t i ) 39 in 40 { 41 assert( i < len ); 42 } 43 do 44 { 45 static if (size_t.sizeof == 8) 46 return ((ptr[i >> 6] & (1L << (i & 63)))) != 0; 47 else static if (size_t.sizeof == 4) 48 return ((ptr[i >> 5] & (1 << (i & 31)))) != 0; 49 else 50 static assert(0); 51 } 52 } 53 54 struct Cover // one of these for each module being analyzed 55 { 56 string filename; 57 BitArray valid; // bit array of which source lines are executable code lines 58 uint[] data; // array of line execution counts 59 ubyte minPercent; // minimum percentage coverage required 60 } 61 62 __gshared 63 { 64 Cover[] gdata; 65 Config config; 66 } 67 68 struct Config 69 { 70 string srcpath; 71 string dstpath; 72 bool merge; 73 74 @nogc nothrow: 75 76 bool initialize() 77 { 78 import core.internal.parseoptions : initConfigOptions; 79 return initConfigOptions(this, this.errorName); 80 } 81 82 void help() 83 { 84 string s = "Code coverage options are specified as whitespace separated assignments: 85 merge:0|1 - 0 overwrites existing reports, 1 merges current run with existing coverage reports (default: %d) 86 dstpath:<PATH> - writes code coverage reports to <PATH> (default: current 87 working directory) 88 srcpath:<PATH> - sets the path where the source files are located to <PATH> 89 (default: current working directory) 90 "; 91 printf(s.ptr, merge); 92 } 93 94 string errorName() { return "covopt"; } 95 } 96 } 97 98 99 /** 100 * Set path to where source files are located. 101 * 102 * Params: 103 * pathname = The new path name. 104 */ 105 extern (C) void dmd_coverSourcePath( string pathname ) 106 { 107 config.srcpath = pathname; 108 } 109 110 111 /** 112 * Set path to where listing files are to be written. 113 * 114 * Params: 115 * pathname = The new path name. 116 */ 117 extern (C) void dmd_coverDestPath( string pathname ) 118 { 119 config.dstpath = pathname; 120 } 121 122 123 /** 124 * Set merge mode. 125 * 126 * Params: 127 * flag = true means new data is summed with existing data in the listing 128 * file; false means a new listing file is always created. 129 */ 130 extern (C) void dmd_coverSetMerge( bool flag ) 131 { 132 config.merge = flag; 133 } 134 135 136 /** 137 * The coverage callback. 138 * 139 * Params: 140 * filename = The name of the coverage file. 141 * valid = Bit array containing the valid code lines for coverage 142 * data = Array containg the coverage hits of each line 143 * minPercent = minimal coverage of the module 144 */ 145 extern (C) void _d_cover_register2(string filename, size_t[] valid, uint[] data, ubyte minPercent) 146 { 147 assert(minPercent <= 100); 148 149 Cover c; 150 151 c.filename = filename; 152 c.valid.ptr = valid.ptr; 153 c.valid.len = valid.length; 154 c.data = data; 155 c.minPercent = minPercent; 156 gdata ~= c; 157 } 158 159 /* Kept for the moment for backwards compatibility. 160 */ 161 extern (C) void _d_cover_register( string filename, size_t[] valid, uint[] data ) 162 { 163 _d_cover_register2(filename, valid, data, 0); 164 } 165 166 private: 167 168 // returns 0 if s isn't a number 169 uint parseNum(const(char)[] s) 170 { 171 while (s.length && s[0] == ' ') 172 s = s[1 .. $]; 173 uint res; 174 while (s.length && s[0] >= '0' && s[0] <= '9') 175 { 176 res = 10 * res + s[0] - '0'; 177 s = s[1 .. $]; 178 } 179 return res; 180 } 181 182 const(char)[] parseContent(const(char)[] s) 183 { 184 while (s.length && s[0] != '|') 185 s = s[1 .. $]; 186 return s[1 .. $]; 187 } 188 189 bool lstEquals(char[][] sourceLines, char[][] lstLines) 190 { 191 if (sourceLines.length != lstLines.length - 1U) 192 return false; 193 194 foreach (i, line; sourceLines) 195 { 196 auto content = parseContent(lstLines[i]); 197 // length mismatch 198 if (line.length != content.length) return false; 199 200 // char content mismatch 201 foreach (j, c; content) 202 if (line[j] != c) return false; 203 } 204 205 return true; 206 } 207 208 unittest 209 { 210 char[][] src = cast(char[][])[ "12345", " | 12345, asasd", "|", ".;" ]; 211 char[][] lst = cast(char[][])[ " |12345", " | | 12345, asasd", " 1||", "0000000|.;", "" ]; 212 char[][] badLst = cast(char[][])[ " |12344", " | | 12345, asasd", " 1||", "0000000|.;", "" ]; 213 assert(lstEquals(src, lst)); 214 assert(!lstEquals(src, [])); 215 assert(!lstEquals(src, badLst)); 216 } 217 218 shared static this() 219 { 220 config.initialize(); 221 } 222 223 shared static ~this() 224 { 225 if (!gdata.length) return; 226 227 const NUMLINES = 16384 - 1; 228 const NUMCHARS = 16384 * 16 - 1; 229 230 auto buf = new char[NUMCHARS]; 231 auto lines = new char[][NUMLINES]; 232 auto lstLines = new char[][NUMLINES]; 233 234 foreach (c; gdata) 235 { 236 auto fname = appendFN(config.dstpath, addExt(baseName(c.filename), "lst")); 237 auto flst = openOrCreateFile(fname); 238 if (flst is null) 239 continue; 240 lockFile(fileno(flst)); // gets unlocked by fclose 241 scope(exit) fclose(flst); 242 243 if (!readFile(appendFN(config.srcpath, c.filename), buf)) 244 continue; 245 splitLines(buf, lines); 246 247 // Calculate the minimum line length between the source file and c.data 248 auto minLineLength = min(c.data.length, lines.length); 249 250 foreach (i; 0 .. minLineLength) 251 lines[i] = expandTabs(lines[i]); 252 253 auto buf2 = new char[NUMCHARS]; 254 if (config.merge && readFile(flst, buf2)) 255 { 256 splitLines(buf2, lstLines); 257 258 // check if source is the same before merge 259 if (lstEquals(lines, lstLines)) 260 foreach (i, line; lstLines[0 .. min($, c.data.length)]) 261 c.data[i] += parseNum(line); 262 } 263 264 // Calculate the maximum number of digits in the line with the greatest 265 // number of calls. 266 uint maxCallCount; 267 foreach (n; c.data[0 .. minLineLength]) 268 maxCallCount = max(maxCallCount, n); 269 270 // Make sure that there are a minimum of seven columns in each file so 271 // that unless there are a very large number of calls, the columns in 272 // each files lineup. 273 immutable maxDigits = max(7, digits(maxCallCount)); 274 275 uint nno; 276 uint nyes; 277 278 // rewind for overwriting 279 fseek(flst, 0, SEEK_SET); 280 281 foreach (i, n; c.data[0 .. minLineLength]) 282 { 283 auto line = lines[i]; 284 285 if (n == 0) 286 { 287 if (c.valid[i]) 288 { 289 ++nno; 290 fprintf(flst, "%0*u|%.*s\n", maxDigits, 0, cast(int)line.length, line.ptr); 291 } 292 else 293 { 294 fprintf(flst, "%*s|%.*s\n", maxDigits, " ".ptr, cast(int)line.length, line.ptr); 295 } 296 } 297 else 298 { 299 ++nyes; 300 fprintf(flst, "%*u|%.*s\n", maxDigits, n, cast(int)line.length, line.ptr); 301 } 302 } 303 304 if (nyes + nno) // no divide by 0 bugs 305 { 306 uint percent = ( nyes * 100 ) / ( nyes + nno ); 307 fprintf(flst, "%.*s is %d%% covered\n", cast(int)c.filename.length, c.filename.ptr, percent); 308 if (percent < c.minPercent) 309 { 310 fprintf(stderr, "Error: %.*s is %d%% covered, less than required %d%%\n", 311 cast(int)c.filename.length, c.filename.ptr, percent, c.minPercent); 312 exit(EXIT_FAILURE); 313 } 314 } 315 else 316 { 317 fprintf(flst, "%.*s has no code\n", cast(int)c.filename.length, c.filename.ptr); 318 } 319 320 version (Windows) 321 SetEndOfFile(handle(fileno(flst))); 322 else 323 ftruncate(fileno(flst), ftell(flst)); 324 } 325 } 326 327 uint digits(uint number) 328 { 329 import core.stdc.math; 330 return number ? cast(uint)floor(log10(number)) + 1 : 1; 331 } 332 333 unittest 334 { 335 static void testDigits(uint num, uint dgts) 336 { 337 assert(digits(num) == dgts); 338 assert(digits(num - 1) == dgts - 1); 339 assert(digits(num + 1) == dgts); 340 } 341 assert(digits(0) == 1); 342 assert(digits(1) == 1); 343 testDigits(10, 2); 344 testDigits(1_000, 4); 345 testDigits(1_000_000, 7); 346 testDigits(1_000_000_000, 10); 347 } 348 349 string appendFN( string path, string name ) 350 { 351 if (!path.length) return name; 352 353 version (Windows) 354 const char sep = '\\'; 355 else version (Posix) 356 const char sep = '/'; 357 358 auto dest = path; 359 360 if ( dest.length && dest[$ - 1] != sep ) 361 dest ~= sep; 362 dest ~= name; 363 return dest; 364 } 365 366 367 string baseName( string name, string ext = null ) 368 { 369 string ret; 370 foreach (c; name) 371 { 372 switch (c) 373 { 374 case ':': 375 case '\\': 376 case '/': 377 ret ~= '-'; 378 break; 379 default: 380 ret ~= c; 381 } 382 } 383 return ext.length ? chomp(ret, ext) : ret; 384 } 385 386 387 string getExt( string name ) 388 { 389 auto i = name.length; 390 391 while ( i > 0 ) 392 { 393 if ( name[i - 1] == '.' ) 394 return name[i .. $]; 395 --i; 396 version (Windows) 397 { 398 if ( name[i] == ':' || name[i] == '\\' ) 399 break; 400 } 401 else version (Posix) 402 { 403 if ( name[i] == '/' ) 404 break; 405 } 406 } 407 return null; 408 } 409 410 411 string addExt( string name, string ext ) 412 { 413 auto existing = getExt( name ); 414 415 if ( existing.length == 0 ) 416 { 417 if ( name.length && name[$ - 1] == '.' ) 418 name ~= ext; 419 else 420 name = name ~ "." ~ ext; 421 } 422 else 423 { 424 name = name[0 .. $ - existing.length] ~ ext; 425 } 426 return name; 427 } 428 429 430 string chomp( string str, string delim = null ) 431 { 432 if ( delim is null ) 433 { 434 auto len = str.length; 435 436 if ( len ) 437 { 438 auto c = str[len - 1]; 439 440 if ( c == '\r' ) 441 --len; 442 else if ( c == '\n' && str[--len - 1] == '\r' ) 443 --len; 444 } 445 return str[0 .. len]; 446 } 447 else if ( str.length >= delim.length ) 448 { 449 if ( str[$ - delim.length .. $] == delim ) 450 return str[0 .. $ - delim.length]; 451 } 452 return str; 453 } 454 455 // open/create file for read/write, pointer at beginning 456 FILE* openOrCreateFile(string name) 457 { 458 import core.internal.utf : toUTF16z; 459 460 version (Windows) 461 immutable fd = _wopen(toUTF16z(name), _O_RDWR | _O_CREAT | _O_BINARY, _S_IREAD | _S_IWRITE); 462 else 463 immutable fd = open((name ~ '\0').ptr, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | 464 S_IROTH | S_IWOTH); 465 version (CRuntime_Microsoft) 466 alias fdopen = _fdopen; 467 version (Posix) 468 import core.sys.posix.stdio; 469 return fdopen(fd, "r+b"); 470 } 471 472 version (Windows) HANDLE handle(int fd) 473 { 474 version (CRuntime_DigitalMars) 475 return _fdToHandle(fd); 476 else 477 return cast(HANDLE)_get_osfhandle(fd); 478 } 479 480 void lockFile(int fd) 481 { 482 version (CRuntime_Bionic) 483 { 484 import core.sys.bionic.fcntl : LOCK_EX; 485 import core.sys.bionic.unistd : flock; 486 flock(fd, LOCK_EX); // exclusive lock 487 } 488 else version (Posix) 489 lockf(fd, F_LOCK, 0); // exclusive lock 490 else version (Windows) 491 { 492 OVERLAPPED off; 493 // exclusively lock first byte 494 LockFileEx(handle(fd), LOCKFILE_EXCLUSIVE_LOCK, 0, 1, 0, &off); 495 } 496 else 497 static assert(0, "unimplemented"); 498 } 499 500 bool readFile(FILE* file, ref char[] buf) 501 { 502 if (fseek(file, 0, SEEK_END) != 0) 503 assert(0, "fseek failed"); 504 immutable len = ftell(file); 505 if (len == -1) 506 assert(0, "ftell failed"); 507 else if (len == 0) 508 return false; 509 510 buf.length = len; 511 fseek(file, 0, SEEK_SET); 512 if (fread(buf.ptr, 1, buf.length, file) != buf.length) 513 assert(0, "fread failed"); 514 if (fgetc(file) != EOF) 515 assert(0, "EOF not reached"); 516 return true; 517 } 518 519 version (Windows) extern (C) nothrow @nogc FILE* _wfopen(scope const wchar* filename, scope const wchar* mode); 520 version (Windows) extern (C) int chsize(int fd, c_long size); 521 522 523 bool readFile(string name, ref char[] buf) 524 { 525 import core.internal.utf : toUTF16z; 526 527 version (Windows) 528 auto file = _wfopen(toUTF16z(name), "rb"w.ptr); 529 else 530 auto file = fopen((name ~ '\0').ptr, "rb".ptr); 531 if (file is null) return false; 532 scope(exit) fclose(file); 533 return readFile(file, buf); 534 } 535 536 void splitLines( char[] buf, ref char[][] lines ) 537 { 538 size_t beg = 0, 539 pos = 0; 540 541 lines.length = 0; 542 for ( ; pos < buf.length; ++pos ) 543 { 544 char c = buf[pos]; 545 546 switch ( buf[pos] ) 547 { 548 case '\r': 549 case '\n': 550 lines ~= buf[beg .. pos]; 551 beg = pos + 1; 552 if ( buf[pos] == '\r' && pos < buf.length - 1 && buf[pos + 1] == '\n' ) 553 { 554 ++pos; ++beg; 555 } 556 continue; 557 default: 558 continue; 559 } 560 } 561 if ( beg != pos ) 562 { 563 lines ~= buf[beg .. pos]; 564 } 565 } 566 567 568 char[] expandTabs( char[] str, int tabsize = 8 ) 569 { 570 const dchar LS = '\u2028'; // UTF line separator 571 const dchar PS = '\u2029'; // UTF paragraph separator 572 573 bool changes = false; 574 char[] result = str; 575 int column; 576 int nspaces; 577 578 foreach ( size_t i, dchar c; str ) 579 { 580 switch ( c ) 581 { 582 case '\t': 583 nspaces = tabsize - (column % tabsize); 584 if ( !changes ) 585 { 586 changes = true; 587 result = null; 588 result.length = str.length + nspaces - 1; 589 result.length = i + nspaces; 590 result[0 .. i] = str[0 .. i]; 591 result[i .. i + nspaces] = ' '; 592 } 593 else 594 { auto j = result.length; 595 result.length = j + nspaces; 596 result[j .. j + nspaces] = ' '; 597 } 598 column += nspaces; 599 break; 600 601 case '\r': 602 case '\n': 603 case PS: 604 case LS: 605 column = 0; 606 goto L1; 607 608 default: 609 column++; 610 L1: 611 if (changes) 612 { 613 if (c <= 0x7F) 614 result ~= cast(char)c; 615 else 616 { 617 dchar[1] ca = c; 618 foreach (char ch; ca[]) 619 result ~= ch; 620 } 621 } 622 break; 623 } 624 } 625 return result; 626 }