blob: d0b57d6a230fbb743064a846245c530298f3c77f [file] [log] [blame]
// Copyright 2016 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// A script for reading an lcov file and printing out the coverage information
// to the console, in a human-friendly table format.
import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
Future<Null> main(List<String> args) async {
// Expect to get exactly one argument.
if (args.length != 1) {
stderr.writeln('Usage: dart report_coverage.dart <path_to_lcov_info>');
exit(1);
}
// Existence check.
File reportFile = File(args[0]);
if (!await reportFile.exists()) {
stderr.writeln('The file ${args[0]} does not exist.');
exit(1);
}
// Process line by line.
String currentFile;
Set<String> files = SplayTreeSet<String>();
Map<String, int> totalLines = HashMap<String, int>();
Map<String, int> coveredLines = HashMap<String, int>();
await reportFile
.openRead()
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen((String line) {
if (line.startsWith('SF:')) {
// Process a new source file.
assert(currentFile == null);
currentFile = line.substring(3);
files.add(currentFile);
totalLines[currentFile] = 0;
coveredLines[currentFile] = 0;
} else if (line.startsWith('DA:')) {
// Add this line info to the map.
List<String> tokens = line.substring(3).split(',');
assert(tokens.length == 2);
totalLines[currentFile]++;
coveredLines[currentFile] += int.parse(tokens[1]) > 0 ? 1 : 0;
} else if (line == 'end_of_record') {
currentFile = null;
} else {
stderr.writeln('Unknown directive: $line');
exit(1);
}
}).asFuture();
// Report to console.
TableWriter writer = TableWriter(stdout)
..setHeaders(<Object>['Filepath', 'Covered', 'Total', 'Percentage'])
..setRightAlignment(<bool>[false, true, true, true]);
for (String file in files) {
writer.addRow(<Object>[
file,
coveredLines[file],
totalLines[file],
'${_getPercentage(coveredLines[file], totalLines[file])}%',
]);
}
// Last line for total.
int totalCoveredLines = coveredLines.values.fold(0, (int a, int b) => a + b);
int totalTotalLines = totalLines.values.fold(0, (int a, int b) => a + b);
writer
..setFooters(<Object>[
'Total',
totalCoveredLines,
totalTotalLines,
'${_getPercentage(totalCoveredLines, totalTotalLines)}%',
])
..render();
}
double _getPercentage(int covered, int total) {
return (covered * 1000 / total).roundToDouble() / 10;
}
/// A utility class for pretty-printing the data
class TableWriter {
/// Creates a new [TableWriter] instance with the specified output target.
TableWriter(Stdout out) : _out = out;
final Stdout _out;
final List<int> _colLen = <int>[];
final List<List<String>> _rows = <List<String>>[];
List<String> _headers;
List<String> _footers;
List<bool> _rightAlignments;
/// Sets the header strings.
void setHeaders(List<Object> headers) {
assert(_headers == null);
_headers = _processRow(headers);
}
/// Sets the footer strings.
void setFooters(List<Object> footers) {
assert(_footers == null);
_footers = _processRow(footers);
}
// ignore: use_setters_to_change_properties
/// Sets whether each column needs to be right aligned.
void setRightAlignment(List<bool> rightAlignments) {
_rightAlignments = rightAlignments;
}
/// Adds a new data row.
void addRow(List<Object> row) {
_rows.add(_processRow(row));
}
/// Render the table to the target provided to the constructor.
void render() {
if (_headers != null) {
_renderRow(_headers);
_renderSeparationLine();
}
_rows.forEach(_renderRow);
if (_footers != null) {
_renderSeparationLine();
_renderRow(_footers);
}
}
List<String> _processRow(List<Object> row) {
while (row.length > _colLen.length) {
_colLen.add(0);
}
List<String> stringRow = row.map((Object x) => x.toString()).toList();
for (int i = 0; i < stringRow.length; ++i) {
_colLen[i] = max(_colLen[i], stringRow[i].length);
}
return stringRow;
}
void _renderRow(List<String> row) {
for (int i = 0; i < row.length; ++i) {
if (i > 0) {
_out.write(' ');
}
if (_shouldBeRightAligned(i)) {
_out..write(' ' * (_colLen[i] - row[i].length))..write(row[i]);
} else {
_out..write(row[i])..write(' ' * (_colLen[i] - row[i].length));
}
}
_out.writeln();
}
void _renderSeparationLine() {
int count = _colLen.fold(_colLen.length - 1, (int x, int y) => x + y);
_out.writeln('=' * count);
}
bool _shouldBeRightAligned(int colIndex) {
if (_rightAlignments != null && colIndex < _rightAlignments.length) {
return _rightAlignments[colIndex];
}
return false;
}
}