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