monocart-coverage-reports 2.13.2 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +314 -116
- package/README.zh-Hans.md +315 -117
- package/lib/assets.js +6 -6
- package/lib/cli.js +191 -95
- package/lib/client/cdp-client.js +74 -43
- package/lib/client/coverage-client.js +24 -8
- package/lib/client/ws-session.js +115 -37
- package/lib/converter/ast-visitor.js +5 -7
- package/lib/converter/ast.js +29 -8
- package/lib/converter/collect-source-maps.js +42 -21
- package/lib/converter/converter.js +124 -129
- package/lib/converter/css-coverage.js +76 -0
- package/lib/converter/fake-source-coverage.js +64 -0
- package/lib/converter/find-original-range.js +214 -6
- package/lib/converter/flatten-source-maps.js +2 -4
- package/lib/converter/ignore.js +2 -4
- package/lib/converter/info-branch.js +5 -3
- package/lib/converter/info-function.js +5 -3
- package/lib/converter/info-statement.js +5 -3
- package/lib/converter/range-index.js +78 -0
- package/lib/converter/source-range-merge.js +103 -0
- package/lib/converter/untested.js +30 -60
- package/lib/default/options.js +13 -1
- package/lib/generate.js +44 -26
- package/lib/index.d.ts +52 -4
- package/lib/index.js +35 -41
- package/lib/istanbul/istanbul-summary.js +2 -2
- package/lib/istanbul/istanbul.js +35 -13
- package/lib/packages/monocart-coverage-assets.js +4 -3
- package/lib/packages/monocart-coverage-converter.js +27 -0
- package/lib/packages/monocart-coverage-vendor.js +2 -41
- package/lib/packages/monocart-coverage-vendor.js.LICENSE +3 -221
- package/lib/platform/concurrency.js +47 -43
- package/lib/platform/share.js +8 -1
- package/lib/register/hooks.js +19 -13
- package/lib/register/register.js +5 -3
- package/lib/reports/codacy.js +3 -5
- package/lib/reports/codecov.js +3 -5
- package/lib/reports/console-details.js +5 -5
- package/lib/reports/console-summary.js +4 -4
- package/lib/reports/custom.js +8 -24
- package/lib/reports/markdown-details.js +4 -6
- package/lib/reports/markdown-summary.js +4 -6
- package/lib/reports/raw.js +10 -12
- package/lib/reports/v8-json.js +3 -5
- package/lib/reports/v8.js +3 -5
- package/lib/utils/dedupe.js +1 -1
- package/lib/utils/gc.js +30 -14
- package/lib/utils/markdown.js +2 -2
- package/lib/utils/merge/merge.js +2 -4
- package/lib/utils/merge/range-tree.js +1 -1
- package/lib/utils/request.js +85 -73
- package/lib/utils/snapshot.js +4 -4
- package/lib/utils/source-path.js +26 -11
- package/lib/utils/util.js +67 -42
- package/lib/v8/v8-summary.js +2 -4
- package/lib/v8/v8.js +11 -11
- package/migrate-to-3.md +175 -0
- package/package.json +54 -42
- package/skills/monocart-coverage-reports/SKILL.md +106 -0
- package/skills/monocart-coverage-reports/references/best-practices.md +218 -0
- package/skills/monocart-coverage-reports/references/cli.md +108 -0
- package/skills/monocart-coverage-reports/references/collection.md +154 -0
- package/skills/monocart-coverage-reports/references/filtering-and-sourcemaps.md +170 -0
- package/skills/monocart-coverage-reports/references/reports-and-ci.md +219 -0
- package/skills/monocart-coverage-reports/references/repository-map.md +59 -0
- package/lib/index.mjs +0 -9
- package/lib/register/hooks.mjs +0 -3
- package/lib/register/register.mjs +0 -3
|
@@ -4,27 +4,31 @@
|
|
|
4
4
|
* @author cenfun@gmail.com
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
import EC from 'eight-colors';
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
import { Locator } from 'monocart-locator';
|
|
10
|
+
import { Util } from '../utils/util.js';
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
import { findOriginalRange } from './find-original-range.js';
|
|
13
|
+
import { isUncoveredRange } from './source-range-merge.js';
|
|
14
|
+
import { mergeCssBytes } from './css-coverage.js';
|
|
15
|
+
import { handleUncoveredInCovered } from './fake-source-coverage.js';
|
|
16
|
+
import { getJsAstInfo, getCssAstInfo } from './ast.js';
|
|
17
|
+
import { getIgnoredRanges } from './ignore.js';
|
|
15
18
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const {
|
|
19
|
+
import { getUntestedList } from './untested.js';
|
|
20
|
+
import {
|
|
19
21
|
sortRanges, dedupeCountRanges, mergeRangesWith
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
+
} from '../utils/dedupe.js';
|
|
23
|
+
import {
|
|
24
|
+
getSourceType, initSourceMapSourcesPath, cssTypes
|
|
25
|
+
} from '../utils/source-path.js';
|
|
22
26
|
|
|
23
|
-
|
|
27
|
+
import { decode } from '@jridgewell/sourcemap-codec';
|
|
24
28
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
import { InfoBranch } from './info-branch.js';
|
|
30
|
+
import { InfoFunction } from './info-function.js';
|
|
31
|
+
import { InfoStatement } from './info-statement.js';
|
|
28
32
|
|
|
29
33
|
// ========================================================================================================
|
|
30
34
|
|
|
@@ -212,7 +216,7 @@ const calculateV8Summary = (list) => {
|
|
|
212
216
|
* * `f` - hit count for functions
|
|
213
217
|
* * `b` - hit count for branches
|
|
214
218
|
*/
|
|
215
|
-
const collectFileCoverage = (v8Data, state, options) => {
|
|
219
|
+
const collectFileCoverage = (v8Data, state, options, needsIstanbul) => {
|
|
216
220
|
|
|
217
221
|
const {
|
|
218
222
|
bytes,
|
|
@@ -220,7 +224,6 @@ const collectFileCoverage = (v8Data, state, options) => {
|
|
|
220
224
|
branches,
|
|
221
225
|
statements,
|
|
222
226
|
|
|
223
|
-
sourcePath,
|
|
224
227
|
locator
|
|
225
228
|
} = state;
|
|
226
229
|
|
|
@@ -313,7 +316,19 @@ const collectFileCoverage = (v8Data, state, options) => {
|
|
|
313
316
|
v8Data.summary = summary;
|
|
314
317
|
|
|
315
318
|
// ==========================================
|
|
316
|
-
//
|
|
319
|
+
// Istanbul location maps are not needed by V8-only reports. Keep the
|
|
320
|
+
// V8 ranges, counts and summaries above identical in either mode.
|
|
321
|
+
if (!needsIstanbul) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return collectIstanbulCoverage(state);
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const collectIstanbulCoverage = (state) => {
|
|
329
|
+
const {
|
|
330
|
+
sourcePath, locator, statements, functions, branches
|
|
331
|
+
} = state;
|
|
317
332
|
const istanbulData = {
|
|
318
333
|
path: sourcePath,
|
|
319
334
|
|
|
@@ -818,16 +833,71 @@ const handleOriginalEmptyBytesCoverage = (state, originalStateMap) => {
|
|
|
818
833
|
});
|
|
819
834
|
};
|
|
820
835
|
|
|
821
|
-
|
|
822
836
|
const handleAllOriginalBytesCoverage = (state, originalStateMap) => {
|
|
823
837
|
|
|
824
|
-
const {
|
|
838
|
+
const {
|
|
839
|
+
js, coverageList, type
|
|
840
|
+
} = state;
|
|
841
|
+
|
|
842
|
+
if (cssTypes.has(type)) {
|
|
843
|
+
// An intentional exclusion is not a broken source map, even when all
|
|
844
|
+
// sources were excluded and there is nothing left to map.
|
|
845
|
+
if (state.filteredSourceIndexes.size > 0 && state.filteredSourceIndexes.size === state.sourceMap.sources.length) {
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
const positionMap = new Map();
|
|
849
|
+
const couldMapAllRanges = coverageList.every((range) => {
|
|
850
|
+
const {
|
|
851
|
+
start, end, originalState, error, filtered
|
|
852
|
+
} = findOriginalRange(range.start, range.end, state, originalStateMap, {
|
|
853
|
+
fixOriginalRange: true
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
if (filtered) {
|
|
857
|
+
return true;
|
|
858
|
+
}
|
|
859
|
+
if (error) {
|
|
860
|
+
return false;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// Offsets are local to each source, not unique across the bundle.
|
|
864
|
+
let positions = positionMap.get(originalState);
|
|
865
|
+
if (!positions) {
|
|
866
|
+
positions = new Set();
|
|
867
|
+
positionMap.set(originalState, positions);
|
|
868
|
+
}
|
|
869
|
+
const positionKey = `${start}-${end}`;
|
|
870
|
+
if (positions.has(positionKey)) {
|
|
871
|
+
return false;
|
|
872
|
+
}
|
|
873
|
+
positions.add(positionKey);
|
|
874
|
+
originalState.bytes.push({
|
|
875
|
+
start, end, count: range.count
|
|
876
|
+
});
|
|
877
|
+
return true;
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
// make sure every range mapped correctly,
|
|
881
|
+
// otherwise discard changes and fall back to dist file to show at least some meaningful coverage.
|
|
882
|
+
// Sass `@include` is a common case: one generated rule maps to both the
|
|
883
|
+
// call site and the mixin body, which cannot be one contiguous original
|
|
884
|
+
// range. See "Sass mixins and @include" in README.
|
|
885
|
+
if (!couldMapAllRanges) {
|
|
886
|
+
Util.logDebug(`Could not map all ranges, fallback to dist file for ${EC.yellow(state.sourcePath)}`);
|
|
887
|
+
state.v8Data.dedupe = false;
|
|
888
|
+
// The fallback is now a real report file, not a debug-only copy.
|
|
889
|
+
delete state.v8Data.debug;
|
|
890
|
+
state.originalList = [];
|
|
891
|
+
originalStateMap.clear();
|
|
892
|
+
generateCoverageForDist(state);
|
|
893
|
+
}
|
|
825
894
|
|
|
826
|
-
// only for js, no sourcemap for css for now
|
|
827
|
-
if (!js) {
|
|
828
895
|
return;
|
|
829
896
|
}
|
|
830
897
|
|
|
898
|
+
if (!js) {
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
831
901
|
// v8 coverage
|
|
832
902
|
coverageList.forEach((block) => {
|
|
833
903
|
block.ranges.forEach((range) => {
|
|
@@ -1033,7 +1103,6 @@ const getOriginalDecodedMappings = (originalDecodedMap, sourceIndex, locator) =>
|
|
|
1033
1103
|
};
|
|
1034
1104
|
|
|
1035
1105
|
// ========================================================================================================
|
|
1036
|
-
|
|
1037
1106
|
const initOriginalList = (state, originalDecodedMap, options) => {
|
|
1038
1107
|
|
|
1039
1108
|
// source filter
|
|
@@ -1041,6 +1110,9 @@ const initOriginalList = (state, originalDecodedMap, options) => {
|
|
|
1041
1110
|
|
|
1042
1111
|
// create original content mappings
|
|
1043
1112
|
const originalStateMap = new Map();
|
|
1113
|
+
if (!state.js) {
|
|
1114
|
+
state.filteredSourceIndexes = new Set();
|
|
1115
|
+
}
|
|
1044
1116
|
|
|
1045
1117
|
const { sources, sourcesContent } = state.sourceMap;
|
|
1046
1118
|
|
|
@@ -1052,7 +1124,7 @@ const initOriginalList = (state, originalDecodedMap, options) => {
|
|
|
1052
1124
|
// filter
|
|
1053
1125
|
// do not change for sourceIndex
|
|
1054
1126
|
if (!sourceFilter(sourcePath)) {
|
|
1055
|
-
|
|
1127
|
+
state.filteredSourceIndexes?.add(sourceIndex);
|
|
1056
1128
|
return;
|
|
1057
1129
|
}
|
|
1058
1130
|
// console.log(sourcePath);
|
|
@@ -1069,14 +1141,12 @@ const initOriginalList = (state, originalDecodedMap, options) => {
|
|
|
1069
1141
|
|
|
1070
1142
|
const decodedMappings = getOriginalDecodedMappings(originalDecodedMap, sourceIndex, locator);
|
|
1071
1143
|
|
|
1072
|
-
// unpacked file always is js
|
|
1073
|
-
|
|
1074
1144
|
const type = getSourceType(sourcePath);
|
|
1075
1145
|
|
|
1076
1146
|
const originalState = {
|
|
1077
1147
|
original: true,
|
|
1078
|
-
//
|
|
1079
|
-
js:
|
|
1148
|
+
// Only known style types are CSS; preserve JS coverage for all other sources.
|
|
1149
|
+
js: !cssTypes.has(type),
|
|
1080
1150
|
type,
|
|
1081
1151
|
source: sourceContent,
|
|
1082
1152
|
sourcePath,
|
|
@@ -1136,6 +1206,12 @@ const collectOriginalList = (state, originalStateMap) => {
|
|
|
1136
1206
|
source
|
|
1137
1207
|
};
|
|
1138
1208
|
|
|
1209
|
+
// Sourcemap gaps must not count as covered bytes for untested files.
|
|
1210
|
+
// Keep the marker so real coverage can replace this entry during merge.
|
|
1211
|
+
if (state.v8Data.empty) {
|
|
1212
|
+
sourceItem.empty = true;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1139
1215
|
// save v8 data and add to originalList
|
|
1140
1216
|
originalState.v8Data = sourceItem;
|
|
1141
1217
|
state.originalList.push(originalState);
|
|
@@ -1157,56 +1233,6 @@ const generateCoverageForDist = (state) => {
|
|
|
1157
1233
|
|
|
1158
1234
|
};
|
|
1159
1235
|
|
|
1160
|
-
const handleUncoveredInCovered = (state, originalStateMap) => {
|
|
1161
|
-
if (!state.fake) {
|
|
1162
|
-
return;
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
// fake statements/branches/functions
|
|
1166
|
-
originalStateMap.forEach((originalState) => {
|
|
1167
|
-
const { bytes } = originalState;
|
|
1168
|
-
const list = [];
|
|
1169
|
-
originalState.bytes = bytes.filter((item) => {
|
|
1170
|
-
if (item.count === 0) {
|
|
1171
|
-
const range = bytes.find((it) => {
|
|
1172
|
-
if (it.count === 0) {
|
|
1173
|
-
return false;
|
|
1174
|
-
}
|
|
1175
|
-
if (it.start > item.end) {
|
|
1176
|
-
return false;
|
|
1177
|
-
}
|
|
1178
|
-
if (it.end < item.start) {
|
|
1179
|
-
return false;
|
|
1180
|
-
}
|
|
1181
|
-
return true;
|
|
1182
|
-
});
|
|
1183
|
-
if (range) {
|
|
1184
|
-
list.push(item);
|
|
1185
|
-
return false;
|
|
1186
|
-
}
|
|
1187
|
-
}
|
|
1188
|
-
return true;
|
|
1189
|
-
});
|
|
1190
|
-
|
|
1191
|
-
if (!list.length) {
|
|
1192
|
-
return;
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
list.forEach((item) => {
|
|
1196
|
-
['statements', 'branches', 'functions'].forEach((key) => {
|
|
1197
|
-
originalState[key] = originalState[key].filter((it) => {
|
|
1198
|
-
if (it.start === item.start && it.end === item.end) {
|
|
1199
|
-
return false;
|
|
1200
|
-
}
|
|
1201
|
-
return true;
|
|
1202
|
-
});
|
|
1203
|
-
});
|
|
1204
|
-
});
|
|
1205
|
-
|
|
1206
|
-
});
|
|
1207
|
-
|
|
1208
|
-
};
|
|
1209
|
-
|
|
1210
1236
|
const unpackSourceMap = (state, options) => {
|
|
1211
1237
|
|
|
1212
1238
|
const { sourceMap, sourcePath } = state;
|
|
@@ -1525,44 +1551,6 @@ const convertCoverages = (list, options, untested) => {
|
|
|
1525
1551
|
|
|
1526
1552
|
// ========================================================================================================
|
|
1527
1553
|
|
|
1528
|
-
const isUncoveredRange = (range, key, uncoveredGroups) => {
|
|
1529
|
-
|
|
1530
|
-
let uncovered = true;
|
|
1531
|
-
uncoveredGroups.forEach((group) => {
|
|
1532
|
-
const { groupMap, state } = group;
|
|
1533
|
-
|
|
1534
|
-
// ========================================================
|
|
1535
|
-
// self key
|
|
1536
|
-
if (groupMap.has(key)) {
|
|
1537
|
-
return;
|
|
1538
|
-
}
|
|
1539
|
-
|
|
1540
|
-
// ========================================================
|
|
1541
|
-
// in all group
|
|
1542
|
-
for (const item of groupMap.values()) {
|
|
1543
|
-
if (range.start >= item.start && range.end <= item.end) {
|
|
1544
|
-
return;
|
|
1545
|
-
}
|
|
1546
|
-
}
|
|
1547
|
-
|
|
1548
|
-
// ========================================================
|
|
1549
|
-
// no sourcemap mappings in range
|
|
1550
|
-
// check original range in decodedMappings
|
|
1551
|
-
const { decodedMappings } = state;
|
|
1552
|
-
const item = decodedMappings.find((it) => it.originalOffset >= range.start && it.originalOffset <= range.end);
|
|
1553
|
-
if (!item) {
|
|
1554
|
-
return;
|
|
1555
|
-
}
|
|
1556
|
-
|
|
1557
|
-
// ========================================================
|
|
1558
|
-
uncovered = false;
|
|
1559
|
-
|
|
1560
|
-
});
|
|
1561
|
-
|
|
1562
|
-
return uncovered;
|
|
1563
|
-
|
|
1564
|
-
};
|
|
1565
|
-
|
|
1566
1554
|
const mergeV8Data = (state, stateList) => {
|
|
1567
1555
|
// console.log(stateList);
|
|
1568
1556
|
|
|
@@ -1747,7 +1735,7 @@ const addUntestedFiles = async (stateList, options) => {
|
|
|
1747
1735
|
|
|
1748
1736
|
};
|
|
1749
1737
|
|
|
1750
|
-
const generateV8DataList = (stateList, options) => {
|
|
1738
|
+
const generateV8DataList = (stateList, options, needsIstanbul) => {
|
|
1751
1739
|
|
|
1752
1740
|
const stateMap = new Map();
|
|
1753
1741
|
|
|
@@ -1770,8 +1758,14 @@ const generateV8DataList = (stateList, options) => {
|
|
|
1770
1758
|
// exists item
|
|
1771
1759
|
const prevState = stateMap.get(id);
|
|
1772
1760
|
if (prevState) {
|
|
1773
|
-
//
|
|
1774
|
-
|
|
1761
|
+
// Empty entries and debug copies must not replace real original
|
|
1762
|
+
// coverage, nor contribute a duplicate execution to its merge.
|
|
1763
|
+
if (!prevState.v8Data.empty && !prevState.v8Data.debug) {
|
|
1764
|
+
// An untested build may contain different mapped AST nodes
|
|
1765
|
+
// (for example ES5 helpers). It must not alter real metrics.
|
|
1766
|
+
if (v8Data.empty) {
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1775
1769
|
if (mergeMap.has(id)) {
|
|
1776
1770
|
mergeMap.get(id).push(originalState);
|
|
1777
1771
|
} else {
|
|
@@ -1790,7 +1784,7 @@ const generateV8DataList = (stateList, options) => {
|
|
|
1790
1784
|
if (state.js) {
|
|
1791
1785
|
mergeV8Data(state, mergeMap.get(id));
|
|
1792
1786
|
} else {
|
|
1793
|
-
|
|
1787
|
+
state.bytes = mergeCssBytes(mergeMap.get(id).map((it) => it.bytes));
|
|
1794
1788
|
}
|
|
1795
1789
|
}
|
|
1796
1790
|
|
|
@@ -1801,11 +1795,13 @@ const generateV8DataList = (stateList, options) => {
|
|
|
1801
1795
|
const coverageData = {};
|
|
1802
1796
|
stateMap.forEach((state) => {
|
|
1803
1797
|
const { v8Data } = state;
|
|
1804
|
-
const istanbulData = collectFileCoverage(v8Data, state, options);
|
|
1805
|
-
const { sourcePath, source } = v8Data;
|
|
1798
|
+
const istanbulData = collectFileCoverage(v8Data, state, options, needsIstanbul);
|
|
1806
1799
|
v8DataList.push(v8Data);
|
|
1807
|
-
|
|
1808
|
-
|
|
1800
|
+
if (needsIstanbul) {
|
|
1801
|
+
const { sourcePath, source } = v8Data;
|
|
1802
|
+
fileSources[sourcePath] = source;
|
|
1803
|
+
coverageData[sourcePath] = istanbulData;
|
|
1804
|
+
}
|
|
1809
1805
|
});
|
|
1810
1806
|
|
|
1811
1807
|
// sort v8DataList (files)
|
|
@@ -1823,7 +1819,8 @@ const generateV8DataList = (stateList, options) => {
|
|
|
1823
1819
|
};
|
|
1824
1820
|
};
|
|
1825
1821
|
|
|
1826
|
-
|
|
1822
|
+
// Direct users of the converter retain the full result by default.
|
|
1823
|
+
const convertV8List = async (v8list, options, needsIstanbul = true) => {
|
|
1827
1824
|
|
|
1828
1825
|
// for tested files
|
|
1829
1826
|
const stateList = convertCoverages(v8list, options);
|
|
@@ -1832,13 +1829,11 @@ const convertV8List = async (v8list, options) => {
|
|
|
1832
1829
|
await addUntestedFiles(stateList, options);
|
|
1833
1830
|
|
|
1834
1831
|
const time_start_convert = Date.now();
|
|
1835
|
-
const dataList = generateV8DataList(stateList, options);
|
|
1832
|
+
const dataList = generateV8DataList(stateList, options, needsIstanbul);
|
|
1836
1833
|
const dataFiles = EC.cyan(`${dataList.v8DataList.length} files`);
|
|
1837
1834
|
Util.logTime(`${EC.magenta('│ ')}${EC.cyan('├')} [convert] converted data list (${dataFiles})`, time_start_convert);
|
|
1838
1835
|
|
|
1839
1836
|
return dataList;
|
|
1840
1837
|
};
|
|
1841
1838
|
|
|
1842
|
-
|
|
1843
|
-
convertV8List
|
|
1844
|
-
};
|
|
1839
|
+
export { convertV8List };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const getCssRangePoints = (groups) => {
|
|
2
|
+
const points = [];
|
|
3
|
+
groups.forEach((ranges) => {
|
|
4
|
+
ranges.forEach(({
|
|
5
|
+
start, end, count
|
|
6
|
+
}) => {
|
|
7
|
+
if (start >= end) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
const covered = count > 0 ? 1 : 0;
|
|
11
|
+
const uncovered = count > 0 ? 0 : 1;
|
|
12
|
+
points.push({
|
|
13
|
+
offset: start, covered, uncovered
|
|
14
|
+
}, {
|
|
15
|
+
offset: end, covered: -covered, uncovered: -uncovered
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
return points.sort((a, b) => a.offset - b.offset);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const appendCssRange = (merged, start, end, count) => {
|
|
23
|
+
const last = merged[merged.length - 1];
|
|
24
|
+
if (last && last.end === start && last.count === count) {
|
|
25
|
+
last.end = end;
|
|
26
|
+
} else {
|
|
27
|
+
merged.push({
|
|
28
|
+
start, end, count
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const sweepCssBytes = (groups, uncoveredWins) => {
|
|
34
|
+
const points = getCssRangePoints(groups);
|
|
35
|
+
const merged = [];
|
|
36
|
+
let start = 0;
|
|
37
|
+
let covered = 0;
|
|
38
|
+
let uncovered = 0;
|
|
39
|
+
for (const point of points) {
|
|
40
|
+
const end = point.offset;
|
|
41
|
+
if (start < end && (covered > 0 || uncovered > 0)) {
|
|
42
|
+
const count = uncoveredWins ? Number(uncovered === 0) : Number(covered > 0);
|
|
43
|
+
appendCssRange(merged, start, end, count);
|
|
44
|
+
}
|
|
45
|
+
covered += point.covered;
|
|
46
|
+
uncovered += point.uncovered;
|
|
47
|
+
start = end;
|
|
48
|
+
}
|
|
49
|
+
return merged;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const isSortedDisjoint = (ranges) => {
|
|
53
|
+
let end = -Infinity;
|
|
54
|
+
for (const range of ranges) {
|
|
55
|
+
if (!(range.start >= end && range.start < range.end)) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
end = range.end;
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Within one bundle an uncovered child overrides its covered wrapper. Only
|
|
64
|
+
// after that interpretation may coverage from separate bundles be unioned.
|
|
65
|
+
// Unmapped gaps stay absent from this union; this function does not turn them
|
|
66
|
+
// into covered ranges. Byte summaries still derive covered bytes from total
|
|
67
|
+
// length minus explicit uncovered ranges, so gaps without an uncovered range
|
|
68
|
+
// are reported as covered. Use the line/code views when gaps matter.
|
|
69
|
+
const mergeCssBytes = (groups) => {
|
|
70
|
+
// Ordered, disjoint ranges already have unambiguous per-bundle coverage.
|
|
71
|
+
// Reuse them without allocating intermediate events and normalized ranges.
|
|
72
|
+
const normalized = groups.map((ranges) => (isSortedDisjoint(ranges) ? ranges : sweepCssBytes([ranges], true)));
|
|
73
|
+
return sweepCssBytes(normalized, false);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export { mergeCssBytes };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const rangeKey = (range) => `${range.start}_${range.end}`;
|
|
2
|
+
|
|
3
|
+
const handleUncoveredInCovered = (state, originalStateMap) => {
|
|
4
|
+
if (!state.fake) {
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
originalStateMap.forEach((originalState) => {
|
|
9
|
+
const { bytes } = originalState;
|
|
10
|
+
// The old check treats every non-zero count as covered, not just > 0.
|
|
11
|
+
const covered = bytes.filter((range) => range.count !== 0);
|
|
12
|
+
if (!covered.length || covered.length === bytes.length) {
|
|
13
|
+
originalState.bytes = bytes.slice();
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Sort a separate array so byte order and original range objects stay
|
|
18
|
+
// unchanged. Prefix maxima also handle nested/overlapping coverage.
|
|
19
|
+
covered.sort((a, b) => a.start - b.start);
|
|
20
|
+
const starts = new Float64Array(covered.length);
|
|
21
|
+
const maxEnds = new Float64Array(covered.length);
|
|
22
|
+
let maxEnd = -Infinity;
|
|
23
|
+
for (let i = 0; i < covered.length; i++) {
|
|
24
|
+
starts[i] = covered[i].start;
|
|
25
|
+
maxEnd = Math.max(maxEnd, covered[i].end);
|
|
26
|
+
maxEnds[i] = maxEnd;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const removed = new Set();
|
|
30
|
+
originalState.bytes = bytes.filter((range) => {
|
|
31
|
+
if (range.count !== 0) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
let low = 0;
|
|
35
|
+
let high = starts.length;
|
|
36
|
+
while (low < high) {
|
|
37
|
+
const mid = Math.floor((low + high) / 2);
|
|
38
|
+
// Preserve the original inclusive overlap test: touching
|
|
39
|
+
// endpoints count as overlap even though ranges are half-open.
|
|
40
|
+
if (starts[mid] <= range.end) {
|
|
41
|
+
low = mid + 1;
|
|
42
|
+
} else {
|
|
43
|
+
high = mid;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (low > 0 && maxEnds[low - 1] >= range.start) {
|
|
47
|
+
removed.add(rangeKey(range));
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (!removed.size) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
// Remove exact start/end matches, not every overlapping AST range.
|
|
57
|
+
// Each metadata array is filtered once, regardless of removal count.
|
|
58
|
+
for (const key of ['statements', 'branches', 'functions']) {
|
|
59
|
+
originalState[key] = originalState[key].filter((range) => !removed.has(rangeKey(range)));
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export { handleUncoveredInCovered };
|