feat: add support for lcov

Signed-off-by: Anton Ganhammar <ganhammar@gmail.com>
This commit is contained in:
Anton Ganhammar
2023-08-20 11:56:40 +02:00
parent b2ff304ddd
commit 9601909430
13 changed files with 431 additions and 9 deletions
@@ -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;
@@ -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,