• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

dart-lang / coverage / 4841411363

pending completion
4841411363

push

github

GitHub
Run no response bot daily (#440)

550 of 589 relevant lines covered (93.38%)

3.78 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.78
/lib/src/hitmap.dart
1
// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
2
// for details. All rights reserved. Use of this source code is governed by a
3
// BSD-style license that can be found in the LICENSE file.
4

5
import 'dart:convert' show json;
6
import 'dart:io';
7

8
import 'package:coverage/src/resolver.dart';
9
import 'package:coverage/src/util.dart';
10

11
/// Contains line and function hit information for a single script.
12
class HitMap {
13
  /// Constructs a HitMap.
14
  HitMap([
7✔
15
    Map<int, int>? lineHits,
16
    this.funcHits,
17
    this.funcNames,
18
    this.branchHits,
19
  ]) : lineHits = lineHits ?? {};
7✔
20

21
  /// Map from line to hit count for that line.
22
  final Map<int, int> lineHits;
23

24
  /// Map from the first line of each function, to the hit count for that
25
  /// function. Null if function coverage info was not gathered.
26
  Map<int, int>? funcHits;
27

28
  /// Map from the first line of each function, to the function name. Null if
29
  /// function coverage info was not gathered.
30
  Map<int, String>? funcNames;
31

32
  /// Map from branch line, to the hit count for that branch. Null if branch
33
  /// coverage info was not gathered.
34
  Map<int, int>? branchHits;
35

36
  /// Creates a single hitmap from a raw json object.
37
  ///
38
  /// Note that when [checkIgnoredLines] is `true` all files will be
39
  /// read to get ignore comments. This will add some overhead.
40
  /// To combat this when calling this function multiple times from the
41
  /// same source (e.g. test runs of different files) a cache is taken
42
  /// via [ignoredLinesInFilesCache]. If this cache contains the parsed
43
  /// data for the specific file already, the file will not be read and
44
  /// parsed again.
45
  ///
46
  /// Throws away all entries that are not resolvable.
47
  static Map<String, HitMap> parseJsonSync(
5✔
48
    List<Map<String, dynamic>> jsonResult, {
49
    required bool checkIgnoredLines,
50
    required Map<String, List<List<int>>?> ignoredLinesInFilesCache,
51
    required Resolver resolver,
52
  }) {
53
    final loader = Loader();
5✔
54

55
    // Map of source file to map of line to hit count for that line.
56
    final globalHitMap = <String, HitMap>{};
5✔
57

58
    for (var e in jsonResult) {
10✔
59
      final source = e['source'] as String?;
5✔
60
      if (source == null) {
61
        // Couldn't resolve import, so skip this entry.
62
        continue;
63
      }
64

65
      var ignoredLinesList = <List<int>>[];
5✔
66

67
      if (checkIgnoredLines) {
68
        if (ignoredLinesInFilesCache.containsKey(source)) {
2✔
69
          final List<List<int>>? cacheHit = ignoredLinesInFilesCache[source];
1✔
70
          if (cacheHit == null) {
71
            // Null-entry indicates that the whole file was ignored.
72
            continue;
73
          }
74
          ignoredLinesList = cacheHit;
75
        } else {
76
          final path = resolver.resolve(source);
2✔
77
          if (path != null) {
78
            final lines = loader.loadSync(path) ?? [];
2✔
79
            ignoredLinesList = getIgnoredLines(lines);
2✔
80

81
            // Ignore the whole file.
82
            if (ignoredLinesList.length == 1 &&
4✔
83
                ignoredLinesList[0][0] == 0 &&
6✔
84
                ignoredLinesList[0][1] == lines.length) {
8✔
85
              // Null-entry indicates that the whole file was ignored.
86
              ignoredLinesInFilesCache[source] = null;
2✔
87
              continue;
88
            }
89
            ignoredLinesInFilesCache[source] = ignoredLinesList;
2✔
90
          } else {
91
            // Couldn't resolve source. Allow cache to answer next time
92
            // anyway.
93
            ignoredLinesInFilesCache[source] = ignoredLinesList;
1✔
94
          }
95
        }
96
      }
97

98
      // Move to the first ignore range.
99
      final ignoredLines = ignoredLinesList.iterator;
5✔
100
      var hasCurrent = ignoredLines.moveNext();
5✔
101

102
      bool shouldIgnoreLine(Iterator<List<int>> ignoredRanges, int line) {
5✔
103
        if (!hasCurrent || ignoredRanges.current.isEmpty) {
4✔
104
          return false;
105
        }
106

107
        if (line < ignoredRanges.current[0]) return false;
6✔
108

109
        while (hasCurrent &&
110
            ignoredRanges.current.isNotEmpty &&
4✔
111
            ignoredRanges.current[1] < line) {
6✔
112
          hasCurrent = ignoredRanges.moveNext();
2✔
113
        }
114

115
        if (hasCurrent &&
116
            ignoredRanges.current.isNotEmpty &&
4✔
117
            ignoredRanges.current[0] <= line &&
6✔
118
            line <= ignoredRanges.current[1]) {
6✔
119
          return true;
120
        }
121

122
        return false;
123
      }
124

125
      void addToMap(Map<int, int> map, int line, int count) {
5✔
126
        final oldCount = map.putIfAbsent(line, () => 0);
10✔
127
        map[line] = count + oldCount;
10✔
128
      }
129

130
      void fillHitMap(List hits, Map<int, int> hitMap) {
5✔
131
        // Ignore line annotations require hits to be sorted.
132
        hits = _sortHits(hits);
5✔
133
        // hits is a flat array of the following format:
134
        // [ <line|linerange>, <hitcount>,...]
135
        // line: number.
136
        // linerange: '<line>-<line>'.
137
        for (var i = 0; i < hits.length; i += 2) {
15✔
138
          final k = hits[i];
5✔
139
          if (k is int) {
5✔
140
            // Single line.
141
            if (shouldIgnoreLine(ignoredLines, k)) continue;
5✔
142

143
            addToMap(hitMap, k, hits[i + 1] as int);
15✔
144
          } else if (k is String) {
×
145
            // Linerange. We expand line ranges to actual lines at this point.
146
            final splitPos = k.indexOf('-');
×
147
            final start = int.parse(k.substring(0, splitPos));
×
148
            final end = int.parse(k.substring(splitPos + 1));
×
149
            for (var j = start; j <= end; j++) {
×
150
              if (shouldIgnoreLine(ignoredLines, j)) continue;
×
151

152
              addToMap(hitMap, j, hits[i + 1] as int);
×
153
            }
154
          } else {
155
            throw StateError('Expected value of type int or String');
×
156
          }
157
        }
158
      }
159

160
      final sourceHitMap = globalHitMap.putIfAbsent(source, () => HitMap());
15✔
161
      fillHitMap(e['hits'] as List, sourceHitMap.lineHits);
15✔
162
      if (e.containsKey('funcHits')) {
5✔
163
        sourceHitMap.funcHits ??= <int, int>{};
6✔
164
        fillHitMap(e['funcHits'] as List, sourceHitMap.funcHits!);
9✔
165
      }
166
      if (e.containsKey('funcNames')) {
5✔
167
        sourceHitMap.funcNames ??= <int, String>{};
6✔
168
        final funcNames = e['funcNames'] as List;
3✔
169
        for (var i = 0; i < funcNames.length; i += 2) {
9✔
170
          sourceHitMap.funcNames![funcNames[i] as int] =
9✔
171
              funcNames[i + 1] as String;
6✔
172
        }
173
      }
174
      if (e.containsKey('branchHits')) {
5✔
175
        sourceHitMap.branchHits ??= <int, int>{};
4✔
176
        fillHitMap(e['branchHits'] as List, sourceHitMap.branchHits!);
6✔
177
      }
178
    }
179
    return globalHitMap;
180
  }
181

182
  /// Creates a single hitmap from a raw json object.
183
  ///
184
  /// Throws away all entries that are not resolvable.
185
  static Future<Map<String, HitMap>> parseJson(
5✔
186
    List<Map<String, dynamic>> jsonResult, {
187
    bool checkIgnoredLines = false,
188
    @Deprecated('Use packagePath') String? packagesPath,
189
    String? packagePath,
190
  }) async {
191
    final Resolver resolver = await Resolver.create(
5✔
192
        packagesPath: packagesPath, packagePath: packagePath);
193
    return parseJsonSync(jsonResult,
5✔
194
        checkIgnoredLines: checkIgnoredLines,
195
        ignoredLinesInFilesCache: {},
5✔
196
        resolver: resolver);
197
  }
198

199
  /// Generates a merged hitmap from a set of coverage JSON files.
200
  static Future<Map<String, HitMap>> parseFiles(
1✔
201
    Iterable<File> files, {
202
    bool checkIgnoredLines = false,
203
    @Deprecated('Use packagePath') String? packagesPath,
204
    String? packagePath,
205
  }) async {
206
    final globalHitmap = <String, HitMap>{};
1✔
207
    for (var file in files) {
2✔
208
      final contents = file.readAsStringSync();
1✔
209
      final jsonMap = json.decode(contents) as Map<String, dynamic>;
1✔
210
      if (jsonMap.containsKey('coverage')) {
1✔
211
        final jsonResult = jsonMap['coverage'] as List;
1✔
212
        globalHitmap.merge(await HitMap.parseJson(
2✔
213
          jsonResult.cast<Map<String, dynamic>>(),
1✔
214
          checkIgnoredLines: checkIgnoredLines,
215
          // ignore: deprecated_member_use_from_same_package
216
          packagesPath: packagesPath,
217
          packagePath: packagePath,
218
        ));
219
      }
220
    }
221
    return globalHitmap;
222
  }
223
}
224

225
extension FileHitMaps on Map<String, HitMap> {
226
  /// Merges [newMap] into this one.
227
  void merge(Map<String, HitMap> newMap) {
1✔
228
    newMap.forEach((file, v) {
2✔
229
      final fileResult = this[file];
1✔
230
      if (fileResult != null) {
231
        _mergeHitCounts(v.lineHits, fileResult.lineHits);
3✔
232
        if (v.funcHits != null) {
1✔
233
          fileResult.funcHits ??= <int, int>{};
1✔
234
          _mergeHitCounts(v.funcHits!, fileResult.funcHits!);
3✔
235
        }
236
        if (v.funcNames != null) {
1✔
237
          fileResult.funcNames ??= <int, String>{};
1✔
238
          v.funcNames?.forEach((line, name) {
3✔
239
            fileResult.funcNames![line] = name;
2✔
240
          });
241
        }
242
        if (v.branchHits != null) {
1✔
243
          fileResult.branchHits ??= <int, int>{};
×
244
          _mergeHitCounts(v.branchHits!, fileResult.branchHits!);
×
245
        }
246
      } else {
247
        this[file] = v;
1✔
248
      }
249
    });
250
  }
251

252
  static void _mergeHitCounts(Map<int, int> src, Map<int, int> dest) {
1✔
253
    src.forEach((line, count) {
2✔
254
      final lineFileResult = dest[line];
1✔
255
      if (lineFileResult == null) {
256
        dest[line] = count;
1✔
257
      } else {
258
        dest[line] = lineFileResult + count;
2✔
259
      }
260
    });
261
  }
262
}
263

264
/// Class containing information about a coverage hit.
265
class _HitInfo {
266
  _HitInfo(this.firstLine, this.hitRange, this.hitCount);
5✔
267

268
  /// The line number of the first line of this hit range.
269
  final int firstLine;
270

271
  /// A hit range is either a number (1 line) or a String of the form
272
  /// "start-end" (multi-line range).
273
  final dynamic hitRange;
274

275
  /// How many times this hit range was executed.
276
  final int hitCount;
277
}
278

279
/// Creates a single hitmap from a raw json object.
280
///
281
/// Throws away all entries that are not resolvable.
282
@Deprecated('Migrate to HitMap.parseJson')
1✔
283
Future<Map<String, Map<int, int>>> createHitmap(
284
  List<Map<String, dynamic>> jsonResult, {
285
  bool checkIgnoredLines = false,
286
  @Deprecated('Use packagePath') String? packagesPath,
287
  String? packagePath,
288
}) async {
289
  final result = await HitMap.parseJson(
1✔
290
    jsonResult,
291
    checkIgnoredLines: checkIgnoredLines,
292
    packagesPath: packagesPath,
293
    packagePath: packagePath,
294
  );
295
  return result.map((key, value) => MapEntry(key, value.lineHits));
4✔
296
}
297

298
/// Merges [newMap] into [result].
299
@Deprecated('Migrate to FileHitMaps.merge')
1✔
300
void mergeHitmaps(
301
    Map<String, Map<int, int>> newMap, Map<String, Map<int, int>> result) {
302
  newMap.forEach((file, v) {
2✔
303
    final fileResult = result[file];
1✔
304
    if (fileResult != null) {
305
      v.forEach((line, count) {
2✔
306
        final lineFileResult = fileResult[line];
1✔
307
        if (lineFileResult == null) {
308
          fileResult[line] = count;
1✔
309
        } else {
310
          fileResult[line] = lineFileResult + count;
2✔
311
        }
312
      });
313
    } else {
314
      result[file] = v;
1✔
315
    }
316
  });
317
}
318

319
/// Generates a merged hitmap from a set of coverage JSON files.
320
@Deprecated('Migrate to HitMap.parseFiles')
1✔
321
Future<Map<String, Map<int, int>>> parseCoverage(
322
  Iterable<File> files,
323
  int _, {
324
  bool checkIgnoredLines = false,
325
  @Deprecated('Use packagePath') String? packagesPath,
326
  String? packagePath,
327
}) async {
328
  final result = await HitMap.parseFiles(files,
1✔
329
      checkIgnoredLines: checkIgnoredLines,
330
      packagesPath: packagesPath,
331
      packagePath: packagePath);
332
  return result.map((key, value) => MapEntry(key, value.lineHits));
4✔
333
}
334

335
/// Returns a JSON hit map backward-compatible with pre-1.16.0 SDKs.
336
@Deprecated('Will be removed in 2.0.0')
×
337
Map<String, dynamic> toScriptCoverageJson(Uri scriptUri, Map<int, int> hitMap) {
338
  return hitmapToJson(HitMap(hitMap), scriptUri);
×
339
}
340

341
List<T> _flattenMap<T>(Map map) {
5✔
342
  final kvs = <T>[];
5✔
343
  map.forEach((k, v) {
10✔
344
    kvs.add(k as T);
5✔
345
    kvs.add(v as T);
5✔
346
  });
347
  return kvs;
348
}
349

350
/// Returns a JSON hit map backward-compatible with pre-1.16.0 SDKs.
351
Map<String, dynamic> hitmapToJson(HitMap hitmap, Uri scriptUri) =>
5✔
352
    <String, dynamic>{
5✔
353
      'source': '$scriptUri',
10✔
354
      'script': {
10✔
355
        'type': '@Script',
356
        'fixedId': true,
357
        'id':
358
            'libraries/1/scripts/${Uri.encodeComponent(scriptUri.toString())}',
15✔
359
        'uri': '$scriptUri',
5✔
360
        '_kind': 'library',
361
      },
362
      'hits': _flattenMap<int>(hitmap.lineHits),
15✔
363
      if (hitmap.funcHits != null)
5✔
364
        'funcHits': _flattenMap<int>(hitmap.funcHits!),
6✔
365
      if (hitmap.funcNames != null)
5✔
366
        'funcNames': _flattenMap<dynamic>(hitmap.funcNames!),
6✔
367
      if (hitmap.branchHits != null)
5✔
368
        'branchHits': _flattenMap<int>(hitmap.branchHits!),
6✔
369
    };
370

371
/// Sorts the hits array based on the line numbers.
372
List _sortHits(List hits) {
5✔
373
  final structuredHits = <_HitInfo>[];
5✔
374
  for (var i = 0; i < hits.length - 1; i += 2) {
20✔
375
    final lineOrLineRange = hits[i];
5✔
376
    final firstLineInRange = lineOrLineRange is int
5✔
377
        ? lineOrLineRange
378
        : int.parse(lineOrLineRange.split('-')[0] as String);
×
379
    structuredHits.add(_HitInfo(firstLineInRange, hits[i], hits[i + 1] as int));
25✔
380
  }
381
  structuredHits.sort((a, b) => a.firstLine.compareTo(b.firstLine));
25✔
382
  return structuredHits
383
      .map((item) => [item.hitRange, item.hitCount])
25✔
384
      .expand((item) => item)
10✔
385
      .toList();
5✔
386
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc