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:
@@ -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;
|
||||
}
|
||||
`,
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user