@backstage-community/plugin-code-coverage-backend 0.3.1 → 0.3.3
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/CHANGELOG.md +12 -0
- package/dist/index.cjs.js +4 -640
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/plugin.cjs.js +45 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/service/CodeCoverageDatabase.cjs.js +69 -0
- package/dist/service/CodeCoverageDatabase.cjs.js.map +1 -0
- package/dist/service/CoverageUtils.cjs.js +120 -0
- package/dist/service/CoverageUtils.cjs.js.map +1 -0
- package/dist/service/converter/cobertura.cjs.js +93 -0
- package/dist/service/converter/cobertura.cjs.js.map +1 -0
- package/dist/service/converter/jacoco.cjs.js +67 -0
- package/dist/service/converter/jacoco.cjs.js.map +1 -0
- package/dist/service/converter/lcov.cjs.js +97 -0
- package/dist/service/converter/lcov.cjs.js.map +1 -0
- package/dist/service/router.cjs.js +196 -0
- package/dist/service/router.cjs.js.map +1 -0
- package/package.json +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @backstage-community/plugin-code-coverage-backend
|
|
2
2
|
|
|
3
|
+
## 0.3.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 37bd870: Deprecated `createRouter` and its router options in favour of the new backend system.
|
|
8
|
+
|
|
9
|
+
## 0.3.2
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- f5b0d2a: Backstage version bump to v1.32.2
|
|
14
|
+
|
|
3
15
|
## 0.3.1
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/dist/index.cjs.js
CHANGED
|
@@ -2,647 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
|
-
var
|
|
6
|
-
var
|
|
7
|
-
var BodyParser = require('body-parser');
|
|
8
|
-
var bodyParserXml = require('body-parser-xml');
|
|
9
|
-
var catalogClient = require('@backstage/catalog-client');
|
|
10
|
-
var backendCommon = require('@backstage/backend-common');
|
|
11
|
-
var errors = require('@backstage/errors');
|
|
12
|
-
var integration = require('@backstage/integration');
|
|
13
|
-
var catalogModel = require('@backstage/catalog-model');
|
|
14
|
-
var uuid = require('uuid');
|
|
15
|
-
var rootHttpRouter = require('@backstage/backend-defaults/rootHttpRouter');
|
|
16
|
-
var backendPluginApi = require('@backstage/backend-plugin-api');
|
|
5
|
+
var router = require('./service/router.cjs.js');
|
|
6
|
+
var plugin = require('./plugin.cjs.js');
|
|
17
7
|
|
|
18
|
-
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
19
8
|
|
|
20
|
-
var express__default = /*#__PURE__*/_interopDefaultCompat(express);
|
|
21
|
-
var Router__default = /*#__PURE__*/_interopDefaultCompat(Router);
|
|
22
|
-
var BodyParser__default = /*#__PURE__*/_interopDefaultCompat(BodyParser);
|
|
23
|
-
var bodyParserXml__default = /*#__PURE__*/_interopDefaultCompat(bodyParserXml);
|
|
24
9
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return 0;
|
|
28
|
-
}
|
|
29
|
-
return parseFloat((covered / available * 100).toFixed(2));
|
|
30
|
-
};
|
|
31
|
-
const aggregateCoverage = (c) => {
|
|
32
|
-
let availableLine = 0;
|
|
33
|
-
let coveredLine = 0;
|
|
34
|
-
let availableBranch = 0;
|
|
35
|
-
let coveredBranch = 0;
|
|
36
|
-
c.files.forEach((f) => {
|
|
37
|
-
availableLine += Object.keys(f.lineHits).length;
|
|
38
|
-
coveredLine += Object.values(f.lineHits).filter((l) => l > 0).length;
|
|
39
|
-
availableBranch += Object.keys(f.branchHits).map((b) => parseInt(b, 10)).map((b) => f.branchHits[b].available).filter(Boolean).reduce((acc, curr) => acc + curr, 0);
|
|
40
|
-
coveredBranch += Object.keys(f.branchHits).map((b) => parseInt(b, 10)).map((b) => f.branchHits[b].covered).filter(Boolean).reduce((acc, curr) => acc + curr, 0);
|
|
41
|
-
});
|
|
42
|
-
return {
|
|
43
|
-
timestamp: c.metadata.generationTime,
|
|
44
|
-
branch: {
|
|
45
|
-
available: availableBranch,
|
|
46
|
-
covered: coveredBranch,
|
|
47
|
-
missed: availableBranch - coveredBranch,
|
|
48
|
-
percentage: calculatePercentage(availableBranch, coveredBranch)
|
|
49
|
-
},
|
|
50
|
-
line: {
|
|
51
|
-
available: availableLine,
|
|
52
|
-
covered: coveredLine,
|
|
53
|
-
missed: availableLine - coveredLine,
|
|
54
|
-
percentage: calculatePercentage(availableLine, coveredLine)
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
};
|
|
58
|
-
class CoverageUtils {
|
|
59
|
-
constructor(scm, urlReader) {
|
|
60
|
-
this.scm = scm;
|
|
61
|
-
this.urlReader = urlReader;
|
|
62
|
-
}
|
|
63
|
-
async processCoveragePayload(entity, req) {
|
|
64
|
-
const enforceScmFiles = entity.metadata.annotations?.["backstage.io/code-coverage"] === "scm-only" || false;
|
|
65
|
-
let sourceLocation = void 0;
|
|
66
|
-
let vcs = void 0;
|
|
67
|
-
let scmFiles = [];
|
|
68
|
-
if (enforceScmFiles) {
|
|
69
|
-
try {
|
|
70
|
-
const sl = catalogModel.getEntitySourceLocation(entity);
|
|
71
|
-
sourceLocation = sl.target;
|
|
72
|
-
} catch (e) {
|
|
73
|
-
}
|
|
74
|
-
if (!sourceLocation) {
|
|
75
|
-
throw new errors.InputError(
|
|
76
|
-
`No "backstage.io/source-location" annotation on entity ${catalogModel.stringifyEntityRef(
|
|
77
|
-
entity
|
|
78
|
-
)}`
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
vcs = this.scm.byUrl?.(sourceLocation);
|
|
82
|
-
if (!vcs) {
|
|
83
|
-
throw new errors.InputError(`Unable to determine SCM from ${sourceLocation}`);
|
|
84
|
-
}
|
|
85
|
-
const scmTree = await this.urlReader.readTree?.(sourceLocation);
|
|
86
|
-
if (!scmTree) {
|
|
87
|
-
throw new errors.NotFoundError(`Unable to read tree from ${sourceLocation}`);
|
|
88
|
-
}
|
|
89
|
-
scmFiles = (await scmTree.files()).map((f) => f.path);
|
|
90
|
-
}
|
|
91
|
-
const body = this.validateRequestBody(req);
|
|
92
|
-
if (Object.keys(body).length === 0) {
|
|
93
|
-
throw new errors.InputError("Unable to parse body");
|
|
94
|
-
}
|
|
95
|
-
return {
|
|
96
|
-
sourceLocation,
|
|
97
|
-
vcs,
|
|
98
|
-
scmFiles,
|
|
99
|
-
body
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
async buildCoverage(entity, sourceLocation, vcs, files) {
|
|
103
|
-
return {
|
|
104
|
-
metadata: {
|
|
105
|
-
vcs: {
|
|
106
|
-
type: vcs?.type || "unknown",
|
|
107
|
-
location: sourceLocation || "unknown"
|
|
108
|
-
},
|
|
109
|
-
generationTime: Date.now()
|
|
110
|
-
},
|
|
111
|
-
entity: {
|
|
112
|
-
name: entity.metadata.name,
|
|
113
|
-
namespace: entity.metadata.namespace || "default",
|
|
114
|
-
kind: entity.kind
|
|
115
|
-
},
|
|
116
|
-
files
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
validateRequestBody(req) {
|
|
120
|
-
const contentType = req.headers["content-type"];
|
|
121
|
-
if (!contentType) {
|
|
122
|
-
throw new errors.InputError("Content-Type header missing");
|
|
123
|
-
} else if (!contentType.match(/^text\/xml|plain($|;)/)) {
|
|
124
|
-
throw new errors.InputError(
|
|
125
|
-
`Content-Type header "${contentType}" not supported, expected "text/xml" or "text/plain" possibly followed by a charset`
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
const body = req.body;
|
|
129
|
-
if (!body) {
|
|
130
|
-
throw new errors.InputError("Missing request body");
|
|
131
|
-
}
|
|
132
|
-
return body;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const migrationsDir = backendCommon.resolvePackagePath(
|
|
137
|
-
"@backstage-community/plugin-code-coverage-backend",
|
|
138
|
-
"migrations"
|
|
139
|
-
);
|
|
140
|
-
class CodeCoverageDatabase {
|
|
141
|
-
constructor(db) {
|
|
142
|
-
this.db = db;
|
|
143
|
-
}
|
|
144
|
-
static async create(database) {
|
|
145
|
-
const knex = await database.getClient();
|
|
146
|
-
if (!database.migrations?.skip) {
|
|
147
|
-
await knex.migrate.latest({
|
|
148
|
-
directory: migrationsDir
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
return new CodeCoverageDatabase(knex);
|
|
152
|
-
}
|
|
153
|
-
async insertCodeCoverage(coverage) {
|
|
154
|
-
const codeCoverageId = uuid.v4();
|
|
155
|
-
const entity = catalogModel.stringifyEntityRef({
|
|
156
|
-
kind: coverage.entity.kind,
|
|
157
|
-
namespace: coverage.entity.namespace,
|
|
158
|
-
name: coverage.entity.name
|
|
159
|
-
});
|
|
160
|
-
await this.db("code_coverage").insert({
|
|
161
|
-
id: codeCoverageId,
|
|
162
|
-
entity,
|
|
163
|
-
coverage: JSON.stringify(coverage)
|
|
164
|
-
});
|
|
165
|
-
return { codeCoverageId };
|
|
166
|
-
}
|
|
167
|
-
async getCodeCoverage(entity) {
|
|
168
|
-
const [result] = await this.db("code_coverage").where({ entity }).orderBy("index", "desc").limit(1).select();
|
|
169
|
-
if (!result) {
|
|
170
|
-
throw new errors.NotFoundError(
|
|
171
|
-
`No coverage for entity '${JSON.stringify(entity)}' found`
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
try {
|
|
175
|
-
return JSON.parse(result.coverage);
|
|
176
|
-
} catch (error) {
|
|
177
|
-
throw new Error(`Failed to parse coverage for '${entity}', ${error}`);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
async getHistory(entity, limit) {
|
|
181
|
-
const res = await this.db("code_coverage").where({ entity }).orderBy("index", "desc").limit(limit).select();
|
|
182
|
-
const history = res.map((r) => JSON.parse(r.coverage)).map((c) => aggregateCoverage(c));
|
|
183
|
-
const entityName = catalogModel.parseEntityRef(entity);
|
|
184
|
-
return {
|
|
185
|
-
entity: {
|
|
186
|
-
name: entityName.name,
|
|
187
|
-
kind: entityName.kind,
|
|
188
|
-
namespace: entityName.namespace
|
|
189
|
-
},
|
|
190
|
-
history
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
class Cobertura {
|
|
196
|
-
constructor(logger) {
|
|
197
|
-
this.logger = logger;
|
|
198
|
-
this.logger = logger;
|
|
199
|
-
}
|
|
200
|
-
/**
|
|
201
|
-
* convert cobertura into shared json coverage format
|
|
202
|
-
*
|
|
203
|
-
* @param xml - cobertura xml object
|
|
204
|
-
* @param scmFiles - list of files that are committed to SCM
|
|
205
|
-
*/
|
|
206
|
-
convert(xml, scmFiles) {
|
|
207
|
-
const ppc = xml.coverage.packages?.flatMap((p) => p.package).filter(Boolean).flatMap((p) => p.classes);
|
|
208
|
-
const pc = xml.coverage.package?.filter(Boolean).flatMap((p) => p.classes);
|
|
209
|
-
const classes = [ppc, pc].flat().filter(Boolean).flatMap((c) => c.class).filter(Boolean);
|
|
210
|
-
const jscov = [];
|
|
211
|
-
classes.forEach((c) => {
|
|
212
|
-
const packageAndFilename = c.$.filename;
|
|
213
|
-
const lines = this.extractLines(c);
|
|
214
|
-
const lineHits = {};
|
|
215
|
-
const branchHits = {};
|
|
216
|
-
lines.forEach((l) => {
|
|
217
|
-
if (!lineHits[l.number]) {
|
|
218
|
-
lineHits[l.number] = 0;
|
|
219
|
-
}
|
|
220
|
-
lineHits[l.number] += l.hits;
|
|
221
|
-
if (l.branch && l["condition-coverage"]) {
|
|
222
|
-
const bh = this.parseBranch(l["condition-coverage"]);
|
|
223
|
-
if (bh) {
|
|
224
|
-
branchHits[l.number] = bh;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
});
|
|
228
|
-
const currentFile = scmFiles.map((f) => f.trimEnd()).find((f) => f.endsWith(packageAndFilename));
|
|
229
|
-
this.logger.debug(`matched ${packageAndFilename} to ${currentFile}`);
|
|
230
|
-
if (scmFiles.length === 0 || Object.keys(lineHits).length > 0 && currentFile) {
|
|
231
|
-
jscov.push({
|
|
232
|
-
filename: currentFile || packageAndFilename,
|
|
233
|
-
branchHits,
|
|
234
|
-
lineHits
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
});
|
|
238
|
-
return jscov;
|
|
239
|
-
}
|
|
240
|
-
/**
|
|
241
|
-
* Parses branch coverage information from condition-coverage
|
|
242
|
-
*
|
|
243
|
-
* @param condition - condition-coverage value from line coverage
|
|
244
|
-
*/
|
|
245
|
-
parseBranch(condition) {
|
|
246
|
-
const pattern = /[0-9\.]+\%\s\(([0-9]+)\/([0-9]+)\)/;
|
|
247
|
-
const match = condition.match(pattern);
|
|
248
|
-
if (!match) {
|
|
249
|
-
return null;
|
|
250
|
-
}
|
|
251
|
-
const covered = parseInt(match[1], 10);
|
|
252
|
-
const available = parseInt(match[2], 10);
|
|
253
|
-
return {
|
|
254
|
-
covered,
|
|
255
|
-
missed: available - covered,
|
|
256
|
-
available
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
/**
|
|
260
|
-
* Extract line hits from a class coverage entry
|
|
261
|
-
*
|
|
262
|
-
* @param clz - class coverage information
|
|
263
|
-
*/
|
|
264
|
-
extractLines(clz) {
|
|
265
|
-
const classLines = clz.lines.flatMap((l) => l.line);
|
|
266
|
-
const methodLines = clz.methods?.flatMap((m) => m.method).filter(Boolean).flatMap((m) => m.lines).filter(Boolean).flatMap((l) => l.line).filter(
|
|
267
|
-
({ $: methodLine }) => classLines.some(
|
|
268
|
-
({ $: classLine }) => methodLine.number === classLine.number
|
|
269
|
-
) === false
|
|
270
|
-
);
|
|
271
|
-
const lines = [classLines, methodLines].flat().filter(Boolean);
|
|
272
|
-
const lineHits = lines.map((l) => {
|
|
273
|
-
return {
|
|
274
|
-
number: parseInt(l.$.number, 10),
|
|
275
|
-
hits: parseInt(l.$.hits, 10),
|
|
276
|
-
"condition-coverage": l.$["condition-coverage"],
|
|
277
|
-
branch: l.$.branch
|
|
278
|
-
};
|
|
279
|
-
});
|
|
280
|
-
return lineHits;
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
class Jacoco {
|
|
285
|
-
constructor(logger) {
|
|
286
|
-
this.logger = logger;
|
|
287
|
-
this.logger = logger;
|
|
288
|
-
}
|
|
289
|
-
/**
|
|
290
|
-
* Converts jacoco into shared json coverage format
|
|
291
|
-
*
|
|
292
|
-
* @param xml - jacoco xml object
|
|
293
|
-
* @param scmFiles - list of files that are committed to SCM
|
|
294
|
-
*/
|
|
295
|
-
convert(xml, scmFiles) {
|
|
296
|
-
const jscov = [];
|
|
297
|
-
xml.report.package.forEach((r) => {
|
|
298
|
-
const packageName = r.$.name;
|
|
299
|
-
r.sourcefile.forEach((sf) => {
|
|
300
|
-
const fileName = sf.$.name;
|
|
301
|
-
const lines = this.extractLines(sf);
|
|
302
|
-
const lineHits = {};
|
|
303
|
-
const branchHits = {};
|
|
304
|
-
lines.forEach((l) => {
|
|
305
|
-
if (!lineHits[l.number]) {
|
|
306
|
-
lineHits[l.number] = 0;
|
|
307
|
-
}
|
|
308
|
-
lineHits[l.number] += l.covered_instructions;
|
|
309
|
-
const ab = l.covered_branches + l.missed_branches;
|
|
310
|
-
if (ab > 0) {
|
|
311
|
-
branchHits[l.number] = {
|
|
312
|
-
covered: l.covered_branches,
|
|
313
|
-
missed: l.missed_branches,
|
|
314
|
-
available: ab
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
});
|
|
318
|
-
const packageAndFilename = `${packageName}/${fileName}`;
|
|
319
|
-
const currentFile = scmFiles.map((f) => f.trimEnd()).find((f) => f.endsWith(packageAndFilename));
|
|
320
|
-
this.logger.debug(`matched ${packageAndFilename} to ${currentFile}`);
|
|
321
|
-
if (scmFiles.length === 0 || Object.keys(lineHits).length > 0 && currentFile) {
|
|
322
|
-
jscov.push({
|
|
323
|
-
filename: currentFile || packageAndFilename,
|
|
324
|
-
branchHits,
|
|
325
|
-
lineHits
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
});
|
|
329
|
-
});
|
|
330
|
-
return jscov;
|
|
331
|
-
}
|
|
332
|
-
extractLines(sourcefile) {
|
|
333
|
-
const parsed = [];
|
|
334
|
-
sourcefile.line?.forEach((l) => {
|
|
335
|
-
parsed.push({
|
|
336
|
-
number: parseInt(l.$.nr, 10),
|
|
337
|
-
missed_instructions: parseInt(l.$.mi, 10),
|
|
338
|
-
covered_instructions: parseInt(l.$.ci, 10),
|
|
339
|
-
missed_branches: parseInt(l.$.mb, 10),
|
|
340
|
-
covered_branches: parseInt(l.$.cb, 10)
|
|
341
|
-
});
|
|
342
|
-
});
|
|
343
|
-
return parsed;
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
class Lcov {
|
|
348
|
-
constructor(logger) {
|
|
349
|
-
this.logger = logger;
|
|
350
|
-
this.logger = logger;
|
|
351
|
-
}
|
|
352
|
-
/**
|
|
353
|
-
* convert lcov into shared json coverage format
|
|
354
|
-
*
|
|
355
|
-
* @param raw - raw lcov report
|
|
356
|
-
* @param scmFiles - list of files that are committed to SCM
|
|
357
|
-
*/
|
|
358
|
-
convert(raw, scmFiles) {
|
|
359
|
-
const lines = raw.split(/\r?\n/);
|
|
360
|
-
const jscov = [];
|
|
361
|
-
let currentFile = null;
|
|
362
|
-
lines.forEach((line) => {
|
|
363
|
-
const [section, value] = line.split(":");
|
|
364
|
-
switch (section) {
|
|
365
|
-
// If the line starts with SF, it's a new file
|
|
366
|
-
case "SF":
|
|
367
|
-
currentFile = this.processNewFile(value, scmFiles);
|
|
368
|
-
break;
|
|
369
|
-
// If the line starts with DA, it's a line hit
|
|
370
|
-
case "DA":
|
|
371
|
-
this.processLineHit(currentFile, value);
|
|
372
|
-
break;
|
|
373
|
-
// If the line starts with BRDA, it's a branch line
|
|
374
|
-
case "BRDA":
|
|
375
|
-
this.processBranchHit(currentFile, value);
|
|
376
|
-
break;
|
|
377
|
-
// If the line starts with end_of_record, it's the end of current file
|
|
378
|
-
case "end_of_record":
|
|
379
|
-
if (currentFile) {
|
|
380
|
-
jscov.push(currentFile);
|
|
381
|
-
currentFile = null;
|
|
382
|
-
}
|
|
383
|
-
break;
|
|
384
|
-
}
|
|
385
|
-
});
|
|
386
|
-
return jscov;
|
|
387
|
-
}
|
|
388
|
-
/**
|
|
389
|
-
* Parses a new file entry
|
|
390
|
-
*
|
|
391
|
-
* @param file - file name from coverage report
|
|
392
|
-
* @param scmFiles - list of files that are committed to SCM
|
|
393
|
-
*/
|
|
394
|
-
processNewFile(file, scmFiles) {
|
|
395
|
-
const filename = scmFiles.map((f) => f.trimEnd()).find((f) => file.endsWith(f));
|
|
396
|
-
this.logger.debug(`matched ${file} to ${filename}`);
|
|
397
|
-
if (scmFiles.length === 0 || filename) {
|
|
398
|
-
return {
|
|
399
|
-
filename: filename || file,
|
|
400
|
-
lineHits: {},
|
|
401
|
-
branchHits: {}
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
return null;
|
|
405
|
-
}
|
|
406
|
-
/**
|
|
407
|
-
* Parses line coverage information
|
|
408
|
-
*
|
|
409
|
-
* @param currentFile - current file entry
|
|
410
|
-
* @param value - line coverage information
|
|
411
|
-
*/
|
|
412
|
-
processLineHit(currentFile, value) {
|
|
413
|
-
if (!currentFile) return;
|
|
414
|
-
const [lineNumber, hits] = value.split(",");
|
|
415
|
-
currentFile.lineHits[Number(lineNumber)] = Number(hits);
|
|
416
|
-
}
|
|
417
|
-
/**
|
|
418
|
-
* Parses branch coverage information
|
|
419
|
-
*
|
|
420
|
-
* @param currentFile - current file entry
|
|
421
|
-
* @param value - branch coverage information
|
|
422
|
-
*/
|
|
423
|
-
processBranchHit(currentFile, value) {
|
|
424
|
-
if (!currentFile) return;
|
|
425
|
-
const [lineNumber, , , hits] = value.split(",");
|
|
426
|
-
const lineNumberNum = Number(lineNumber);
|
|
427
|
-
const isHit = Number(hits) > 0;
|
|
428
|
-
const branch = currentFile.branchHits[lineNumberNum] || {
|
|
429
|
-
covered: 0,
|
|
430
|
-
available: 0,
|
|
431
|
-
missed: 0
|
|
432
|
-
};
|
|
433
|
-
branch.available++;
|
|
434
|
-
branch.covered += isHit ? 1 : 0;
|
|
435
|
-
branch.missed += isHit ? 0 : 1;
|
|
436
|
-
currentFile.branchHits[lineNumberNum] = branch;
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
const makeRouter = async (options) => {
|
|
441
|
-
const { config, logger, discovery, database, urlReader } = options;
|
|
442
|
-
const codeCoverageDatabase = await CodeCoverageDatabase.create(database);
|
|
443
|
-
const codecovUrl = await discovery.getExternalBaseUrl("code-coverage");
|
|
444
|
-
const catalogApi = options.catalogApi ?? new catalogClient.CatalogClient({ discoveryApi: discovery });
|
|
445
|
-
const scm = integration.ScmIntegrations.fromConfig(config);
|
|
446
|
-
const { auth, httpAuth } = backendCommon.createLegacyAuthAdapters(options);
|
|
447
|
-
const bodySizeLimit = config.getOptionalString("codeCoverage.bodySizeLimit") ?? "100kb";
|
|
448
|
-
bodyParserXml__default.default(BodyParser__default.default);
|
|
449
|
-
const router = Router__default.default();
|
|
450
|
-
router.use(
|
|
451
|
-
BodyParser__default.default.xml({
|
|
452
|
-
limit: bodySizeLimit
|
|
453
|
-
})
|
|
454
|
-
);
|
|
455
|
-
router.use(
|
|
456
|
-
BodyParser__default.default.text({
|
|
457
|
-
limit: bodySizeLimit
|
|
458
|
-
})
|
|
459
|
-
);
|
|
460
|
-
router.use(express__default.default.json());
|
|
461
|
-
const utils = new CoverageUtils(scm, urlReader);
|
|
462
|
-
router.get("/health", async (_req, res) => {
|
|
463
|
-
res.status(200).json({ status: "ok" });
|
|
464
|
-
});
|
|
465
|
-
router.get("/report", async (req, res) => {
|
|
466
|
-
const { entity } = req.query;
|
|
467
|
-
const entityLookup = await catalogApi.getEntityByRef(
|
|
468
|
-
entity,
|
|
469
|
-
await auth.getPluginRequestToken({
|
|
470
|
-
onBehalfOf: await httpAuth.credentials(req),
|
|
471
|
-
targetPluginId: "catalog"
|
|
472
|
-
})
|
|
473
|
-
);
|
|
474
|
-
if (!entityLookup) {
|
|
475
|
-
throw new errors.NotFoundError(`No entity found matching ${entity}`);
|
|
476
|
-
}
|
|
477
|
-
const stored = await codeCoverageDatabase.getCodeCoverage(entity);
|
|
478
|
-
const aggregate = aggregateCoverage(stored);
|
|
479
|
-
res.status(200).json({
|
|
480
|
-
...stored,
|
|
481
|
-
aggregate: {
|
|
482
|
-
line: aggregate.line,
|
|
483
|
-
branch: aggregate.branch
|
|
484
|
-
}
|
|
485
|
-
});
|
|
486
|
-
});
|
|
487
|
-
router.get("/history", async (req, res) => {
|
|
488
|
-
const { entity } = req.query;
|
|
489
|
-
const entityLookup = await catalogApi.getEntityByRef(
|
|
490
|
-
entity,
|
|
491
|
-
await auth.getPluginRequestToken({
|
|
492
|
-
onBehalfOf: await httpAuth.credentials(req),
|
|
493
|
-
targetPluginId: "catalog"
|
|
494
|
-
})
|
|
495
|
-
);
|
|
496
|
-
if (!entityLookup) {
|
|
497
|
-
throw new errors.NotFoundError(`No entity found matching ${entity}`);
|
|
498
|
-
}
|
|
499
|
-
const { limit } = req.query;
|
|
500
|
-
const history = await codeCoverageDatabase.getHistory(
|
|
501
|
-
entity,
|
|
502
|
-
parseInt(limit?.toString() || "10", 10)
|
|
503
|
-
);
|
|
504
|
-
res.status(200).json(history);
|
|
505
|
-
});
|
|
506
|
-
router.get("/file-content", async (req, res) => {
|
|
507
|
-
const { entity, path } = req.query;
|
|
508
|
-
const entityLookup = await catalogApi.getEntityByRef(
|
|
509
|
-
entity,
|
|
510
|
-
await auth.getPluginRequestToken({
|
|
511
|
-
onBehalfOf: await httpAuth.credentials(req),
|
|
512
|
-
targetPluginId: "catalog"
|
|
513
|
-
})
|
|
514
|
-
);
|
|
515
|
-
if (!entityLookup) {
|
|
516
|
-
throw new errors.NotFoundError(`No entity found matching ${entity}`);
|
|
517
|
-
}
|
|
518
|
-
if (!path) {
|
|
519
|
-
throw new errors.InputError("Need path query parameter");
|
|
520
|
-
}
|
|
521
|
-
const sourceLocation = catalogModel.getEntitySourceLocation(entityLookup);
|
|
522
|
-
if (!sourceLocation) {
|
|
523
|
-
throw new errors.InputError(
|
|
524
|
-
`No "backstage.io/source-location" annotation on entity ${entity}`
|
|
525
|
-
);
|
|
526
|
-
}
|
|
527
|
-
const vcs = scm.byUrl(sourceLocation.target);
|
|
528
|
-
if (!vcs) {
|
|
529
|
-
throw new errors.InputError(`Unable to determine SCM from ${sourceLocation}`);
|
|
530
|
-
}
|
|
531
|
-
const scmTree = await urlReader.readTree(sourceLocation.target);
|
|
532
|
-
const scmFile = (await scmTree.files()).find((f) => f.path === path);
|
|
533
|
-
if (!scmFile) {
|
|
534
|
-
res.status(404).json({
|
|
535
|
-
message: "Couldn't find file in SCM",
|
|
536
|
-
file: path,
|
|
537
|
-
scm: vcs.title
|
|
538
|
-
});
|
|
539
|
-
return;
|
|
540
|
-
}
|
|
541
|
-
const content = await scmFile?.content();
|
|
542
|
-
if (!content) {
|
|
543
|
-
res.status(400).json({
|
|
544
|
-
message: "Couldn't process content of file in SCM",
|
|
545
|
-
file: path,
|
|
546
|
-
scm: vcs.title
|
|
547
|
-
});
|
|
548
|
-
return;
|
|
549
|
-
}
|
|
550
|
-
const data = content.toString();
|
|
551
|
-
res.status(200).contentType("text/plain").send(data);
|
|
552
|
-
});
|
|
553
|
-
router.post("/report", async (req, res) => {
|
|
554
|
-
const { entity: entityRef, coverageType } = req.query;
|
|
555
|
-
const entity = await catalogApi.getEntityByRef(
|
|
556
|
-
entityRef,
|
|
557
|
-
await auth.getPluginRequestToken({
|
|
558
|
-
onBehalfOf: await httpAuth.credentials(req),
|
|
559
|
-
targetPluginId: "catalog"
|
|
560
|
-
})
|
|
561
|
-
);
|
|
562
|
-
if (!entity) {
|
|
563
|
-
throw new errors.NotFoundError(`No entity found matching ${entityRef}`);
|
|
564
|
-
}
|
|
565
|
-
let converter;
|
|
566
|
-
if (!coverageType) {
|
|
567
|
-
throw new errors.InputError("Need coverageType query parameter");
|
|
568
|
-
} else if (coverageType === "jacoco") {
|
|
569
|
-
converter = new Jacoco(logger);
|
|
570
|
-
} else if (coverageType === "cobertura") {
|
|
571
|
-
converter = new Cobertura(logger);
|
|
572
|
-
} else if (coverageType === "lcov") {
|
|
573
|
-
converter = new Lcov(logger);
|
|
574
|
-
} else {
|
|
575
|
-
throw new errors.InputError(`Unsupported coverage type '${coverageType}`);
|
|
576
|
-
}
|
|
577
|
-
const { sourceLocation, vcs, scmFiles, body } = await utils.processCoveragePayload(entity, req);
|
|
578
|
-
const files = converter.convert(body, scmFiles);
|
|
579
|
-
if (!files || files.length === 0) {
|
|
580
|
-
throw new errors.InputError(`Unable to parse body as ${coverageType}`);
|
|
581
|
-
}
|
|
582
|
-
const coverage = await utils.buildCoverage(
|
|
583
|
-
entity,
|
|
584
|
-
sourceLocation,
|
|
585
|
-
vcs,
|
|
586
|
-
files
|
|
587
|
-
);
|
|
588
|
-
await codeCoverageDatabase.insertCodeCoverage(coverage);
|
|
589
|
-
res.status(201).json({
|
|
590
|
-
links: [
|
|
591
|
-
{
|
|
592
|
-
rel: "coverage",
|
|
593
|
-
href: `${codecovUrl}/report?entity=${entityRef}`
|
|
594
|
-
}
|
|
595
|
-
]
|
|
596
|
-
});
|
|
597
|
-
});
|
|
598
|
-
const middleware = rootHttpRouter.MiddlewareFactory.create({ logger, config });
|
|
599
|
-
router.use(middleware.error());
|
|
600
|
-
return router;
|
|
601
|
-
};
|
|
602
|
-
async function createRouter(options) {
|
|
603
|
-
const logger = options.logger;
|
|
604
|
-
logger.info("Initializing Code Coverage backend");
|
|
605
|
-
return makeRouter(options);
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
const codeCoveragePlugin = backendPluginApi.createBackendPlugin({
|
|
609
|
-
pluginId: "code-coverage",
|
|
610
|
-
register(env) {
|
|
611
|
-
env.registerInit({
|
|
612
|
-
deps: {
|
|
613
|
-
config: backendPluginApi.coreServices.rootConfig,
|
|
614
|
-
logger: backendPluginApi.coreServices.logger,
|
|
615
|
-
urlReader: backendPluginApi.coreServices.urlReader,
|
|
616
|
-
httpRouter: backendPluginApi.coreServices.httpRouter,
|
|
617
|
-
discovery: backendPluginApi.coreServices.discovery,
|
|
618
|
-
database: backendPluginApi.coreServices.database
|
|
619
|
-
},
|
|
620
|
-
async init({
|
|
621
|
-
config,
|
|
622
|
-
logger,
|
|
623
|
-
urlReader,
|
|
624
|
-
httpRouter,
|
|
625
|
-
discovery,
|
|
626
|
-
database
|
|
627
|
-
}) {
|
|
628
|
-
httpRouter.use(
|
|
629
|
-
await createRouter({
|
|
630
|
-
config,
|
|
631
|
-
logger,
|
|
632
|
-
urlReader,
|
|
633
|
-
discovery,
|
|
634
|
-
database
|
|
635
|
-
})
|
|
636
|
-
);
|
|
637
|
-
httpRouter.addAuthPolicy({
|
|
638
|
-
path: "/health",
|
|
639
|
-
allow: "unauthenticated"
|
|
640
|
-
});
|
|
641
|
-
}
|
|
642
|
-
});
|
|
643
|
-
}
|
|
644
|
-
});
|
|
645
|
-
|
|
646
|
-
exports.createRouter = createRouter;
|
|
647
|
-
exports.default = codeCoveragePlugin;
|
|
10
|
+
exports.createRouter = router.createRouter;
|
|
11
|
+
exports.default = plugin.codeCoveragePlugin;
|
|
648
12
|
//# sourceMappingURL=index.cjs.js.map
|