1 | const path = require('path');
|
2 | const libCoverage = require('istanbul-lib-coverage');
|
3 | const libSourceMaps = require('istanbul-lib-source-maps');
|
4 | const libReport = require('istanbul-lib-report');
|
5 | const libReports = require('istanbul-reports');
|
6 | const util = require('./util');
|
7 |
|
8 | const BROWSER_PLACEHOLDER = '%browser%';
|
9 |
|
10 | function checkThresholds(thresholds, summary) {
|
11 | const failedTypes = [];
|
12 |
|
13 | Object.keys(thresholds).forEach((key) => {
|
14 | const coverage = summary[key].pct;
|
15 | if (coverage < thresholds[key]) {
|
16 | failedTypes.push(key);
|
17 | }
|
18 | });
|
19 |
|
20 | return failedTypes;
|
21 | }
|
22 |
|
23 | function CoverageIstanbulReporter(baseReporterDecorator, logger, config) {
|
24 | baseReporterDecorator(this);
|
25 |
|
26 |
|
27 |
|
28 | const reporterName = 'coverage-istanbul';
|
29 | const hasTrailingReporters =
|
30 | config.reporters.slice(-1).pop() !== reporterName;
|
31 | if (hasTrailingReporters) {
|
32 | this.writeCommonMsg = () => {};
|
33 | }
|
34 |
|
35 | const log = logger.create('reporter.coverage-istanbul');
|
36 | const browserCoverage = new WeakMap();
|
37 | const coverageConfig = Object.assign({}, config.coverageIstanbulReporter);
|
38 |
|
39 | function addCoverage(coverageMap, browser) {
|
40 | const coverage = browserCoverage.get(browser);
|
41 | browserCoverage.delete(browser);
|
42 |
|
43 | if (!coverage) {
|
44 | return;
|
45 | }
|
46 |
|
47 | Object.keys(coverage).forEach((filename) => {
|
48 | const fileCoverage = coverage[filename];
|
49 | if (fileCoverage.inputSourceMap && coverageConfig.fixWebpackSourcePaths) {
|
50 | fileCoverage.inputSourceMap = util.fixWebpackSourcePaths(
|
51 | fileCoverage.inputSourceMap,
|
52 | config.webpack
|
53 | );
|
54 | }
|
55 |
|
56 | if (
|
57 | coverageConfig.skipFilesWithNoCoverage &&
|
58 | Object.keys(fileCoverage.statementMap).length === 0 &&
|
59 | Object.keys(fileCoverage.fnMap).length === 0 &&
|
60 | Object.keys(fileCoverage.branchMap).length === 0
|
61 | ) {
|
62 | log.debug(`File [${filename}] ignored, nothing could be mapped`);
|
63 | } else {
|
64 | coverageMap.addFileCoverage(fileCoverage);
|
65 | }
|
66 | });
|
67 | }
|
68 |
|
69 | function logThresholdMessage(thresholds, message) {
|
70 | if (thresholds.emitWarning) {
|
71 | log.warn(message);
|
72 | } else {
|
73 | log.error(message);
|
74 | }
|
75 | }
|
76 |
|
77 | async function createReport(browserOrBrowsers, results) {
|
78 | const reportConfigOverride =
|
79 | !coverageConfig.combineBrowserReports && coverageConfig.dir
|
80 | ? {
|
81 | dir: coverageConfig.dir.replace(
|
82 | BROWSER_PLACEHOLDER,
|
83 | browserOrBrowsers.name
|
84 | ),
|
85 | }
|
86 | : {};
|
87 |
|
88 | const reportConfig = {
|
89 | instrumentation: Object.assign({}, coverageConfig.instrumentation),
|
90 | verbose: coverageConfig.verbose === true,
|
91 | reporting: Object.assign({}, coverageConfig, reportConfigOverride),
|
92 | summarizer: coverageConfig.summarizer,
|
93 | };
|
94 | const reportTypes = reportConfig.reporting.reports;
|
95 |
|
96 | const coverageMap = libCoverage.createCoverageMap();
|
97 | const sourceMapStore = libSourceMaps.createSourceMapStore();
|
98 |
|
99 | if (coverageConfig.combineBrowserReports) {
|
100 | browserOrBrowsers.forEach((browser) => addCoverage(coverageMap, browser));
|
101 | } else {
|
102 | addCoverage(coverageMap, browserOrBrowsers);
|
103 | }
|
104 |
|
105 | const remappedCoverageMap = await sourceMapStore.transformCoverage(
|
106 | coverageMap
|
107 | );
|
108 | const { sourceFinder } = sourceMapStore;
|
109 |
|
110 | if (!coverageConfig.skipFilesWithNoCoverage) {
|
111 |
|
112 | const fixedFilePaths = {};
|
113 | remappedCoverageMap.files().forEach((path) => {
|
114 | fixedFilePaths[util.fixPathSeparators(path)] = true;
|
115 | });
|
116 | coverageMap.files().forEach((path) => {
|
117 | if (!(util.fixPathSeparators(path) in fixedFilePaths)) {
|
118 |
|
119 | remappedCoverageMap.addFileCoverage(path);
|
120 | }
|
121 | });
|
122 | }
|
123 |
|
124 | log.debug('Writing coverage reports:', reportTypes);
|
125 | writeReports(reportConfig, remappedCoverageMap, sourceFinder);
|
126 |
|
127 | const userThresholds = coverageConfig.thresholds;
|
128 |
|
129 | const thresholds = {
|
130 | emitWarning: false,
|
131 | global: {
|
132 | statements: 0,
|
133 | lines: 0,
|
134 | branches: 0,
|
135 | functions: 0,
|
136 | },
|
137 | each: {
|
138 | statements: 0,
|
139 | lines: 0,
|
140 | branches: 0,
|
141 | functions: 0,
|
142 | overrides: {},
|
143 | },
|
144 | };
|
145 |
|
146 | if (userThresholds) {
|
147 | if (userThresholds.global || userThresholds.each) {
|
148 | Object.assign(thresholds.global, userThresholds.global);
|
149 | Object.assign(thresholds.each, userThresholds.each);
|
150 | if (userThresholds.emitWarning === true) {
|
151 | thresholds.emitWarning = true;
|
152 | }
|
153 | } else {
|
154 | Object.assign(thresholds.global, userThresholds);
|
155 | }
|
156 | }
|
157 |
|
158 | let thresholdCheckFailed = false;
|
159 |
|
160 |
|
161 | const globalSummary = remappedCoverageMap.getCoverageSummary();
|
162 | const failedGlobalTypes = checkThresholds(thresholds.global, globalSummary);
|
163 | failedGlobalTypes.forEach((type) => {
|
164 | thresholdCheckFailed = true;
|
165 | logThresholdMessage(
|
166 | thresholds,
|
167 | `Coverage for ${type} (${globalSummary[type].pct}%) does not meet global threshold (${thresholds.global[type]}%)`
|
168 | );
|
169 | });
|
170 |
|
171 | remappedCoverageMap.files().forEach((file) => {
|
172 | const fileThresholds = Object.assign(
|
173 | {},
|
174 | thresholds.each,
|
175 | util.overrideThresholds(
|
176 | file,
|
177 | thresholds.each.overrides,
|
178 | config.basePath
|
179 | )
|
180 | );
|
181 | delete fileThresholds.overrides;
|
182 | const fileSummary = remappedCoverageMap.fileCoverageFor(file).toSummary()
|
183 | .data;
|
184 | const failedFileTypes = checkThresholds(fileThresholds, fileSummary);
|
185 |
|
186 | failedFileTypes.forEach((type) => {
|
187 | thresholdCheckFailed = true;
|
188 | if (coverageConfig.fixWebpackSourcePaths) {
|
189 | file = util.fixWebpackFilePath(file);
|
190 | }
|
191 |
|
192 | logThresholdMessage(
|
193 | thresholds,
|
194 | `Coverage for ${type} (${fileSummary[type].pct}%) in file ${file} does not meet per file threshold (${fileThresholds[type]}%)`
|
195 | );
|
196 | });
|
197 | });
|
198 |
|
199 | if (thresholdCheckFailed && results && !thresholds.emitWarning) {
|
200 | results.exitCode = 1;
|
201 | }
|
202 | }
|
203 |
|
204 | function writeReports(config, coverageMap, sourceFinder) {
|
205 | const dir = path.resolve(config.reporting.dir);
|
206 | const contextOptions = {
|
207 | dir,
|
208 | watermarks: config.reporting.watermarks || [],
|
209 | sourceFinder,
|
210 | coverageMap,
|
211 | };
|
212 | const context = libReport.createContext(contextOptions);
|
213 |
|
214 | const tree = context.getTree(config.summarizer);
|
215 | config.reporting.reports.forEach((name) => {
|
216 | const reportConfig = (config.reporting['report-config'] || [])[name];
|
217 | const report = libReports.create(name, reportConfig);
|
218 | tree.visit(report, context);
|
219 | });
|
220 | }
|
221 |
|
222 | this.onBrowserComplete = function (browser, result) {
|
223 | if (result && result.coverage) {
|
224 | browserCoverage.set(browser, result.coverage);
|
225 | }
|
226 | };
|
227 |
|
228 | const baseReporterOnRunComplete = this.onRunComplete;
|
229 | this.onRunComplete = async function (browsers, results) {
|
230 | Reflect.apply(baseReporterOnRunComplete, this, arguments);
|
231 |
|
232 | if (coverageConfig.combineBrowserReports) {
|
233 | await createReport(browsers, results);
|
234 | } else {
|
235 | await Promise.all(
|
236 | browsers.map((browser) => createReport(browser, results))
|
237 | );
|
238 | }
|
239 | };
|
240 | }
|
241 |
|
242 | CoverageIstanbulReporter.$inject = [
|
243 | 'baseReporterDecorator',
|
244 | 'logger',
|
245 | 'config',
|
246 | ];
|
247 |
|
248 | module.exports = {
|
249 | 'reporter:coverage-istanbul': ['type', CoverageIstanbulReporter],
|
250 | };
|