diff --git a/docs/features/software-catalog/well-known-annotations.md b/docs/features/software-catalog/well-known-annotations.md index 5cfe8f39ef..67b92bd941 100644 --- a/docs/features/software-catalog/well-known-annotations.md +++ b/docs/features/software-catalog/well-known-annotations.md @@ -283,9 +283,9 @@ metadata: ``` The value of this annotation controls the code-coverage backstage plugin. If set -to smc-only the plugin will only take into account files stored in source -control (e.g. ignoring generated code). If set to any other non-false:y value -all files covered by a coverage report will be taken into account. +to `scm-only`, the plugin will only take into account files stored in source +control (e.g. ignoring generated code). If set to `enabled`, all files covered +by a coverage report will be taken into account. ## Deprecated Annotations diff --git a/plugins/code-coverage-backend/README.md b/plugins/code-coverage-backend/README.md index 8d1491ef14..101bee3e3f 100644 --- a/plugins/code-coverage-backend/README.md +++ b/plugins/code-coverage-backend/README.md @@ -1,19 +1,23 @@ -# code-coverage +# code-coverage-backend -This is the backend part of the code-coverage plugin. It takes care of processing various coverage formats and standardizing them into a single json format, used by the frontend. +This is the backend part of the `code-coverage` plugin. It takes care of processing various coverage formats and standardizing them into a single json format, used by the frontend. ## Configuring your entity In order to use this plugin, you must set the `backstage.io/code-coverage` annotation. ```yaml -backstage.io/code-coverage: enabled +metadata: + annotations: + backstage.io/code-coverage: enabled ``` There's a feature to only include files that are in VCS in the coverage report, this is helpful to not count generated files for example. To enable this set the `backstage.io/code-coverage` annotation to `scm-only`. ```yaml -backstage.io/code-coverage: scm-only +metadata: + annotations: + backstage.io/code-coverage: scm-only ``` Note: It may be required to set the [`backstage.io/source-location` annotation](https://backstage.io/docs/features/software-catalog/well-known-annotations#backstageiosource-location), however this should generally not be needed. @@ -31,7 +35,7 @@ Example: { "links": [ { - "href": "http://localhost:7000/api/code-coverage/Component/default/entity-name", + "href": "http://localhost:7000/api/code-coverage/report?entity=component:default/entity-name", "rel": "coverage" } ] @@ -49,7 +53,7 @@ Example: { "links": [ { - "href": "http://localhost:7000/api/code-coverage/Component/default/entity-name", + "href": "http://localhost:7000/api/code-coverage/report?entity=component:default/entity-name", "rel": "coverage" } ] diff --git a/plugins/code-coverage-backend/migrations/20210302_init.js b/plugins/code-coverage-backend/migrations/20210302_init.js index b156a9d08d..538f658948 100644 --- a/plugins/code-coverage-backend/migrations/20210302_init.js +++ b/plugins/code-coverage-backend/migrations/20210302_init.js @@ -20,14 +20,23 @@ * @param {import('knex').Knex} knex */ exports.up = async function up(knex) { + // Note for the reader: the knex increments types automatically make it a + // primary column, whether you like it or not. That's why the id column is + // not marked as primary as one might have expected; it's only used for + // lookups by ID. Because, SQLite and MySQL don't return RETURNING on + // inserts ... so we want a manually generated key for lookups (an uuid), + // and also an index for ordering guarantees :) await knex.schema.createTable('code_coverage', table => { table.comment('The table of code coverage'); table - .uuid('id') - .primary() + .bigIncrements('index') .notNullable() - .comment('The ID of the code coverage'); - table.text('entity').notNullable().comment('entity string reference'); + .comment('An insert counter to ensure ordering'); + table.uuid('id').notNullable().comment('The ID of the code coverage'); + table + .text('entity') + .notNullable() + .comment('The entity ref that this code coverage applies to'); table .text('coverage') .notNullable() @@ -37,6 +46,9 @@ exports.up = async function up(knex) { .defaultTo(knex.fn.now()) .notNullable() .comment('The timestamp when this entry was created'); + table.index('index', 'code_coverage_index_idx'); + table.index('id', 'code_coverage_id_idx'); + table.index('entity', 'code_coverage_entity_idx'); }); }; @@ -44,5 +56,10 @@ exports.up = async function up(knex) { * @param {import('knex').Knex} knex */ exports.down = async function down(knex) { + await knex.schema.alterTable('code_coverage', table => { + table.dropIndex([], 'code_coverage_index_idx'); + table.dropIndex([], 'code_coverage_id_idx'); + table.dropIndex([], 'code_coverage_entity_idx'); + }); await knex.schema.dropTable('code_coverage'); }; diff --git a/plugins/code-coverage-backend/package.json b/plugins/code-coverage-backend/package.json index 8bcce8784d..1e7903b016 100644 --- a/plugins/code-coverage-backend/package.json +++ b/plugins/code-coverage-backend/package.json @@ -19,13 +19,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.5.4", - "@backstage/catalog-client": "^0.3.6", - "@backstage/catalog-model": "^0.7.2", - "@backstage/config": "^0.1.3", + "@backstage/backend-common": "^0.6.2", + "@backstage/catalog-client": "^0.3.10", + "@backstage/catalog-model": "^0.7.7", + "@backstage/config": "^0.1.4", "@backstage/errors": "^0.1.1", - "@backstage/integration": "^0.5.0", - "@backstage/plugin-catalog-backend": "^0.6.5", + "@backstage/integration": "^0.5.1", "@types/express": "^4.17.6", "cross-fetch": "^3.0.6", "express": "^4.17.1", @@ -37,7 +36,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.6.2", + "@backstage/cli": "^0.6.8", "@types/express-xml-bodyparser": "^0.3.2", "@types/supertest": "^2.0.8", "msw": "^0.21.2", diff --git a/plugins/code-coverage-backend/src/index.ts b/plugins/code-coverage-backend/src/index.ts index 12de31b541..7612c392a2 100644 --- a/plugins/code-coverage-backend/src/index.ts +++ b/plugins/code-coverage-backend/src/index.ts @@ -15,5 +15,3 @@ */ export * from './service/router'; - -export * from './service/jsoncoverage-types'; diff --git a/plugins/code-coverage-backend/src/service/CodeCoverageDatabase.test.ts b/plugins/code-coverage-backend/src/service/CodeCoverageDatabase.test.ts index 108146287d..688eeb4ba2 100644 --- a/plugins/code-coverage-backend/src/service/CodeCoverageDatabase.test.ts +++ b/plugins/code-coverage-backend/src/service/CodeCoverageDatabase.test.ts @@ -13,17 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - getVoidLogger, - SingleConnectionDatabaseManager, -} from '@backstage/backend-common'; +import { SingleConnectionDatabaseManager } from '@backstage/backend-common'; import { stringifyEntityRef } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import { CodeCoverageDatabase, CodeCoverageStore, } from './CodeCoverageDatabase'; -import { JsonCodeCoverage } from './jsoncoverage-types'; +import { JsonCodeCoverage } from './types'; const db = SingleConnectionDatabaseManager.fromConfig( new ConfigReader({ @@ -103,10 +100,9 @@ let database: CodeCoverageStore; describe('CodeCoverageDatabase', () => { beforeAll(async () => { const client = await db.getClient(); - database = await CodeCoverageDatabase.create(client, getVoidLogger()); - database.insertCodeCoverage(coverage[0]); - await new Promise(r => setTimeout(r, 1000)); - database.insertCodeCoverage(coverage[1]); + database = await CodeCoverageDatabase.create(client); + await database.insertCodeCoverage(coverage[0]); + await database.insertCodeCoverage(coverage[1]); }); describe('insertCodeCoverage', () => { diff --git a/plugins/code-coverage-backend/src/service/CodeCoverageDatabase.ts b/plugins/code-coverage-backend/src/service/CodeCoverageDatabase.ts index cb83c2f606..390fd815f1 100644 --- a/plugins/code-coverage-backend/src/service/CodeCoverageDatabase.ts +++ b/plugins/code-coverage-backend/src/service/CodeCoverageDatabase.ts @@ -18,15 +18,15 @@ import { NotFoundError } from '@backstage/errors'; import { parseEntityName, stringifyEntityRef } from '@backstage/catalog-model'; import { Knex } from 'knex'; import { v4 as uuid } from 'uuid'; -import { Logger } from 'winston'; import { aggregateCoverage } from './CoverageUtils'; -import { JsonCodeCoverage, JsonCoverageHistory } from './jsoncoverage-types'; +import { JsonCodeCoverage, JsonCoverageHistory } from './types'; export type RawDbCoverageRow = { id: string; entity: string; coverage: string; }; + export interface CodeCoverageStore { insertCodeCoverage( coverage: JsonCodeCoverage, @@ -39,44 +39,49 @@ const migrationsDir = resolvePackagePath( '@backstage/plugin-code-coverage-backend', 'migrations', ); + export class CodeCoverageDatabase implements CodeCoverageStore { - static async create(knex: Knex, logger: Logger): Promise { + static async create(knex: Knex): Promise { await knex.migrate.latest({ directory: migrationsDir, }); - return new CodeCoverageDatabase(knex, logger); + return new CodeCoverageDatabase(knex); } - constructor(private readonly db: Knex, private readonly logger: Logger) {} + constructor(private readonly db: Knex) {} async insertCodeCoverage( coverage: JsonCodeCoverage, ): Promise<{ codeCoverageId: string }> { const codeCoverageId = uuid(); - this.logger.error(JSON.stringify(coverage.entity)); const entity = stringifyEntityRef({ kind: coverage.entity.kind, namespace: coverage.entity.namespace, name: coverage.entity.name, }); + await this.db('code_coverage').insert({ id: codeCoverageId, entity: entity, coverage: JSON.stringify(coverage), }); + return { codeCoverageId }; } + async getCodeCoverage(entity: string): Promise { const [result] = await this.db('code_coverage') .where({ entity: entity }) - .orderBy('created_at', 'desc') + .orderBy('index', 'desc') .limit(1) .select(); + if (!result) { throw new NotFoundError( `No coverage for entity '${JSON.stringify(entity)}' found`, ); } + try { return JSON.parse(result.coverage); } catch (error) { @@ -90,7 +95,7 @@ export class CodeCoverageDatabase implements CodeCoverageStore { ): Promise { const res = await this.db('code_coverage') .where({ entity: entity }) - .orderBy('created_at', 'desc') + .orderBy('index', 'desc') .limit(limit) .select(); @@ -99,6 +104,7 @@ export class CodeCoverageDatabase implements CodeCoverageStore { .map(c => aggregateCoverage(c)); const entityName = parseEntityName(entity); + return { entity: { name: entityName.name, diff --git a/plugins/code-coverage-backend/src/service/CoverageUtils.ts b/plugins/code-coverage-backend/src/service/CoverageUtils.ts index 4850ac9b1b..f41a1d05a0 100644 --- a/plugins/code-coverage-backend/src/service/CoverageUtils.ts +++ b/plugins/code-coverage-backend/src/service/CoverageUtils.ts @@ -22,11 +22,7 @@ import { stringifyEntityRef, } from '@backstage/catalog-model'; import { ScmIntegration, ScmIntegrations } from '@backstage/integration'; -import { - AggregateCoverage, - FileEntry, - JsonCodeCoverage, -} from './jsoncoverage-types'; +import { AggregateCoverage, FileEntry, JsonCodeCoverage } from './types'; export const calculatePercentage = ( available: number, diff --git a/plugins/code-coverage-backend/src/service/converter/Converter.ts b/plugins/code-coverage-backend/src/service/converter/Converter.ts index 20c6920c99..43d9855e61 100644 --- a/plugins/code-coverage-backend/src/service/converter/Converter.ts +++ b/plugins/code-coverage-backend/src/service/converter/Converter.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { FileEntry } from '../jsoncoverage-types'; +import { FileEntry } from '../types'; export interface Converter { convert(xml: unknown, scmFiles: Array): Array; diff --git a/plugins/code-coverage-backend/src/service/converter/cobertura.ts b/plugins/code-coverage-backend/src/service/converter/cobertura.ts index fd3cfe9c01..02ac187387 100644 --- a/plugins/code-coverage-backend/src/service/converter/cobertura.ts +++ b/plugins/code-coverage-backend/src/service/converter/cobertura.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { BranchHit, FileEntry } from '../jsoncoverage-types'; +import { BranchHit, FileEntry } from '../types'; import { CoberturaXML, InnerClass, LineHit } from './types'; import { Logger } from 'winston'; import { Converter } from './Converter'; @@ -63,7 +63,7 @@ export class Cobertura implements Converter { }); const currentFile = scmFiles.find(f => f.endsWith(packageAndFilename)); - this.logger.info(`matched ${packageAndFilename} to ${currentFile}`); + this.logger.debug(`matched ${packageAndFilename} to ${currentFile}`); if ( scmFiles.length === 0 || (Object.keys(lineHits).length > 0 && currentFile) diff --git a/plugins/code-coverage-backend/src/service/converter/jacoco.ts b/plugins/code-coverage-backend/src/service/converter/jacoco.ts index 8dfcf0cb51..4c554fec91 100644 --- a/plugins/code-coverage-backend/src/service/converter/jacoco.ts +++ b/plugins/code-coverage-backend/src/service/converter/jacoco.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { BranchHit, FileEntry } from '../jsoncoverage-types'; +import { BranchHit, FileEntry } from '../types'; import { JacocoSourceFile, JacocoXML } from './types'; import { Logger } from 'winston'; import { Converter } from './Converter'; @@ -35,7 +35,7 @@ export class Jacoco implements Converter { * Converts jacoco into shared json coverage format * * @param xml jacoco xml object - * @param scmFiles list of files that are commited to SCM + * @param scmFiles list of files that are committed to SCM */ convert(xml: JacocoXML, scmFiles: Array): Array { const jscov: Array = []; @@ -64,7 +64,7 @@ export class Jacoco implements Converter { const packageAndFilename = `${packageName}/${fileName}`; const currentFile = scmFiles.find(f => f.endsWith(packageAndFilename)); - this.logger.info(`matched ${packageAndFilename} to ${currentFile}`); + this.logger.debug(`matched ${packageAndFilename} to ${currentFile}`); if (Object.keys(lineHits).length > 0 && currentFile) { jscov.push({ filename: currentFile, diff --git a/plugins/code-coverage-backend/src/service/router.ts b/plugins/code-coverage-backend/src/service/router.ts index f2e34cd07a..78aeab313c 100644 --- a/plugins/code-coverage-backend/src/service/router.ts +++ b/plugins/code-coverage-backend/src/service/router.ts @@ -58,7 +58,6 @@ export const makeRouter = async ( const codeCoverageDatabase = await CodeCoverageDatabase.create( await database.getClient(), - logger, ); const codecovUrl = await discovery.getExternalBaseUrl('code-coverage'); const catalogApi = new CatalogClient({ discoveryApi: discovery }); @@ -147,8 +146,8 @@ export const makeRouter = async ( const scmTree = await urlReader.readTree(sourceLocation.target); const scmFile = (await scmTree.files()).find(f => f.path === path); if (!scmFile) { - res.status(400).json({ - message: "couldn't find file in SCM", + res.status(404).json({ + message: "Couldn't find file in SCM", file: path, scm: vcs.title, }); @@ -157,7 +156,7 @@ export const makeRouter = async ( const content = await scmFile?.content(); if (!content) { res.status(400).json({ - message: "couldn't process content of file in SCM", + message: "Couldn't process content of file in SCM", file: path, scm: vcs.title, }); @@ -187,7 +186,7 @@ export const makeRouter = async ( } else if (coverageType === 'cobertura') { converter = new Cobertura(logger); } else { - throw new NotFoundError(`unsupported coverage type '${coverageType}`); + throw new InputError(`Unsupported coverage type '${coverageType}`); } const { diff --git a/plugins/code-coverage-backend/src/service/standaloneServer.ts b/plugins/code-coverage-backend/src/service/standaloneServer.ts index 8a3c4eaea1..2fb9627936 100644 --- a/plugins/code-coverage-backend/src/service/standaloneServer.ts +++ b/plugins/code-coverage-backend/src/service/standaloneServer.ts @@ -22,9 +22,9 @@ import { useHotMemoize, } from '@backstage/backend-common'; import { Server } from 'http'; +import knexFactory from 'knex'; import { Logger } from 'winston'; import { createRouter } from './router'; -import { DatabaseManager } from '@backstage/plugin-catalog-backend'; export interface ServerOptions { port: number; @@ -38,13 +38,23 @@ export async function startStandaloneServer( const logger = options.logger.child({ service: 'code-coverage-backend' }); const config = await loadBackendConfig({ logger, argv: process.argv }); - const db = useHotMemoize(module, () => - DatabaseManager.createInMemoryDatabaseConnection(), - ); + const db = useHotMemoize(module, () => { + const knex = knexFactory({ + client: 'sqlite3', + connection: ':memory:', + useNullAsDefault: true, + }); + + knex.client.pool.on('createSuccess', (_eventId: any, resource: any) => { + resource.run('PRAGMA foreign_keys = ON', () => {}); + }); + + return knex; + }); logger.debug('Starting application server...'); const router = await createRouter({ - database: { getClient: () => db }, + database: { getClient: async () => db }, config, discovery: SingleHostDiscovery.fromConfig(config), urlReader: UrlReaders.default({ logger, config }), diff --git a/plugins/code-coverage-backend/src/service/jsoncoverage-types.ts b/plugins/code-coverage-backend/src/service/types.ts similarity index 100% rename from plugins/code-coverage-backend/src/service/jsoncoverage-types.ts rename to plugins/code-coverage-backend/src/service/types.ts diff --git a/plugins/code-coverage/README.md b/plugins/code-coverage/README.md index c271b9ff36..8903668507 100644 --- a/plugins/code-coverage/README.md +++ b/plugins/code-coverage/README.md @@ -1,19 +1,23 @@ # code-coverage -This is the frontend part of the code-coverage plugin. It takes care of processing various coverage formats and standardizing them into a single json format, used by the frontend. +This is the frontend part of the code-coverage plugin. It displays code coverage summaries for your entities. ## Configuring your entity -In order to use this plugin, you must set the `backstage.io/code-coverage` annotation. +In order to use this plugin, you must set the `backstage.io/code-coverage` annotation on entities for which coverage ingestion has been enabled. ```yaml -backstage.io/code-coverage: enabled +metadata: + annotations: + backstage.io/code-coverage: enabled ``` There's a feature to only include files that are in VCS in the coverage report, this is helpful to not count generated files for example. To enable this set the `backstage.io/code-coverage` annotation to `scm-only`. ```yaml -backstage.io/code-coverage: scm-only +metadata: + annotations: + backstage.io/code-coverage: scm-only ``` Note: It may be required to set the [`backstage.io/source-location` annotation](https://backstage.io/docs/features/software-catalog/well-known-annotations#backstageiosource-location), however this should generally not be needed. diff --git a/plugins/code-coverage/package.json b/plugins/code-coverage/package.json index 974e93e430..1d02da3dea 100644 --- a/plugins/code-coverage/package.json +++ b/plugins/code-coverage/package.json @@ -20,13 +20,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.2", - "@backstage/config": "^0.1.3", + "@backstage/catalog-model": "^0.7.7", + "@backstage/config": "^0.1.4", "@backstage/core": "^0.7.5", - "@backstage/core-api": "^0.2.12", - "@backstage/errors": "^0.1.0", - "@backstage/plugin-catalog-react": "^0.1.0", - "@backstage/plugin-code-coverage-backend": "^0.1.0", + "@backstage/errors": "^0.1.1", + "@backstage/plugin-catalog-react": "^0.1.4", "@backstage/theme": "^0.2.5", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", diff --git a/plugins/code-coverage/src/api.ts b/plugins/code-coverage/src/api.ts index d168f50d95..6331f8020f 100644 --- a/plugins/code-coverage/src/api.ts +++ b/plugins/code-coverage/src/api.ts @@ -14,27 +14,10 @@ * limitations under the License. */ -import { createApiRef, DiscoveryApi } from '@backstage/core'; import { EntityName, stringifyEntityRef } from '@backstage/catalog-model'; -import { - JsonCodeCoverage, - JsonCoverageHistory, -} from '@backstage/plugin-code-coverage-backend'; +import { createApiRef, DiscoveryApi } from '@backstage/core'; import { ResponseError } from '@backstage/errors'; - -export class FetchError extends Error { - get name(): string { - return this.constructor.name; - } - - static async forResponse(resp: Response): Promise { - return new FetchError( - `Request failed with status code ${ - resp.status - }.\nReason: ${await resp.text()}`, - ); - } -} +import { JsonCodeCoverage, JsonCoverageHistory } from './types'; export type CodeCoverageApi = { discovery: DiscoveryApi; @@ -79,7 +62,7 @@ export class CodeCoverageRestApi implements CodeCoverageApi { async getCoverageForEntity( entityName: EntityName, ): Promise { - const entity = encodeURI(stringifyEntityRef(entityName)); + const entity = encodeURIComponent(stringifyEntityRef(entityName)); return (await this.fetch( `/report?entity=${entity}`, )) as JsonCodeCoverage; @@ -89,7 +72,7 @@ export class CodeCoverageRestApi implements CodeCoverageApi { entityName: EntityName, filePath: string, ): Promise { - const entity = encodeURI(stringifyEntityRef(entityName)); + const entity = encodeURIComponent(stringifyEntityRef(entityName)); return await this.fetch( `/file-content?entity=${entity}&path=${encodeURI(filePath)}`, ); @@ -99,11 +82,11 @@ export class CodeCoverageRestApi implements CodeCoverageApi { entityName: EntityName, limit?: number, ): Promise { - const entity = encodeURI(stringifyEntityRef(entityName)); + const entity = encodeURIComponent(stringifyEntityRef(entityName)); const hasValidLimit = limit && limit > 0; return (await this.fetch( `/history?entity=${entity}${ - hasValidLimit ? `&limit=${encodeURI(`${limit}`)}` : '' + hasValidLimit ? `&limit=${encodeURIComponent(String(limit))}` : '' }`, )) as JsonCoverageHistory; } diff --git a/plugins/code-coverage/src/components/CoverageHistoryChart/CoverageHistoryChart.tsx b/plugins/code-coverage/src/components/CoverageHistoryChart/CoverageHistoryChart.tsx index f9927a17d3..7bb4648f6f 100644 --- a/plugins/code-coverage/src/components/CoverageHistoryChart/CoverageHistoryChart.tsx +++ b/plugins/code-coverage/src/components/CoverageHistoryChart/CoverageHistoryChart.tsx @@ -14,7 +14,9 @@ * limitations under the License. */ -import React from 'react'; +import { Progress, ResponseErrorPanel, useApi } from '@backstage/core'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { BackstageTheme } from '@backstage/theme'; import { Box, Card, @@ -23,27 +25,24 @@ import { makeStyles, Typography, } from '@material-ui/core'; +import TrendingDownIcon from '@material-ui/icons/TrendingDown'; +import TrendingFlatIcon from '@material-ui/icons/TrendingFlat'; +import TrendingUpIcon from '@material-ui/icons/TrendingUp'; +import { Alert } from '@material-ui/lab'; +import { ClassNameMap } from '@material-ui/styles'; +import React from 'react'; +import { useAsync } from 'react-use'; import { - LineChart, - XAxis, - YAxis, - Tooltip, + CartesianGrid, Legend, Line, - CartesianGrid, + LineChart, ResponsiveContainer, + Tooltip, + XAxis, + YAxis, } from 'recharts'; -import { useAsync } from 'react-use'; -import { useApi } from '@backstage/core-api'; -import { useEntity } from '@backstage/plugin-catalog-react'; import { codeCoverageApiRef } from '../../api'; -import { Progress, ResponseErrorPanel } from '@backstage/core'; -import { Alert } from '@material-ui/lab'; -import TrendingDownIcon from '@material-ui/icons/TrendingDown'; -import TrendingUpIcon from '@material-ui/icons/TrendingUp'; -import TrendingFlatIcon from '@material-ui/icons/TrendingFlat'; -import { BackstageTheme } from '@backstage/theme'; -import { ClassNameMap } from '@material-ui/styles'; type Coverage = 'line' | 'branch'; diff --git a/plugins/code-coverage/src/components/FileExplorer/CoverageRow.tsx b/plugins/code-coverage/src/components/FileExplorer/CodeRow.tsx similarity index 98% rename from plugins/code-coverage/src/components/FileExplorer/CoverageRow.tsx rename to plugins/code-coverage/src/components/FileExplorer/CodeRow.tsx index 8b9da9d9e8..bd982b0f6b 100644 --- a/plugins/code-coverage/src/components/FileExplorer/CoverageRow.tsx +++ b/plugins/code-coverage/src/components/FileExplorer/CodeRow.tsx @@ -13,9 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; -// styles +import React from 'react'; import { makeStyles } from '@material-ui/core'; const useStyles = makeStyles(theme => ({ @@ -64,7 +63,8 @@ type CodeRowProps = { lineContent: string; lineHits?: number | null; }; -const CodeRow = ({ + +export const CodeRow = ({ lineNumber, lineContent, lineHits = null, @@ -98,5 +98,3 @@ const CodeRow = ({ ); }; - -export default CodeRow; diff --git a/plugins/code-coverage/src/components/FileExplorer/FileContent.tsx b/plugins/code-coverage/src/components/FileExplorer/FileContent.tsx index afaff61340..b43874123d 100644 --- a/plugins/code-coverage/src/components/FileExplorer/FileContent.tsx +++ b/plugins/code-coverage/src/components/FileExplorer/FileContent.tsx @@ -14,17 +14,16 @@ * limitations under the License. */ -import React from 'react'; -import { useApi } from '@backstage/core-api'; +import { Progress, ResponseErrorPanel, useApi } from '@backstage/core'; import { useEntity } from '@backstage/plugin-catalog-react'; +import { makeStyles, Paper } from '@material-ui/core'; +import { Alert } from '@material-ui/lab'; +import React from 'react'; import { useAsync } from 'react-use'; import { codeCoverageApiRef } from '../../api'; -import { Progress, ResponseErrorPanel } from '@backstage/core'; -import { Alert } from '@material-ui/lab'; -import { makeStyles, Paper } from '@material-ui/core'; +import { FileEntry } from '../../types'; +import { CodeRow } from './CodeRow'; import { highlightLines } from './Highlighter'; -import CoverageRow from './CoverageRow'; -import { FileEntry } from '@backstage/plugin-code-coverage-backend'; type Props = { filename: string; @@ -61,7 +60,7 @@ const FormattedLines = ({ {highlightedLines.map((lineContent, idx) => { const line = idx + 1; return ( - ; diff --git a/plugins/code-coverage/src/types.ts b/plugins/code-coverage/src/types.ts new file mode 100644 index 0000000000..34fcf57968 --- /dev/null +++ b/plugins/code-coverage/src/types.ts @@ -0,0 +1,71 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { EntityName } from '@backstage/catalog-model'; + +export type JsonCodeCoverage = { + metadata: CoverageMetadata; + entity: EntityName; + files: Array; +}; + +export type JsonCoverageHistory = { + entity: EntityName; + history: Array; +}; + +export type CoverageHistory = { + line: { + available: number; + covered: number; + }; + branch: BranchHit; +}; + +export type CoverageMetadata = { + vcs: { + type: string; + location: string; + }; + generationTime: number; +}; + +export type BranchHit = { + covered: number; + missed: number; + available: number; +}; + +export type FileEntry = { + filename: string; + lineHits: Record; + branchHits: Record; +}; + +export type AggregateCoverage = { + timestamp: number; + line: { + available: number; + covered: number; + missed: number; + percentage: number; + }; + branch: { + available: number; + covered: number; + missed: number; + percentage: number; + }; +}; diff --git a/yarn.lock b/yarn.lock index 79f7d85b38..f614a7713a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1616,7 +1616,7 @@ core-js-pure "^3.0.0" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.0", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.10.4", "@babel/runtime@^7.10.5", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.0", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.4", "@babel/runtime@^7.10.5", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.13.10" resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.13.10.tgz#47d42a57b6095f4468da440388fdbad8bebf0d7d" integrity sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw== @@ -1679,95 +1679,6 @@ lodash "^4.17.19" to-fast-properties "^2.0.0" -"@backstage/backend-common@^0.5.4": - version "0.5.6" - resolved "https://registry.npmjs.org/@backstage/backend-common/-/backend-common-0.5.6.tgz#e49840eaa6738f6697319ccdfff04ce40cc54b72" - integrity sha512-BzKlcTZLAVDpH87fi2TCWIFO9DfVdaPIIZ8gZSorCuLLErG4X7CS/zQiujJZ/zD5HGtO2QB57fuRkwjBMhJhhw== - dependencies: - "@backstage/cli-common" "^0.1.1" - "@backstage/config" "^0.1.2" - "@backstage/config-loader" "^0.5.1" - "@backstage/integration" "^0.5.1" - "@octokit/rest" "^18.0.12" - "@types/cors" "^2.8.6" - "@types/dockerode" "^3.2.1" - "@types/express" "^4.17.6" - archiver "^5.0.2" - compression "^1.7.4" - concat-stream "^2.0.0" - cors "^2.8.5" - cross-fetch "^3.0.6" - dockerode "^3.2.1" - express "^4.17.1" - express-promise-router "^3.0.3" - fs-extra "^9.0.1" - git-url-parse "^11.4.4" - helmet "^4.0.0" - isomorphic-git "^1.8.0" - knex "^0.95.1" - lodash "^4.17.15" - logform "^2.1.1" - minimatch "^3.0.4" - minimist "^1.2.5" - morgan "^1.10.0" - selfsigned "^1.10.7" - stoppable "^1.1.0" - tar "^6.0.5" - unzipper "^0.10.11" - winston "^3.2.1" - -"@backstage/config-loader@^0.5.1": - version "0.5.1" - resolved "https://registry.npmjs.org/@backstage/config-loader/-/config-loader-0.5.1.tgz#a80f2047e209d2f41b17ad715c632ab142385b39" - integrity sha512-PmwXERs5BIf77sUIg3Fhp/elkR6ELj0czmpUgP4cuEdtazi0WcmdjCUC2DjgsKJWvYggsL1o372QKHh/M5AMTQ== - dependencies: - "@backstage/cli-common" "^0.1.1" - "@backstage/config" "^0.1.1" - ajv "^7.0.3" - fs-extra "^9.0.0" - json-schema "^0.2.5" - json-schema-merge-allof "^0.7.0" - typescript-json-schema "^0.47.0" - yaml "^1.9.2" - yup "^0.29.3" - -"@backstage/plugin-catalog-backend@^0.6.5": - version "0.6.7" - resolved "https://registry.npmjs.org/@backstage/plugin-catalog-backend/-/plugin-catalog-backend-0.6.7.tgz#97d0ed6db6aeb04e09ed0fee05e76bacf66cc179" - integrity sha512-Ey9PXyUjmi8IxQNSXBhF6Sl2SmZ/iXCFHXnffH8XCdBFTZS8HDRE2tSOlIIU4XQKd6lJVuAGTekas2DkaS9/lA== - dependencies: - "@azure/msal-node" "^1.0.0-beta.3" - "@backstage/backend-common" "^0.6.0" - "@backstage/catalog-model" "^0.7.4" - "@backstage/config" "^0.1.4" - "@backstage/errors" "^0.1.1" - "@backstage/integration" "^0.5.1" - "@backstage/plugin-search-backend-node" "^0.1.2" - "@backstage/search-common" "^0.1.1" - "@octokit/graphql" "^4.5.8" - "@types/express" "^4.17.6" - "@types/ldapjs" "^1.0.9" - aws-sdk "^2.840.0" - codeowners-utils "^1.0.2" - core-js "^3.6.5" - cross-fetch "^3.0.6" - express "^4.17.1" - express-promise-router "^4.1.0" - fs-extra "^9.0.0" - git-url-parse "^11.4.4" - glob "^7.1.6" - knex "^0.95.1" - ldapjs "^2.2.0" - lodash "^4.17.15" - morgan "^1.10.0" - p-limit "^3.0.2" - qs "^6.9.4" - uuid "^8.0.0" - winston "^3.2.1" - yaml "^1.9.2" - yn "^4.0.0" - yup "^0.29.3" - "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -5609,20 +5520,6 @@ lz-string "^1.4.4" pretty-format "^26.6.2" -"@testing-library/dom@^7.22.3": - version "7.30.3" - resolved "https://registry.npmjs.org/@testing-library/dom/-/dom-7.30.3.tgz#779ea9bbb92d63302461800a388a5a890ac22519" - integrity sha512-7JhIg2MW6WPwyikH2iL3o7z+FTVgSOd2jqCwTAHqK7Qal2gRRYiUQyURAxtbK9VXm/UTyG9bRihv8C5Tznr2zw== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/runtime" "^7.12.5" - "@types/aria-query" "^4.2.0" - aria-query "^4.2.2" - chalk "^4.1.0" - dom-accessibility-api "^0.5.4" - lz-string "^1.4.4" - pretty-format "^26.6.2" - "@testing-library/jest-dom@^5.10.1": version "5.11.9" resolved "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.11.9.tgz#e6b3cd687021f89f261bd53cbe367041fbd3e975" @@ -5653,14 +5550,6 @@ "@babel/runtime" "^7.5.4" "@types/testing-library__react-hooks" "^3.4.0" -"@testing-library/react@^10.4.1": - version "10.4.9" - resolved "https://registry.npmjs.org/@testing-library/react/-/react-10.4.9.tgz#9faa29c6a1a217bf8bbb96a28bd29d7a847ca150" - integrity sha512-pHZKkqUy0tmiD81afs8xfiuseXfU/N7rAX3iKjeZYje86t9VaB0LrxYVa+OOsvkrveX5jCK3IjajVn2MbePvqA== - dependencies: - "@babel/runtime" "^7.10.3" - "@testing-library/dom" "^7.22.3" - "@testing-library/react@^11.2.5": version "11.2.6" resolved "https://registry.npmjs.org/@testing-library/react/-/react-11.2.6.tgz#586a23adc63615985d85be0c903f374dab19200b" @@ -6439,11 +6328,6 @@ resolved "https://registry.npmjs.org/@types/node/-/node-10.17.35.tgz#58058f29b870e6ae57b20e4f6e928f02b7129f56" integrity sha512-gXx7jAWpMddu0f7a+L+txMplp3FnHl53OhQIF9puXKq3hDGY/GjH+MF04oWnV/adPSCrbtHumDCFwzq2VhltWA== -"@types/node@^12.0.0": - version "12.20.10" - resolved "https://registry.npmjs.org/@types/node/-/node-12.20.10.tgz#4dcb8a85a8f1211acafb88d72fafc7e3d2685583" - integrity sha512-TxCmnSSppKBBOzYzPR2BR25YlX5Oay8z2XGwFBInuA/Co0V9xJhLlW4kjbxKtgeNo3NOMbQP1A5Rc03y+XecPw== - "@types/node@^12.7.1": version "12.12.58" resolved "https://registry.npmjs.org/@types/node/-/node-12.12.58.tgz#46dae9b2b9ee5992818c8f7cee01ff4ce03ab44c" @@ -12975,15 +12859,6 @@ expect@^26.6.2: jest-message-util "^26.6.2" jest-regex-util "^26.0.0" -express-promise-router@^3.0.3: - version "3.0.3" - resolved "https://registry.npmjs.org/express-promise-router/-/express-promise-router-3.0.3.tgz#5e6d22a5a3f013d71833172fe8d7ab780c3f6b70" - integrity sha1-Xm0ipaPwE9cYMxcv6NereAw/a3A= - dependencies: - is-promise "^2.1.0" - lodash.flattendeep "^4.0.0" - methods "^1.0.0" - express-promise-router@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/express-promise-router/-/express-promise-router-4.1.0.tgz#79160e145c27610ba411bceb0552a36f11dbab4f" @@ -25778,17 +25653,6 @@ typedarray@^0.0.6: resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript-json-schema@^0.47.0: - version "0.47.0" - resolved "https://registry.npmjs.org/typescript-json-schema/-/typescript-json-schema-0.47.0.tgz#84dde5460b127c6774da81bf70b23c7e04857b13" - integrity sha512-A6NVwSOTSsNDHfaqDcDeKwwyXEeKqBHoAr20jcetnYj4e8C6zVFofAVhAuwsBXCRYiWEE/lyHrcxpsSpbIk0Mg== - dependencies: - "@types/json-schema" "^7.0.6" - glob "^7.1.6" - json-stable-stringify "^1.0.1" - typescript "^4.1.3" - yargs "^16.2.0" - typescript-json-schema@^0.49.0: version "0.49.0" resolved "https://registry.npmjs.org/typescript-json-schema/-/typescript-json-schema-0.49.0.tgz#442f6347ca85fb0d9811f217fb0d6537b68734b3"