| // Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| import 'package:coverage/src/util.dart'; |
| import 'package:source_maps/parser.dart'; |
| |
| /// Returns a Dart based hit-map containing coverage report for the provided |
| /// Chrome [preciseCoverage]. |
| /// |
| /// [sourceProvider] returns the source content for the Chrome scriptId, or null |
| /// if not available. |
| /// |
| /// [sourceMapProvider] returns the associated source map content for the Chrome |
| /// scriptId, or null if not available. |
| /// |
| /// [sourceUriProvider] returns the uri for the provided sourceUrl and |
| /// associated scriptId. |
| /// |
| /// Chrome coverage information for which the corresponding source map or source |
| /// content is null will be ignored. |
| Future<Map<String, dynamic>> parseChromeCoverage( |
| List<Map<String, dynamic>> preciseCoverage, |
| Future<String> Function(String scriptId) sourceProvider, |
| Future<String> Function(String scriptId) sourceMapProvider, |
| Future<Uri> Function(String sourceUrl, String scriptId) sourceUriProvider, |
| ) async { |
| final coverageReport = <Uri, Map<int, bool>>{}; |
| for (var entry in preciseCoverage) { |
| final scriptId = entry['scriptId'] as String; |
| |
| final mapResponse = await sourceMapProvider(scriptId); |
| if (mapResponse == null) continue; |
| |
| SingleMapping mapping; |
| try { |
| mapping = parse(mapResponse) as SingleMapping; |
| } on FormatException { |
| continue; |
| } on ArgumentError { |
| continue; |
| } |
| |
| final compiledSource = await sourceProvider(scriptId); |
| if (compiledSource == null) continue; |
| |
| final coverageInfo = _coverageInfoFor(entry); |
| final offsetCoverage = _offsetCoverage(coverageInfo, compiledSource.length); |
| final coveredPositions = _coveredPositions(compiledSource, offsetCoverage); |
| |
| for (var lineEntry in mapping.lines) { |
| for (var columnEntry in lineEntry.entries) { |
| if (columnEntry.sourceUrlId == null) continue; |
| final sourceUrl = mapping.urls[columnEntry.sourceUrlId]; |
| |
| // Ignore coverage information for the SDK. |
| if (sourceUrl.startsWith('org-dartlang-sdk:')) continue; |
| |
| final uri = await sourceUriProvider(sourceUrl, scriptId); |
| final coverage = coverageReport.putIfAbsent(uri, () => <int, bool>{}); |
| |
| final current = coverage[columnEntry.sourceLine + 1] ?? false; |
| coverage[columnEntry.sourceLine + 1] = current || |
| coveredPositions.contains( |
| _Position(lineEntry.line + 1, columnEntry.column + 1)); |
| } |
| } |
| } |
| |
| final coverageHitMaps = <Uri, Map<int, int>>{}; |
| coverageReport.forEach((uri, coverage) { |
| final hitMap = <int, int>{}; |
| for (var line in coverage.keys.toList()..sort()) { |
| hitMap[line] = coverage[line] ? 1 : 0; |
| } |
| coverageHitMaps[uri] = hitMap; |
| }); |
| |
| final allCoverage = <Map<String, dynamic>>[]; |
| coverageHitMaps.forEach((uri, hitMap) { |
| allCoverage.add(toScriptCoverageJson(uri, hitMap)); |
| }); |
| return <String, dynamic>{'type': 'CodeCoverage', 'coverage': allCoverage}; |
| } |
| |
| /// Returns all covered positions in a provided source. |
| Set<_Position> _coveredPositions( |
| String compiledSource, List<bool> offsetCoverage) { |
| final positions = <_Position>{}; |
| // Line is 1 based. |
| var line = 1; |
| // Column is 1 based. |
| var column = 0; |
| for (var offset = 0; offset < compiledSource.length; offset++) { |
| if (compiledSource[offset] == '\n') { |
| line++; |
| column = 0; |
| } else { |
| column++; |
| } |
| if (offsetCoverage[offset]) positions.add(_Position(line, column)); |
| } |
| return positions; |
| } |
| |
| /// Returns coverage information for a Chrome entry. |
| List<_CoverageInfo> _coverageInfoFor(Map<String, dynamic> entry) { |
| final result = <_CoverageInfo>[]; |
| for (Map<String, dynamic> functions in entry['functions']) { |
| for (Map<String, dynamic> range in functions['ranges']) { |
| result.add(_CoverageInfo( |
| range['startOffset'] as int, |
| range['endOffset'] as int, |
| (range['count'] as int) > 0, |
| )); |
| } |
| } |
| return result; |
| } |
| |
| /// Returns the coverage information for each offset. |
| List<bool> _offsetCoverage(List<_CoverageInfo> coverageInfo, int sourceLength) { |
| final offsetCoverage = List.filled(sourceLength, false); |
| |
| // Sort coverage information by their size. |
| // Coverage information takes granularity as precedence. |
| coverageInfo.sort((a, b) => |
| (b.endOffset - b.startOffset).compareTo(a.endOffset - a.startOffset)); |
| |
| for (var range in coverageInfo) { |
| for (var i = range.startOffset; i < range.endOffset; i++) { |
| offsetCoverage[i] = range.isCovered; |
| } |
| } |
| |
| return offsetCoverage; |
| } |
| |
| class _CoverageInfo { |
| _CoverageInfo(this.startOffset, this.endOffset, this.isCovered); |
| |
| /// 0 based byte offset. |
| final int startOffset; |
| |
| /// 0 based byte offset. |
| final int endOffset; |
| |
| final bool isCovered; |
| } |
| |
| /// A covered position in a source file where [line] and [column] are 1 based. |
| class _Position { |
| _Position(this.line, this.column); |
| |
| final int line; |
| final int column; |
| |
| @override |
| int get hashCode => hash2(line, column); |
| |
| @override |
| bool operator ==(dynamic o) => |
| o is _Position && o.line == line && o.column == column; |
| } |