#!/usr/bin/env perl6 use v6; sub MAIN ( Str $coverage where *.IO.e, # full-cover Str $source where *.IO.e, # gen/moar/CORE.c.setting Str :$annotations, # setting ) { my (%annotations, %covered-lines) := await (start get-annotations-from $annotations), start get-coverage-from $coverage; my (%all, %stats is BagHash, $current-file, @lines, int $i); for $source.IO.lines -> $line { $i++; when $line.starts-with: '#line 1 SETTING::src' { $current-file andthen %all{$_} = process-stats %(|%stats), @lines andthen create-coverage-file $_, @lines; %stats = file => $current-file = $line.words.tail; @lines = (); $i = 0; } @lines.push: $_ => ($line, %covered-lines{$current-file}{~$i}.keys.sort // ()) with do with $current-file { when so %covered-lines{$_}{~$i} { %stats++; 'c' } when so %annotations{$_}{~$i} { %stats++; 'u' } %stats++; 'i'; } } # also build a little overview page with "coverage/index.html".IO.open(:w) -> $outfile { LEAVE $outfile.close; $outfile.say: Q:c:to/TMPL/; coverage overview for {$source} {$*css} TMPL $outfile.say: Q:to/TMPL/; TMPL for %all.sort { my $name = .key; my $v = .value; $outfile.say: qq:to/TMPL/; TMPL } $outfile.say: Q:to/TMPL/;
Filename Covered Percentage Uncovered Ignored Total
$name $v $v% $v $v $v
TMPL } spurt 'coverage/tablesort.js', $*tablesort; spurt 'coverage/tablesort.number.js', $*tablesort-number; } sub process-stats ($_, @lines) { . = ..subst(:g, /\W/, '_') ~ ".coverage.html"; . = +@lines.grep: *.key eq 'c'; . = +@lines.grep: *.key eq 'u'; . = +@lines.grep: *.key eq 'i'; . = . + . + .; with . + . -> $all { . = ($all == 0 ?? 0 !! 100 * . / $all).round: .01 } $_ } sub create-coverage-file (%stats, @lines) { with "coverage/%stats".IO { .spurt: join "\n", Q:h:to/TMPL/, coverage report for %stats<file>
    TMPL @lines.map({ my $extra = ''; my @fls = .value[1]; if @fls { $extra ~= "\t| "; if @fls > 3 { @fls.splice(2, *, '…and @fls[2..*].elems() more|); } $extra ~= @fls.join(', '); } '
  1. ' ~ .value[0].trans(['<'] => ['<']) ~ $extra }), '
'; note "Wrote $_"; } } multi get-annotations-from (Any:D $ann where .?IO.e) { note "Analyzing annotations file $ann"; my %annotations .= push: $ann.IO.lines.grep( *.starts-with: ' annotation: SETTING::' ).map(*.substr: chars ' annotation: ').map: { # filename => line number substr($_, 0, rindex($_, ':') ) => substr($_, rindex($_, ':')+1) } note "Done analyzing annotations file: { %annotations{*;}».elems.sum } lines found"; %annotations».Set } multi get-annotations-from ($ann) { note Q:c:to/END/; No annotations file { $ann ?? "found at $ann" !! "supplied" }. This tool relies on a file generated with moar --dump foobar.moarvm that contains annotations for every line that can potentially be hit END % } sub get-coverage-from ($file) { note "Reading coverage report from $file"; my %coverage; my $current-file = ''; my $current-file's-line-number = -1; for $file.IO.lines.grep({.starts-with('HIT SETTING::') || .starts-with('HIT t/spec')}) { my ($filename, $line-number) = .words[1, *-1]; if !$filename.starts-with('SETTING::') { $current-file = $filename; $current-file's-line-number = $line-number; } %coverage{$filename}{$line-number}{"$current-file:$current-file's-line-number"}++; } note "Coverage report read: {%coverage{*;}».keys.sum} lines covered."; %coverage } my $*css = q:to/CSS/; CSS my $*tablesort = q:to/TABLESORT/; /*! * tablesort v4.0.1 (2016-03-30) * http://tristen.ca/tablesort/demo/ * Copyright (c) 2016 ; Licensed MIT */! (function() { function Tablesort(el, options) { if (!(this instanceof Tablesort)) return new Tablesort(el, options); if (!el || el.tagName !== 'TABLE') { throw new Error('Element must be a table'); } this.init(el, options || {}); } var sortOptions = []; var createEvent = function(name) { var evt; if (!window.CustomEvent || typeof window.CustomEvent !== 'function') { evt = document.createEvent('CustomEvent'); evt.initCustomEvent(name, false, false, undefined); } else { evt = new CustomEvent(name); } return evt; }; var getInnerText = function(el) { return el.getAttribute('data-sort') || el.textContent || el.innerText || ''; }; // Default sort method if no better sort method is found var caseInsensitiveSort = function(a, b) { a = a.toLowerCase(); b = b.toLowerCase(); if (a === b) return 0; if (a < b) return 1; return -1; }; // Stable sort function // If two elements are equal under the original sort function, // then there relative order is reversed var stabilize = function(sort, antiStabilize) { return function(a, b) { var unstableResult = sort(a.td, b.td); if (unstableResult === 0) { if (antiStabilize) return b.index - a.index; return a.index - b.index; } return unstableResult; }; }; Tablesort.extend = function(name, pattern, sort) { if (typeof pattern !== 'function' || typeof sort !== 'function') { throw new Error('Pattern and sort must be a function'); } sortOptions.push({ name: name, pattern: pattern, sort: sort }); }; Tablesort.prototype = { init: function(el, options) { var that = this, firstRow, defaultSort, i, cell; that.table = el; that.thead = false; that.options = options; if (el.rows && el.rows.length > 0) { if (el.tHead && el.tHead.rows.length > 0) { firstRow = el.tHead.rows[el.tHead.rows.length - 1]; that.thead = true; } else { firstRow = el.rows[0]; } } if (!firstRow) return; var onClick = function() { if (that.current && that.current !== this) { that.current.classList.remove('sort-up'); that.current.classList.remove('sort-down'); } that.current = this; that.sortTable(this); }; // Assume first row is the header and attach a click handler to each. for (i = 0; i < firstRow.cells.length; i++) { cell = firstRow.cells[i]; if (!cell.classList.contains('no-sort')) { cell.classList.add('sort-header'); cell.tabindex = 0; cell.addEventListener('click', onClick, false); if (cell.classList.contains('sort-default')) { defaultSort = cell; } } } if (defaultSort) { that.current = defaultSort; that.sortTable(defaultSort); } }, sortTable: function(header, update) { var that = this, column = header.cellIndex, sortFunction = caseInsensitiveSort, item = '', items = [], i = that.thead ? 0 : 1, sortDir, sortMethod = header.getAttribute('data-sort-method'), sortOrder = header.getAttribute('data-sort-order'); that.table.dispatchEvent(createEvent('beforeSort')); // If updating an existing sort `sortDir` should remain unchanged. if (update) { sortDir = header.classList.contains('sort-up') ? 'sort-up' : 'sort-down'; } else { if (header.classList.contains('sort-up')) { sortDir = 'sort-down'; } else if (header.classList.contains('sort-down')) { sortDir = 'sort-up'; } else if (sortOrder === 'asc') { sortDir = 'sort-down'; } else if (sortOrder === 'desc') { sortDir = 'sort-up'; } else { sortDir = that.options.descending ? 'sort-up' : 'sort-down'; } header.classList.remove(sortDir === 'sort-down' ? 'sort-up' : 'sort-down'); header.classList.add(sortDir); } if (that.table.rows.length < 2) return; // If we force a sort method, it is not necessary to check rows if (!sortMethod) { while (items.length < 3 && i < that.table.tBodies[0].rows.length) { item = getInnerText(that.table.tBodies[0].rows[i].cells[column]); item = item.trim(); if (item.length > 0) { items.push(item); } i++; } if (!items) return; } for (i = 0; i < sortOptions.length; i++) { item = sortOptions[i]; if (sortMethod) { if (item.name === sortMethod) { sortFunction = item.sort; break; } } else if (items.every(item.pattern)) { sortFunction = item.sort; break; } } that.col = column; for (i = 0; i < that.table.tBodies.length; i++) { var newRows = [], noSorts = {}, j, totalRows = 0, noSortsSoFar = 0; if (that.table.tBodies[i].rows.length < 2) continue; for (j = 0; j < that.table.tBodies[i].rows.length; j++) { item = that.table.tBodies[i].rows[j]; if (item.classList.contains('no-sort')) { // keep no-sorts in separate list to be able to insert // them back at their original position later noSorts[totalRows] = item; } else { // Save the index for stable sorting newRows.push({ tr: item, td: getInnerText(item.cells[that.col]), index: totalRows }); } totalRows++; } // Before we append should we reverse the new array or not? // If we reverse, the sort needs to be `anti-stable` so that // the double negatives cancel out if (sortDir === 'sort-down') { newRows.sort(stabilize(sortFunction, true)); newRows.reverse(); } else { newRows.sort(stabilize(sortFunction, false)); } // append rows that already exist rather than creating new ones for (j = 0; j < totalRows; j++) { if (noSorts[j]) { // We have a no-sort row for this position, insert it here. item = noSorts[j]; noSortsSoFar++; } else { item = newRows[j - noSortsSoFar].tr; } // appendChild(x) moves x if already present somewhere else in the DOM that.table.tBodies[i].appendChild(item); } } that.table.dispatchEvent(createEvent('afterSort')); }, refresh: function() { if (this.current !== undefined) { this.sortTable(this.current, true); } } }; if (typeof module !== 'undefined' && module.exports) { module.exports = Tablesort; } else { window.Tablesort = Tablesort; } })(); TABLESORT my $*tablesort-number = q:to/TABLESORT-NUMBER/; (function(){ var cleanNumber = function(i) { return i.replace(/[^\-?0-9.]/g, ''); }, compareNumber = function(a, b) { a = parseFloat(a); b = parseFloat(b); a = isNaN(a) ? 0 : a; b = isNaN(b) ? 0 : b; return a - b; }; Tablesort.extend('number', function(item) { return item.match(/^-?[£\x24Û¢´€]?\d+\s*([,\.]\d{0,2})/) || // Prefixed currency item.match(/^-?\d+\s*([,\.]\d{0,2})?[£\x24Û¢´€]/) || // Suffixed currency item.match(/^-?(\d)*-?([,\.]){0,1}-?(\d)+([E,e][\-+][\d]+)?%?$/); // Number }, function(a, b) { a = cleanNumber(a); b = cleanNumber(b); return compareNumber(b, a); }); }()); TABLESORT-NUMBER # vim: ft=perl6 expandtab sw=4