feat: add support for lcov
Signed-off-by: Anton Ganhammar <ganhammar@gmail.com>
This commit is contained in:
@@ -224,11 +224,11 @@ describe('CodeCoverageUtils', () => {
|
||||
err = error;
|
||||
}
|
||||
expect(err?.message).toEqual(
|
||||
'Content-Type header "application/json" not supported, expected "text/xml" possibly followed by a charset',
|
||||
'Content-Type header "application/json" not supported, expected "text/xml" or "text/plain" possibly followed by a charset',
|
||||
);
|
||||
});
|
||||
|
||||
it('parses the body', () => {
|
||||
it('parses the xml body', () => {
|
||||
const data: Readable = utils.validateRequestBody({
|
||||
headers: {
|
||||
'content-type': 'text/xml',
|
||||
@@ -240,6 +240,19 @@ describe('CodeCoverageUtils', () => {
|
||||
|
||||
expect(data.read()).toContain('<?xml');
|
||||
});
|
||||
|
||||
it('parses the plain body', () => {
|
||||
const data: Readable = utils.validateRequestBody({
|
||||
headers: {
|
||||
'content-type': 'text/plain',
|
||||
},
|
||||
body: Readable.from(
|
||||
'TN:\nSF:/src/index.js\nFNF:0\nFNH:0\nLF:1\nLH:1\nBRF:0\nBRH:0\nend_of_record',
|
||||
),
|
||||
} as Request);
|
||||
|
||||
expect(data.read()).toMatch(/^TN:\nSF:\/src\/index.js/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('processCoveragePayload', () => {
|
||||
|
||||
@@ -163,9 +163,10 @@ export class CoverageUtils {
|
||||
const contentType = req.headers['content-type'];
|
||||
if (!contentType) {
|
||||
throw new InputError('Content-Type header missing');
|
||||
} else if (!contentType.match(/^text\/xml($|;)/)) {
|
||||
// text/xml or text/plain is allowed
|
||||
} else if (!contentType.match(/^text\/xml|plain($|;)/)) {
|
||||
throw new InputError(
|
||||
`Content-Type header "${contentType}" not supported, expected "text/xml" possibly followed by a charset`,
|
||||
`Content-Type header "${contentType}" not supported, expected "text/xml" or "text/plain" possibly followed by a charset`,
|
||||
);
|
||||
}
|
||||
const body = req.body;
|
||||
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
[
|
||||
{
|
||||
"filename": "src/index.js",
|
||||
"lineHits": {
|
||||
"1": 1,
|
||||
"2": 1,
|
||||
"3": 1,
|
||||
"4": 5,
|
||||
"5": 5,
|
||||
"6": 1,
|
||||
"7": 5,
|
||||
"8": 0,
|
||||
"9": 5,
|
||||
"10": 3,
|
||||
"11": 5,
|
||||
"12": 1,
|
||||
"13": 5,
|
||||
"14": 5
|
||||
},
|
||||
"branchHits": {
|
||||
"3": {
|
||||
"covered": 1,
|
||||
"missed": 0,
|
||||
"available": 1
|
||||
},
|
||||
"5": {
|
||||
"covered": 1,
|
||||
"missed": 0,
|
||||
"available": 1
|
||||
},
|
||||
"7": {
|
||||
"covered": 0,
|
||||
"missed": 1,
|
||||
"available": 1
|
||||
},
|
||||
"9": {
|
||||
"covered": 1,
|
||||
"missed": 0,
|
||||
"available": 1
|
||||
},
|
||||
"11": {
|
||||
"covered": 1,
|
||||
"missed": 0,
|
||||
"available": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "src/math.js",
|
||||
"lineHits": {
|
||||
"1": 1,
|
||||
"2": 1,
|
||||
"3": 1,
|
||||
"4": 1,
|
||||
"5": 1,
|
||||
"6": 0,
|
||||
"7": 0,
|
||||
"8": 1,
|
||||
"9": 1,
|
||||
"10": 3,
|
||||
"11": 0,
|
||||
"12": 3,
|
||||
"13": 1,
|
||||
"14": 3,
|
||||
"15": 1,
|
||||
"16": 1,
|
||||
"17": 1,
|
||||
"18": 1
|
||||
},
|
||||
"branchHits": {
|
||||
"2": {
|
||||
"covered": 1,
|
||||
"missed": 0,
|
||||
"available": 1
|
||||
},
|
||||
"9": {
|
||||
"covered": 1,
|
||||
"missed": 0,
|
||||
"available": 1
|
||||
},
|
||||
"10": {
|
||||
"covered": 0,
|
||||
"missed": 1,
|
||||
"available": 1
|
||||
},
|
||||
"12": {
|
||||
"covered": 2,
|
||||
"missed": 0,
|
||||
"available": 2
|
||||
},
|
||||
"14": {
|
||||
"covered": 2,
|
||||
"missed": 0,
|
||||
"available": 2
|
||||
},
|
||||
"17": {
|
||||
"covered": 1,
|
||||
"missed": 0,
|
||||
"available": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,47 @@
|
||||
[
|
||||
{
|
||||
"filename": "src/Calc/Main.cs",
|
||||
"lineHits": {
|
||||
"5": 4,
|
||||
"6": 4,
|
||||
"7": 1,
|
||||
"8": 0,
|
||||
"9": 2,
|
||||
"10": 1,
|
||||
"11": 4
|
||||
},
|
||||
"branchHits": {
|
||||
"5": {
|
||||
"covered": 5,
|
||||
"missed": 1,
|
||||
"available": 6
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "src/Calc/Math.cs",
|
||||
"lineHits": {
|
||||
"5": 1,
|
||||
"6": 0,
|
||||
"7": 2,
|
||||
"8": 2,
|
||||
"9": 0,
|
||||
"11": 3,
|
||||
"12": 1,
|
||||
"14": 1,
|
||||
"15": 1
|
||||
},
|
||||
"branchHits": {
|
||||
"8": {
|
||||
"covered": 1,
|
||||
"missed": 1,
|
||||
"available": 2
|
||||
},
|
||||
"11": {
|
||||
"covered": 2,
|
||||
"missed": 0,
|
||||
"available": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,2 @@
|
||||
src/index.js
|
||||
src/math.js
|
||||
@@ -0,0 +1,2 @@
|
||||
src/Calc/Main.cs
|
||||
src/Calc/Math.cs
|
||||
@@ -0,0 +1,71 @@
|
||||
TN:
|
||||
SF:src/index.js
|
||||
FN:3,__vite_ssr_exports__.default
|
||||
FNF:1
|
||||
FNH:1
|
||||
FNDA:5,__vite_ssr_exports__.default
|
||||
DA:1,1
|
||||
DA:2,1
|
||||
DA:3,1
|
||||
DA:4,5
|
||||
DA:5,5
|
||||
DA:6,1
|
||||
DA:7,5
|
||||
DA:8,0
|
||||
DA:9,5
|
||||
DA:10,3
|
||||
DA:11,5
|
||||
DA:12,1
|
||||
DA:13,5
|
||||
DA:14,5
|
||||
LF:14
|
||||
LH:13
|
||||
BRDA:3,0,0,5
|
||||
BRDA:5,1,0,1
|
||||
BRDA:7,2,0,0
|
||||
BRDA:9,3,0,3
|
||||
BRDA:11,4,0,1
|
||||
BRF:5
|
||||
BRH:4
|
||||
end_of_record
|
||||
TN:
|
||||
SF:src/math.js
|
||||
FN:2,addition
|
||||
FN:5,subtraction
|
||||
FN:9,division
|
||||
FNF:3
|
||||
FNH:2
|
||||
FNDA:1,addition
|
||||
FNDA:0,subtraction
|
||||
FNDA:3,division
|
||||
DA:1,1
|
||||
DA:2,1
|
||||
DA:3,1
|
||||
DA:4,1
|
||||
DA:5,1
|
||||
DA:6,0
|
||||
DA:7,0
|
||||
DA:8,1
|
||||
DA:9,1
|
||||
DA:10,3
|
||||
DA:11,0
|
||||
DA:12,3
|
||||
DA:13,1
|
||||
DA:14,3
|
||||
DA:15,1
|
||||
DA:16,1
|
||||
DA:17,1
|
||||
DA:18,1
|
||||
LF:18
|
||||
LH:15
|
||||
BRDA:2,0,0,1
|
||||
BRDA:9,1,0,3
|
||||
BRDA:10,2,0,0
|
||||
BRDA:12,3,0,2
|
||||
BRDA:12,4,0,1
|
||||
BRDA:14,5,0,2
|
||||
BRDA:14,6,0,1
|
||||
BRDA:17,7,0,1
|
||||
BRF:8
|
||||
BRH:7
|
||||
end_of_record
|
||||
@@ -0,0 +1,50 @@
|
||||
SF:/Some/Path/src/Calc/Main.cs
|
||||
FN:4,System.Int32 Calc.Main::Calc(System.Int32,System.Int32,System.String)
|
||||
FNDA:4,System.Int32 Calc.Main::Calc(System.Int32,System.Int32,System.String)
|
||||
DA:5,4
|
||||
DA:6,4
|
||||
DA:7,1
|
||||
DA:8,0
|
||||
DA:9,2
|
||||
DA:10,1
|
||||
DA:11,4
|
||||
BRDA:5,15,0,3
|
||||
BRDA:5,28,0,3
|
||||
BRDA:5,15,1,1
|
||||
BRDA:5,28,1,0
|
||||
BRDA:5,41,1,2
|
||||
BRDA:5,41,0,1
|
||||
LF:7
|
||||
LH:6
|
||||
BRF:6
|
||||
BRH:5
|
||||
FNF:1
|
||||
FNH:1
|
||||
end_of_record
|
||||
SF:/Some/Path/src/Calc/Math.cs
|
||||
FN:4,System.Int32 Calc.Math::Addition(System.Int32,System.Int32)
|
||||
FNDA:1,System.Int32 Calc.Math::Addition(System.Int32,System.Int32)
|
||||
DA:5,1
|
||||
FN:5,System.Int32 Calc.Math::Subtraction(System.Int32,System.Int32)
|
||||
FNDA:0,System.Int32 Calc.Math::Subtraction(System.Int32,System.Int32)
|
||||
DA:6,0
|
||||
FN:6,System.Int32 Calc.Math::Division(System.Int32,System.Int32)
|
||||
FNDA:2,System.Int32 Calc.Math::Division(System.Int32,System.Int32)
|
||||
DA:7,2
|
||||
DA:8,2
|
||||
DA:9,0
|
||||
DA:11,3
|
||||
DA:12,1
|
||||
DA:14,1
|
||||
DA:15,1
|
||||
BRDA:8,7,0,0
|
||||
BRDA:8,7,1,2
|
||||
BRDA:11,22,0,1
|
||||
BRDA:11,22,1,1
|
||||
LF:9
|
||||
LH:7
|
||||
BRF:4
|
||||
BRH:3
|
||||
FNF:3
|
||||
FNH:2
|
||||
end_of_record
|
||||
@@ -17,3 +17,4 @@
|
||||
export * from './Converter';
|
||||
export { Cobertura } from './cobertura';
|
||||
export { Jacoco } from './jacoco';
|
||||
export { Lcov } from './lcov';
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { Lcov } from './lcov';
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
|
||||
describe('convert lcov', () => {
|
||||
const converter = new Lcov(getVoidLogger());
|
||||
[1, 2].forEach(idx => {
|
||||
const lcov = fs
|
||||
.readFileSync(
|
||||
path.resolve(`${__dirname}/../__fixtures__/lcov-testdata-${idx}.info`),
|
||||
)
|
||||
.toString();
|
||||
const expected = JSON.parse(
|
||||
fs
|
||||
.readFileSync(
|
||||
path.resolve(
|
||||
`${__dirname}/../__fixtures__/lcov-jsoncoverage-files-${idx}.json`,
|
||||
),
|
||||
)
|
||||
.toString(),
|
||||
);
|
||||
const scmFiles = fs
|
||||
.readFileSync(
|
||||
path.resolve(
|
||||
`${__dirname}/../__fixtures__/lcov-sourcefiles-${idx}.txt`,
|
||||
),
|
||||
)
|
||||
.toString()
|
||||
.split('\n');
|
||||
|
||||
it(`should convert the lcov report with index ${idx} to json`, async () => {
|
||||
const files = converter.convert(lcov, scmFiles);
|
||||
|
||||
expect(files).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { FileEntry } from '../types';
|
||||
import { Logger } from 'winston';
|
||||
import { Converter } from './Converter';
|
||||
|
||||
export class Lcov implements Converter {
|
||||
constructor(readonly logger: Logger) {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
convert(raw: string, scmFiles: string[]): FileEntry[] {
|
||||
const lines = raw.split(/\r?\n/);
|
||||
const jscov: Array<FileEntry> = [];
|
||||
let currentFile: FileEntry | null = null;
|
||||
|
||||
lines.forEach(line => {
|
||||
// If the line starts with SF, it's a new file
|
||||
if (/^SF:/.test(line)) {
|
||||
const coverageFilename = line.replace(/^SF:/, '');
|
||||
const filename = scmFiles
|
||||
.map(f => f.trimEnd())
|
||||
.find(f => coverageFilename.endsWith(f));
|
||||
|
||||
this.logger.debug(`matched ${coverageFilename} to ${currentFile}`);
|
||||
|
||||
if (filename) {
|
||||
currentFile = {
|
||||
filename,
|
||||
lineHits: {},
|
||||
branchHits: {},
|
||||
};
|
||||
}
|
||||
|
||||
// If the line starts with DA, it's a line hit
|
||||
} else if (currentFile && /^DA:/.test(line)) {
|
||||
const [, lineNumber, hits] = line.split(/[,:]+/);
|
||||
currentFile.lineHits[Number(lineNumber)] = Number(hits);
|
||||
|
||||
// If the line starts with BRDA, it's a branch
|
||||
} else if (currentFile && /^BRDA:/.test(line)) {
|
||||
const [, lineNumber, , , hits] = line.split(/[,:]+/);
|
||||
const isHit = Number(hits) > 0;
|
||||
|
||||
if (!currentFile.branchHits[Number(lineNumber)]) {
|
||||
currentFile.branchHits[Number(lineNumber)] = {
|
||||
covered: isHit ? 1 : 0,
|
||||
available: 1,
|
||||
missed: isHit ? 0 : 1,
|
||||
};
|
||||
} else {
|
||||
currentFile.branchHits[Number(lineNumber)].available += 1;
|
||||
currentFile.branchHits[Number(lineNumber)].covered += isHit ? 1 : 0;
|
||||
currentFile.branchHits[Number(lineNumber)].missed += isHit ? 0 : 1;
|
||||
}
|
||||
|
||||
// If the line starts with end_of_record, it's the end of current file
|
||||
} else if (currentFile && /^end_of_record/.test(line)) {
|
||||
jscov.push(currentFile);
|
||||
currentFile = null;
|
||||
}
|
||||
});
|
||||
|
||||
return jscov;
|
||||
}
|
||||
}
|
||||
@@ -30,9 +30,7 @@ import { Config } from '@backstage/config';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import { CodeCoverageDatabase } from './CodeCoverageDatabase';
|
||||
import { aggregateCoverage, CoverageUtils } from './CoverageUtils';
|
||||
import { Cobertura } from './converter/cobertura';
|
||||
import { Jacoco } from './converter/jacoco';
|
||||
import { Converter } from './converter';
|
||||
import { Converter, Jacoco, Cobertura, Lcov } from './converter';
|
||||
import { getEntitySourceLocation } from '@backstage/catalog-model';
|
||||
|
||||
/**
|
||||
@@ -182,6 +180,8 @@ export const makeRouter = async (
|
||||
converter = new Jacoco(logger);
|
||||
} else if (coverageType === 'cobertura') {
|
||||
converter = new Cobertura(logger);
|
||||
} else if (coverageType === 'lcov') {
|
||||
converter = new Lcov(logger);
|
||||
} else {
|
||||
throw new InputError(`Unsupported coverage type '${coverageType}`);
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
|
||||
import {
|
||||
createServiceBuilder,
|
||||
HostDiscovery,
|
||||
loadBackendConfig,
|
||||
SingleHostDiscovery,
|
||||
UrlReaders,
|
||||
useHotMemoize,
|
||||
} from '@backstage/backend-common';
|
||||
@@ -71,7 +71,7 @@ export async function startStandaloneServer(
|
||||
const router = await createRouter({
|
||||
database: { getClient: async () => db },
|
||||
config,
|
||||
discovery: SingleHostDiscovery.fromConfig(config),
|
||||
discovery: HostDiscovery.fromConfig(config),
|
||||
urlReader: UrlReaders.default({ logger, config }),
|
||||
logger,
|
||||
catalogApi,
|
||||
|
||||
Reference in New Issue
Block a user