Merge pull request #20064 from ganhammar/feat/improve-code-coverage-contrasts

Fix current statistics and contrasts in code-coverage-plugin
This commit is contained in:
Johan Haals
2023-09-26 11:10:34 +02:00
committed by GitHub
10 changed files with 526 additions and 89 deletions
@@ -0,0 +1,174 @@
{
"metadata": {
"vcs": {
"type": "github",
"location": "https://github.com/backstage/backstage/tree/main/"
},
"generationTime": 1694413579498
},
"entity": {
"name": "backstage",
"namespace": "default",
"kind": "Component"
},
"files": [
{
"filename": "src/index.js",
"lineHits": {
"1": 1,
"2": 1,
"3": 1,
"4": 13,
"5": 13,
"6": 3,
"7": 13,
"8": 3,
"9": 13,
"10": 3,
"11": 13,
"12": 3,
"13": 13,
"14": 1,
"15": 13,
"16": 13
},
"branchHits": {
"3": {
"covered": 1,
"available": 1,
"missed": 0
},
"5": {
"covered": 1,
"available": 1,
"missed": 0
},
"7": {
"covered": 1,
"available": 1,
"missed": 0
},
"9": {
"covered": 1,
"available": 1,
"missed": 0
},
"11": {
"covered": 1,
"available": 1,
"missed": 0
},
"13": {
"covered": 1,
"available": 1,
"missed": 0
}
}
},
{
"filename": "src/math.js",
"lineHits": {
"1": 1,
"2": 3,
"3": 2,
"4": 2,
"5": 1,
"6": 1,
"7": 1,
"8": 1,
"9": 3,
"10": 2,
"11": 2,
"12": 1,
"13": 1,
"14": 1,
"15": 1,
"16": 3,
"17": 2,
"18": 2,
"19": 1,
"20": 1,
"21": 1,
"22": 1,
"23": 3,
"24": 2,
"25": 3,
"26": 0,
"27": 0,
"28": 1,
"29": 1
},
"branchHits": {
"2": {
"covered": 3,
"available": 3,
"missed": 0
},
"5": {
"covered": 1,
"available": 1,
"missed": 0
},
"8": {
"covered": 1,
"available": 1,
"missed": 0
},
"9": {
"covered": 2,
"available": 2,
"missed": 0
},
"12": {
"covered": 1,
"available": 1,
"missed": 0
},
"15": {
"covered": 1,
"available": 1,
"missed": 0
},
"16": {
"covered": 2,
"available": 2,
"missed": 0
},
"19": {
"covered": 1,
"available": 1,
"missed": 0
},
"22": {
"covered": 1,
"available": 1,
"missed": 0
},
"23": {
"covered": 2,
"available": 2,
"missed": 0
},
"25": {
"covered": 1,
"available": 2,
"missed": 1
}
}
}
],
"aggregate": {
"line": {
"available": 45,
"covered": 43,
"missed": 2,
"percentage": 95.56
},
"branch": {
"available": 23,
"covered": 22,
"missed": 1,
"percentage": 95.65
}
}
}
@@ -0,0 +1,84 @@
{
"entity": {
"name": "backstage",
"kind": "component",
"namespace": "default"
},
"history": [
{
"timestamp": 1694413579498,
"branch": {
"available": 23,
"covered": 22,
"missed": 1,
"percentage": 95.65
},
"line": {
"available": 45,
"covered": 43,
"missed": 2,
"percentage": 95.56
}
},
{
"timestamp": 1694178927072,
"branch": {
"available": 23,
"covered": 22,
"missed": 1,
"percentage": 95.65
},
"line": {
"available": 45,
"covered": 43,
"missed": 2,
"percentage": 95.56
}
},
{
"timestamp": 1694178830777,
"branch": {
"available": 19,
"covered": 18,
"missed": 1,
"percentage": 94.74
},
"line": {
"available": 45,
"covered": 43,
"missed": 2,
"percentage": 95.56
}
},
{
"timestamp": 1694178708402,
"branch": {
"available": 15,
"covered": 10,
"missed": 5,
"percentage": 66.67
},
"line": {
"available": 45,
"covered": 36,
"missed": 9,
"percentage": 80
}
},
{
"timestamp": 1694178270486,
"branch": {
"available": 10,
"covered": 5,
"missed": 5,
"percentage": 50
},
"line": {
"available": 45,
"covered": 26,
"missed": 19,
"percentage": 57.78
}
}
]
}
@@ -0,0 +1,16 @@
import { add, multiply, subtract, divide } from "./math";
export default (a, b, operator) => {
switch(operator) {
case '+':
return add(a, b);
case '-':
return subtract(a, b);
case '*':
return multiply(a, b);
case '/':
return divide(a, b);
default:
throw new Error('Invalid operator');
}
};
@@ -0,0 +1,29 @@
export function add(a, b) {
if (isNaN(a) || isNaN(b)) {
throw new Error('Invalid input');
}
return a + b;
}
export function subtract(a, b) {
if (isNaN(a) || isNaN(b)) {
throw new Error('Invalid input');
}
return a - b;
}
export function multiply(a, b) {
if (isNaN(a) || isNaN(b)) {
throw new Error('Invalid input');
}
return a * b;
}
export function divide(a, b) {
if (isNaN(a) || isNaN(b)) {
throw new Error('Invalid input');
} else if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}
@@ -0,0 +1,64 @@
/*
* Copyright 2023 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.
*/
export default {
'src/index.js': `import { add, multiply, subtract, divide } from \"./math\";
export default (a, b, operator) => {
switch(operator) {
case '+':
return add(a, b);
case '-':
return subtract(a, b);
case '*':
return multiply(a, b);
case '/':
return divide(a, b);
default:
throw new Error('Invalid operator');
}
};
`,
'src/math.js': `export function add(a, b) {
if (isNaN(a) || isNaN(b)) {
throw new Error('Invalid input');
}
return a + b;
}
export function subtract(a, b) {
if (isNaN(a) || isNaN(b)) {
throw new Error('Invalid input');
}
return a - b;
}
export function multiply(a, b) {
if (isNaN(a) || isNaN(b)) {
throw new Error('Invalid input');
}
return a * b;
}
export function divide(a, b) {
if (isNaN(a) || isNaN(b)) {
throw new Error('Invalid input');
} else if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}
`,
};
+55 -1
View File
@@ -16,11 +16,65 @@
import React from 'react';
import { createDevApp } from '@backstage/dev-utils';
import { codeCoveragePlugin, EntityCodeCoverageContent } from '../src/plugin';
import { CompoundEntityRef, Entity } from '@backstage/catalog-model';
import { EntityProvider } from '@backstage/plugin-catalog-react';
import { codeCoverageApiRef, CodeCoverageApi } from '../src/api';
import coverageForEntity from './__fixtures__/coverage-for-entity.json';
import coverageHistoryForEntity from './__fixtures__/coverage-history-for-entity.json';
import fileContentFromEntity from './__fixtures__/get-file-content-from-entity';
const mockEntity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'backstage',
description: 'backstage.io',
annotations: {
'backstage.io/code-coverage': 'enabled',
},
},
spec: {
lifecycle: 'production',
type: 'website',
owner: 'user:guest',
},
};
const mockCodeCoverageApi: CodeCoverageApi = {
async getCoverageForEntity(_entity: CompoundEntityRef) {
return coverageForEntity as any;
},
async getFileContentFromEntity(_entity: CompoundEntityRef, filePath: string) {
switch (filePath) {
case 'src/index.js':
return fileContentFromEntity['src/index.js'];
case 'src/math.js':
return fileContentFromEntity['src/math.js'];
default:
return '';
}
},
async getCoverageHistoryForEntity(
_entity: CompoundEntityRef,
_limit?: number,
) {
return coverageHistoryForEntity;
},
};
createDevApp()
.registerApi({
api: codeCoverageApiRef,
deps: {},
factory: () => mockCodeCoverageApi,
})
.registerPlugin(codeCoveragePlugin)
.addPage({
element: <EntityCodeCoverageContent />,
element: (
<EntityProvider entity={mockEntity}>
<EntityCodeCoverageContent />
</EntityProvider>
),
title: 'Root Page',
})
.render();
@@ -41,9 +41,11 @@ const useStyles = makeStyles(theme => ({
},
hitCountRoundedRectangle: {
backgroundColor: `${theme.palette.success.main}`,
color: `${theme.palette.success.contrastText}`,
},
notHitCountRoundedRectangle: {
backgroundColor: `${theme.palette.error.main}`,
color: `${theme.palette.error.contrastText}`,
},
codeLine: {
paddingLeft: `${theme.spacing(1)}`,
@@ -52,9 +54,11 @@ const useStyles = makeStyles(theme => ({
},
hitCodeLine: {
backgroundColor: `${theme.palette.success.main}`,
color: `${theme.palette.success.contrastText}`,
},
notHitCodeLine: {
backgroundColor: `${theme.palette.error.main}`,
color: `${theme.palette.error.contrastText}`,
},
}));
@@ -15,15 +15,9 @@
*/
import { useEntity } from '@backstage/plugin-catalog-react';
import {
Box,
Card,
CardContent,
CardHeader,
Modal,
Tooltip,
} from '@material-ui/core';
import DescriptionIcon from '@material-ui/icons/Description';
import { Box, Modal, makeStyles } from '@material-ui/core';
import FolderIcon from '@material-ui/icons/Folder';
import FileOutlinedIcon from '@material-ui/icons/InsertDriveFileOutlined';
import { Alert } from '@material-ui/lab';
import React, { Fragment, useEffect, useState } from 'react';
import useAsync from 'react-use/lib/useAsync';
@@ -38,6 +32,19 @@ import {
} from '@backstage/core-components';
import { useApi } from '@backstage/core-plugin-api';
const useStyles = makeStyles(theme => ({
container: {
marginTop: theme.spacing(2),
},
icon: {
marginRight: theme.spacing(1),
},
link: {
color: theme.palette.primary.main,
cursor: 'pointer',
},
}));
type FileStructureObject = Record<string, any>;
type CoverageTableRow = {
@@ -143,6 +150,7 @@ export const getObjectsAtPath = (
};
export const FileExplorer = () => {
const styles = useStyles();
const { entity } = useEntity();
const [curData, setCurData] = useState<CoverageTableRow | undefined>();
const [tableData, setTableData] = useState<CoverageTableRow[] | undefined>();
@@ -188,6 +196,7 @@ export const FileExplorer = () => {
const moveUpIntoPath = (idx: number) => {
const path = curPath.split('/').slice(0, idx + 1);
setCurFile('');
setCurPath(path.join('/'));
setTableData(getObjectsAtPath(curData, path.slice(1)));
};
@@ -197,43 +206,32 @@ export const FileExplorer = () => {
title: 'Path',
type: 'string',
field: 'path',
render: (row: CoverageTableRow) => {
if (row.files?.length) {
return (
<div
role="button"
tabIndex={row.tableData!.id}
style={{ color: 'lightblue', cursor: 'pointer' }}
onKeyDown={() => {
setCurPath(`${curPath}/${row.path}`);
moveDownIntoPath(row.path);
}}
onClick={() => {
setCurPath(`${curPath}/${row.path}`);
moveDownIntoPath(row.path);
}}
>
{row.path}
</div>
);
}
return (
<Box display="flex" alignItems="center">
{row.path}
<Tooltip title="View file content">
<DescriptionIcon
fontSize="small"
style={{ color: 'lightblue', cursor: 'pointer' }}
onClick={() => {
setCurFile(`${curPath.slice(1)}/${row.path}`);
setModalOpen(true);
}}
/>
</Tooltip>
</Box>
);
},
render: (row: CoverageTableRow) => (
<Box
display="flex"
alignItems="center"
role="button"
tabIndex={row.tableData!.id}
className={styles.link}
onClick={() => {
if (row.files?.length) {
setCurPath(`${curPath}/${row.path}`);
moveDownIntoPath(row.path);
} else {
setCurFile(`${curPath.slice(1)}/${row.path}`);
setModalOpen(true);
}
}}
>
{row.files?.length > 0 && (
<FolderIcon fontSize="small" className={styles.icon} />
)}
{row.files?.length === 0 && (
<FileOutlinedIcon fontSize="small" className={styles.icon} />
)}
{row.path}
</Box>
),
},
{
title: 'Coverage',
@@ -264,42 +262,50 @@ export const FileExplorer = () => {
}
return (
<Card>
<CardHeader title="Explore Files" />
<CardContent>
<Box mb={2} display="flex">
{pathArray.map((pathElement, idx) => (
<Fragment key={pathElement || 'root'}>
<div
role="button"
tabIndex={idx}
style={{
color: `${idx !== lastPathElementIndex && 'lightblue'}`,
cursor: `${idx !== lastPathElementIndex && 'pointer'}`,
}}
onKeyDown={() => moveUpIntoPath(idx)}
onClick={() => moveUpIntoPath(idx)}
>
{pathElement || 'root'}
</div>
<div>{'\u00A0/\u00A0'}</div>
</Fragment>
))}
</Box>
<Table
emptyContent={<>No files found</>}
data={tableData || []}
columns={columns}
/>
<Modal
open={modalOpen}
onClick={event => event.stopPropagation()}
onClose={() => setModalOpen(false)}
style={{ overflow: 'scroll' }}
>
<FileContent filename={curFile} coverage={fileCoverage} />
</Modal>
</CardContent>
</Card>
<Box className={styles.container}>
<Table
emptyContent={<>No files found</>}
data={tableData || []}
columns={columns}
title={
<>
<Box>Explore Files</Box>
<Box
mt={1}
style={{
fontSize: '0.875rem',
fontWeight: 'normal',
display: 'flex',
}}
>
{pathArray.map((pathElement, idx) => (
<Fragment key={pathElement || 'root'}>
<div
role="button"
tabIndex={idx}
className={
idx !== lastPathElementIndex ? styles.link : undefined
}
onKeyDown={() => moveUpIntoPath(idx)}
onClick={() => moveUpIntoPath(idx)}
>
{pathElement || 'root'}
</div>
{idx !== lastPathElementIndex && <div>{'\u00A0/\u00A0'}</div>}
</Fragment>
))}
</Box>
</>
}
/>
<Modal
open={modalOpen}
onClick={event => event.stopPropagation()}
onClose={() => setModalOpen(false)}
style={{ overflow: 'scroll' }}
>
<FileContent filename={curFile} coverage={fileCoverage} />
</Modal>
</Box>
);
};
@@ -14,8 +14,8 @@
* limitations under the License.
*/
import 'highlight.js/styles/atom-one-dark.css';
import highlight from 'highlight.js';
import 'highlight.js/styles/mono-blue.css';
import { highlight } from 'highlight.js';
/*
* Given a file extension, repo name, and array of code lines, return a Promise resolving
@@ -30,7 +30,6 @@ import highlight from 'highlight.js';
*/
export const highlightLines = (fileExtension: string, lines: Array<string>) => {
const formattedLines: Array<string> = [];
let state: CompiledMode | Language | undefined;
let fileformat = fileExtension;
if (fileExtension === 'm') {
fileformat = 'objectivec';
@@ -46,8 +45,10 @@ export const highlightLines = (fileExtension: string, lines: Array<string>) => {
}
lines.forEach(line => {
const result = highlight.highlight(fileformat, line, true, state);
state = result.top;
const result = highlight(line, {
language: fileformat,
ignoreIllegals: true,
});
formattedLines.push(result.value);
});
return formattedLines;