diff --git a/.changeset/chatty-parents-search.md b/.changeset/chatty-parents-search.md new file mode 100644 index 0000000000..d9b1222d8f --- /dev/null +++ b/.changeset/chatty-parents-search.md @@ -0,0 +1,5 @@ +--- +'@backstage/core': patch +--- + +Sort the table filter options by name. diff --git a/.changeset/giant-onions-obey.md b/.changeset/giant-onions-obey.md new file mode 100644 index 0000000000..fbf9faa0a3 --- /dev/null +++ b/.changeset/giant-onions-obey.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-import': patch +--- + +allow import from HTTP repositories diff --git a/.changeset/gorgeous-pumas-cover.md b/.changeset/gorgeous-pumas-cover.md new file mode 100644 index 0000000000..c4c9facac4 --- /dev/null +++ b/.changeset/gorgeous-pumas-cover.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-welcome': patch +--- + +Australian Greeting diff --git a/.changeset/grumpy-games-shop.md b/.changeset/grumpy-games-shop.md new file mode 100644 index 0000000000..7426786f89 --- /dev/null +++ b/.changeset/grumpy-games-shop.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +remove use of deprecated type HelmetOptions diff --git a/.changeset/light-fireants-flow.md b/.changeset/light-fireants-flow.md new file mode 100644 index 0000000000..a8f7b09278 --- /dev/null +++ b/.changeset/light-fireants-flow.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +Fix the schema / code mismatch in LDAP `set` config diff --git a/.changeset/light-horses-hammer.md b/.changeset/light-horses-hammer.md new file mode 100644 index 0000000000..0d421c7056 --- /dev/null +++ b/.changeset/light-horses-hammer.md @@ -0,0 +1,5 @@ +--- +'@backstage/theme': patch +--- + +Replace the link color in dark theme diff --git a/.changeset/new-shirts-move.md b/.changeset/new-shirts-move.md new file mode 100644 index 0000000000..3896c85bbb --- /dev/null +++ b/.changeset/new-shirts-move.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Add low german greeting diff --git a/.changeset/strong-houses-lick.md b/.changeset/strong-houses-lick.md new file mode 100644 index 0000000000..9739bdfda1 --- /dev/null +++ b/.changeset/strong-houses-lick.md @@ -0,0 +1,6 @@ +--- +'@backstage/core-api': patch +'@backstage/core': patch +--- + +Fixed a potentially confusing error being thrown about misuse of routable extensions where the error was actually something different. diff --git a/.changeset/unlucky-starfishes-vanish.md b/.changeset/unlucky-starfishes-vanish.md new file mode 100644 index 0000000000..3bb341bee9 --- /dev/null +++ b/.changeset/unlucky-starfishes-vanish.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-fossa': patch +--- + +Add a `FossaPage` that shows the license compliance status of all components in the catalog. +See the projects `Readme` on how to use it. diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4e21603c63..803ebd6445 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,6 +8,8 @@ /docs/features/techdocs @backstage/techdocs-core /docs/features/search @backstage/techdocs-core /docs/assets/search @backstage/techdocs-core +/plugins/code-coverage @alde @nissayeva +/plugins/code-coverage-backend @alde @nissayeva /plugins/cost-insights @backstage/silver-lining /plugins/cloudbuild @trivago/ebarrios /plugins/search @backstage/techdocs-core diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index 0472a199ba..a12f638a74 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -10,6 +10,7 @@ Chai Changesets Chanwit Cloudformation +Cobertura Codecov Codehilite Config @@ -22,6 +23,7 @@ Dockerize Docusaurus Env Expedia +Fargate Figma Firekube Fiverr @@ -33,6 +35,7 @@ Hackathons Heroku Hostname Iain +JaCoCo JavaScript Kaewkasi Knex @@ -77,6 +80,7 @@ Tolerations WWW Weaveworks Webpack +XML Zalando Zhou abc diff --git a/app-config.yaml b/app-config.yaml index ac59874878..3b94faa3f9 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -124,6 +124,9 @@ integrations: github: - host: github.com token: ${GITHUB_TOKEN} + - host: ghe.spotify.net + apiBaseUrl: https://ghe.spotify.net/api/v3 + token: ${GHE_TOKEN} ### Example for how to add your GitHub Enterprise instance using the API: # - host: ghe.example.net # apiBaseUrl: https://ghe.example.net/api/v3 diff --git a/contrib/docs/tutorials/aws-fargate-deployment.md b/contrib/docs/tutorials/aws-fargate-deployment.md new file mode 100644 index 0000000000..bc530441c6 --- /dev/null +++ b/contrib/docs/tutorials/aws-fargate-deployment.md @@ -0,0 +1,16 @@ +# Deploying Backstage with AWS Fargate and Aurora + +There are any number of ways to deploy backstage containers on AWS. One of the +simplest from a management and ops perspective is to leverage AWS Fargate and +Aurora PostgreSQL, where both the container orchestration and database clusters +are AWS managed services. + +However, to use them properly there are a large number of supporting resources +(VPCs, Security Groups, Load Balancers, Certificate, etc) required. One approach +is to use [AWS Cloud Development Kit (CDK)](https://aws.amazon.com/cdk/) to +simplify and automate the creation of the entire infrastructure stack along with +automating the build and deploy of the container. + +Check out the [Backstage On AWS](https://github.com/rbogle/backstage-on-aws) +repository, for a ready to use CDK application for deploying your custom +Backstage app onto AWS ECS Fargate and Aurora. diff --git a/docs/deployment/helm.md b/docs/deployment/helm.md index fd36aeb81f..cbfcb1bc57 100644 --- a/docs/deployment/helm.md +++ b/docs/deployment/helm.md @@ -1,12 +1,10 @@ --- id: helm -title: Deploying Backstage with Helm +title: Deploying with Helm description: How to deploy Backstage with Helm and Kubernetes sidebar_label: Helm --- -# Helm charts - An example Backstage app can be deployed in Kubernetes using the [Backstage Helm charts](https://github.com/backstage/backstage/tree/master/contrib/chart/backstage). diff --git a/docs/deployment/index.md b/docs/deployment/index.md index db4ceca6f1..86ad03d1ae 100644 --- a/docs/deployment/index.md +++ b/docs/deployment/index.md @@ -30,7 +30,9 @@ There is also an example of deploying on [Heroku](heroku.md), which only requires the first two steps. An example of deploying Backstage with a [Helm chart](helm.md), a common pattern -in AWS, is also available. +in AWS, is also available. There is also a contrib guide to deploying Backstage +with +[AWS Fargate and Aurora PostgreSQL](https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/aws-fargate-deployment.md) Please consider contributing other deployment guides if you get Backstage set up on common infrastructure, it would be a great benefit to the community. diff --git a/docs/features/software-catalog/well-known-annotations.md b/docs/features/software-catalog/well-known-annotations.md index 0053c14b79..67b92bd941 100644 --- a/docs/features/software-catalog/well-known-annotations.md +++ b/docs/features/software-catalog/well-known-annotations.md @@ -273,6 +273,20 @@ project within your organization. Specifying this annotation may enable SonarQube related features in Backstage for that entity. +### backstage.io/code-coverage + +```yaml +# Example: +metadata: + annotations: + backstage.io/code-coverage: scm-only +``` + +The value of this annotation controls the code-coverage backstage plugin. If set +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 The following annotations are deprecated, and only listed here to aid in diff --git a/docs/integrations/datadog-rum/installation.md b/docs/integrations/datadog-rum/installation.md index 50ebe722be..f96298a33b 100644 --- a/docs/integrations/datadog-rum/installation.md +++ b/docs/integrations/datadog-rum/installation.md @@ -3,28 +3,32 @@ id: installation title: Datadog RUM Installation sidebar_label: Installation # prettier-ignore -description: Adding Datadog RUM to Your App +description: Adding Datadog Real User Monitoring (RUM) to Your App --- -There is a basic [Datadog](https://docs.datadoghq.com/real_user_monitoring/) +Datadog Real User Monitoring (RUM) allows you to visualize and analyze the +real-time performance and user journeys of your application's individual users. +This is an option to profile and monitor the user experience of your Backstage +installation. + +There is a basic [Datadog RUM](https://docs.datadoghq.com/real_user_monitoring/) integration built into Backstage. You can enable it by adding the following to -your app configuration: +your `app-config.yaml`: ```yaml app: datadogRum: clientToken: '123456789' applicationId: qwerty - # site: # datadoghq.eu default = datadoghq.com - # env: # optional + # site: datadoghq.eu + # env: 'staging' ``` -Replace the clientToken and applicationId with the ones generated for you -Datadog. +The `clientToken` and `applicationId` are generated from the Datadog RUM page +following +[these instructions](https://docs.datadoghq.com/real_user_monitoring/browser/). -optional arguments: +There are two optional arguments: -``` -site: datadoghq.eu # default equals datadoghq.com -env: dev # allow to specify the environment -``` +- `site`: The Datadog site of your organization; defaults to `datadoghq.com` +- `env`: The application environment for Datadog events (no default) diff --git a/microsite/sidebars.json b/microsite/sidebars.json index 1ab242ba2d..e2a57bd426 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -109,6 +109,11 @@ "integrations/bitbucket/discovery" ] }, + { + "type": "subcategory", + "label": "Datadog", + "ids": ["integrations/datadog-rum/installation"] + }, { "type": "subcategory", "label": "GitHub", diff --git a/packages/app/package.json b/packages/app/package.json index aad797c436..a3ef3698cb 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -15,6 +15,7 @@ "@backstage/plugin-catalog-react": "^0.1.3", "@backstage/plugin-circleci": "^0.2.12", "@backstage/plugin-cloudbuild": "^0.2.13", + "@backstage/plugin-code-coverage": "^0.1.0", "@backstage/plugin-cost-insights": "^0.8.4", "@backstage/plugin-explore": "^0.3.2", "@backstage/plugin-gcp-projects": "^0.2.5", diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index d4db2b0e63..601134362d 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -102,6 +102,7 @@ import { EntityTravisCIOverviewCard, isTravisciAvailable, } from '@roadiehq/backstage-plugin-travis-ci'; +import { EntityCodeCoverageContent } from '@backstage/plugin-code-coverage'; const EntityLayoutWrapper = (props: { children?: ReactNode }) => { const [badgesDialogOpen, setBadgesDialogOpen] = useState(false); @@ -303,6 +304,10 @@ const serviceEntityPage = ( + + + + @@ -347,6 +352,10 @@ const websiteEntityPage = ( + + + + diff --git a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts index 07da975184..c17545779a 100644 --- a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts +++ b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts @@ -18,7 +18,7 @@ import { Config } from '@backstage/config'; import compression from 'compression'; import cors from 'cors'; import express, { Router } from 'express'; -import helmet, { HelmetOptions } from 'helmet'; +import helmet from 'helmet'; import * as http from 'http'; import stoppable from 'stoppable'; import { Logger } from 'winston'; @@ -195,14 +195,7 @@ export class ServiceBuilderImpl implements ServiceBuilder { }); } - private getOptions(): { - port: number; - host: string; - logger: Logger; - corsOptions?: cors.CorsOptions; - httpsSettings?: HttpsSettings; - helmetOptions: HelmetOptions; - } { + private getOptions() { return { port: this.port ?? DEFAULT_PORT, host: this.host ?? DEFAULT_HOST, diff --git a/packages/backend/package.json b/packages/backend/package.json index 40e5a1cd2f..984b8b60a5 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -35,6 +35,7 @@ "@backstage/plugin-auth-backend": "^0.3.7", "@backstage/plugin-badges-backend": "^0.1.1", "@backstage/plugin-catalog-backend": "^0.7.0", + "@backstage/plugin-code-coverage-backend": "^0.1.0", "@backstage/plugin-graphql-backend": "^0.1.6", "@backstage/plugin-kubernetes-backend": "^0.3.3", "@backstage/plugin-kafka-backend": "^0.2.3", diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index cbbdaecf5e..00ed248df5 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -37,6 +37,7 @@ import { Config } from '@backstage/config'; import healthcheck from './plugins/healthcheck'; import auth from './plugins/auth'; import catalog from './plugins/catalog'; +import codeCoverage from './plugins/codecoverage'; import kubernetes from './plugins/kubernetes'; import kafka from './plugins/kafka'; import rollbar from './plugins/rollbar'; @@ -75,6 +76,9 @@ async function main() { const healthcheckEnv = useHotMemoize(module, () => createEnv('healthcheck')); const catalogEnv = useHotMemoize(module, () => createEnv('catalog')); + const codeCoverageEnv = useHotMemoize(module, () => + createEnv('code-coverage'), + ); const scaffolderEnv = useHotMemoize(module, () => createEnv('scaffolder')); const authEnv = useHotMemoize(module, () => createEnv('auth')); const proxyEnv = useHotMemoize(module, () => createEnv('proxy')); @@ -90,6 +94,7 @@ async function main() { const apiRouter = Router(); apiRouter.use('/catalog', await catalog(catalogEnv)); + apiRouter.use('/code-coverage', await codeCoverage(codeCoverageEnv)); apiRouter.use('/rollbar', await rollbar(rollbarEnv)); apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv)); apiRouter.use('/auth', await auth(authEnv)); diff --git a/packages/backend/src/plugins/codecoverage.ts b/packages/backend/src/plugins/codecoverage.ts new file mode 100644 index 0000000000..c06e0e516f --- /dev/null +++ b/packages/backend/src/plugins/codecoverage.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2020 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 { createRouter } from '@backstage/plugin-code-coverage-backend'; +import { PluginEnvironment } from '../types'; + +export default async function createPlugin(env: PluginEnvironment) { + return await createRouter({ + config: env.config, + discovery: env.discovery, + database: env.database, + urlReader: env.reader, + logger: env.logger, + }); +} diff --git a/packages/core-api/src/extensions/extensions.tsx b/packages/core-api/src/extensions/extensions.tsx index 8ff4055e61..9f6412f960 100644 --- a/packages/core-api/src/extensions/extensions.tsx +++ b/packages/core-api/src/extensions/extensions.tsx @@ -47,12 +47,15 @@ export function createRoutableExtension< // Validate that the routing is wired up correctly in the App.tsx try { useRouteRef(mountPoint); - } catch { - throw new Error( - `Routable extension component with mount point ${mountPoint} was not discovered in the app element tree. ` + - 'Routable extension components may not be rendered by other components and must be ' + - 'directly available as an element within the App provider component.', - ); + } catch (error) { + if (error?.message.startsWith('No path for ')) { + throw new Error( + `Routable extension component with mount point ${mountPoint} was not discovered in the app element tree. ` + + 'Routable extension components may not be rendered by other components and must be ' + + 'directly available as an element within the App provider component.', + ); + } + throw error; } return ; }; diff --git a/packages/core/src/components/Table/Table.tsx b/packages/core/src/components/Table/Table.tsx index d061be6d60..2251b8304e 100644 --- a/packages/core/src/components/Table/Table.tsx +++ b/packages/core/src/components/Table/Table.tsx @@ -363,7 +363,7 @@ export function Table({ placeholder: 'All results', label: filter.column, multiple: filter.type === 'multiple-select', - items: [...extractDistinctValues(filter.column)].map(value => ({ + items: [...extractDistinctValues(filter.column)].sort().map(value => ({ label: value, value, })), diff --git a/packages/theme/src/themes.ts b/packages/theme/src/themes.ts index 0cdd41508c..94cf38656e 100644 --- a/packages/theme/src/themes.ts +++ b/packages/theme/src/themes.ts @@ -102,7 +102,8 @@ export const darkTheme = createTheme({ }, }, primary: { - main: '#8CB4E5', + main: '#9CC9FF', + dark: '#82BAFD', }, secondary: { main: '#FF88B2', @@ -124,8 +125,8 @@ export const darkTheme = createTheme({ errorText: '#CA001B', infoText: '#004e8a', warningText: '#000000', - linkHover: '#2196F3', - link: '#0A6EBE', + linkHover: '#82BAFD', + link: '#9CC9FF', gold: yellow.A700, navigation: { background: '#424242', diff --git a/plugins/catalog-backend/src/ingestion/processors/ldap/config.test.ts b/plugins/catalog-backend/src/ingestion/processors/ldap/config.test.ts index aac9a091b8..607581e153 100644 --- a/plugins/catalog-backend/src/ingestion/processors/ldap/config.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/ldap/config.test.ts @@ -88,7 +88,7 @@ describe('readLdapConfig', () => { filter: 'f', paged: true, }, - set: [{ path: 'p', value: 'v' }], + set: { p: 'v' }, map: { rdn: 'u', name: 'v', @@ -107,7 +107,7 @@ describe('readLdapConfig', () => { filter: 'f', paged: true, }, - set: [{ path: 'p', value: 'v' }], + set: { p: 'v' }, map: { rdn: 'u', name: 'v', @@ -136,7 +136,7 @@ describe('readLdapConfig', () => { filter: 'f', paged: true, }, - set: [{ path: 'p', value: 'v' }], + set: { p: 'v' }, map: { rdn: 'u', name: 'v', @@ -155,7 +155,7 @@ describe('readLdapConfig', () => { filter: 'f', paged: true, }, - set: [{ path: 'p', value: 'v' }], + set: { p: 'v' }, map: { rdn: 'u', name: 'v', @@ -182,9 +182,9 @@ describe('readLdapConfig', () => { dn: 'udn', options: { filter: ` - (| - (cn=foo bar) - (cn=bar) + (| + (cn=foo bar) + (cn=bar) ) `, }, diff --git a/plugins/catalog-backend/src/ingestion/processors/ldap/config.ts b/plugins/catalog-backend/src/ingestion/processors/ldap/config.ts index 625282ca05..aca02e6e10 100644 --- a/plugins/catalog-backend/src/ingestion/processors/ldap/config.ts +++ b/plugins/catalog-backend/src/ingestion/processors/ldap/config.ts @@ -57,7 +57,7 @@ export type UserConfig = { // default is scope "one" and attributes "*" and "+". options: SearchOptions; // JSON paths (on a.b.c form) and hard coded values to set on those paths - set?: { path: string; value: JsonValue }[]; + set?: { [path: string]: JsonValue }; // Mappings from well known entity fields, to LDAP attribute names map: { // The name of the attribute that holds the relative distinguished name of @@ -94,7 +94,7 @@ export type GroupConfig = { // Only the scope, filter, attributes, and paged fields are supported. options: SearchOptions; // JSON paths (on a.b.c form) and hard coded values to set on those paths - set?: { path: string; value: JsonValue }[]; + set?: { [path: string]: JsonValue }; // Mappings from well known entity fields, to LDAP attribute names map: { // The name of the attribute that holds the relative distinguished name of @@ -189,15 +189,12 @@ export function readLdapConfig(config: Config): LdapProviderConfig[] { } function readSetConfig( - c: Config[] | undefined, - ): { path: string; value: JsonValue }[] | undefined { + c: Config | undefined, + ): { [path: string]: JsonValue } | undefined { if (!c) { return undefined; } - return c.map(entry => ({ - path: entry.getString('path'), - value: entry.get('value'), - })); + return Object.fromEntries(c.keys().map(path => [path, c.get(path)])); } function readUserMapConfig( @@ -244,7 +241,7 @@ export function readLdapConfig(config: Config): LdapProviderConfig[] { return { dn: c.getString('dn'), options: readOptionsConfig(c.getOptionalConfig('options')), - set: readSetConfig(c.getOptionalConfigArray('set')), + set: readSetConfig(c.getOptionalConfig('set')), map: readUserMapConfig(c.getOptionalConfig('map')), }; } @@ -255,7 +252,7 @@ export function readLdapConfig(config: Config): LdapProviderConfig[] { return { dn: c.getString('dn'), options: readOptionsConfig(c.getOptionalConfig('options')), - set: readSetConfig(c.getOptionalConfigArray('set')), + set: readSetConfig(c.getOptionalConfig('set')), map: readGroupMapConfig(c.getOptionalConfig('map')), }; } diff --git a/plugins/catalog-backend/src/ingestion/processors/ldap/read.ts b/plugins/catalog-backend/src/ingestion/processors/ldap/read.ts index 3162fbd01c..68542b5b30 100644 --- a/plugins/catalog-backend/src/ingestion/processors/ldap/read.ts +++ b/plugins/catalog-backend/src/ingestion/processors/ldap/read.ts @@ -15,6 +15,7 @@ */ import { GroupEntity, UserEntity } from '@backstage/catalog-model'; +import { SearchEntry } from 'ldapjs'; import lodashSet from 'lodash/set'; import { buildOrgHierarchy } from '../util/org'; import { LdapClient } from './client'; @@ -25,7 +26,6 @@ import { LDAP_UUID_ANNOTATION, } from './constants'; import { LdapVendor } from './vendors'; -import { SearchEntry } from 'ldapjs'; /** * Reads users out of an LDAP provider. @@ -63,7 +63,7 @@ export async function readLdapUsers( }; if (set) { - for (const { path, value } of set) { + for (const [path, value] of Object.entries(set)) { lodashSet(entity, path, value); } } @@ -142,7 +142,7 @@ export async function readLdapGroups( }; if (set) { - for (const { path, value } of set) { + for (const [path, value] of Object.entries(set)) { lodashSet(entity, path, value); } } diff --git a/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.test.tsx b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.test.tsx index bfb2434926..3e918db419 100644 --- a/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.test.tsx +++ b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.test.tsx @@ -127,7 +127,9 @@ describe('', () => { expect(catalogImportApi.analyzeUrl).toBeCalledTimes(0); expect(onAnalysisFn).toBeCalledTimes(0); expect(errorApi.post).toBeCalledTimes(0); - expect(getByText('Must start with https://.')).toBeInTheDocument(); + expect( + getByText('Must start with http:// or https://.'), + ).toBeInTheDocument(); }); it('should analyze single location', async () => { diff --git a/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.tsx b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.tsx index a33fe388d8..49bd776db5 100644 --- a/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.tsx +++ b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.tsx @@ -132,8 +132,8 @@ export const StepInitAnalyzeUrl = ({ validate: { httpsValidator: (value: any) => (typeof value === 'string' && - value.match(/^https:\/\//) !== null) || - 'Must start with https://.', + value.match(/^http[s]?:\/\//) !== null) || + 'Must start with http:// or https://.', }, })} required diff --git a/plugins/catalog/src/components/CatalogPage/utils/locales/goodAfternoon.locales.json b/plugins/catalog/src/components/CatalogPage/utils/locales/goodAfternoon.locales.json index 4d48666666..9010ba4359 100644 --- a/plugins/catalog/src/components/CatalogPage/utils/locales/goodAfternoon.locales.json +++ b/plugins/catalog/src/components/CatalogPage/utils/locales/goodAfternoon.locales.json @@ -11,6 +11,7 @@ "Armenian": "Բարի կէսօր:", "Assamese": "শুভ আবেলি", "Asturian": "Bones tardes", + "Australian": "G'day", "Azerbaijani": "Günortanız xeyir", "Bashkir": "хәйерле көн", "Basque": "Arratsalde on", @@ -68,6 +69,7 @@ "Latvian": "Labdien", "Limburgish": "Gojemiddig", "Lithuanian": "Laba diena", + "Low German": "Moin", "Lozi": "Ki musihali", "Lule Sámi": "Buorre biejvve", "Luxembourgish": "Gudde Mëtteg", diff --git a/plugins/catalog/src/components/CatalogPage/utils/locales/goodEvening.locales.json b/plugins/catalog/src/components/CatalogPage/utils/locales/goodEvening.locales.json index 36d48eaf39..5a98abafaa 100644 --- a/plugins/catalog/src/components/CatalogPage/utils/locales/goodEvening.locales.json +++ b/plugins/catalog/src/components/CatalogPage/utils/locales/goodEvening.locales.json @@ -10,6 +10,7 @@ "Asante": "Maadwó", "Asturian": "Bona nuechi", "Ateso": "Akwar najokan", + "Australian": "G'day", "Aymara": "Winas tartis", "Azerbaijani": "Axşamınız xeyir", "Basque": "Arratsalde on", @@ -56,6 +57,7 @@ "Kurdish": "Evarbash", "Latvian": "Labvakar", "Lithuanian": "Labas vakaras", + "Low German": "Moin", "Luganda": "Osiibye otya nno", "Luo": "Oimore", "Luxembourgish": "Gudden Owend", diff --git a/plugins/catalog/src/components/CatalogPage/utils/locales/goodMorning.locales.json b/plugins/catalog/src/components/CatalogPage/utils/locales/goodMorning.locales.json index febbc32c49..a9629edbbb 100644 --- a/plugins/catalog/src/components/CatalogPage/utils/locales/goodMorning.locales.json +++ b/plugins/catalog/src/components/CatalogPage/utils/locales/goodMorning.locales.json @@ -4,6 +4,7 @@ "Albanian": "Mirëmëngjes", "Arabic": "صباح الخير", "Armenian": "Paree looys", + "Australian": "G'day", "Azerbaijani": "Sabahınız xeyir", "Bengali": "shuprobhat", "Bulgarian": "Dobro utro", @@ -32,6 +33,7 @@ "Kazakh": "Kayırlı tan", "Kurdish": "Beyanî baş", "Lithuanian": "Labas rytas", + "Low German": "Moin", "Macedonian": "Добро утро", "Malay": "Selamat pagi", "Maltese": "Bonġu", diff --git a/plugins/code-coverage-backend/.eslintrc.js b/plugins/code-coverage-backend/.eslintrc.js new file mode 100644 index 0000000000..16a033dbc6 --- /dev/null +++ b/plugins/code-coverage-backend/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/plugins/code-coverage-backend/README.md b/plugins/code-coverage-backend/README.md new file mode 100644 index 0000000000..101bee3e3f --- /dev/null +++ b/plugins/code-coverage-backend/README.md @@ -0,0 +1,154 @@ +# 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. + +## Configuring your entity + +In order to use this plugin, you must set the `backstage.io/code-coverage` annotation. + +```yaml +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 +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. + +## API + +### Adding a Cobertura report + +POST a Cobertura XML file to `/report` + +Example: + +```json +// curl -X POST -H "Content-Type:text/xml" -d @cobertura.xml "localhost:7000/api/code-coverage/report?entity=component:default/entity-name&coverageType=cobertura" +{ + "links": [ + { + "href": "http://localhost:7000/api/code-coverage/report?entity=component:default/entity-name", + "rel": "coverage" + } + ] +} +``` + +### Adding a JaCoCo report + +POST a JaCoCo XML file to `/report` + +Example: + +```json +// curl -X POST -H "Content-Type:text/xml" -d @jacoco.xml "localhost:7000/api/code-coverage/report?entity=component:default/entity-name&coverageType=jacoco" +{ + "links": [ + { + "href": "http://localhost:7000/api/code-coverage/report?entity=component:default/entity-name", + "rel": "coverage" + } + ] +} +``` + +### Reading json coverage + +GET `/report` + +Example: + +```json +// curl localhost:7000/api/code-coverage/report?entity=component:default/entity-name +{ + "aggregate": { + "branch": { + "available": 0, + "covered": 0, + "missed": 0, + "percentage": 0 + }, + "line": { + "available": 5, + "covered": 4, + "missed": 1, + "percentage": 80 + } + }, + "entity": { + "kind": "Component", + "name": "entity-name", + "namespace": "default" + }, + "files": [ + { + "branchHits": {}, + "filename": "main.go", + "lineHits": { + "117": 12, + "142": 8, + "34": 8, + "42": 0, + "58": 6 + } + } + ] +} +``` + +### Coverage history + +GET `/history` + +Example + +```json +// curl localhost:7000/api/code-coverage/history?entity=component:default/entity-name +{ + "entity": { + "kind": "Component", + "name": "entity-name", + "namespace": "default" + }, + "history": [ + { + "branch": { + "available": 0, + "covered": 0, + "missed": 0, + "percentage": 0 + }, + "line": { + "available": 299, + "covered": 116, + "missed": 183, + "percentage": 38.8 + }, + "timestamp": 1615490766141 + }, + { + "branch": { + "available": 0, + "covered": 0, + "missed": 0, + "percentage": 0 + }, + "line": { + "available": 299, + "covered": 116, + "missed": 183, + "percentage": 38.8 + }, + "timestamp": 1615406307929 + } + ] +} +``` diff --git a/plugins/code-coverage-backend/migrations/20210302_init.js b/plugins/code-coverage-backend/migrations/20210302_init.js new file mode 100644 index 0000000000..538f658948 --- /dev/null +++ b/plugins/code-coverage-backend/migrations/20210302_init.js @@ -0,0 +1,65 @@ +/* + * 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. + */ + +// @ts-check + +/** + * @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 + .bigIncrements('index') + .notNullable() + .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() + .comment('The coverage json as a string'); + table + .dateTime('created_at') + .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'); + }); +}; + +/** + * @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 new file mode 100644 index 0000000000..1e7903b016 --- /dev/null +++ b/plugins/code-coverage-backend/package.json @@ -0,0 +1,49 @@ +{ + "name": "@backstage/plugin-code-coverage-backend", + "version": "0.1.1", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "start": "backstage-cli backend:dev", + "build": "backstage-cli backend:build", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@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.1", + "@types/express": "^4.17.6", + "cross-fetch": "^3.0.6", + "express": "^4.17.1", + "express-promise-router": "^4.1.0", + "express-xml-bodyparser": "^0.3.0", + "knex": "^0.95.1", + "uuid": "^8.3.2", + "winston": "^3.2.1", + "yn": "^4.0.0" + }, + "devDependencies": { + "@backstage/cli": "^0.6.8", + "@types/express-xml-bodyparser": "^0.3.2", + "@types/supertest": "^2.0.8", + "msw": "^0.21.2", + "supertest": "^4.0.2", + "xml2js": "^0.4.23" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/code-coverage-backend/src/index.ts b/plugins/code-coverage-backend/src/index.ts new file mode 100644 index 0000000000..7612c392a2 --- /dev/null +++ b/plugins/code-coverage-backend/src/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 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. + */ + +export * from './service/router'; diff --git a/plugins/code-coverage-backend/src/run.ts b/plugins/code-coverage-backend/src/run.ts new file mode 100644 index 0000000000..b96989e4b8 --- /dev/null +++ b/plugins/code-coverage-backend/src/run.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2020 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 { getRootLogger } from '@backstage/backend-common'; +import yn from 'yn'; +import { startStandaloneServer } from './service/standaloneServer'; + +const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7000; +const enableCors = yn(process.env.PLUGIN_CORS, { default: false }); +const logger = getRootLogger(); + +startStandaloneServer({ port, enableCors, logger }).catch(err => { + logger.error(err); + process.exit(1); +}); + +process.on('SIGINT', () => { + logger.info('CTRL+C pressed; exiting.'); + process.exit(0); +}); diff --git a/plugins/code-coverage-backend/src/service/CodeCoverageDatabase.test.ts b/plugins/code-coverage-backend/src/service/CodeCoverageDatabase.test.ts new file mode 100644 index 0000000000..688eeb4ba2 --- /dev/null +++ b/plugins/code-coverage-backend/src/service/CodeCoverageDatabase.test.ts @@ -0,0 +1,198 @@ +/* + * 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 { SingleConnectionDatabaseManager } from '@backstage/backend-common'; +import { stringifyEntityRef } from '@backstage/catalog-model'; +import { ConfigReader } from '@backstage/config'; +import { + CodeCoverageDatabase, + CodeCoverageStore, +} from './CodeCoverageDatabase'; +import { JsonCodeCoverage } from './types'; + +const db = SingleConnectionDatabaseManager.fromConfig( + new ConfigReader({ + backend: { + database: { + client: 'sqlite3', + connection: ':memory:', + }, + }, + }), +).forPlugin('code-coverage'); + +const coverage: Array = [ + { + metadata: { + generationTime: 1234567890, + vcs: { + location: 'local', + type: 'local', + }, + }, + entity: { + kind: 'Component', + name: 'test-entity', + namespace: 'default', + }, + files: [ + { + filename: 'src/main.py', + lineHits: { + '10': 5, + '11': 4, + '12': 4, + }, + branchHits: {}, + }, + ], + }, + { + metadata: { + generationTime: 2345678901, + vcs: { + location: 'local', + type: 'local', + }, + }, + entity: { + kind: 'Component', + name: 'test-entity', + namespace: 'default', + }, + files: [ + { + filename: 'src/main.py', + lineHits: { + '10': 5, + '11': 4, + '12': 4, + '22': 0, + '23': 0, + '24': 0, + '30': 1, + }, + branchHits: { + '10': { + available: 2, + covered: 1, + missed: 1, + }, + }, + }, + ], + }, +]; + +let database: CodeCoverageStore; +describe('CodeCoverageDatabase', () => { + beforeAll(async () => { + const client = await db.getClient(); + database = await CodeCoverageDatabase.create(client); + await database.insertCodeCoverage(coverage[0]); + await database.insertCodeCoverage(coverage[1]); + }); + + describe('insertCodeCoverage', () => { + it('can insert code coverage', async () => { + const ncov = { + metadata: { + generationTime: 3456789012, + vcs: { + location: 'local', + type: 'local', + }, + }, + entity: { + kind: 'Component', + name: 'test-entity-for-insert', + namespace: 'default', + }, + files: [ + { + filename: 'src/main.py', + lineHits: { + '10': 5, + '11': 4, + '12': 4, + }, + branchHits: {}, + }, + ], + }; + const { codeCoverageId } = await database.insertCodeCoverage(ncov); + expect(codeCoverageId.length).not.toBe(0); + }); + }); + + describe('getCodeCoverage', () => { + it("can get coverage that's in the database", async () => { + const cov = await database.getCodeCoverage( + stringifyEntityRef({ + name: 'test-entity', + kind: 'Component', + namespace: 'default', + }), + ); + expect(cov).toEqual(coverage[1]); + }); + }); + + describe('getHistory', () => { + it("can get history that's in the database", async () => { + const cov = await database.getHistory( + stringifyEntityRef({ + name: 'test-entity', + kind: 'Component', + namespace: 'default', + }), + 5, + ); + expect(cov.history.length).toEqual(2); + expect(cov.history).toEqual([ + { + branch: { + available: 2, + covered: 1, + missed: 1, + percentage: 50, + }, + line: { + available: 7, + covered: 4, + missed: 3, + percentage: 57.14, + }, + timestamp: 2345678901, + }, + { + branch: { + available: 0, + covered: 0, + missed: 0, + percentage: 0, + }, + line: { + available: 3, + covered: 3, + missed: 0, + percentage: 100, + }, + timestamp: 1234567890, + }, + ]); + }); + }); +}); diff --git a/plugins/code-coverage-backend/src/service/CodeCoverageDatabase.ts b/plugins/code-coverage-backend/src/service/CodeCoverageDatabase.ts new file mode 100644 index 0000000000..390fd815f1 --- /dev/null +++ b/plugins/code-coverage-backend/src/service/CodeCoverageDatabase.ts @@ -0,0 +1,117 @@ +/* + * 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 { resolvePackagePath } from '@backstage/backend-common'; +import { NotFoundError } from '@backstage/errors'; +import { parseEntityName, stringifyEntityRef } from '@backstage/catalog-model'; +import { Knex } from 'knex'; +import { v4 as uuid } from 'uuid'; +import { aggregateCoverage } from './CoverageUtils'; +import { JsonCodeCoverage, JsonCoverageHistory } from './types'; + +export type RawDbCoverageRow = { + id: string; + entity: string; + coverage: string; +}; + +export interface CodeCoverageStore { + insertCodeCoverage( + coverage: JsonCodeCoverage, + ): Promise<{ codeCoverageId: string }>; + getCodeCoverage(entity: string): Promise; + getHistory(entity: string, limit: number): Promise; +} + +const migrationsDir = resolvePackagePath( + '@backstage/plugin-code-coverage-backend', + 'migrations', +); + +export class CodeCoverageDatabase implements CodeCoverageStore { + static async create(knex: Knex): Promise { + await knex.migrate.latest({ + directory: migrationsDir, + }); + return new CodeCoverageDatabase(knex); + } + + constructor(private readonly db: Knex) {} + + async insertCodeCoverage( + coverage: JsonCodeCoverage, + ): Promise<{ codeCoverageId: string }> { + const codeCoverageId = uuid(); + 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('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) { + throw new Error(`Failed to parse coverage for '${entity}', ${error}`); + } + } + + async getHistory( + entity: string, + limit: number, + ): Promise { + const res = await this.db('code_coverage') + .where({ entity: entity }) + .orderBy('index', 'desc') + .limit(limit) + .select(); + + const history = res + .map(r => JSON.parse(r.coverage)) + .map(c => aggregateCoverage(c)); + + const entityName = parseEntityName(entity); + + return { + entity: { + name: entityName.name, + kind: entityName.kind, + namespace: entityName.namespace, + }, + history: history, + }; + } +} diff --git a/plugins/code-coverage-backend/src/service/CoverageUtils.test.ts b/plugins/code-coverage-backend/src/service/CoverageUtils.test.ts new file mode 100644 index 0000000000..4a96e67338 --- /dev/null +++ b/plugins/code-coverage-backend/src/service/CoverageUtils.test.ts @@ -0,0 +1,326 @@ +/* + * 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 fs from 'fs'; +import { Readable } from 'stream'; +import { + calculatePercentage, + aggregateCoverage, + CoverageUtils, +} from './CoverageUtils'; +import { Entity } from '@backstage/catalog-model'; +import { parseString } from 'xml2js'; + +describe('calculatePercentage', () => { + [ + [100, 25, 25], + [100, 100, 100], + [133, 13, 9.77], + [0, 0, 0], + ].forEach(([a, c, e]) => { + it(`${c}/${a} === ${e}`, () => { + expect(calculatePercentage(a, c)).toEqual(e); + }); + }); +}); + +/* eslint-disable no-restricted-syntax */ + +describe('aggregateCoverage', () => { + [ + { + file: 'jacoco-jsoncoverage-files-1.json', + expected: { + branch: { + available: 68, + covered: 48, + missed: 20, + percentage: 70.59, + }, + line: { + available: 162, + covered: 105, + missed: 57, + percentage: 64.81, + }, + timestamp: 1234567890, + }, + }, + { + file: 'cobertura-jsoncoverage-files-1.json', + expected: { + branch: { + available: 0, + covered: 0, + missed: 0, + percentage: 0, + }, + line: { + available: 146, + covered: 145, + missed: 1, + percentage: 99.32, + }, + timestamp: 1234567890, + }, + }, + { + file: 'cobertura-jsoncoverage-files-2.json', + expected: { + branch: { + available: 466, + covered: 380, + missed: 86, + percentage: 81.55, + }, + line: { + available: 2632, + covered: 2079, + missed: 553, + percentage: 78.99, + }, + timestamp: 1234567890, + }, + }, + { + file: 'cobertura-jsoncoverage-files-3.json', + expected: { + branch: { + available: 73, + covered: 49, + missed: 24, + percentage: 67.12, + }, + line: { + available: 110, + covered: 91, + missed: 19, + percentage: 82.73, + }, + timestamp: 1234567890, + }, + }, + { + file: 'cobertura-jsoncoverage-files-4.json', + expected: { + branch: { + available: 0, + covered: 0, + missed: 0, + percentage: 0, + }, + line: { + available: 325, + covered: 0, + missed: 325, + percentage: 0, + }, + timestamp: 1234567890, + }, + }, + { + file: 'cobertura-jsoncoverage-files-5.json', + expected: { + branch: { + available: 38, + covered: 21, + missed: 17, + percentage: 55.26, + }, + line: { + available: 175, + covered: 124, + missed: 51, + percentage: 70.86, + }, + timestamp: 1234567890, + }, + }, + ].forEach(td => { + it(`processes ${td.file}`, () => { + const json = JSON.parse( + fs.readFileSync(`${__dirname}/__fixtures__/${td.file}`).toString(), + ); + const aggregate = aggregateCoverage({ + metadata: { + generationTime: 1234567890, + vcs: { + location: 'foo', + type: 'foo', + }, + }, + entity: { + kind: 'foo', + name: 'foo', + namespace: 'default', + }, + files: json, + }); + + expect(aggregate).toEqual(td.expected); + }); + }); +}); + +describe('CodeCoverageUtils', () => { + const scmFilesFixture = fs + .readFileSync(`${__dirname}/__fixtures__/cobertura-sourcefiles-1.txt`) + .toString() + .split('\n') + .map(f => { + return { path: f }; + }); + const scmIntegraions = { + byUrl: jest.fn().mockReturnValue({ + type: 'local', + title: 'local', + }), + }; + const scmTree = { + files: jest + .fn() + .mockReturnValue(new Promise((r, _e) => r(scmFilesFixture))), + }; + const urlReader = { + readTree: jest.fn().mockReturnValue(new Promise((r, _e) => r(scmTree))), + }; + const utils = new CoverageUtils(scmIntegraions, urlReader); + + describe('validateRequestBody', () => { + it('rejects missing content type', () => { + let err: Error | null = null; + try { + const mockRequest: Partial = { + // @ts-ignore + headers: {}, + }; + // @ts-ignore + utils.validateRequestBody(mockRequest as Request); + } catch (error) { + err = error; + } + expect(err?.message).toEqual('Content-Type missing'); + }); + + it('rejects unsupported content type', () => { + let err: Error | null = null; + try { + const mockRequest: Partial = { + headers: { + // @ts-ignore + 'content-type': 'application/json', + }, + }; + + // @ts-ignore + utils.validateRequestBody(mockRequest as Request); + } catch (error) { + err = error; + } + expect(err?.message).toEqual('Illegal Content-Type'); + }); + + it('parses the body', () => { + const mockRequest: Partial = { + headers: { + // @ts-ignore + 'content-type': 'text/xml', + }, + // @ts-ignore + body: Readable.from( + '', + ), + }; + + // @ts-ignore + const data: Readable = utils.validateRequestBody(mockRequest as Request); + + expect(data.read()).toContain(' { + const mockRequest: Partial = { + headers: { + // @ts-ignore + 'content-type': 'text/xml', + }, + // @ts-ignore + body: Readable.from( + '', + ), + }; + + it('ignores scm if annotation is not set', async () => { + const entity: Entity = { + kind: 'Component', + metadata: { + name: 'test-entity', + namespace: 'test', + }, + apiVersion: 'backstage.io/v1alpha1', + }; + + const { + scmFiles, + sourceLocation, + vcs, + body, // @ts-ignore + } = await utils.processCoveragePayload(entity, mockRequest); + let data; + // in normal flow the express app will already have done this through the middleware + parseString((body as Readable).read(), (_e, r) => { + data = r; + }); + + expect(scmFiles.length).toBe(0); + expect(vcs).toBeUndefined(); + expect(sourceLocation).toBeUndefined(); + expect(data).toEqual({ + report: { + $: { + name: 'example', + }, + }, + }); + }); + + it('populates scm data if annotation is set', async () => { + const entity: Entity = { + kind: 'Component', + metadata: { + name: 'test-entity', + namespace: 'test', + annotations: { + 'backstage.io/code-coverage': 'scm-only', + 'backstage.io/source-location': + 'url:https://github.com/example/test/', + }, + }, + apiVersion: 'backstage.io/v1alpha1', + }; + + const { + scmFiles, + sourceLocation, + vcs, // @ts-ignore + } = await utils.processCoveragePayload(entity, mockRequest); + + expect(scmFiles.length).toBe(scmFiles.length); + expect(vcs).not.toBeUndefined(); + expect(sourceLocation).not.toBeUndefined(); + }); + }); +}); diff --git a/plugins/code-coverage-backend/src/service/CoverageUtils.ts b/plugins/code-coverage-backend/src/service/CoverageUtils.ts new file mode 100644 index 0000000000..f41a1d05a0 --- /dev/null +++ b/plugins/code-coverage-backend/src/service/CoverageUtils.ts @@ -0,0 +1,175 @@ +/* + * 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 { Request } from 'express'; +import { UrlReader } from '@backstage/backend-common'; +import { InputError, NotFoundError } from '@backstage/errors'; +import { + Entity, + getEntitySourceLocation, + stringifyEntityRef, +} from '@backstage/catalog-model'; +import { ScmIntegration, ScmIntegrations } from '@backstage/integration'; +import { AggregateCoverage, FileEntry, JsonCodeCoverage } from './types'; + +export const calculatePercentage = ( + available: number, + covered: number, +): number => { + if (available === 0) { + return 0; + } + return parseFloat(((covered / available) * 100).toFixed(2)); +}; + +export const aggregateCoverage = (c: JsonCodeCoverage): AggregateCoverage => { + let availableLine = 0; + let coveredLine = 0; + let availableBranch = 0; + let coveredBranch = 0; + c.files.forEach(f => { + availableLine += Object.keys(f.lineHits).length; + coveredLine += Object.values(f.lineHits).filter(l => l > 0).length; + + availableBranch += Object.keys(f.branchHits) + .map(b => parseInt(b, 10)) + .map((b: number) => f.branchHits[b].available) + .filter(Boolean) + .reduce((acc, curr) => acc + curr, 0); + coveredBranch += Object.keys(f.branchHits) + .map(b => parseInt(b, 10)) + .map((b: number) => f.branchHits[b].covered) + .filter(Boolean) + .reduce((acc, curr) => acc + curr, 0); + }); + + return { + timestamp: c.metadata.generationTime, + branch: { + available: availableBranch, + covered: coveredBranch, + missed: availableBranch - coveredBranch, + percentage: calculatePercentage(availableBranch, coveredBranch), + }, + line: { + available: availableLine, + covered: coveredLine, + missed: availableLine - coveredLine, + percentage: calculatePercentage(availableLine, coveredLine), + }, + }; +}; + +export class CoverageUtils { + constructor( + readonly scm: Partial, + readonly urlReader: Partial, + ) {} + + async processCoveragePayload( + entity: Entity, + req: Request, + ): Promise<{ + sourceLocation?: string; + vcs?: ScmIntegration; + scmFiles: string[]; + body: {}; + }> { + const enforceScmFiles = + entity.metadata.annotations?.['backstage.io/code-coverage'] === + 'scm-only' || false; + + let sourceLocation: string | undefined = undefined; + let vcs: ScmIntegration | undefined = undefined; + let scmFiles: string[] = []; + + if (enforceScmFiles) { + try { + const sl = getEntitySourceLocation(entity); + sourceLocation = sl.target; + } catch (e: unknown) { + // TODO: logging + } + + if (!sourceLocation) { + throw new InputError( + `No "backstage.io/source-location" annotation on entity ${stringifyEntityRef( + entity, + )}`, + ); + } + + vcs = this.scm.byUrl?.(sourceLocation); + if (!vcs) { + throw new InputError(`Unable to determine SCM from ${sourceLocation}`); + } + + const scmTree = await this.urlReader.readTree?.(sourceLocation); + if (!scmTree) { + throw new NotFoundError(`Unable to read tree from ${sourceLocation}`); + } + scmFiles = (await scmTree.files()).map(f => f.path); + } + + const body = this.validateRequestBody(req); + if (Object.keys(body).length === 0) { + throw new InputError('Unable to parse body'); + } + + return { + sourceLocation, + vcs, + scmFiles, + body, + }; + } + + async buildCoverage( + entity: Entity, + sourceLocation: string | undefined, + vcs: ScmIntegration | undefined, + files: FileEntry[], + ): Promise { + return { + metadata: { + vcs: { + type: vcs?.type || 'unknown', + location: sourceLocation || 'unknown', + }, + generationTime: Date.now(), + }, + entity: { + name: entity.metadata.name, + namespace: entity.metadata.namespace || 'default', + kind: entity.kind, + }, + files, + }; + } + + validateRequestBody(req: Request) { + const contentType = req.headers['content-type']; + if (!contentType) { + throw new InputError('Content-Type missing'); + } else if (!contentType.match(/^text\/xml($|;)/)) { + throw new InputError('Illegal Content-Type'); + } + const body = req.body; + if (!body) { + throw new InputError('Missing request body'); + } + return body; + } +} diff --git a/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-jsoncoverage-files-1.json b/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-jsoncoverage-files-1.json new file mode 100644 index 0000000000..b5b861254c --- /dev/null +++ b/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-jsoncoverage-files-1.json @@ -0,0 +1,166 @@ +[ + { + "filename": "src/main/scala/com/example/Main.scala", + "lineHits": { + "100": 4, + "103": 2, + "104": 4, + "105": 4, + "112": 2, + "114": 4, + "115": 2, + "124": 3, + "125": 2, + "126": 3, + "127": 3, + "135": 3, + "136": 1, + "138": 2, + "139": 1, + "140": 1, + "143": 1, + "145": 1, + "146": 1, + "147": 1, + "148": 1, + "149": 1, + "150": 1, + "160": 2, + "163": 8, + "164": 4, + "166": 2, + "167": 4, + "168": 6, + "169": 6, + "170": 4, + "178": 1, + "179": 1, + "181": 1, + "182": 1, + "183": 3, + "187": 1, + "188": 3, + "202": 4, + "203": 6, + "204": 4, + "210": 5, + "26": 4, + "27": 4, + "28": 4, + "30": 4, + "33": 4, + "35": 2, + "36": 2, + "37": 4, + "38": 2, + "42": 2, + "44": 2, + "46": 2, + "48": 2, + "50": 2, + "52": 2, + "53": 2, + "55": 2, + "56": 2, + "58": 2, + "60": 2, + "61": 2, + "64": 2, + "65": 6, + "68": 4, + "73": 2, + "75": 10, + "77": 4, + "83": 1, + "84": 2, + "85": 3, + "86": 3, + "87": 1, + "88": 6, + "95": 2, + "97": 8, + "99": 2 + }, + "branchHits": {} + }, + { + "filename": "src/main/scala/com/example/Other.scala", + "lineHits": { + "103": 6, + "104": 2, + "105": 5, + "109": 1, + "110": 3, + "111": 3, + "113": 2, + "115": 2, + "116": 2, + "117": 2, + "119": 2, + "123": 3, + "124": 1, + "129": 7, + "132": 2, + "133": 4, + "134": 2, + "138": 1, + "139": 2, + "140": 2, + "142": 4, + "143": 3, + "144": 5, + "145": 3, + "146": 5, + "147": 3, + "149": 2, + "20": 1, + "21": 1, + "23": 1, + "25": 2, + "26": 1, + "27": 2, + "29": 2, + "31": 2, + "32": 2, + "33": 2, + "34": 2, + "37": 0, + "42": 4, + "44": 2, + "45": 2, + "46": 2, + "49": 4, + "50": 10, + "52": 4, + "59": 5, + "61": 4, + "64": 2, + "65": 1, + "66": 1, + "68": 1, + "70": 4, + "71": 2, + "72": 2, + "73": 1, + "74": 2, + "79": 1, + "85": 1, + "86": 3, + "87": 1, + "91": 1, + "92": 4, + "93": 3, + "94": 2 + }, + "branchHits": {} + }, + { + "filename": "src/main/scala/com/example/utils/Util.scala", + "lineHits": { + "13": 2, + "8": 4, + "7": 2 + }, + "branchHits": {} + } +] diff --git a/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-jsoncoverage-files-2.json b/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-jsoncoverage-files-2.json new file mode 100644 index 0000000000..ddfcc729fd --- /dev/null +++ b/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-jsoncoverage-files-2.json @@ -0,0 +1,3952 @@ +[ + { + "filename": "src/index.ts", + "lineHits": { + "1": 3, + "3": 3, + "5": 3, + "6": 3, + "7": 3, + "8": 3, + "9": 3, + "11": 3, + "12": 3, + "14": 3 + }, + "branchHits": {} + }, + { + "filename": "src/utils.ts", + "lineHits": { + "4": 104, + "7": 1178, + "12": 20, + "16": 2, + "29": 3105, + "33": 1601, + "37": 15, + "56": 20, + "64": 42, + "65": 2, + "1": 18, + "8": 589, + "9": 589, + "15": 1, + "18": 2, + "20": 0, + "22": 2, + "28": 18, + "30": 776, + "34": 1601, + "42": 158, + "48": 1, + "53": 15, + "57": 0, + "58": 1, + "60": 19, + "61": 19, + "63": 19, + "66": 2, + "67": 2, + "68": 2, + "69": 1, + "70": 1, + "73": 19 + }, + "branchHits": { + "8": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "57": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "68": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "69": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "73": { + "covered": 2, + "missed": 0, + "available": 2 + } + } + }, + { + "filename": "src/controller/VideoContextPlayerCoordinator.ts", + "lineHits": { + "50": 8, + "74": 66, + "137": 33, + "143": 5, + "157": 18, + "217": 5, + "240": 5, + "309": 48, + "343": 48, + "357": 48, + "405": 8, + "420": 20, + "442": 19, + "445": 1, + "460": 20, + "466": 20, + "480": 16, + "488": 8, + "520": 16, + "530": 14, + "619": 0, + "627": 2, + "636": 0, + "640": 8, + "651": 8, + "661": 12, + "673": 37, + "690": 8, + "711": 7, + "736": 4, + "740": 29, + "1": 4, + "2": 4, + "31": 4, + "33": 4, + "37": 4, + "39": 4, + "41": 4, + "76": 33, + "77": 33, + "78": 33, + "79": 33, + "80": 33, + "81": 33, + "82": 33, + "83": 33, + "84": 33, + "85": 33, + "86": 33, + "87": 33, + "90": 33, + "91": 33, + "92": 33, + "93": 33, + "94": 33, + "95": 33, + "96": 33, + "100": 33, + "103": 33, + "106": 33, + "107": 33, + "110": 33, + "111": 33, + "112": 33, + "113": 33, + "115": 33, + "116": 33, + "118": 33, + "120": 33, + "121": 33, + "122": 0, + "128": 33, + "131": 33, + "138": 0, + "139": 0, + "147": 1, + "148": 1, + "149": 1, + "150": 1, + "159": 2, + "162": 0, + "164": 0, + "167": 1, + "171": 1, + "174": 1, + "176": 1, + "180": 1, + "183": 7, + "185": 0, + "186": 7, + "188": 7, + "189": 7, + "194": 0, + "196": 0, + "200": 0, + "201": 1, + "203": 1, + "205": 1, + "207": 1, + "209": 1, + "211": 1, + "213": 1, + "225": 1, + "233": 0, + "234": 0, + "245": 1, + "247": 0, + "250": 0, + "253": 1, + "254": 1, + "255": 0, + "258": 0, + "263": 1, + "276": 0, + "278": 1, + "283": 1, + "285": 1, + "286": 0, + "288": 1, + "289": 0, + "291": 0, + "293": 0, + "301": 1, + "315": 24, + "316": 24, + "318": 24, + "319": 24, + "322": 10, + "323": 10, + "325": 0, + "328": 0, + "332": 0, + "333": 0, + "337": 0, + "338": 0, + "341": 24, + "346": 24, + "347": 24, + "351": 24, + "352": 24, + "354": 24, + "359": 24, + "361": 24, + "362": 24, + "363": 0, + "366": 24, + "377": 24, + "378": 24, + "379": 24, + "380": 24, + "381": 24, + "383": 24, + "384": 24, + "386": 24, + "396": 24, + "397": 24, + "406": 4, + "408": 0, + "413": 4, + "418": 20, + "429": 0, + "433": 0, + "434": 0, + "435": 0, + "440": 20, + "443": 19, + "450": 1, + "451": 1, + "452": 1, + "462": 0, + "464": 20, + "468": 20, + "470": 0, + "485": 8, + "490": 0, + "491": 0, + "493": 8, + "496": 0, + "497": 0, + "500": 8, + "502": 8, + "507": 8, + "525": 8, + "527": 8, + "528": 8, + "531": 14, + "534": 0, + "535": 0, + "538": 14, + "539": 14, + "543": 8, + "544": 8, + "549": 8, + "552": 0, + "553": 1, + "554": 1, + "555": 1, + "559": 1, + "561": 1, + "562": 1, + "567": 2, + "570": 1, + "572": 2, + "574": 2, + "575": 2, + "579": 1, + "580": 1, + "582": 1, + "583": 1, + "588": 1, + "589": 1, + "592": 1, + "594": 1, + "595": 1, + "599": 0, + "600": 0, + "605": 0, + "606": 0, + "610": 0, + "615": 0, + "616": 0, + "620": 0, + "621": 0, + "625": 8, + "628": 2, + "629": 0, + "630": 2, + "632": 1, + "647": 4, + "653": 4, + "654": 4, + "656": 4, + "662": 8, + "663": 8, + "665": 0, + "666": 0, + "667": 0, + "675": 8, + "676": 8, + "679": 8, + "680": 8, + "682": 33, + "683": 33, + "684": 33, + "685": 33, + "687": 33, + "694": 4, + "695": 4, + "697": 4, + "698": 4, + "699": 4, + "700": 4, + "720": 1, + "721": 1, + "722": 1, + "726": 0, + "727": 1, + "732": 1, + "734": 2, + "737": 2, + "745": 1, + "746": 1, + "749": 1, + "750": 0, + "752": 1, + "754": 24, + "756": 4 + }, + "branchHits": { + "122": { + "covered": 0, + "missed": 1, + "available": 1 + }, + "138": { + "covered": 0, + "missed": 2, + "available": 2 + }, + "162": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "164": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "185": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "200": { + "covered": 7, + "missed": 0, + "available": 7 + }, + "233": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "234": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "255": { + "covered": 3, + "missed": 0, + "available": 3 + }, + "283": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "286": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "289": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "291": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "293": { + "covered": 2, + "missed": 2, + "available": 4 + }, + "323": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "332": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "351": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "359": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "361": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "363": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "386": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "408": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "470": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "531": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "552": { + "covered": 5, + "missed": 3, + "available": 8 + }, + "579": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "629": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "726": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "749": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "750": { + "covered": 1, + "missed": 1, + "available": 2 + } + } + }, + { + "filename": "src/controller/VideoContextPlayerCoordinatorFactory.ts", + "lineHits": { + "9": 8, + "11": 33, + "14": 66, + "2": 4, + "3": 4, + "4": 4, + "5": 4, + "6": 4, + "12": 33, + "15": 33, + "16": 33, + "17": 33, + "18": 33, + "19": 33, + "20": 33, + "22": 33, + "24": 33, + "25": 33, + "28": 0, + "29": 0, + "32": 33, + "36": 33, + "42": 0, + "47": 33, + "52": 33, + "62": 4 + }, + "branchHits": { + "24": { + "covered": 1, + "missed": 1, + "available": 2 + } + } + }, + { + "filename": "src/controller/index.ts", + "lineHits": { + "1": 3, + "2": 3, + "4": 3, + "5": 3 + }, + "branchHits": {} + }, + { + "filename": "src/controller-x/VideoContextPlayerCoordinatorX.ts", + "lineHits": { + "43": 6, + "59": 60, + "99": 24, + "126": 0, + "132": 0, + "219": 4, + "233": 16, + "290": 3, + "338": 34, + "341": 17, + "362": 34, + "363": 0, + "422": 10, + "435": 8, + "444": 33, + "458": 6, + "466": 4, + "477": 4, + "507": 3, + "1": 3, + "2": 3, + "25": 3, + "26": 3, + "27": 3, + "28": 3, + "29": 3, + "31": 3, + "34": 3, + "57": 30, + "60": 30, + "61": 30, + "62": 30, + "68": 30, + "73": 30, + "74": 0, + "75": 0, + "79": 0, + "81": 30, + "83": 30, + "85": 30, + "86": 30, + "89": 30, + "90": 30, + "92": 30, + "100": 21, + "104": 21, + "109": 21, + "110": 0, + "117": 21, + "121": 21, + "123": 21, + "129": 21, + "136": 3, + "139": 11, + "141": 11, + "142": 0, + "143": 0, + "146": 5, + "147": 5, + "148": 10, + "150": 5, + "155": 5, + "158": 1, + "159": 1, + "160": 1, + "162": 1, + "164": 1, + "166": 1, + "168": 2, + "170": 2, + "174": 1, + "176": 2, + "178": 2, + "180": 1, + "182": 1, + "183": 1, + "185": 1, + "187": 1, + "189": 1, + "191": 0, + "193": 0, + "195": 0, + "198": 0, + "201": 0, + "203": 6, + "206": 3, + "209": 0, + "210": 2, + "212": 2, + "214": 2, + "216": 0, + "223": 1, + "224": 1, + "225": 1, + "226": 1, + "235": 1, + "238": 0, + "240": 0, + "243": 1, + "247": 1, + "250": 1, + "252": 1, + "256": 1, + "259": 7, + "261": 0, + "262": 7, + "264": 7, + "265": 7, + "269": 0, + "273": 0, + "274": 1, + "276": 1, + "278": 1, + "280": 1, + "282": 1, + "284": 1, + "286": 1, + "291": 0, + "293": 0, + "299": 3, + "303": 21, + "306": 21, + "307": 21, + "310": 7, + "311": 7, + "313": 0, + "316": 0, + "320": 0, + "321": 0, + "325": 0, + "328": 21, + "333": 21, + "334": 21, + "335": 21, + "337": 4, + "339": 17, + "350": 0, + "354": 0, + "355": 0, + "356": 0, + "360": 17, + "368": 4, + "371": 21, + "373": 21, + "374": 21, + "376": 21, + "379": 21, + "386": 0, + "390": 21, + "391": 21, + "392": 21, + "393": 21, + "394": 21, + "397": 21, + "398": 21, + "400": 21, + "402": 21, + "412": 0, + "415": 21, + "416": 21, + "424": 7, + "428": 7, + "430": 7, + "436": 5, + "438": 5, + "446": 0, + "447": 0, + "450": 0, + "451": 0, + "453": 30, + "454": 30, + "455": 30, + "459": 3, + "461": 2, + "462": 2, + "467": 1, + "471": 1, + "478": 0, + "479": 1, + "482": 1, + "483": 0, + "486": 1, + "487": 0, + "491": 0, + "493": 0, + "495": 0, + "499": 1, + "508": 0, + "510": 3 + }, + "branchHits": { + "110": { + "covered": 1, + "missed": 0, + "available": 1 + }, + "139": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "182": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "209": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "238": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "240": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "261": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "273": { + "covered": 7, + "missed": 0, + "available": 7 + }, + "311": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "320": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "373": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "376": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "386": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "400": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "478": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "483": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "487": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "491": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "493": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "495": { + "covered": 3, + "missed": 1, + "available": 4 + } + } + }, + { + "filename": "src/controller-x/VideoContextPlayerCoordinatorXObserver.ts", + "lineHits": { + "5": 6, + "9": 3, + "13": 4, + "17": 7, + "24": 3, + "26": 4, + "30": 3, + "32": 3, + "34": 4, + "38": 3, + "40": 3, + "42": 3, + "44": 4, + "48": 3, + "50": 3, + "52": 3, + "54": 4, + "58": 4, + "62": 4, + "66": 4, + "70": 4, + "74": 3, + "76": 3, + "78": 3, + "2": 3, + "10": 3, + "14": 1, + "18": 4, + "20": 2, + "27": 1, + "35": 1, + "45": 1, + "55": 1, + "59": 1, + "63": 1, + "67": 1, + "71": 1, + "79": 3 + }, + "branchHits": {} + }, + { + "filename": "src/controller-x/index.ts", + "lineHits": { + "1": 3, + "2": 3 + }, + "branchHits": {} + }, + { + "filename": "src/events/InternalPlaybackObserver.ts", + "lineHits": { + "17": 22, + "21": 107, + "26": 100, + "32": 0, + "39": 0, + "42": 100, + "48": 89, + "57": 11, + "59": 0, + "63": 100, + "65": 89, + "69": 38, + "71": 27, + "75": 16, + "77": 5, + "81": 12, + "83": 1, + "87": 13, + "89": 2, + "93": 18, + "95": 7, + "99": 12, + "101": 1, + "106": 13, + "108": 2, + "113": 13, + "115": 2, + "120": 109, + "122": 98, + "126": 15, + "128": 4, + "132": 15, + "134": 4, + "138": 104, + "144": 93, + "149": 12, + "151": 1, + "156": 12, + "158": 1, + "163": 12, + "165": 1, + "170": 12, + "172": 1, + "177": 15, + "179": 4, + "184": 19, + "186": 8, + "191": 14, + "193": 3, + "22": 107, + "23": 107, + "31": 89, + "33": 0, + "47": 89, + "49": 0, + "58": 0, + "60": 0, + "64": 89, + "66": 0, + "70": 27, + "72": 0, + "76": 5, + "78": 0, + "82": 1, + "84": 0, + "88": 2, + "90": 0, + "94": 7, + "96": 0, + "100": 1, + "102": 0, + "107": 2, + "109": 0, + "114": 2, + "116": 0, + "121": 98, + "123": 0, + "127": 4, + "129": 0, + "133": 4, + "135": 0, + "143": 93, + "145": 0, + "150": 1, + "152": 0, + "157": 1, + "159": 0, + "164": 1, + "166": 0, + "171": 1, + "173": 0, + "178": 4, + "180": 0, + "185": 8, + "187": 0, + "192": 3, + "194": 0, + "197": 11 + }, + "branchHits": {} + }, + { + "filename": "src/events/PlaybackEventsObserver.ts", + "lineHits": { + "24": 22, + "28": 90, + "36": 100, + "48": 23, + "52": 17, + "56": 100, + "71": 11, + "75": 100, + "88": 38, + "97": 15, + "101": 12, + "105": 12, + "112": 13, + "116": 18, + "124": 12, + "129": 13, + "134": 13, + "142": 109, + "150": 15, + "157": 15, + "165": 104, + "177": 12, + "184": 12, + "191": 12, + "196": 12, + "204": 15, + "211": 19, + "219": 14, + "227": 472, + "13": 11, + "32": 90, + "33": 90, + "41": 89, + "49": 12, + "53": 6, + "61": 89, + "66": 89, + "72": 0, + "80": 89, + "81": 89, + "89": 27, + "90": 27, + "98": 4, + "99": 4, + "102": 1, + "103": 1, + "106": 1, + "107": 1, + "113": 2, + "114": 2, + "117": 7, + "118": 7, + "125": 1, + "126": 1, + "130": 2, + "131": 2, + "135": 2, + "136": 2, + "143": 98, + "144": 98, + "151": 4, + "152": 4, + "158": 4, + "159": 4, + "170": 93, + "171": 93, + "178": 1, + "179": 1, + "185": 1, + "186": 1, + "192": 1, + "193": 1, + "197": 1, + "198": 1, + "205": 4, + "206": 4, + "212": 8, + "213": 8, + "220": 3, + "221": 3, + "228": 461, + "229": 461, + "231": 11 + }, + "branchHits": {} + }, + { + "filename": "src/events/TimeObservable.ts", + "lineHits": { + "4": 26, + "8": 202, + "13": 101, + "14": 0, + "17": 0, + "22": 13, + "27": 0, + "31": 0, + "41": 24, + "46": 11, + "9": 101, + "10": 101, + "15": 0, + "18": 0, + "26": 0, + "28": 0, + "29": 0, + "30": 0, + "33": 0, + "34": 0, + "36": 0, + "45": 11, + "47": 0, + "48": 0, + "52": 0, + "53": 0, + "57": 13 + }, + "branchHits": {} + }, + { + "filename": "src/events/index.ts", + "lineHits": { + "1": 11, + "2": 11, + "4": 11, + "5": 11 + }, + "branchHits": {} + }, + { + "filename": "src/player/BetamaxPlayer.ts", + "lineHits": { + "25": 20, + "31": 14, + "277": 24, + "283": 890, + "288": 456, + "47": 26, + "58": 34, + "74": 34, + "76": 0, + "87": 10, + "95": 10, + "108": 10, + "121": 11, + "134": 10, + "146": 10, + "154": 11, + "162": 10, + "168": 10, + "179": 10, + "191": 0, + "203": 0, + "216": 0, + "228": 10, + "232": 27, + "244": 13, + "252": 11, + "256": 34, + "266": 459, + "8": 10, + "9": 10, + "15": 10, + "17": 10, + "35": 14, + "36": 14, + "37": 14, + "38": 14, + "48": 16, + "62": 17, + "64": 17, + "69": 17, + "77": 0, + "89": 0, + "91": 0, + "97": 0, + "99": 0, + "110": 0, + "112": 0, + "122": 1, + "124": 0, + "126": 1, + "136": 0, + "138": 0, + "148": 0, + "150": 0, + "156": 1, + "157": 1, + "158": 1, + "164": 0, + "170": 0, + "181": 0, + "183": 0, + "193": 0, + "195": 0, + "205": 0, + "207": 0, + "218": 0, + "220": 0, + "229": 0, + "233": 0, + "234": 2, + "238": 2, + "239": 2, + "240": 2, + "246": 3, + "247": 3, + "248": 3, + "253": 1, + "261": 17, + "267": 442, + "270": 17, + "271": 17, + "273": 0, + "278": 0, + "279": 1, + "284": 6, + "285": 6, + "293": 442, + "295": 10 + }, + "branchHits": { + "35": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "229": { + "covered": 0, + "missed": 2, + "available": 2 + }, + "233": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "238": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "278": { + "covered": 2, + "missed": 0, + "available": 2 + } + } + }, + { + "filename": "src/player/BetamaxPlayerFactory.ts", + "lineHits": { + "6": 20, + "9": 68, + "15": 78, + "2": 10, + "3": 10, + "12": 68, + "16": 68, + "19": 68, + "21": 10 + }, + "branchHits": {} + }, + { + "filename": "src/player/PlaybackSession.ts", + "lineHits": { + "50": 20, + "71": 90, + "165": 182, + "170": 2, + "177": 179, + "179": 102, + "90": 91, + "136": 100, + "146": 103, + "185": 178, + "189": 178, + "301": 0, + "310": 0, + "324": 89, + "334": 178, + "339": 979, + "343": 445, + "349": 14, + "351": 44, + "355": 20, + "361": 10, + "365": 15, + "392": 178, + "443": 0, + "458": 14, + "485": 99, + "501": 11, + "514": 14, + "525": 11, + "529": 23, + "558": 29, + "567": 24, + "571": 25, + "581": 15, + "596": 14, + "608": 37, + "619": 10, + "623": 110, + "714": 18, + "719": 16, + "762": 11, + "768": 12, + "782": 13, + "803": 195, + "840": 11, + "844": 180, + "849": 180, + "860": 16, + "869": 221, + "875": 18, + "879": 4, + "885": 18, + "922": 13, + "939": 11, + "952": 101, + "965": 1572, + "969": 784, + "983": 11, + "987": 11, + "991": 19, + "1026": 106, + "1035": 16, + "1040": 18, + "1076": 10, + "1123": 16, + "1134": 11, + "1138": 100, + "1142": 0, + "1146": 0, + "1150": 0, + "1154": 2, + "1158": 0, + "1": 10, + "7": 10, + "8": 10, + "9": 10, + "10": 10, + "11": 10, + "33": 10, + "34": 10, + "42": 10, + "44": 10, + "45": 10, + "48": 10, + "75": 90, + "76": 90, + "77": 90, + "78": 90, + "83": 1, + "87": 1, + "92": 1, + "96": 1, + "97": 1, + "101": 90, + "111": 90, + "116": 90, + "128": 90, + "143": 90, + "150": 1, + "153": 1, + "156": 1, + "159": 1, + "162": 89, + "167": 182, + "169": 92, + "171": 2, + "172": 0, + "181": 0, + "191": 89, + "195": 89, + "199": 89, + "201": 89, + "204": 89, + "208": 89, + "212": 89, + "216": 1, + "220": 1, + "222": 1, + "229": 1, + "232": 89, + "233": 89, + "235": 89, + "236": 89, + "241": 89, + "243": 0, + "245": 89, + "247": 0, + "253": 0, + "259": 89, + "266": 89, + "267": 89, + "268": 89, + "269": 89, + "271": 89, + "273": 89, + "276": 89, + "283": 89, + "286": 89, + "290": 89, + "295": 89, + "300": 89, + "302": 0, + "308": 0, + "312": 0, + "313": 0, + "314": 0, + "316": 0, + "325": 89, + "328": 89, + "329": 89, + "330": 89, + "335": 89, + "340": 890, + "344": 356, + "350": 4, + "352": 40, + "356": 16, + "362": 0, + "368": 3, + "369": 3, + "370": 0, + "374": 2, + "375": 2, + "383": 1, + "384": 1, + "393": 89, + "409": 0, + "410": 0, + "411": 0, + "413": 0, + "426": 10, + "428": 0, + "431": 0, + "434": 3, + "436": 3, + "437": 0, + "440": 0, + "441": 0, + "442": 0, + "444": 0, + "445": 0, + "449": 0, + "451": 2, + "454": 1, + "460": 0, + "465": 1, + "471": 0, + "476": 2, + "477": 0, + "479": 2, + "480": 2, + "486": 89, + "487": 89, + "488": 0, + "489": 1, + "492": 1, + "494": 89, + "502": 1, + "505": 1, + "507": 1, + "515": 4, + "516": 4, + "517": 4, + "522": 4, + "526": 1, + "530": 0, + "532": 1, + "535": 12, + "537": 12, + "538": 12, + "540": 12, + "549": 12, + "550": 12, + "553": 2, + "554": 2, + "559": 19, + "560": 0, + "561": 16, + "562": 16, + "564": 19, + "572": 0, + "574": 6, + "582": 5, + "583": 5, + "590": 4, + "592": 1, + "597": 4, + "601": 4, + "602": 4, + "604": 2, + "609": 27, + "610": 27, + "611": 27, + "612": 27, + "620": 0, + "625": 99, + "639": 100, + "640": 100, + "642": 0, + "643": 6, + "646": 0, + "647": 6, + "650": 100, + "652": 100, + "653": 100, + "654": 100, + "657": 0, + "663": 2, + "664": 2, + "665": 2, + "668": 100, + "671": 0, + "677": 2, + "680": 100, + "683": 100, + "684": 0, + "693": 2, + "695": 98, + "698": 0, + "699": 84, + "701": 100, + "711": 100, + "715": 0, + "716": 0, + "721": 1, + "723": 7, + "726": 2, + "742": 4, + "749": 4, + "755": 5, + "763": 1, + "771": 1, + "774": 1, + "775": 1, + "784": 3, + "787": 3, + "793": 3, + "806": 93, + "808": 6, + "810": 93, + "816": 93, + "823": 0, + "827": 4, + "833": 92, + "841": 1, + "846": 0, + "850": 90, + "851": 90, + "853": 90, + "854": 90, + "861": 6, + "862": 6, + "863": 6, + "864": 6, + "865": 6, + "866": 6, + "870": 211, + "871": 0, + "877": 2, + "880": 2, + "882": 6, + "887": 8, + "888": 8, + "892": 1, + "895": 7, + "896": 7, + "897": 7, + "899": 4, + "900": 4, + "909": 2, + "912": 0, + "915": 0, + "919": 7, + "927": 0, + "929": 3, + "930": 0, + "933": 3, + "936": 0, + "941": 0, + "946": 1, + "949": 0, + "954": 1, + "955": 1, + "957": 90, + "958": 90, + "960": 91, + "961": 91, + "962": 91, + "966": 1562, + "970": 774, + "971": 774, + "972": 774, + "975": 563, + "978": 774, + "984": 1, + "988": 1, + "992": 9, + "995": 0, + "997": 9, + "999": 2, + "1005": 1, + "1008": 9, + "1013": 9, + "1014": 9, + "1021": 9, + "1023": 9, + "1029": 96, + "1030": 96, + "1031": 96, + "1032": 96, + "1036": 6, + "1037": 6, + "1041": 8, + "1042": 8, + "1046": 8, + "1048": 8, + "1052": 8, + "1054": 0, + "1055": 0, + "1056": 0, + "1058": 0, + "1064": 8, + "1069": 0, + "1073": 8, + "1078": 0, + "1084": 10, + "1085": 4, + "1086": 0, + "1087": 0, + "1089": 4, + "1091": 4, + "1093": 4, + "1094": 4, + "1099": 4, + "1100": 0, + "1101": 0, + "1102": 0, + "1105": 8, + "1106": 0, + "1108": 0, + "1112": 0, + "1115": 4, + "1117": 4, + "1118": 4, + "1119": 4, + "1125": 6, + "1135": 1, + "1139": 90, + "1143": 0, + "1147": 0, + "1151": 0, + "1155": 2, + "1159": 0, + "1161": 10 + }, + "branchHits": { + "75": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "167": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "172": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "243": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "247": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "370": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "409": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "410": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "411": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "413": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "428": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "440": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "460": { + "covered": 4, + "missed": 0, + "available": 4 + }, + "471": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "477": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "488": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "530": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "535": { + "covered": 3, + "missed": 0, + "available": 3 + }, + "560": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "572": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "609": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "610": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "642": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "646": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "657": { + "covered": 3, + "missed": 0, + "available": 3 + }, + "671": { + "covered": 5, + "missed": 0, + "available": 5 + }, + "683": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "684": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "698": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "715": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "808": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "823": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "870": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "871": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "880": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "912": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "927": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "930": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "941": { + "covered": 4, + "missed": 0, + "available": 4 + }, + "966": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "997": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "1086": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "1087": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "1101": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "1159": { + "covered": 0, + "missed": 2, + "available": 2 + } + } + }, + { + "filename": "src/player/PlaybackSessionFactory.ts", + "lineHits": { + "10": 20, + "13": 68, + "19": 27, + "70": 17, + "2": 10, + "3": 10, + "4": 10, + "16": 68, + "23": 17, + "25": 0, + "33": 0, + "34": 0, + "35": 0, + "36": 0, + "37": 0, + "38": 0, + "39": 0, + "42": 0, + "53": 0, + "62": 0, + "63": 0, + "68": 17, + "71": 0, + "72": 17, + "78": 10 + }, + "branchHits": { + "23": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "33": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "62": { + "covered": 1, + "missed": 1, + "available": 2 + } + } + }, + { + "filename": "src/player/index.ts", + "lineHits": { + "1": 10, + "2": 10, + "3": 10, + "4": 10, + "5": 10, + "6": 10 + }, + "branchHits": {} + }, + { + "filename": "src/player/player.ts", + "lineHits": { + "9": 24, + "12": 89, + "16": 101, + "18": 89, + "25": 198, + "29": 101, + "37": 12, + "41": 12, + "45": 12, + "49": 12, + "53": 38, + "57": 368, + "61": 28, + "65": 12, + "69": 12, + "73": 12, + "1": 12, + "4": 12, + "5": 12, + "13": 89, + "19": 0, + "22": 89, + "26": 186, + "34": 89, + "38": 0, + "42": 0, + "46": 0, + "50": 0, + "54": 26, + "58": 356, + "62": 16, + "66": 0, + "70": 0, + "74": 0, + "76": 12 + }, + "branchHits": {} + }, + { + "filename": "src/player/shaka.ts", + "lineHits": { + "2": 13, + "5": 13, + "6": 0, + "7": 0, + "10": 13 + }, + "branchHits": { + "6": { + "covered": 2, + "missed": 1, + "available": 3 + } + } + }, + { + "filename": "src/plugins/SPManifestParser.ts", + "lineHits": { + "26": 5, + "40": 14, + "45": 14, + "60": 10, + "67": 9, + "70": 9, + "75": 9, + "87": 14, + "94": 10, + "113": 14, + "152": 14, + "159": 13, + "173": 14, + "175": 44, + "186": 14, + "191": 23, + "211": 0, + "218": 0, + "226": 18, + "227": 26, + "242": 22, + "281": 14, + "289": 16, + "298": 13, + "313": 43, + "332": 0, + "366": 52, + "367": 28, + "368": 6, + "388": 68, + "399": 10, + "405": 4, + "461": 7944, + "467": 3974, + "468": 6, + "18": 9, + "27": 5, + "28": 5, + "29": 5, + "30": 5, + "31": 5, + "32": 5, + "33": 5, + "34": 5, + "35": 5, + "36": 5, + "41": 5, + "47": 0, + "50": 5, + "59": 5, + "61": 5, + "62": 5, + "88": 5, + "91": 5, + "92": 5, + "95": 5, + "98": 5, + "114": 5, + "117": 0, + "120": 0, + "122": 0, + "130": 5, + "134": 0, + "141": 5, + "153": 5, + "154": 5, + "156": 5, + "160": 13, + "174": 5, + "188": 5, + "189": 5, + "192": 18, + "194": 0, + "195": 13, + "196": 13, + "198": 5, + "199": 5, + "201": 0, + "206": 0, + "207": 0, + "210": 0, + "217": 0, + "225": 5, + "228": 13, + "231": 5, + "247": 13, + "249": 13, + "250": 0, + "252": 13, + "257": 0, + "260": 0, + "265": 0, + "284": 5, + "286": 0, + "299": 8, + "319": 34, + "322": 34, + "324": 13, + "327": 13, + "330": 34, + "333": 0, + "338": 0, + "343": 0, + "344": 0, + "345": 0, + "348": 0, + "369": 4, + "373": 2, + "376": 26, + "393": 34, + "395": 34, + "396": 34, + "400": 2, + "402": 0, + "406": 2, + "412": 2, + "417": 8, + "428": 26, + "430": 26, + "431": 26, + "433": 3972, + "439": 3972, + "440": 3972, + "441": 3972, + "446": 34, + "448": 34, + "469": 4, + "474": 2, + "477": 3972, + "487": 9 + }, + "branchHits": { + "194": { + "covered": 2, + "missed": 1, + "available": 3 + }, + "206": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "210": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "217": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "247": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "249": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "250": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "257": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "260": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "265": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "286": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "338": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "343": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "344": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "345": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "348": { + "covered": 2, + "missed": 0, + "available": 2 + } + } + }, + { + "filename": "src/plugins/SpotifyVideoMediaExtension.ts", + "lineHits": { + "9": 16, + "12": 59, + "29": 10, + "34": 12, + "38": 10, + "2": 8, + "3": 8, + "13": 59, + "14": 59, + "18": 59, + "22": 59, + "23": 59, + "30": 2, + "31": 2, + "35": 4, + "42": 2, + "46": 8 + }, + "branchHits": {} + }, + { + "filename": "src/plugins/SpotifyVideoUrl.ts", + "lineHits": { + "7": 16, + "8": 39, + "1": 8, + "9": 31, + "11": 8 + }, + "branchHits": {} + }, + { + "filename": "src/plugins/TextParser.js", + "lineHits": { + "25": 0, + "37": 8, + "47": 8, + "57": 8, + "64": 8, + "78": 8, + "96": 8, + "116": 8, + "30": 0, + "33": 0, + "38": 0, + "48": 0, + "58": 0, + "65": 0, + "79": 0, + "80": 0, + "81": 0, + "84": 0, + "85": 0, + "98": 0, + "101": 0, + "103": 0, + "105": 0, + "117": 0, + "118": 0, + "120": 0, + "122": 0, + "130": 8 + }, + "branchHits": { + "80": { + "covered": 0, + "missed": 3, + "available": 3 + } + } + }, + { + "filename": "src/plugins/VttTextParser.js", + "lineHits": { + "26": 0, + "29": 8, + "37": 8, + "121": 8, + "144": 8, + "223": 8, + "260": 8, + "261": 0, + "285": 8, + "299": 8, + "315": 8, + "330": 8, + "362": 8, + "19": 8, + "20": 8, + "30": 0, + "38": 0, + "40": 0, + "41": 0, + "42": 0, + "45": 0, + "52": 0, + "60": 0, + "72": 0, + "76": 0, + "77": 0, + "78": 0, + "79": 0, + "80": 0, + "81": 0, + "84": 0, + "91": 0, + "92": 0, + "93": 0, + "95": 0, + "96": 0, + "101": 0, + "102": 0, + "103": 0, + "104": 0, + "106": 0, + "110": 0, + "122": 0, + "126": 0, + "129": 0, + "130": 0, + "132": 0, + "145": 0, + "148": 0, + "149": 0, + "154": 0, + "159": 0, + "162": 0, + "163": 0, + "165": 0, + "166": 0, + "170": 0, + "171": 0, + "172": 0, + "173": 0, + "175": 0, + "176": 0, + "183": 0, + "184": 0, + "187": 0, + "192": 0, + "195": 0, + "196": 0, + "199": 0, + "205": 0, + "206": 0, + "210": 0, + "212": 0, + "224": 0, + "225": 0, + "227": 0, + "229": 0, + "231": 0, + "237": 0, + "239": 0, + "242": 0, + "244": 0, + "247": 0, + "250": 0, + "262": 0, + "265": 0, + "270": 0, + "274": 0, + "277": 0, + "286": 0, + "288": 0, + "290": 0, + "300": 0, + "301": 0, + "302": 0, + "303": 0, + "304": 0, + "306": 0, + "316": 0, + "318": 0, + "320": 0, + "334": 0, + "335": 0, + "337": 0, + "338": 0, + "340": 0, + "343": 0, + "344": 0, + "346": 0, + "349": 0, + "352": 0, + "364": 0, + "366": 0, + "370": 0, + "371": 0, + "372": 0, + "373": 0, + "374": 0, + "375": 0, + "378": 0, + "385": 8, + "387": 8 + }, + "branchHits": { + "77": { + "covered": 0, + "missed": 2, + "available": 2 + }, + "148": { + "covered": 0, + "missed": 2, + "available": 2 + }, + "175": { + "covered": 0, + "missed": 3, + "available": 3 + }, + "301": { + "covered": 0, + "missed": 2, + "available": 2 + }, + "303": { + "covered": 0, + "missed": 2, + "available": 2 + }, + "370": { + "covered": 0, + "missed": 2, + "available": 2 + }, + "374": { + "covered": 0, + "missed": 2, + "available": 2 + } + } + }, + { + "filename": "src/plugins/WidevineDownloader.ts", + "lineHits": { + "14": 14, + "21": 14, + "190": 12, + "34": 11, + "40": 4, + "44": 5, + "66": 4, + "69": 0, + "78": 9, + "90": 8, + "107": 11, + "137": 2, + "147": 1, + "165": 14, + "174": 8, + "2": 7, + "9": 7, + "10": 7, + "11": 7, + "12": 7, + "22": 7, + "26": 7, + "27": 7, + "28": 7, + "29": 7, + "36": 0, + "38": 4, + "41": 4, + "46": 0, + "49": 1, + "51": 0, + "59": 0, + "63": 0, + "64": 0, + "70": 0, + "80": 0, + "84": 1, + "86": 1, + "91": 1, + "92": 1, + "93": 1, + "94": 1, + "108": 4, + "111": 0, + "112": 2, + "113": 2, + "120": 1, + "121": 1, + "128": 1, + "129": 1, + "133": 0, + "136": 1, + "139": 1, + "140": 1, + "150": 0, + "152": 0, + "153": 0, + "166": 7, + "171": 0, + "172": 0, + "177": 7, + "179": 6, + "181": 3, + "182": 3, + "183": 3, + "184": 3, + "185": 3, + "187": 3, + "189": 3, + "191": 6, + "193": 3, + "198": 7, + "200": 3, + "201": 1, + "202": 1, + "203": 1, + "205": 1, + "207": 7 + }, + "branchHits": { + "59": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "80": { + "covered": 2, + "missed": 3, + "available": 5 + }, + "111": { + "covered": 4, + "missed": 6, + "available": 10 + }, + "179": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "184": { + "covered": 1, + "missed": 1, + "available": 2 + } + } + }, + { + "filename": "src/plugins/index.ts", + "lineHits": { + "1": 8, + "2": 8, + "3": 8, + "4": 8, + "6": 8, + "7": 8, + "8": 8 + }, + "branchHits": {} + }, + { + "filename": "src/plugins/types.ts", + "lineHits": { + "19": 14, + "20": 7, + "21": 7, + "22": 7, + "23": 7, + "24": 7, + "25": 7, + "26": 7, + "27": 7, + "28": 7, + "29": 7, + "30": 7 + }, + "branchHits": { + "19": { + "covered": 2, + "missed": 0, + "available": 2 + } + } + }, + { + "filename": "src/tracking/DefaultEventsCalculator.ts", + "lineHits": { + "34": 108, + "46": 7, + "95": 68, + "108": 17, + "116": 17, + "124": 92, + "145": 92, + "164": 41, + "168": 48, + "181": 41, + "185": 6, + "194": 41, + "198": 2, + "207": 42, + "215": 4, + "229": 41, + "233": 2, + "242": 42, + "246": 50, + "255": 44, + "259": 12, + "276": 42, + "284": 2, + "298": 41, + "302": 2, + "311": 42, + "315": 10, + "326": 42, + "334": 2, + "348": 43, + "352": 22, + "386": 42, + "398": 10, + "417": 45, + "475": 43, + "479": 83, + "483": 86, + "496": 41, + "500": 76, + "505": 86, + "522": 41, + "526": 76, + "531": 86, + "548": 39, + "1": 16, + "11": 16, + "12": 16, + "13": 16, + "37": 35, + "39": 0, + "40": 19, + "43": 0, + "51": 7, + "52": 7, + "55": 0, + "57": 0, + "58": 3, + "65": 3, + "69": 3, + "71": 3, + "78": 2, + "82": 3, + "84": 1, + "87": 1, + "89": 0, + "92": 7, + "99": 52, + "100": 52, + "109": 1, + "111": 1, + "113": 0, + "117": 1, + "119": 1, + "128": 76, + "129": 76, + "130": 0, + "132": 1, + "134": 0, + "136": 1, + "140": 1, + "142": 76, + "149": 76, + "150": 76, + "151": 0, + "152": 1, + "154": 0, + "155": 1, + "159": 1, + "161": 76, + "167": 25, + "184": 25, + "197": 25, + "210": 26, + "214": 26, + "216": 4, + "222": 0, + "232": 25, + "245": 26, + "258": 28, + "261": 0, + "262": 0, + "265": 0, + "266": 0, + "279": 26, + "283": 26, + "285": 2, + "291": 0, + "301": 25, + "314": 26, + "317": 0, + "329": 26, + "333": 26, + "335": 2, + "341": 0, + "351": 27, + "354": 5, + "365": 6, + "375": 0, + "389": 26, + "390": 26, + "397": 26, + "399": 5, + "400": 5, + "433": 86, + "435": 0, + "439": 5, + "444": 3, + "450": 9, + "451": 9, + "454": 9, + "457": 0, + "461": 4, + "466": 4, + "467": 4, + "468": 4, + "472": 29, + "478": 27, + "480": 83, + "499": 25, + "501": 76, + "507": 76, + "508": 76, + "525": 25, + "527": 76, + "533": 76, + "534": 76, + "551": 23 + }, + "branchHits": { + "39": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "55": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "57": { + "covered": 3, + "missed": 1, + "available": 4 + }, + "113": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "129": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "130": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "134": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "150": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "151": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "154": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "222": { + "covered": 1, + "missed": 0, + "available": 1 + }, + "261": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "262": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "265": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "266": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "291": { + "covered": 1, + "missed": 0, + "available": 1 + }, + "317": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "341": { + "covered": 1, + "missed": 0, + "available": 1 + }, + "435": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "457": { + "covered": 2, + "missed": 0, + "available": 2 + } + } + }, + { + "filename": "src/tracking/EventsTracker.ts", + "lineHits": { + "16": 32, + "19": 15, + "23": 17, + "40": 17, + "53": 17, + "63": 17, + "71": 17, + "86": 16, + "101": 28, + "117": 16, + "130": 21, + "145": 19, + "160": 25, + "170": 19, + "180": 21, + "195": 17, + "210": 17, + "225": 17, + "240": 17, + "248": 17, + "256": 17, + "271": 17, + "286": 17, + "301": 17, + "14": 16, + "20": 15, + "29": 1, + "44": 1, + "54": 1, + "64": 1, + "76": 1, + "91": 0, + "107": 12, + "121": 0, + "135": 5, + "150": 3, + "161": 9, + "171": 3, + "185": 5, + "200": 1, + "215": 1, + "230": 1, + "241": 1, + "249": 1, + "261": 1, + "276": 1, + "291": 1, + "302": 1, + "310": 16 + }, + "branchHits": {} + }, + { + "filename": "src/tracking/ProductStatesStreamingRulesProvider.ts", + "lineHits": { + "4": 32, + "9": 6, + "14": 2, + "17": 0, + "21": 20, + "10": 3, + "11": 3, + "12": 3, + "15": 2, + "22": 4, + "24": 16 + }, + "branchHits": {} + }, + { + "filename": "src/tracking/VideoPlaybackEvents.ts", + "lineHits": { + "1": 36, + "7": 36, + "17": 36, + "2": 18, + "3": 18, + "4": 18, + "8": 18, + "9": 18, + "10": 18, + "13": 18, + "18": 18, + "19": 18, + "20": 18, + "21": 18, + "22": 18, + "23": 18, + "24": 18, + "25": 18, + "26": 18, + "27": 18, + "28": 18, + "29": 18, + "30": 18, + "31": 18, + "32": 18, + "33": 18, + "34": 18, + "35": 18, + "36": 18, + "37": 18, + "38": 18, + "39": 18, + "40": 18, + "41": 18, + "42": 18, + "45": 18 + }, + "branchHits": { + "1": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "7": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "17": { + "covered": 2, + "missed": 0, + "available": 2 + } + } + }, + { + "filename": "src/tracking/index.ts", + "lineHits": { + "1": 15, + "2": 15, + "3": 15, + "4": 15, + "5": 15, + "7": 15, + "8": 15, + "9": 15, + "11": 15, + "12": 15, + "13": 15, + "15": 15, + "16": 15 + }, + "branchHits": {} + }, + { + "filename": "src/tracking/types.ts", + "lineHits": { + "13": 32, + "95": 32, + "10": 16, + "14": 16, + "15": 16, + "16": 16, + "17": 16, + "18": 16, + "19": 16, + "20": 16, + "21": 16, + "22": 16, + "23": 16, + "24": 16, + "25": 16, + "26": 16, + "27": 16, + "28": 16, + "29": 16, + "30": 16, + "31": 16, + "32": 16, + "33": 16, + "34": 16, + "35": 16, + "36": 16, + "37": 16, + "38": 16, + "39": 16, + "40": 16, + "41": 16, + "42": 16, + "43": 16, + "44": 16, + "45": 16, + "46": 16, + "96": 16, + "97": 16, + "98": 16, + "99": 16, + "100": 16, + "101": 16, + "102": 16, + "103": 16, + "104": 16, + "105": 16, + "106": 16, + "107": 16, + "108": 16, + "109": 16, + "110": 16, + "111": 16, + "112": 16, + "113": 16, + "114": 16, + "115": 16, + "116": 16, + "117": 16, + "118": 16, + "119": 16, + "120": 16, + "121": 16, + "122": 16, + "123": 16, + "124": 16, + "125": 16, + "126": 16, + "127": 16, + "128": 16, + "129": 16, + "130": 16, + "131": 16, + "132": 16, + "133": 16, + "134": 16, + "135": 16, + "136": 16, + "137": 16, + "138": 16, + "139": 16, + "140": 16, + "141": 16, + "142": 16, + "143": 16, + "144": 16, + "145": 16, + "146": 16, + "147": 16, + "148": 16 + }, + "branchHits": { + "13": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "95": { + "covered": 2, + "missed": 0, + "available": 2 + } + } + }, + { + "filename": "src/tracking/utils.ts", + "lineHits": { + "44": 64, + "109": 225, + "113": 1944, + "115": 1902, + "123": 514, + "128": 486, + "133": 91, + "138": 95, + "143": 239, + "148": 314, + "153": 167, + "158": 91, + "163": 91, + "168": 261, + "173": 167, + "178": 17, + "183": 139, + "188": 93, + "193": 475, + "198": 61, + "203": 95, + "208": 19, + "220": 1501, + "224": 484, + "228": 462, + "232": 900, + "236": 113, + "245": 96, + "250": 460, + "251": 56, + "254": 46, + "259": 158, + "261": 19, + "269": 78, + "2": 17, + "4": 17, + "7": 17, + "9": 17, + "11": 17, + "12": 17, + "13": 17, + "14": 17, + "15": 17, + "16": 17, + "17": 17, + "18": 17, + "19": 17, + "20": 17, + "21": 17, + "22": 17, + "23": 17, + "24": 17, + "26": 17, + "42": 17, + "47": 47, + "48": 47, + "53": 47, + "58": 47, + "63": 47, + "64": 47, + "69": 47, + "74": 47, + "75": 47, + "76": 47, + "77": 47, + "78": 47, + "100": 17, + "102": 17, + "104": 17, + "112": 208, + "126": 497, + "131": 469, + "136": 74, + "141": 78, + "146": 222, + "151": 297, + "156": 150, + "161": 74, + "166": 74, + "171": 244, + "176": 150, + "181": 0, + "186": 122, + "191": 76, + "196": 458, + "201": 44, + "206": 78, + "209": 2, + "211": 17, + "222": 497, + "226": 132, + "230": 110, + "234": 248, + "237": 96, + "239": 96, + "249": 79, + "258": 29, + "262": 19, + "263": 0, + "270": 0, + "271": 0, + "273": 61, + "274": 0, + "275": 0, + "277": 61 + }, + "branchHits": { + "209": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "222": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "226": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "230": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "234": { + "covered": 4, + "missed": 0, + "available": 4 + }, + "263": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "270": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "274": { + "covered": 2, + "missed": 0, + "available": 2 + } + } + }, + { + "filename": "src/tracking/endvideo/EndVideoEventsCalculator.ts", + "lineHits": { + "39": 24, + "52": 11, + "71": 6, + "79": 14, + "88": 11, + "92": 19, + "120": 28, + "123": 36, + "145": 20, + "181": 36, + "190": 27, + "206": 25, + "223": 26, + "240": 26, + "258": 42, + "264": 48, + "272": 42, + "276": 2, + "285": 42, + "289": 2, + "298": 43, + "306": 50, + "328": 40, + "332": 46, + "341": 45, + "345": 4, + "356": 43, + "361": 50, + "371": 43, + "375": 89, + "380": 86, + "394": 43, + "398": 87, + "403": 90, + "416": 46, + "420": 91, + "425": 88, + "442": 40, + "1": 15, + "8": 15, + "9": 15, + "28": 15, + "36": 15, + "37": 15, + "41": 0, + "42": 5, + "44": 0, + "46": 0, + "48": 19, + "57": 8, + "61": 0, + "64": 0, + "66": 0, + "68": 3, + "76": 1, + "78": 5, + "81": 7, + "82": 7, + "89": 11, + "94": 11, + "95": 11, + "97": 8, + "99": 0, + "103": 0, + "105": 0, + "106": 1, + "110": 1, + "113": 3, + "114": 3, + "116": 4, + "127": 21, + "128": 0, + "129": 5, + "133": 5, + "134": 5, + "135": 5, + "140": 21, + "141": 21, + "142": 21, + "149": 5, + "151": 5, + "153": 5, + "157": 5, + "159": 0, + "160": 4, + "161": 0, + "162": 1, + "166": 1, + "167": 1, + "168": 1, + "169": 1, + "174": 0, + "175": 0, + "177": 5, + "178": 5, + "184": 21, + "191": 12, + "193": 12, + "211": 10, + "212": 10, + "215": 10, + "228": 11, + "229": 11, + "232": 11, + "241": 11, + "248": 11, + "249": 11, + "261": 27, + "265": 24, + "266": 0, + "275": 27, + "288": 27, + "301": 28, + "305": 28, + "308": 13, + "310": 0, + "312": 13, + "316": 0, + "320": 12, + "331": 25, + "344": 30, + "347": 0, + "359": 28, + "360": 28, + "363": 0, + "374": 28, + "376": 89, + "397": 28, + "399": 87, + "419": 31, + "421": 91, + "445": 25, + "446": 25, + "489": 25 + }, + "branchHits": { + "41": { + "covered": 2, + "missed": 2, + "available": 4 + }, + "105": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "128": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "159": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "161": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "174": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "266": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "310": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "316": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "347": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "363": { + "covered": 2, + "missed": 0, + "available": 2 + } + } + }, + { + "filename": "src/tracking/endvideo/EndVideoLoggerFactory.ts", + "lineHits": { + "11": 30, + "19": 14, + "32": 29, + "9": 15, + "26": 14, + "27": 14, + "28": 14, + "29": 14, + "30": 14, + "33": 14, + "41": 15 + }, + "branchHits": {} + }, + { + "filename": "src/tracking/endvideo/EndVideoLoggerTracker.ts", + "lineHits": { + "24": 30, + "38": 14, + "54": 14, + "62": 0, + "96": 0, + "139": 24, + "140": 18, + "141": 18, + "154": 40, + "155": 40, + "156": 40, + "166": 35, + "177": 38, + "1": 15, + "4": 15, + "15": 15, + "16": 15, + "17": 15, + "18": 15, + "21": 15, + "22": 15, + "35": 14, + "45": 14, + "46": 14, + "47": 14, + "48": 14, + "49": 14, + "50": 14, + "52": 14, + "53": 14, + "56": 0, + "57": 0, + "59": 0, + "60": 0, + "61": 0, + "64": 0, + "67": 0, + "73": 15, + "79": 11, + "85": 11, + "86": 0, + "90": 3, + "91": 16, + "94": 11, + "97": 0, + "102": 15, + "107": 2, + "108": 2, + "109": 0, + "111": 4, + "114": 15, + "115": 2, + "116": 2, + "118": 4, + "121": 15, + "122": 7, + "123": 7, + "124": 14, + "127": 15, + "132": 4, + "133": 8, + "135": 3, + "143": 0, + "144": 0, + "145": 0, + "147": 9, + "148": 9, + "149": 9, + "158": 0, + "159": 0, + "161": 20, + "178": 23, + "179": 0, + "184": 15, + "187": 23, + "194": 3, + "197": 11, + "198": 18, + "201": 20, + "205": 40, + "208": 15, + "209": 19, + "218": 38, + "220": 15 + }, + "branchHits": { + "45": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "86": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "109": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "179": { + "covered": 2, + "missed": 0, + "available": 2 + } + } + }, + { + "filename": "src/tracking/endvideo/EndVideoReasonEnd.ts", + "lineHits": { + "1": 30, + "2": 15, + "3": 15, + "4": 15, + "5": 15, + "6": 15, + "7": 15, + "8": 15, + "11": 15 + }, + "branchHits": { + "1": { + "covered": 2, + "missed": 0, + "available": 2 + } + } + }, + { + "filename": "src/tracking/endvideo/EndVideoTracker.ts", + "lineHits": { + "24": 30, + "64": 13, + "78": 28, + "110": 26, + "118": 27, + "131": 79, + "237": 21, + "255": 44, + "287": 28, + "295": 22, + "355": 20, + "364": 28, + "417": 28, + "419": 17, + "430": 28, + "438": 24, + "458": 25, + "1": 15, + "2": 15, + "15": 15, + "20": 15, + "71": 13, + "72": 13, + "73": 13, + "74": 13, + "75": 13, + "79": 13, + "80": 13, + "81": 13, + "82": 13, + "83": 13, + "84": 13, + "85": 13, + "86": 13, + "87": 13, + "88": 13, + "89": 13, + "90": 13, + "91": 13, + "92": 13, + "93": 13, + "94": 13, + "95": 13, + "96": 13, + "97": 13, + "98": 13, + "99": 13, + "100": 13, + "101": 13, + "102": 13, + "103": 13, + "104": 13, + "105": 13, + "106": 13, + "108": 13, + "109": 13, + "112": 0, + "113": 0, + "115": 13, + "116": 13, + "117": 13, + "120": 14, + "123": 14, + "128": 13, + "133": 5, + "135": 64, + "136": 0, + "138": 0, + "139": 55, + "140": 55, + "141": 55, + "142": 55, + "143": 0, + "146": 0, + "147": 6, + "149": 0, + "150": 1, + "154": 10, + "157": 10, + "166": 64, + "167": 64, + "168": 64, + "170": 0, + "171": 7, + "174": 2, + "177": 2, + "178": 2, + "184": 3, + "185": 3, + "186": 3, + "190": 0, + "191": 0, + "193": 0, + "194": 0, + "197": 64, + "199": 21, + "201": 64, + "203": 2, + "204": 0, + "205": 1, + "209": 3, + "215": 3, + "218": 0, + "219": 10, + "226": 10, + "227": 10, + "229": 10, + "230": 0, + "231": 10, + "233": 10, + "238": 6, + "245": 6, + "247": 3, + "248": 3, + "250": 3, + "251": 3, + "262": 27, + "264": 16, + "269": 27, + "272": 17, + "274": 10, + "283": 27, + "288": 13, + "289": 13, + "290": 13, + "291": 13, + "292": 13, + "293": 13, + "297": 6, + "299": 1, + "301": 2, + "303": 13, + "304": 13, + "305": 17, + "306": 17, + "307": 17, + "310": 12, + "311": 12, + "313": 5, + "314": 5, + "317": 10, + "319": 7, + "320": 7, + "322": 17, + "324": 13, + "328": 0, + "330": 10, + "333": 4, + "335": 6, + "339": 13, + "342": 6, + "347": 13, + "357": 0, + "358": 1, + "360": 4, + "365": 13, + "366": 13, + "367": 0, + "369": 13, + "377": 0, + "405": 0, + "418": 13, + "421": 0, + "422": 0, + "425": 1, + "432": 0, + "436": 12, + "440": 0, + "441": 0, + "442": 0, + "444": 12, + "445": 12, + "446": 12, + "448": 12, + "452": 0, + "455": 1, + "460": 0, + "462": 10, + "463": 10, + "464": 10, + "466": 15 + }, + "branchHits": { + "136": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "138": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "143": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "146": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "149": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "166": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "168": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "170": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "190": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "193": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "204": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "218": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "226": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "227": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "230": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "357": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "360": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "367": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "377": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "405": { + "covered": 2, + "missed": 0, + "available": 2 + } + } + }, + { + "filename": "src/tracking/session/PlaybackSessionTracker.ts", + "lineHits": { + "7": 32, + "9": 1, + "14": 17, + "1": 16, + "10": 1, + "11": 1, + "19": 1, + "21": 1, + "22": 1, + "25": 16 + }, + "branchHits": { + "10": { + "covered": 2, + "missed": 0, + "available": 2 + } + } + }, + { + "filename": "src/tracking/session/VideoPlaybackSessionLogger.ts", + "lineHits": { + "6": 30, + "9": 0, + "12": 15, + "13": 0, + "3": 15, + "4": 15, + "10": 0, + "14": 0, + "15": 0, + "18": 15 + }, + "branchHits": {} + } +] diff --git a/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-jsoncoverage-files-3.json b/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-jsoncoverage-files-3.json new file mode 100644 index 0000000000..625dcde1a0 --- /dev/null +++ b/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-jsoncoverage-files-3.json @@ -0,0 +1,297 @@ +[ + { + "filename": "src/atoms/BoxShadow/box-shadow.ts", + "lineHits": { + "4": 5 + }, + "branchHits": {} + }, + { + "filename": "src/atoms/Button/button.tsx", + "lineHits": { + "17": 41, + "34": 41, + "70": 82, + "72": 82, + "74": 41, + "31": 41, + "32": 41, + "35": 0, + "36": 0, + "37": 0, + "38": 0, + "39": 0, + "40": 0, + "41": 0, + "42": 0, + "43": 0, + "47": 41, + "68": 10, + "75": 41 + }, + "branchHits": { + "35": { + "covered": 0, + "missed": 4, + "available": 4 + }, + "70": { + "covered": 1, + "missed": 0, + "available": 1 + }, + "72": { + "covered": 3, + "missed": 0, + "available": 3 + }, + "75": { + "covered": 2, + "missed": 0, + "available": 2 + } + } + }, + { + "filename": "src/atoms/Container/components.ts", + "lineHits": { + "5": 70, + "19": 70, + "20": 70, + "23": 70, + "4": 13, + "13": 13 + }, + "branchHits": {} + }, + { + "filename": "src/atoms/Container/container.tsx", + "lineHits": { + "12": 35, + "20": 35 + }, + "branchHits": {} + }, + { + "filename": "src/atoms/DownArrow/down-arrow.tsx", + "lineHits": { + "3": 28, + "4": 28 + }, + "branchHits": {} + }, + { + "filename": "src/molecules/Hero/components.ts", + "lineHits": { + "26": 20, + "28": 10, + "94": 10, + "103": 20, + "121": 10, + "182": 20, + "203": 5, + "218": 5, + "236": 10, + "241": 10, + "249": 8, + "254": 4, + "264": 4, + "277": 8, + "282": 4, + "294": 8, + "299": 6, + "25": 2, + "29": 10, + "37": 3, + "39": 3, + "40": 3, + "42": 3, + "43": 1, + "44": 1, + "45": 0, + "46": 0, + "48": 1, + "52": 0, + "53": 1, + "57": 0, + "59": 1, + "60": 1, + "63": 3, + "95": 10, + "112": 2, + "122": 10, + "125": 9, + "134": 0, + "151": 1, + "170": 2, + "181": 2, + "194": 2, + "204": 5, + "207": 3, + "209": 0, + "211": 1, + "213": 1, + "219": 5, + "222": 3, + "224": 0, + "226": 0, + "228": 1, + "230": 1, + "245": 2, + "255": 4, + "258": 3, + "260": 1, + "265": 4, + "268": 2, + "270": 1, + "272": 1, + "283": 4, + "286": 2, + "288": 1, + "290": 1, + "298": 2, + "304": 2, + "311": 2, + "319": 2, + "329": 2, + "361": 2, + "371": 2 + }, + "branchHits": { + "29": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "39": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "42": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "43": { + "covered": 3, + "missed": 1, + "available": 4 + }, + "45": { + "covered": 0, + "missed": 4, + "available": 4 + }, + "48": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "53": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "59": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "60": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "95": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "122": { + "covered": 2, + "missed": 2, + "available": 4 + }, + "182": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "204": { + "covered": 4, + "missed": 1, + "available": 5 + }, + "219": { + "covered": 4, + "missed": 2, + "available": 6 + }, + "236": { + "covered": 1, + "missed": 0, + "available": 1 + }, + "241": { + "covered": 1, + "missed": 0, + "available": 1 + }, + "249": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "255": { + "covered": 3, + "missed": 0, + "available": 3 + }, + "265": { + "covered": 4, + "missed": 0, + "available": 4 + }, + "277": { + "covered": 1, + "missed": 0, + "available": 1 + }, + "283": { + "covered": 4, + "missed": 0, + "available": 4 + }, + "294": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "299": { + "covered": 1, + "missed": 1, + "available": 2 + } + } + }, + { + "filename": "src/molecules/Hero/hero.tsx", + "lineHits": { + "40": 10, + "72": 0, + "127": 3, + "56": 10, + "58": 10, + "73": 0, + "128": 3 + }, + "branchHits": { + "73": { + "covered": 0, + "missed": 2, + "available": 2 + } + } + } +] diff --git a/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-jsoncoverage-files-4.json b/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-jsoncoverage-files-4.json new file mode 100644 index 0000000000..eada26d5ce --- /dev/null +++ b/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-jsoncoverage-files-4.json @@ -0,0 +1,351 @@ +[ + { + "filename": "Sources/TDSOptions.m", + "lineHits": { + "32": 0, + "33": 0, + "34": 0, + "35": 0, + "36": 0, + "37": 0, + "38": 0, + "45": 0, + "46": 0, + "47": 0, + "48": 0, + "49": 0, + "50": 0, + "51": 0, + "52": 0, + "53": 0, + "54": 0, + "55": 0, + "56": 0, + "57": 0, + "58": 0, + "61": 0, + "62": 0, + "63": 0, + "66": 0, + "67": 0, + "68": 0, + "69": 0, + "70": 0, + "71": 0, + "72": 0, + "73": 0, + "74": 0, + "75": 0, + "76": 0, + "77": 0 + }, + "branchHits": {} + }, + { + "filename": "Sources/TDSUserLock.m", + "lineHits": { + "17": 0, + "18": 0, + "19": 0, + "20": 0, + "21": 0, + "22": 0, + "23": 0, + "24": 0, + "25": 0, + "26": 0, + "27": 0, + "28": 0, + "29": 0, + "30": 0, + "31": 0, + "32": 0, + "33": 0, + "34": 0, + "35": 0, + "36": 0, + "37": 0, + "38": 0, + "39": 0, + "40": 0, + "43": 0, + "44": 0, + "45": 0, + "46": 0, + "47": 0, + "48": 0, + "49": 0, + "58": 0, + "59": 0, + "60": 0, + "61": 0, + "62": 0, + "63": 0, + "64": 0, + "65": 0, + "66": 0, + "67": 0, + "68": 0, + "69": 0, + "70": 0 + }, + "branchHits": {} + }, + { + "filename": "Sources/TDSUserManager.m", + "lineHits": { + "24": 0, + "25": 0, + "26": 0, + "27": 0, + "28": 0, + "29": 0, + "30": 0, + "31": 0, + "34": 0, + "35": 0, + "36": 0, + "37": 0, + "38": 0, + "39": 0, + "40": 0, + "49": 0, + "50": 0, + "51": 0, + "52": 0, + "53": 0, + "54": 0, + "55": 0, + "56": 0, + "57": 0, + "58": 0, + "59": 0, + "60": 0, + "61": 0, + "62": 0, + "63": 0, + "64": 0, + "65": 0, + "66": 0, + "67": 0, + "68": 0, + "69": 0, + "70": 0, + "74": 0, + "75": 0, + "76": 0, + "77": 0, + "78": 0, + "79": 0, + "80": 0, + "81": 0, + "82": 0, + "83": 0, + "84": 0, + "85": 0, + "86": 0, + "87": 0, + "88": 0, + "89": 0, + "90": 0, + "91": 0, + "92": 0, + "93": 0, + "94": 0, + "101": 0, + "102": 0, + "103": 0, + "104": 0, + "105": 0, + "106": 0, + "107": 0, + "108": 0, + "109": 0, + "110": 0, + "111": 0, + "112": 0, + "113": 0, + "114": 0, + "115": 0, + "116": 0, + "117": 0, + "118": 0, + "119": 0, + "120": 0, + "121": 0, + "122": 0, + "123": 0, + "124": 0, + "125": 0, + "126": 0, + "127": 0, + "128": 0, + "129": 0, + "130": 0, + "131": 0, + "136": 0, + "137": 0, + "138": 0, + "139": 0, + "140": 0, + "141": 0, + "142": 0, + "143": 0, + "144": 0, + "145": 0, + "146": 0, + "147": 0, + "148": 0, + "149": 0, + "150": 0, + "151": 0, + "152": 0, + "153": 0, + "158": 0, + "159": 0, + "160": 0, + "161": 0, + "162": 0, + "163": 0, + "164": 0, + "165": 0, + "166": 0, + "167": 0, + "168": 0, + "169": 0, + "170": 0, + "171": 0, + "172": 0, + "173": 0, + "174": 0, + "175": 0, + "176": 0, + "177": 0, + "178": 0, + "179": 0, + "180": 0, + "181": 0, + "182": 0, + "183": 0, + "184": 0, + "189": 0, + "190": 0, + "191": 0, + "192": 0, + "193": 0, + "194": 0, + "195": 0, + "196": 0, + "197": 0, + "198": 0, + "199": 0, + "200": 0, + "201": 0, + "202": 0, + "203": 0, + "204": 0, + "205": 0, + "206": 0, + "207": 0, + "208": 0, + "209": 0, + "210": 0, + "211": 0, + "212": 0, + "213": 0, + "214": 0, + "215": 0, + "216": 0, + "217": 0, + "218": 0, + "219": 0, + "220": 0, + "221": 0, + "222": 0, + "223": 0, + "224": 0, + "225": 0, + "226": 0, + "227": 0, + "228": 0, + "229": 0, + "230": 0, + "231": 0, + "232": 0, + "233": 0, + "234": 0, + "235": 0, + "236": 0, + "239": 0, + "240": 0, + "241": 0, + "242": 0, + "243": 0, + "244": 0, + "245": 0, + "246": 0, + "247": 0, + "248": 0, + "249": 0, + "252": 0, + "253": 0, + "254": 0, + "255": 0, + "256": 0, + "257": 0, + "258": 0, + "259": 0, + "260": 0, + "261": 0, + "262": 0, + "263": 0, + "264": 0, + "265": 0, + "268": 0, + "269": 0, + "270": 0, + "271": 0, + "272": 0, + "273": 0, + "274": 0, + "275": 0, + "276": 0, + "277": 0, + "278": 0, + "279": 0, + "280": 0, + "281": 0, + "282": 0, + "283": 0, + "284": 0, + "287": 0, + "288": 0, + "289": 0, + "290": 0, + "291": 0, + "292": 0, + "293": 0, + "294": 0, + "295": 0, + "296": 0 + }, + "branchHits": {} + }, + { + "filename": "Sources/TDSUtil.m", + "lineHits": { + "8": 0, + "9": 0, + "10": 0, + "11": 0, + "12": 0, + "13": 0, + "16": 0, + "17": 0, + "18": 0, + "19": 0, + "20": 0 + }, + "branchHits": {} + } +] diff --git a/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-jsoncoverage-files-5.json b/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-jsoncoverage-files-5.json new file mode 100644 index 0000000000..41ab6c084f --- /dev/null +++ b/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-jsoncoverage-files-5.json @@ -0,0 +1,325 @@ +[ + { + "filename": "src/graphql/queries.ts", + "lineHits": { + "1": 3, + "3": 3, + "50": 3, + "65": 3, + "80": 3, + "92": 3, + "138": 3 + }, + "branchHits": {} + }, + { + "filename": "src/graphql/resolvers.ts", + "lineHits": { + "22": 17, + "28": 14, + "36": 16, + "27": 8, + "35": 8, + "43": 9, + "51": 15, + "56": 8, + "61": 8, + "50": 8, + "55": 8, + "60": 8, + "128": 7, + "139": 4, + "145": 8, + "160": 8, + "177": 7, + "185": 13, + "70": 12, + "78": 8, + "81": 8, + "84": 4, + "97": 12, + "103": 8, + "114": 8, + "144": 8, + "159": 8, + "176": 8, + "184": 8, + "1": 4, + "2": 4, + "3": 4, + "5": 4, + "6": 4, + "7": 4, + "8": 4, + "9": 4, + "10": 4, + "14": 4, + "21": 4, + "24": 17, + "32": 10, + "37": 12, + "42": 4, + "45": 9, + "47": 9, + "52": 11, + "57": 4, + "62": 4, + "67": 4, + "73": 8, + "83": 4, + "85": 4, + "86": 4, + "87": 4, + "88": 4, + "94": 4, + "95": 4, + "98": 4, + "104": 4, + "107": 4, + "115": 4, + "118": 4, + "119": 4, + "120": 0, + "124": 4, + "130": 7, + "132": 7, + "134": 7, + "136": 7, + "140": 4, + "151": 4, + "165": 4, + "166": 4, + "167": 4, + "168": 1, + "169": 4, + "178": 3, + "186": 9, + "187": 9, + "193": 2, + "194": 0 + }, + "branchHits": { + "86": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "119": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "166": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "167": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "193": { + "covered": 1, + "missed": 1, + "available": 2 + } + } + }, + { + "filename": "src/graphql/router.ts", + "lineHits": { + "21": 1, + "22": 2, + "43": 21, + "53": 40, + "12": 3, + "14": 3, + "23": 1, + "26": 0, + "32": 1, + "33": 1, + "34": 1, + "37": 0, + "54": 11, + "55": 0, + "57": 11, + "58": 11, + "63": 11, + "64": 11, + "65": 10, + "66": 10, + "68": 1 + }, + "branchHits": { + "23": { + "covered": 3, + "missed": 0, + "available": 3 + }, + "26": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "32": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "55": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "65": { + "covered": 1, + "missed": 1, + "available": 2 + } + } + }, + { + "filename": "src/graphql/schema.ts", + "lineHits": { + "9": 7, + "1": 3, + "2": 3, + "4": 3, + "5": 3, + "11": 0, + "13": 4, + "15": 4, + "16": 4, + "17": 4, + "19": 4 + }, + "branchHits": { + "11": { + "covered": 0, + "missed": 1, + "available": 1 + } + } + }, + { + "filename": "src/graphql/transforms.ts", + "lineHits": { + "9": 10, + "20": 5, + "13": 6, + "14": 2, + "15": 6, + "16": 1, + "17": 6, + "23": 1, + "24": 0, + "25": 1 + }, + "branchHits": { + "13": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "15": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "23": { + "covered": 1, + "missed": 1, + "available": 2 + } + } + }, + { + "filename": "src/workers/primary-worker.js", + "lineHits": { + "16": 0, + "25": 0, + "49": 0, + "2": 0, + "3": 0, + "4": 0, + "5": 0, + "6": 0, + "8": 0, + "17": 0, + "21": 0, + "22": 0, + "23": 0, + "24": 0, + "27": 0, + "28": 0, + "31": 0, + "33": 0, + "34": 0, + "37": 0, + "38": 0, + "48": 0, + "51": 0, + "53": 0 + }, + "branchHits": { + "17": { + "covered": 0, + "missed": 1, + "available": 1 + }, + "21": { + "covered": 0, + "missed": 2, + "available": 2 + }, + "49": { + "covered": 0, + "missed": 2, + "available": 2 + } + } + }, + { + "filename": "src/workers/secondary-worker.js", + "lineHits": { + "4": 0, + "13": 0, + "2": 0, + "5": 0, + "9": 0, + "10": 0, + "11": 0, + "12": 0, + "14": 0, + "15": 0, + "16": 0, + "18": 0, + "19": 0, + "22": 0, + "23": 0, + "24": 0, + "25": 0, + "26": 0, + "27": 0, + "29": 0 + }, + "branchHits": { + "5": { + "covered": 0, + "missed": 1, + "available": 1 + }, + "9": { + "covered": 0, + "missed": 2, + "available": 2 + }, + "24": { + "covered": 0, + "missed": 2, + "available": 2 + } + } + } +] diff --git a/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-sourcefiles-1.txt b/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-sourcefiles-1.txt new file mode 100644 index 0000000000..bee95e0bca --- /dev/null +++ b/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-sourcefiles-1.txt @@ -0,0 +1,19 @@ +.dockerignore +.editorconfig +.gitattributes +.gitignore +.gitmodules +.idea/codeStyleSettings.xml +.idea/codeStyles/Project.xml +.idea/codeStyles/codeStyleConfig.xml +.idea/inspectionProfiles/Spotify.xml +.idea/inspectionProfiles/profiles_settings.xml +.mention-bot +.ownership-bot +.slack_mentioner +CONTRIBUTING.md +README.md +WORKSPACE +src/main/scala/com/example/Main.scala +src/main/scala/com/example/Other.scala +src/main/scala/com/example/utils/Util.scala diff --git a/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-sourcefiles-2.txt b/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-sourcefiles-2.txt new file mode 100644 index 0000000000..b8d13fc792 --- /dev/null +++ b/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-sourcefiles-2.txt @@ -0,0 +1,49 @@ +.dockerignore +.editorconfig +.gitattributes +.gitignore +.gitmodules +CONTRIBUTING.md +README.md +WORKSPACE +src/index.ts +src/utils.ts +src/controller/VideoContextPlayerCoordinator.ts +src/controller/VideoContextPlayerCoordinatorFactory.ts +src/controller/index.ts +src/controller-x/VideoContextPlayerCoordinatorX.ts +src/controller-x/VideoContextPlayerCoordinatorXObserver.ts +src/controller-x/index.ts +src/events/InternalPlaybackObserver.ts +src/events/PlaybackEventsObserver.ts +src/events/TimeObservable.ts +src/events/index.ts +src/player/BetamaxPlayer.ts +src/player/BetamaxPlayerFactory.ts +src/player/PlaybackSession.ts +src/player/PlaybackSessionFactory.ts +src/player/index.ts +src/player/player.ts +src/player/shaka.ts +src/plugins/SPManifestParser.ts +src/plugins/SpotifyVideoMediaExtension.ts +src/plugins/SpotifyVideoUrl.ts +src/plugins/TextParser.js +src/plugins/VttTextParser.js +src/plugins/WidevineDownloader.ts +src/plugins/index.ts +src/plugins/types.ts +src/tracking/DefaultEventsCalculator.ts +src/tracking/EventsTracker.ts +src/tracking/ProductStatesStreamingRulesProvider.ts +src/tracking/VideoPlaybackEvents.ts +src/tracking/index.ts +src/tracking/types.ts +src/tracking/utils.ts +src/tracking/endvideo/EndVideoEventsCalculator.ts +src/tracking/endvideo/EndVideoLoggerFactory.ts +src/tracking/endvideo/EndVideoLoggerTracker.ts +src/tracking/endvideo/EndVideoReasonEnd.ts +src/tracking/endvideo/EndVideoTracker.ts +src/tracking/session/PlaybackSessionTracker.ts +src/tracking/session/VideoPlaybackSessionLogger.ts diff --git a/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-sourcefiles-3.txt b/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-sourcefiles-3.txt new file mode 100644 index 0000000000..fb2d4096ed --- /dev/null +++ b/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-sourcefiles-3.txt @@ -0,0 +1,29 @@ +.dockerignore +.editorconfig +.gitattributes +.gitignore +.gitmodules +.idea/codeStyleSettings.xml +.idea/codeStyles/Project.xml +.idea/codeStyles/codeStyleConfig.xml +.idea/inspectionProfiles/Spotify.xml +.idea/inspectionProfiles/profiles_settings.xml +.mention-bot +.ownership-bot +.slack_mentioner +CONTRIBUTING.md +README.md +WORKSPACE +src/index.ts +src/atoms/BoxShadow/box-shadow.ts +src/atoms/BoxShadow/index.ts +src/atoms/Button/button.tsx +src/atoms/Button/index.ts +src/atoms/Container/components.ts +src/atoms/Container/container.tsx +src/atoms/Container/index.ts +src/atoms/DownArrow/down-arrow.tsx +src/atoms/DownArrow/index.ts +src/molecules/Hero/components.ts +src/molecules/Hero/hero.tsx +src/molecules/Hero/index.ts diff --git a/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-sourcefiles-4.txt b/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-sourcefiles-4.txt new file mode 100644 index 0000000000..4e0e3699af --- /dev/null +++ b/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-sourcefiles-4.txt @@ -0,0 +1,20 @@ +.dockerignore +.editorconfig +.gitattributes +.gitignore +.gitmodules +.idea/codeStyleSettings.xml +.idea/codeStyles/Project.xml +.idea/codeStyles/codeStyleConfig.xml +.idea/inspectionProfiles/Spotify.xml +.idea/inspectionProfiles/profiles_settings.xml +.mention-bot +.ownership-bot +.slack_mentioner +CONTRIBUTING.md +README.md +WORKSPACE +Sources/TDSOptions.m +Sources/TDSUserLock.m +Sources/TDSUserManager.m +Sources/TDSUtil.m diff --git a/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-sourcefiles-5.txt b/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-sourcefiles-5.txt new file mode 100644 index 0000000000..d117a9885a --- /dev/null +++ b/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-sourcefiles-5.txt @@ -0,0 +1,26 @@ +.editorconfig +.eslintignore +.eslintrc.js +.gitignore +.npmrc +.nvmrc +Dockerfile +README.md +babel.config.js +nodemon.json +package.json +prettier.config.js +service-info.yaml +src/graphql/__tests__/resolvers.test.ts +src/graphql/__tests__/transforms.test.ts +src/graphql/queries.ts +src/graphql/resolvers.ts +src/graphql/router.ts +src/graphql/schema.ts +src/graphql/transforms.ts +src/start.ts +src/sysmodel_component.ts +src/workers/primary-worker.js +src/workers/secondary-worker.js +tsconfig.json +yarn.lock diff --git a/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-testdata-1.xml b/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-testdata-1.xml new file mode 100644 index 0000000000..37e2c8c560 --- /dev/null +++ b/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-testdata-1.xml @@ -0,0 +1,479 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-testdata-2.xml b/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-testdata-2.xml new file mode 100644 index 0000000000..9e94385cbc --- /dev/null +++ b/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-testdata-2.xml @@ -0,0 +1,5721 @@ + + + + . + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-testdata-3.xml b/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-testdata-3.xml new file mode 100644 index 0000000000..176152791b --- /dev/null +++ b/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-testdata-3.xml @@ -0,0 +1,357 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-testdata-4.xml b/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-testdata-4.xml new file mode 100644 index 0000000000..36e36da3be --- /dev/null +++ b/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-testdata-4.xml @@ -0,0 +1,348 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-testdata-5.xml b/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-testdata-5.xml new file mode 100644 index 0000000000..5bc8e6e45d --- /dev/null +++ b/plugins/code-coverage-backend/src/service/__fixtures__/cobertura-testdata-5.xml @@ -0,0 +1,441 @@ + + + + . + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/code-coverage-backend/src/service/__fixtures__/jacoco-jsoncoverage-files-1.json b/plugins/code-coverage-backend/src/service/__fixtures__/jacoco-jsoncoverage-files-1.json new file mode 100644 index 0000000000..b37f019305 --- /dev/null +++ b/plugins/code-coverage-backend/src/service/__fixtures__/jacoco-jsoncoverage-files-1.json @@ -0,0 +1,319 @@ +[ + { + "filename": "src/main/java/com/example/Main.kt", + "lineHits": { + "28": 0, + "31": 0, + "36": 0, + "37": 0, + "38": 0, + "39": 0, + "40": 0, + "41": 0, + "42": 0, + "43": 0, + "44": 0, + "45": 0, + "46": 0, + "47": 0, + "48": 0, + "49": 0, + "50": 0, + "52": 0, + "53": 0, + "56": 0, + "57": 0, + "58": 0, + "59": 0, + "60": 0, + "62": 0, + "65": 0, + "66": 0, + "67": 0, + "69": 0, + "70": 0, + "71": 0, + "72": 0, + "74": 0, + "76": 0, + "77": 0, + "78": 0, + "79": 0, + "81": 0, + "82": 0, + "83": 0, + "84": 0, + "85": 0, + "86": 0, + "87": 0, + "88": 0, + "89": 0, + "92": 0, + "93": 0 + }, + "branchHits": {} + }, + { + "filename": "src/main/java/com/example/Parser.kt", + "lineHits": { + "5": 9, + "10": 7, + "12": 8, + "13": 2, + "14": 3, + "16": 28, + "17": 1, + "19": 4, + "20": 16, + "21": 18, + "22": 12, + "23": 5, + "24": 14, + "25": 22, + "26": 9, + "28": 7, + "29": 1, + "32": 2, + "34": 13, + "36": 0, + "37": 0, + "38": 0, + "39": 0, + "40": 0, + "43": 7, + "44": 0, + "46": 2, + "49": 7, + "50": 1, + "53": 5, + "54": 3, + "55": 18, + "56": 3, + "57": 1, + "58": 3, + "59": 3, + "60": 5, + "62": 1, + "64": 6, + "65": 2, + "69": 5, + "70": 14, + "71": 8, + "72": 20, + "73": 16, + "74": 22, + "75": 3, + "76": 2, + "77": 19, + "78": 1, + "79": 13, + "80": 5, + "81": 4, + "83": 4, + "84": 12, + "85": 10, + "86": 7, + "87": 9, + "94": 10, + "95": 5, + "96": 4, + "98": 9, + "99": 12, + "100": 10, + "101": 7, + "102": 4, + "103": 9, + "104": 10, + "105": 5, + "106": 10, + "107": 4, + "108": 3, + "109": 2, + "110": 5, + "111": 2, + "113": 1, + "114": 7, + "116": 3, + "117": 12, + "118": 26, + "119": 10, + "120": 7, + "122": 3, + "123": 9, + "124": 14, + "125": 2, + "126": 3 + }, + "branchHits": { + "12": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "16": { + "covered": 2, + "missed": 2, + "available": 4 + }, + "24": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "25": { + "covered": 6, + "missed": 2, + "available": 8 + }, + "38": { + "covered": 0, + "missed": 4, + "available": 4 + }, + "43": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "44": { + "covered": 0, + "missed": 2, + "available": 2 + }, + "55": { + "covered": 4, + "missed": 0, + "available": 4 + }, + "70": { + "covered": 1, + "missed": 3, + "available": 4 + }, + "73": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "75": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "77": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "79": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "85": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "94": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "100": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "104": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "108": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "119": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "124": { + "covered": 2, + "missed": 0, + "available": 2 + } + } + }, + { + "filename": "src/main/java/com/example/models/Thing.kt", + "lineHits": { + "9": 8, + "10": 3, + "11": 0, + "14": 8, + "15": 3, + "16": 1, + "19": 8, + "20": 3, + "21": 0, + "24": 3, + "25": 3, + "26": 1, + "29": 11, + "30": 3, + "31": 3, + "32": 3, + "35": 12, + "36": 12, + "37": 1, + "40": 5, + "41": 3, + "44": 3, + "45": 1, + "48": 12, + "49": 3, + "50": 3, + "51": 0 + }, + "branchHits": { + "14": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "15": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "24": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "25": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "35": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "36": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "44": { + "covered": 1, + "missed": 1, + "available": 2 + } + } + } +] diff --git a/plugins/code-coverage-backend/src/service/__fixtures__/jacoco-sourcefiles-1.txt b/plugins/code-coverage-backend/src/service/__fixtures__/jacoco-sourcefiles-1.txt new file mode 100644 index 0000000000..84e69b0b4b --- /dev/null +++ b/plugins/code-coverage-backend/src/service/__fixtures__/jacoco-sourcefiles-1.txt @@ -0,0 +1,7 @@ +pom.xml +src/main/java/com/example/Parser.kt +src/main/java/com/example/Main.kt +src/main/java/com/example/models/Thing.kt +src/main/resources/app.conf +src/test/java/com/example/ParserTest.kt +src/test/resources/logback-test.xml diff --git a/plugins/code-coverage-backend/src/service/__fixtures__/jacoco-testdata-1.xml b/plugins/code-coverage-backend/src/service/__fixtures__/jacoco-testdata-1.xml new file mode 100644 index 0000000000..d07dd81b50 --- /dev/null +++ b/plugins/code-coverage-backend/src/service/__fixtures__/jacoco-testdata-1.xml @@ -0,0 +1,282 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/code-coverage-backend/src/service/converter/Converter.ts b/plugins/code-coverage-backend/src/service/converter/Converter.ts new file mode 100644 index 0000000000..43d9855e61 --- /dev/null +++ b/plugins/code-coverage-backend/src/service/converter/Converter.ts @@ -0,0 +1,20 @@ +/* + * 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 { FileEntry } from '../types'; + +export interface Converter { + convert(xml: unknown, scmFiles: Array): Array; +} diff --git a/plugins/code-coverage-backend/src/service/converter/cobertura.test.ts b/plugins/code-coverage-backend/src/service/converter/cobertura.test.ts new file mode 100644 index 0000000000..72d4018f94 --- /dev/null +++ b/plugins/code-coverage-backend/src/service/converter/cobertura.test.ts @@ -0,0 +1,56 @@ +/* + * 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 { parseString } from 'xml2js'; +import fs from 'fs'; +import { Cobertura } from './cobertura'; +import { CoberturaXML } from './types'; +import { getVoidLogger } from '@backstage/backend-common'; + +/* eslint-disable no-restricted-syntax */ + +describe('convert cobertura', () => { + const converter = new Cobertura(getVoidLogger()); + [1, 2, 3, 4, 5].forEach(idx => { + let fixture: CoberturaXML; + parseString( + fs.readFileSync( + `${__dirname}/../__fixtures__/cobertura-testdata-${idx}.xml`, + ), + (_e, r) => { + fixture = r; + }, + ); + const expected = JSON.parse( + fs + .readFileSync( + `${__dirname}/../__fixtures__/cobertura-jsoncoverage-files-${idx}.json`, + ) + .toString(), + ); + const scmFiles = fs + .readFileSync( + `${__dirname}/../__fixtures__/cobertura-sourcefiles-${idx}.txt`, + ) + .toString() + .split('\n'); + + it(`${idx} converts a cobertura report`, () => { + const files = converter.convert(fixture, scmFiles); + + expect(files).toEqual(expected); + }); + }); +}); diff --git a/plugins/code-coverage-backend/src/service/converter/cobertura.ts b/plugins/code-coverage-backend/src/service/converter/cobertura.ts new file mode 100644 index 0000000000..02ac187387 --- /dev/null +++ b/plugins/code-coverage-backend/src/service/converter/cobertura.ts @@ -0,0 +1,126 @@ +/* + * 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 { BranchHit, FileEntry } from '../types'; +import { CoberturaXML, InnerClass, LineHit } from './types'; +import { Logger } from 'winston'; +import { Converter } from './Converter'; + +export class Cobertura implements Converter { + constructor(readonly logger: Logger) { + this.logger = logger; + } + + /** + * convert cobertura into shared json coverage format + * + * @param xml cobertura xml object + * @param scmFiles list of files that are commited to SCM + */ + convert(xml: CoberturaXML, scmFiles: string[]): FileEntry[] { + const ppc = xml.coverage.packages + ?.flatMap(p => p.package) + .filter(Boolean) + .flatMap(p => p.classes); + const pc = xml.coverage.package?.filter(Boolean).flatMap(p => p.classes); + + const classes = [ppc, pc] + .flat() + .filter(Boolean) + .flatMap(c => c.class) + .filter(Boolean); + const jscov: Array = []; + + classes.forEach(c => { + const packageAndFilename = c.$.filename; + const lines = this.extractLines(c); + const lineHits: Record = {}; + const branchHits: Record = {}; + + lines.forEach(l => { + if (!lineHits[l.number]) { + lineHits[l.number] = 0; + } + lineHits[l.number] += l.hits; + if (l.branch && l['condition-coverage']) { + const bh = this.parseBranch(l['condition-coverage']); + if (bh) { + branchHits[l.number] = bh; + } + } + }); + + const currentFile = scmFiles.find(f => f.endsWith(packageAndFilename)); + this.logger.debug(`matched ${packageAndFilename} to ${currentFile}`); + if ( + scmFiles.length === 0 || + (Object.keys(lineHits).length > 0 && currentFile) + ) { + jscov.push({ + filename: currentFile || packageAndFilename, + branchHits: branchHits, + lineHits: lineHits, + }); + } + }); + + return jscov; + } + + /** + * Parses branch coverage information from condition-coverage + * + * @param condition condition-coverage value from line coverage + */ + private parseBranch(condition: string): BranchHit | null { + const pattern = /[0-9\.]+\%\s\(([0-9]+)\/([0-9]+)\)/; + const match = condition.match(pattern); + if (!match) { + return null; + } + const covered = parseInt(match[1], 10); + const available = parseInt(match[2], 10); + return { + covered: covered, + missed: available - covered, + available: available, + }; + } + + /** + * Extract line hits from a class coverage entry + * + * @param clz class coverage information + */ + private extractLines(clz: InnerClass): Array { + const classLines = clz.lines.flatMap(l => l.line); + const methodLines = clz.methods + ?.flatMap(m => m.method) + .filter(Boolean) + .flatMap(m => m.lines) + .filter(Boolean) + .flatMap(l => l.line); + const lines = [classLines, methodLines].flat().filter(Boolean); + const lineHits = lines.map(l => { + return { + number: parseInt((l.$.number as unknown) as string, 10), + hits: parseInt((l.$.hits as unknown) as string, 10), + 'condition-coverage': l.$['condition-coverage'], + branch: l.$.branch, + }; + }); + return lineHits; + } +} diff --git a/plugins/code-coverage-backend/src/service/converter/index.ts b/plugins/code-coverage-backend/src/service/converter/index.ts new file mode 100644 index 0000000000..8ea7a41f65 --- /dev/null +++ b/plugins/code-coverage-backend/src/service/converter/index.ts @@ -0,0 +1,19 @@ +/* + * 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. + */ + +export * from './Converter'; +export { Cobertura } from './cobertura'; +export { Jacoco } from './jacoco'; diff --git a/plugins/code-coverage-backend/src/service/converter/jacoco.test.ts b/plugins/code-coverage-backend/src/service/converter/jacoco.test.ts new file mode 100644 index 0000000000..48202748da --- /dev/null +++ b/plugins/code-coverage-backend/src/service/converter/jacoco.test.ts @@ -0,0 +1,50 @@ +/* + * 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 { parseString } from 'xml2js'; +import fs from 'fs'; +import { Jacoco } from './jacoco'; +import { JacocoXML } from './types'; +import { getVoidLogger } from '@backstage/backend-common'; + +/* eslint-disable no-restricted-syntax */ + +describe('convert jacoco', () => { + const converter = new Jacoco(getVoidLogger()); + let fixture: JacocoXML; + parseString( + fs.readFileSync(`${__dirname}/../__fixtures__/jacoco-testdata-1.xml`), + (_e, r) => { + fixture = r; + }, + ); + const expected = JSON.parse( + fs + .readFileSync( + `${__dirname}/../__fixtures__/jacoco-jsoncoverage-files-1.json`, + ) + .toString(), + ); + const scmFiles = fs + .readFileSync(`${__dirname}/../__fixtures__/jacoco-sourcefiles-1.txt`) + .toString() + .split('\n'); + + it('converts a jacoco report', () => { + const files = converter.convert(fixture, scmFiles); + + expect(files.sort()).toEqual(expected.sort()); + }); +}); diff --git a/plugins/code-coverage-backend/src/service/converter/jacoco.ts b/plugins/code-coverage-backend/src/service/converter/jacoco.ts new file mode 100644 index 0000000000..4c554fec91 --- /dev/null +++ b/plugins/code-coverage-backend/src/service/converter/jacoco.ts @@ -0,0 +1,96 @@ +/* + * 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 { BranchHit, FileEntry } from '../types'; +import { JacocoSourceFile, JacocoXML } from './types'; +import { Logger } from 'winston'; +import { Converter } from './Converter'; + +type ParsedLine = { + number: number; + missed_instructions: number; + covered_instructions: number; + missed_branches: number; + covered_branches: number; +}; + +export class Jacoco implements Converter { + constructor(readonly logger: Logger) { + this.logger = logger; + } + + /** + * Converts jacoco into shared json coverage format + * + * @param xml jacoco xml object + * @param scmFiles list of files that are committed to SCM + */ + convert(xml: JacocoXML, scmFiles: Array): Array { + const jscov: Array = []; + + xml.report.package.forEach(r => { + const packageName = r.$.name; + r.sourcefile.forEach(sf => { + const fileName = sf.$.name; + const lines = this.extractLines(sf); + const lineHits: Record = {}; + const branchHits: Record = {}; + lines.forEach(l => { + if (!lineHits[l.number]) { + lineHits[l.number] = 0; + } + lineHits[l.number] += l.covered_instructions; + const ab = l.covered_branches + l.missed_branches; + if (ab > 0) { + branchHits[l.number] = { + covered: l.covered_branches, + missed: l.missed_branches, + available: ab, + }; + } + }); + + const packageAndFilename = `${packageName}/${fileName}`; + const currentFile = scmFiles.find(f => f.endsWith(packageAndFilename)); + this.logger.debug(`matched ${packageAndFilename} to ${currentFile}`); + if (Object.keys(lineHits).length > 0 && currentFile) { + jscov.push({ + filename: currentFile, + branchHits: branchHits, + lineHits: lineHits, + }); + } + }); + }); + + return jscov; + } + + private extractLines(sourcefile: JacocoSourceFile): ParsedLine[] { + const parsed: ParsedLine[] = []; + + sourcefile.line.forEach(l => { + parsed.push({ + number: parseInt(l.$.nr, 10), + missed_instructions: parseInt(l.$.mi, 10), + covered_instructions: parseInt(l.$.ci, 10), + missed_branches: parseInt(l.$.mb, 10), + covered_branches: parseInt(l.$.cb, 10), + }); + }); + + return parsed; + } +} diff --git a/plugins/code-coverage-backend/src/service/converter/types.ts b/plugins/code-coverage-backend/src/service/converter/types.ts new file mode 100644 index 0000000000..35e70faae4 --- /dev/null +++ b/plugins/code-coverage-backend/src/service/converter/types.ts @@ -0,0 +1,92 @@ +/* + * 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. + */ + +// *** Cobertura *** +export type CoberturaXML = { + coverage: Coverage; +}; + +export type Coverage = { + packages: Array; + package: Array; +}; + +export type Package = { + package: Array; +}; + +export type InnerPackage = { + classes: Array; +}; +export type Class = { + class: Array; +}; +export type InnerClass = { + $: { + filename: string; + }; + lines: Array; + methods: Array; +}; +export type Method = { + method: Array; +}; +export type InnerMethod = { + lines: Array; +}; +export type Line = { + line: Array; +}; + +export type InnerLine = { + $: LineHit; +}; + +export type LineHit = { + branch?: boolean; + 'condition-coverage'?: string; + number: number; + hits: number; +}; + +// *** Jacoco *** +export type JacocoXML = { + report: JacocoReport; +}; +export type JacocoReport = { + package: JacocoPackage[]; +}; +export type JacocoPackage = { + $: { + name: string; + }; + sourcefile: JacocoSourceFile[]; +}; +export type JacocoSourceFile = { + $: { + name: string; + }; + line: JacocoLine[]; +}; +export type JacocoLine = { + $: { + nr: string; + mi: string; + ci: string; + mb: string; + cb: string; + }; +}; diff --git a/plugins/code-coverage-backend/src/service/router.test.ts b/plugins/code-coverage-backend/src/service/router.test.ts new file mode 100644 index 0000000000..e18cd75c2f --- /dev/null +++ b/plugins/code-coverage-backend/src/service/router.test.ts @@ -0,0 +1,79 @@ +/* + * Copyright 2020 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 express from 'express'; +import request from 'supertest'; +import { + getVoidLogger, + PluginDatabaseManager, + PluginEndpointDiscovery, + SingleConnectionDatabaseManager, + UrlReaders, +} from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { createRouter } from './router'; + +function createDatabase(): PluginDatabaseManager { + return SingleConnectionDatabaseManager.fromConfig( + new ConfigReader({ + backend: { + database: { + client: 'sqlite3', + connection: ':memory:', + }, + }, + }), + ).forPlugin('code-coverage'); +} + +const testDiscovery: jest.Mocked = { + getBaseUrl: jest + .fn() + .mockResolvedValue('http://localhost:7000/api/code-coverage'), + getExternalBaseUrl: jest.fn(), +}; +const mockUrlReader = UrlReaders.default({ + logger: getVoidLogger(), + config: new ConfigReader({}), +}); + +describe('createRouter', () => { + let app: express.Express; + + beforeAll(async () => { + const router = await createRouter({ + config: new ConfigReader({}), + database: createDatabase(), + discovery: testDiscovery, + urlReader: mockUrlReader, + logger: getVoidLogger(), + }); + app = express().use(router); + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('GET /health', () => { + it('returns ok', async () => { + const response = await request(app).get('/health'); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ status: 'ok' }); + }); + }); +}); diff --git a/plugins/code-coverage-backend/src/service/router.ts b/plugins/code-coverage-backend/src/service/router.ts new file mode 100644 index 0000000000..78aeab313c --- /dev/null +++ b/plugins/code-coverage-backend/src/service/router.ts @@ -0,0 +1,234 @@ +/* + * 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 express from 'express'; +import Router from 'express-promise-router'; +import { Logger } from 'winston'; +import xmlparser from 'express-xml-bodyparser'; +import { CatalogClient } from '@backstage/catalog-client'; +import { + errorHandler, + PluginDatabaseManager, + PluginEndpointDiscovery, + UrlReader, +} from '@backstage/backend-common'; +import { InputError, NotFoundError } from '@backstage/errors'; +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 { + EntityRef, + getEntitySourceLocation, + parseEntityName, +} from '@backstage/catalog-model'; + +export interface RouterOptions { + config: Config; + discovery: PluginEndpointDiscovery; + database: PluginDatabaseManager; + urlReader: UrlReader; + logger: Logger; +} + +export interface CodeCoverageApi { + name: string; +} + +export const makeRouter = async ( + options: RouterOptions, +): Promise => { + const { config, logger, discovery, database, urlReader } = options; + + const codeCoverageDatabase = await CodeCoverageDatabase.create( + await database.getClient(), + ); + const codecovUrl = await discovery.getExternalBaseUrl('code-coverage'); + const catalogApi = new CatalogClient({ discoveryApi: discovery }); + const scm = ScmIntegrations.fromConfig(config); + + const router = Router(); + router.use(xmlparser()); + router.use(express.json()); + + const utils = new CoverageUtils(scm, urlReader); + + router.get('/health', async (_req, res) => { + res.status(200).json({ status: 'ok' }); + }); + + /** + * /report?entity=component:default/mycomponent + */ + router.get('/report', async (req, res) => { + const { entity } = req.query; + const entityName = parseEntityName(entity as EntityRef); + const entityLookup = await catalogApi.getEntityByName(entityName); + if (!entityLookup) { + throw new NotFoundError(`No entity found matching ${entity}`); + } + const stored = await codeCoverageDatabase.getCodeCoverage(entity as string); + + const aggregate = aggregateCoverage(stored); + + res.status(200).json({ + ...stored, + aggregate: { + line: aggregate.line, + branch: aggregate.branch, + }, + }); + }); + + /** + * /history?entity=component:default/mycomponent + */ + router.get('/history', async (req, res) => { + const { entity } = req.query; + const entityName = parseEntityName(entity as EntityRef); + const entityLookup = await catalogApi.getEntityByName(entityName); + if (!entityLookup) { + throw new NotFoundError(`No entity found matching ${entity}`); + } + const { limit } = req.query; + const history = await codeCoverageDatabase.getHistory( + entity as string, + parseInt(limit?.toString() || '10', 10), + ); + + res.status(200).json(history); + }); + + /** + * /file-content?entity=component:default/mycomponent&path=src/some-file.go + */ + router.get('/file-content', async (req, res) => { + const { entity, path } = req.query; + const entityName = parseEntityName(entity as EntityRef); + const entityLookup = await catalogApi.getEntityByName(entityName); + if (!entityLookup) { + throw new NotFoundError(`No entity found matching ${entity}`); + } + + if (!path) { + throw new InputError('Need path query parameter'); + } + + const sourceLocation = getEntitySourceLocation(entityLookup); + + if (!sourceLocation) { + throw new InputError( + `No "backstage.io/source-location" annotation on entity ${entity}`, + ); + } + + const vcs = scm.byUrl(sourceLocation.target); + if (!vcs) { + throw new InputError(`Unable to determine SCM from ${sourceLocation}`); + } + + const scmTree = await urlReader.readTree(sourceLocation.target); + const scmFile = (await scmTree.files()).find(f => f.path === path); + if (!scmFile) { + res.status(404).json({ + message: "Couldn't find file in SCM", + file: path, + scm: vcs.title, + }); + return; + } + const content = await scmFile?.content(); + if (!content) { + res.status(400).json({ + message: "Couldn't process content of file in SCM", + file: path, + scm: vcs.title, + }); + return; + } + + const data = content.toString(); + res.status(200).contentType('text/plain').send(data); + }); + + /** + * /report?entity=component:default/mycomponent&coverageType=cobertura + */ + router.post('/report', async (req, res) => { + const { entity, coverageType } = req.query; + const entityName = parseEntityName(entity as EntityRef); + const entityLookup = await catalogApi.getEntityByName(entityName); + if (!entityLookup) { + throw new NotFoundError(`No entity found matching ${entity}`); + } + + let converter: Converter; + if (!coverageType) { + throw new InputError('Need coverageType query parameter'); + } else if (coverageType === 'jacoco') { + converter = new Jacoco(logger); + } else if (coverageType === 'cobertura') { + converter = new Cobertura(logger); + } else { + throw new InputError(`Unsupported coverage type '${coverageType}`); + } + + const { + sourceLocation, + vcs, + scmFiles, + body, + } = await utils.processCoveragePayload(entityLookup, req); + + const files = converter.convert(body, scmFiles); + if (!files || files.length === 0) { + throw new InputError(`Unable to parse body as ${coverageType}`); + } + + const coverage = await utils.buildCoverage( + entityLookup, + sourceLocation, + vcs, + files, + ); + await codeCoverageDatabase.insertCodeCoverage(coverage); + + res.status(201).json({ + links: [ + { + rel: 'coverage', + href: `${codecovUrl}/report?entity=${entity}`, + }, + ], + }); + }); + + router.use(errorHandler()); + return router; +}; + +export async function createRouter( + options: RouterOptions, +): Promise { + const logger = options.logger; + + logger.info('Initializing Code Coverage backend'); + + return makeRouter(options); +} diff --git a/plugins/code-coverage-backend/src/service/standaloneServer.ts b/plugins/code-coverage-backend/src/service/standaloneServer.ts new file mode 100644 index 0000000000..2fb9627936 --- /dev/null +++ b/plugins/code-coverage-backend/src/service/standaloneServer.ts @@ -0,0 +1,74 @@ +/* + * 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 { + createServiceBuilder, + loadBackendConfig, + SingleHostDiscovery, + UrlReaders, + useHotMemoize, +} from '@backstage/backend-common'; +import { Server } from 'http'; +import knexFactory from 'knex'; +import { Logger } from 'winston'; +import { createRouter } from './router'; + +export interface ServerOptions { + port: number; + enableCors: boolean; + logger: Logger; +} + +export async function startStandaloneServer( + options: ServerOptions, +): Promise { + const logger = options.logger.child({ service: 'code-coverage-backend' }); + const config = await loadBackendConfig({ logger, argv: process.argv }); + + 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: async () => db }, + config, + discovery: SingleHostDiscovery.fromConfig(config), + urlReader: UrlReaders.default({ logger, config }), + logger, + }); + + const service = createServiceBuilder(module) + .enableCors({ origin: 'http://localhost:3000' }) + .addRouter('/code-coverage', router); + + return await service.start().catch(err => { + logger.error(err); + process.exit(1); + }); +} + +module.hot?.accept(); diff --git a/plugins/code-coverage-backend/src/service/types.ts b/plugins/code-coverage-backend/src/service/types.ts new file mode 100644 index 0000000000..34fcf57968 --- /dev/null +++ b/plugins/code-coverage-backend/src/service/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/plugins/code-coverage-backend/src/setupTests.ts b/plugins/code-coverage-backend/src/setupTests.ts new file mode 100644 index 0000000000..ba33cf996b --- /dev/null +++ b/plugins/code-coverage-backend/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 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. + */ + +export {}; diff --git a/plugins/code-coverage/.eslintrc.js b/plugins/code-coverage/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/code-coverage/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/code-coverage/README.md b/plugins/code-coverage/README.md new file mode 100644 index 0000000000..8903668507 --- /dev/null +++ b/plugins/code-coverage/README.md @@ -0,0 +1,23 @@ +# code-coverage + +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 on entities for which coverage ingestion has been enabled. + +```yaml +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 +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/dev/index.tsx b/plugins/code-coverage/dev/index.tsx new file mode 100644 index 0000000000..69ff4bcda2 --- /dev/null +++ b/plugins/code-coverage/dev/index.tsx @@ -0,0 +1,26 @@ +/* + * 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 React from 'react'; +import { createDevApp } from '@backstage/dev-utils'; +import { codeCoveragePlugin, EntityCodeCoverageContent } from '../src/plugin'; + +createDevApp() + .registerPlugin(codeCoveragePlugin) + .addPage({ + element: , + title: 'Root Page', + }) + .render(); diff --git a/plugins/code-coverage/package.json b/plugins/code-coverage/package.json new file mode 100644 index 0000000000..1d02da3dea --- /dev/null +++ b/plugins/code-coverage/package.json @@ -0,0 +1,58 @@ +{ + "name": "@backstage/plugin-code-coverage", + "version": "0.1.1", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "build": "backstage-cli plugin:build", + "start": "backstage-cli plugin:serve", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "diff": "backstage-cli plugin:diff", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/catalog-model": "^0.7.7", + "@backstage/config": "^0.1.4", + "@backstage/core": "^0.7.5", + "@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", + "@material-ui/styles": "^4.11.0", + "@material-ui/lab": "4.0.0-alpha.45", + "highlight.js": "^10.6.0", + "react": "^16.13.1", + "react-dom": "^16.13.1", + "react-router": "6.0.0-beta.0", + "react-router-dom": "6.0.0-beta.0", + "react-use": "^15.3.3", + "recharts": "^1.8.5" + }, + "devDependencies": { + "@backstage/cli": "^0.6.8", + "@backstage/dev-utils": "^0.1.13", + "@backstage/test-utils": "^0.1.10", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^11.2.5", + "@testing-library/user-event": "^12.0.7", + "@types/highlightjs": "^10.1.0", + "@types/jest": "^26.0.7", + "@types/node": "^14.14.32", + "@types/recharts": "^1.8.15", + "cross-fetch": "^3.0.6", + "msw": "^0.21.2" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/code-coverage/src/api.ts b/plugins/code-coverage/src/api.ts new file mode 100644 index 0000000000..6331f8020f --- /dev/null +++ b/plugins/code-coverage/src/api.ts @@ -0,0 +1,93 @@ +/* + * Copyright 2020 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, stringifyEntityRef } from '@backstage/catalog-model'; +import { createApiRef, DiscoveryApi } from '@backstage/core'; +import { ResponseError } from '@backstage/errors'; +import { JsonCodeCoverage, JsonCoverageHistory } from './types'; + +export type CodeCoverageApi = { + discovery: DiscoveryApi; + getCoverageForEntity: (entity: EntityName) => Promise; + getFileContentFromEntity: ( + entity: EntityName, + filePath: string, + ) => Promise; + getCoverageHistoryForEntity: ( + entity: EntityName, + limit?: number, + ) => Promise; +}; + +export const codeCoverageApiRef = createApiRef({ + id: 'plugin.code-coverage.service', + description: 'Used by the code coverage plugin to make requests', +}); + +export class CodeCoverageRestApi implements CodeCoverageApi { + url: string = ''; + + constructor(public discovery: DiscoveryApi) {} + + private async fetch( + path: string, + init?: RequestInit, + ): Promise { + if (!this.url) { + this.url = await this.discovery.getBaseUrl('code-coverage'); + } + const resp = await fetch(`${this.url}${path}`, init); + if (!resp.ok) { + throw await ResponseError.fromResponse(resp); + } + if (resp.headers.get('content-type')?.includes('application/json')) { + return await resp.json(); + } + return await resp.text(); + } + + async getCoverageForEntity( + entityName: EntityName, + ): Promise { + const entity = encodeURIComponent(stringifyEntityRef(entityName)); + return (await this.fetch( + `/report?entity=${entity}`, + )) as JsonCodeCoverage; + } + + async getFileContentFromEntity( + entityName: EntityName, + filePath: string, + ): Promise { + const entity = encodeURIComponent(stringifyEntityRef(entityName)); + return await this.fetch( + `/file-content?entity=${entity}&path=${encodeURI(filePath)}`, + ); + } + + async getCoverageHistoryForEntity( + entityName: EntityName, + limit?: number, + ): Promise { + const entity = encodeURIComponent(stringifyEntityRef(entityName)); + const hasValidLimit = limit && limit > 0; + return (await this.fetch( + `/history?entity=${entity}${ + hasValidLimit ? `&limit=${encodeURIComponent(String(limit))}` : '' + }`, + )) as JsonCoverageHistory; + } +} diff --git a/plugins/code-coverage/src/components/CodeCoveragePage/CodeCoveragePage.tsx b/plugins/code-coverage/src/components/CodeCoveragePage/CodeCoveragePage.tsx new file mode 100644 index 0000000000..47811b93cd --- /dev/null +++ b/plugins/code-coverage/src/components/CodeCoveragePage/CodeCoveragePage.tsx @@ -0,0 +1,31 @@ +/* + * Copyright 2020 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 React from 'react'; +import { Content, ContentHeader, Page } from '@backstage/core'; +import { CoverageHistoryChart } from '../CoverageHistoryChart'; +import { FileExplorer } from '../FileExplorer'; + +export const CodeCoveragePage = () => { + return ( + + + + + + + + ); +}; diff --git a/plugins/code-coverage/src/components/CodeCoveragePage/index.ts b/plugins/code-coverage/src/components/CodeCoveragePage/index.ts new file mode 100644 index 0000000000..c5479c0098 --- /dev/null +++ b/plugins/code-coverage/src/components/CodeCoveragePage/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2020 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. + */ +export { CodeCoveragePage } from './CodeCoveragePage'; diff --git a/plugins/code-coverage/src/components/CoverageHistoryChart/CoverageHistoryChart.tsx b/plugins/code-coverage/src/components/CoverageHistoryChart/CoverageHistoryChart.tsx new file mode 100644 index 0000000000..7bb4648f6f --- /dev/null +++ b/plugins/code-coverage/src/components/CoverageHistoryChart/CoverageHistoryChart.tsx @@ -0,0 +1,166 @@ +/* + * Copyright 2020 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 { Progress, ResponseErrorPanel, useApi } from '@backstage/core'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { BackstageTheme } from '@backstage/theme'; +import { + Box, + Card, + CardContent, + CardHeader, + 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 { + CartesianGrid, + Legend, + Line, + LineChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; +import { codeCoverageApiRef } from '../../api'; + +type Coverage = 'line' | 'branch'; + +const useStyles = makeStyles(theme => ({ + trendDown: { + color: theme.palette.status.warning, + }, + trendUp: { + color: theme.palette.status.ok, + }, +})); + +const getTrendIcon = (trend: number, classes: ClassNameMap) => { + switch (true) { + case trend > 0: + return ; + case trend < 0: + return ; + case trend === 0: + default: + return ; + } +}; + +export const CoverageHistoryChart = () => { + const { entity } = useEntity(); + const codeCoverageApi = useApi(codeCoverageApiRef); + const { + loading: loadingHistory, + error: errorHistory, + value: valueHistory, + } = useAsync( + async () => + await codeCoverageApi.getCoverageHistoryForEntity({ + kind: entity.kind, + namespace: entity.metadata.namespace || 'default', + name: entity.metadata.name, + }), + ); + const classes = useStyles(); + + if (loadingHistory) { + return ; + } + if (errorHistory) { + return ; + } else if (!valueHistory) { + return No history found.; + } + + if (!valueHistory.history.length) { + return ( + + + No coverage history found + + ); + } + + const oldestCoverage = valueHistory.history[0]; + const [latestCoverage] = valueHistory.history.slice(-1); + + const getTrendForCoverage = (type: Coverage) => { + if (!oldestCoverage[type].percentage) { + return 0; + } + return ( + ((latestCoverage[type].percentage - oldestCoverage[type].percentage) / + oldestCoverage[type].percentage) * + 100 + ); + }; + + const lineTrend = getTrendForCoverage('line'); + const branchTrend = getTrendForCoverage('branch'); + + return ( + + + + + + {getTrendIcon(lineTrend, classes)} + + Current line: {latestCoverage.line.percentage}%
( + {Math.floor(lineTrend)}% change over {valueHistory.history.length}{' '} + builds) +
+
+ + {getTrendIcon(branchTrend, classes)} + + Current branch: {latestCoverage.branch.percentage}%
( + {Math.floor(branchTrend)}% change over{' '} + {valueHistory.history.length} builds) +
+
+
+ + + + + + + + + + + + +
+
+ ); +}; diff --git a/plugins/code-coverage/src/components/CoverageHistoryChart/index.ts b/plugins/code-coverage/src/components/CoverageHistoryChart/index.ts new file mode 100644 index 0000000000..b9dd72e34b --- /dev/null +++ b/plugins/code-coverage/src/components/CoverageHistoryChart/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 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. + */ + +export { CoverageHistoryChart } from './CoverageHistoryChart'; diff --git a/plugins/code-coverage/src/components/FileExplorer/CodeRow.tsx b/plugins/code-coverage/src/components/FileExplorer/CodeRow.tsx new file mode 100644 index 0000000000..bd982b0f6b --- /dev/null +++ b/plugins/code-coverage/src/components/FileExplorer/CodeRow.tsx @@ -0,0 +1,100 @@ +/* + * 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 React from 'react'; +import { makeStyles } from '@material-ui/core'; + +const useStyles = makeStyles(theme => ({ + lineNumberCell: { + color: `${theme.palette.grey[500]}`, + fontSize: '90%', + borderRight: `1px solid ${theme.palette.grey[500]}`, + paddingRight: theme.spacing(1), + textAlign: 'right', + }, + hitCountCell: { + width: '50px', + borderRight: `1px solid ${theme.palette.grey[500]}`, + textAlign: 'center', + color: 'white', // need to enforce this color since it needs to stand out against colored background + paddingLeft: theme.spacing(1), + paddingRight: theme.spacing(1), + }, + countRoundedRectangle: { + borderRadius: '45px', + fontSize: '90%', + padding: '1px 3px 1px 3px', + width: '50px', + }, + hitCountRoundedRectangle: { + backgroundColor: `${theme.palette.success.main}`, + }, + notHitCountRoundedRectangle: { + backgroundColor: `${theme.palette.error.main}`, + }, + codeLine: { + paddingLeft: `${theme.spacing(1)}`, + whiteSpace: 'pre', + fontSize: '90%', + }, + hitCodeLine: { + backgroundColor: `${theme.palette.success.main}`, + }, + notHitCodeLine: { + backgroundColor: `${theme.palette.error.main}`, + }, +})); + +type CodeRowProps = { + lineNumber: number; + lineContent: string; + lineHits?: number | null; +}; + +export const CodeRow = ({ + lineNumber, + lineContent, + lineHits = null, +}: CodeRowProps) => { + const classes = useStyles(); + const hitCountRoundedRectangleClass = [classes.countRoundedRectangle]; + const lineContentClass = [classes.codeLine]; + + let hitRoundedRectangle = null; + if (lineHits !== null) { + if (lineHits > 0) { + hitCountRoundedRectangleClass.push(classes.hitCountRoundedRectangle); + lineContentClass.push(classes.hitCodeLine); + } else { + hitCountRoundedRectangleClass.push(classes.notHitCountRoundedRectangle); + lineContentClass.push(classes.notHitCodeLine); + } + hitRoundedRectangle = ( +
{lineHits}
+ ); + } + + return ( + + {lineNumber} + {hitRoundedRectangle} + + + ); +}; diff --git a/plugins/code-coverage/src/components/FileExplorer/FileContent.tsx b/plugins/code-coverage/src/components/FileExplorer/FileContent.tsx new file mode 100644 index 0000000000..b43874123d --- /dev/null +++ b/plugins/code-coverage/src/components/FileExplorer/FileContent.tsx @@ -0,0 +1,132 @@ +/* + * Copyright 2020 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 { 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 { FileEntry } from '../../types'; +import { CodeRow } from './CodeRow'; +import { highlightLines } from './Highlighter'; + +type Props = { + filename: string; + coverage: FileEntry; +}; + +const useStyles = makeStyles(theme => ({ + paper: { + margin: 'auto', + top: '2em', + width: '80%', + border: '2px solid #000', + boxShadow: theme.shadows[5], + padding: theme.spacing(2, 4, 3), + overflow: 'scroll', + }, + coverageFileViewTable: { + borderSpacing: '0px', + width: '80%', + marginTop: theme.spacing(2), + }, +})); + +type FormattedLinesProps = { + highlightedLines: string[]; + lineHits: Record; +}; +const FormattedLines = ({ + highlightedLines, + lineHits, +}: FormattedLinesProps) => { + return ( + <> + {highlightedLines.map((lineContent, idx) => { + const line = idx + 1; + return ( + + ); + })} + + ); +}; + +export const FileContent = ({ filename, coverage }: Props) => { + const { entity } = useEntity(); + const codeCoverageApi = useApi(codeCoverageApiRef); + const { loading, error, value } = useAsync( + async () => + await codeCoverageApi.getFileContentFromEntity( + { + kind: entity.kind, + namespace: entity.metadata.namespace || 'default', + name: entity.metadata.name, + }, + filename, + ), + [entity], + ); + + const classes = useStyles(); + + if (loading) { + return ; + } + if (error) { + return ; + } + if (!value) { + return ( + + Unable to retrieve file content for {filename} + + ); + } + + const [language] = filename.split('.').slice(-1); + const highlightedLines = highlightLines(language, value.split('\n')); + + // List of formatted nodes containing highlighted code + // lineHits array where lineHits[i] is number of hits for line i + 1 + const lineHits = Object.entries(coverage.lineHits).reduce( + (acc: Record, next: [string, number]) => { + acc[next[0]] = next[1]; + return acc; + }, + {}, + ); + + return ( + + + + + +
+
+ ); +}; diff --git a/plugins/code-coverage/src/components/FileExplorer/FileExplorer.tsx b/plugins/code-coverage/src/components/FileExplorer/FileExplorer.tsx new file mode 100644 index 0000000000..b49b78c5dc --- /dev/null +++ b/plugins/code-coverage/src/components/FileExplorer/FileExplorer.tsx @@ -0,0 +1,285 @@ +/* + * Copyright 2020 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 { + Progress, + ResponseErrorPanel, + Table, + TableColumn, + useApi, +} from '@backstage/core'; +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 { Alert } from '@material-ui/lab'; +import React, { Fragment, useEffect, useState } from 'react'; +import { useAsync } from 'react-use'; +import { codeCoverageApiRef } from '../../api'; +import { FileEntry } from '../../types'; +import { FileContent } from './FileContent'; + +type FileStructureObject = Record; + +type CoverageTableRow = { + filename?: string; + files: CoverageTableRow[]; + coverage: number; + missing: number; + tracked: number; + path: string; + tableData?: { id: number }; +}; + +const buildFileStructure = (row: CoverageTableRow) => { + const dataGroupedByPath: FileStructureObject = row.files.reduce( + (acc: FileStructureObject, cur: CoverageTableRow) => { + let path = cur.filename; + if (row.path) { + path = path?.split(`${row.path}/`)[1]; + } + const pathArray = path?.split('/'); + + if (!pathArray) { + return acc; + } + if (!acc.hasOwnProperty(pathArray[0])) { + acc[pathArray[0]] = []; + } + acc[pathArray[0]].push(cur); + return acc; + }, + {}, + ); + + row.files = Object.keys(dataGroupedByPath).map(pathGroup => { + return buildFileStructure({ + path: pathGroup, + files: dataGroupedByPath.hasOwnProperty('files') + ? dataGroupedByPath.files + : dataGroupedByPath[pathGroup], + coverage: + dataGroupedByPath[pathGroup].reduce( + (acc: number, cur: CoverageTableRow) => acc + cur.coverage, + 0, + ) / dataGroupedByPath[pathGroup].length, + missing: dataGroupedByPath[pathGroup].reduce( + (acc: number, cur: CoverageTableRow) => acc + cur.missing, + 0, + ), + tracked: dataGroupedByPath[pathGroup].reduce( + (acc: number, cur: CoverageTableRow) => acc + cur.tracked, + 0, + ), + }); + }); + return row; +}; + +const formatInitialData = (value: any) => { + return buildFileStructure({ + path: '', + coverage: value.aggregate.line.percentage, + missing: value.aggregate.line.missed, + tracked: value.aggregate.line.available, + files: value.files.map((fc: FileEntry) => { + return { + path: '', + filename: fc.filename, + coverage: Math.floor( + (Object.values(fc.lineHits).filter((hits: number) => hits > 0) + .length / + Object.values(fc.lineHits).length) * + 100, + ), + missing: Object.values(fc.lineHits).filter(hits => !hits).length, + tracked: Object.values(fc.lineHits).length, + }; + }), + }); +}; + +export const FileExplorer = () => { + const { entity } = useEntity(); + const [curData, setCurData] = useState(); + const [tableData, setTableData] = useState(); + const [curPath, setCurPath] = useState(''); + const [modalOpen, setModalOpen] = useState(false); + const [curFile, setCurFile] = useState(''); + const codeCoverageApi = useApi(codeCoverageApiRef); + const { loading, error, value } = useAsync( + async () => + await codeCoverageApi.getCoverageForEntity({ + kind: entity.kind, + namespace: entity.metadata.namespace || 'default', + name: entity.metadata.name, + }), + ); + + useEffect(() => { + if (!value) return; + const data = formatInitialData(value); + setCurData(data); + if (data.files) setTableData(data.files); + }, [value]); + + if (loading) { + return ; + } else if (error) { + return ; + } + if (!value) { + return ( + No code coverage found for ${entity} + ); + } + + const moveDownIntoPath = (path: string) => { + const nextPathData = tableData!.find( + (d: CoverageTableRow) => d.path === path, + ); + if (nextPathData && nextPathData.files) { + setTableData(nextPathData.files); + } + }; + + const moveUpIntoPath = (path: string) => { + const pathArray = path.split('/').filter(p => p.length); + let data = curData?.files; + pathArray.forEach(p => { + data = data?.find(d => d.path === p)?.files; + }); + setCurPath(path); + setTableData(data); + }; + + const columns: TableColumn[] = [ + { + title: 'Path', + type: 'string', + field: 'path', + render: (row: CoverageTableRow) => { + if (row.files?.length) { + return ( +
{ + setCurPath(`${curPath}/${row.path}`); + moveDownIntoPath(row.path); + }} + onClick={() => { + setCurPath(`${curPath}/${row.path}`); + moveDownIntoPath(row.path); + }} + > + {row.path} +
+ ); + } + + return ( + + {row.path} + + { + setCurFile(`${curPath.slice(1)}/${row.path}`); + setModalOpen(true); + }} + /> + + + ); + }, + }, + { + title: 'Coverage', + type: 'numeric', + field: 'coverage', + render: (row: CoverageTableRow) => `${row.coverage}%`, + }, + { + title: 'Missing lines', + type: 'numeric', + field: 'missing', + }, + { + title: 'Tracked lines', + type: 'numeric', + field: 'tracked', + }, + ]; + + const pathArray = curPath.split('/'); + const lastPathElementIndex = pathArray.length - 1; + const fileCoverage = value.files.find((f: FileEntry) => + f.filename.endsWith(curFile), + ); + + if (!fileCoverage) { + return null; + } + + return ( + + + + + {pathArray.map((pathElement, idx) => ( + +
moveUpIntoPath(pathElement)} + onClick={() => moveUpIntoPath(pathElement)} + > + {pathElement || 'root'} +
+
{'\u00A0/\u00A0'}
+
+ ))} +
+ No files found} + data={tableData || []} + columns={columns} + /> + event.stopPropagation()} + onClose={() => setModalOpen(false)} + style={{ overflow: 'scroll' }} + > + + + + + ); +}; diff --git a/plugins/code-coverage/src/components/FileExplorer/Highlighter.ts b/plugins/code-coverage/src/components/FileExplorer/Highlighter.ts new file mode 100644 index 0000000000..39f56200b6 --- /dev/null +++ b/plugins/code-coverage/src/components/FileExplorer/Highlighter.ts @@ -0,0 +1,53 @@ +/* + * 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 'highlight.js/styles/atom-one-dark.css'; +import highlight from 'highlight.js'; + +/* + * Given a file extension, repo name, and array of code lines, return a Promise resolving + * to an array of formatted lines with html/css formatting. + * + * @param fileExtension The extension of the source file + * @param lines The source code lines + * + * @returns Promise of formatted lines + * + * @see http://highlightjs.readthedocs.io/en/latest/api.html#highlight-name-value-ignore-illegals-continuation + */ +export const highlightLines = (fileExtension: string, lines: Array) => { + const formattedLines: Array = []; + let state: CompiledMode | Language | undefined; + let fileformat = fileExtension; + if (fileExtension === 'm') { + fileformat = 'objectivec'; + } + if (fileExtension === 'tsx') { + fileformat = 'typescript'; + } + if (fileExtension === 'jsx') { + fileformat = 'javascript'; + } + if (fileExtension === 'kt') { + fileformat = 'kotlin'; + } + + lines.forEach(line => { + const result = highlight.highlight(fileformat, line, true, state); + state = result.top; + formattedLines.push(result.value); + }); + return formattedLines; +}; diff --git a/plugins/code-coverage/src/components/FileExplorer/index.ts b/plugins/code-coverage/src/components/FileExplorer/index.ts new file mode 100644 index 0000000000..88da4059bd --- /dev/null +++ b/plugins/code-coverage/src/components/FileExplorer/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 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. + */ + +export { FileExplorer } from './FileExplorer'; diff --git a/plugins/code-coverage/src/components/Router.tsx b/plugins/code-coverage/src/components/Router.tsx new file mode 100644 index 0000000000..12be6ef5a8 --- /dev/null +++ b/plugins/code-coverage/src/components/Router.tsx @@ -0,0 +1,34 @@ +/* + * Copyright 2020 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 React from 'react'; +import { Entity } from '@backstage/catalog-model'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { MissingAnnotationEmptyState } from '@backstage/core'; +import { CodeCoveragePage } from './CodeCoveragePage'; + +export const isCodeCoverageAvailable = (entity: Entity) => + Boolean(entity.metadata.annotations?.['backstage.io/code-coverage']); + +export const Router = () => { + const { entity } = useEntity(); + + if (!isCodeCoverageAvailable(entity)) { + return ( + + ); + } + return ; +}; diff --git a/plugins/code-coverage/src/index.ts b/plugins/code-coverage/src/index.ts new file mode 100644 index 0000000000..7542f963b5 --- /dev/null +++ b/plugins/code-coverage/src/index.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ +export { codeCoveragePlugin, EntityCodeCoverageContent } from './plugin'; +export { + Router, + isCodeCoverageAvailable, + isCodeCoverageAvailable as isPluginApplicableToEntity, +} from './components/Router'; diff --git a/plugins/code-coverage/src/plugin.test.ts b/plugins/code-coverage/src/plugin.test.ts new file mode 100644 index 0000000000..9a34c66965 --- /dev/null +++ b/plugins/code-coverage/src/plugin.test.ts @@ -0,0 +1,22 @@ +/* + * 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 { codeCoveragePlugin } from './plugin'; + +describe('code-coverage', () => { + it('should export plugin', () => { + expect(codeCoveragePlugin).toBeDefined(); + }); +}); diff --git a/plugins/code-coverage/src/plugin.ts b/plugins/code-coverage/src/plugin.ts new file mode 100644 index 0000000000..68e8291bc4 --- /dev/null +++ b/plugins/code-coverage/src/plugin.ts @@ -0,0 +1,45 @@ +/* + * 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 { + createApiFactory, + createPlugin, + createRoutableExtension, + discoveryApiRef, +} from '@backstage/core'; +import { codeCoverageApiRef, CodeCoverageRestApi } from './api'; + +import { rootRouteRef } from './routes'; + +export const codeCoveragePlugin = createPlugin({ + id: 'code-coverage', + routes: { + root: rootRouteRef, + }, + apis: [ + createApiFactory({ + api: codeCoverageApiRef, + deps: { discoveryApi: discoveryApiRef }, + factory: ({ discoveryApi }) => new CodeCoverageRestApi(discoveryApi), + }), + ], +}); + +export const EntityCodeCoverageContent = codeCoveragePlugin.provide( + createRoutableExtension({ + component: () => import('./components/Router').then(m => m.Router), + mountPoint: rootRouteRef, + }), +); diff --git a/plugins/code-coverage/src/routes.ts b/plugins/code-coverage/src/routes.ts new file mode 100644 index 0000000000..9eaf7859f5 --- /dev/null +++ b/plugins/code-coverage/src/routes.ts @@ -0,0 +1,20 @@ +/* + * 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 { createRouteRef } from '@backstage/core'; + +export const rootRouteRef = createRouteRef({ + title: 'code-coverage', +}); diff --git a/plugins/code-coverage/src/setupTests.ts b/plugins/code-coverage/src/setupTests.ts new file mode 100644 index 0000000000..0cec5b395d --- /dev/null +++ b/plugins/code-coverage/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * 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 '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; 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/plugins/fossa/README.md b/plugins/fossa/README.md index 722d8d2cf3..049a1c25cb 100644 --- a/plugins/fossa/README.md +++ b/plugins/fossa/README.md @@ -75,3 +75,51 @@ spec: owner: CNCF lifecycle: experimental ``` + +## Other Components + +### FOSSA Overview Page + +The plugin provides an optional page that can be used to check the license compliance of all components. + +![FOSSA Overview](./docs/fossa-overview.png) + +Add it to your Backstage application: + +1. Install the FOSSA Plugin (see [Getting Started](#getting-started)). + +2. Register the page: + +```tsx +// packages/app/src/App.tsx + +import { FossaPage } from '@backstage/plugin-fossa'; + +// ... + +const routes = ( + + // ... + } /> + +); +``` + +3. (Optional) Add a Sidebar Icon: + +```tsx +// packages/app/src/components/Root/Root.tsx + +// ... + +export const Root = ({ children }: PropsWithChildren<{}>) => ( + + + // ... + + // ... + + {children} + +); +``` diff --git a/plugins/fossa/dev/index.tsx b/plugins/fossa/dev/index.tsx index 06c71c48b7..563c63faf5 100644 --- a/plugins/fossa/dev/index.tsx +++ b/plugins/fossa/dev/index.tsx @@ -14,15 +14,20 @@ * limitations under the License. */ -import { Entity } from '@backstage/catalog-model'; +import { Entity, RELATION_OWNED_BY } from '@backstage/catalog-model'; import { Content, Header, Page } from '@backstage/core'; import { createDevApp } from '@backstage/dev-utils'; -import { EntityProvider } from '@backstage/plugin-catalog-react'; +import { + CatalogApi, + catalogApiRef, + EntityProvider, +} from '@backstage/plugin-catalog-react'; import { Grid } from '@material-ui/core'; import React from 'react'; -import { EntityFossaCard } from '../src'; -import { FossaApi, fossaApiRef } from '../src/api'; -import { FOSSA_PROJECT_NAME_ANNOTATION } from '../src/components/useProjectName'; +import { EntityFossaCard, fossaPlugin } from '../src'; +import { FindingSummary, FossaApi, fossaApiRef } from '../src/api'; +import { FossaPage } from '../src/components/FossaPage'; +import { FOSSA_PROJECT_NAME_ANNOTATION } from '../src/components/getProjectName'; const entity = (name?: string) => ({ @@ -34,9 +39,16 @@ const entity = (name?: string) => }, name: name, }, + relations: [ + { + type: RELATION_OWNED_BY, + target: { kind: 'Group', namespace: 'default', name }, + }, + ], } as Entity); createDevApp() + .registerPlugin(fossaPlugin) .registerApi({ api: fossaApiRef, deps: {}, @@ -81,8 +93,63 @@ createDevApp() return undefined; } }, + getFindingSummaries: async () => { + await new Promise(r => setTimeout(r, 1000)); + + return new Map([ + [ + 'zero-deps', + { + timestamp: '2000-01-01T00:00:00Z', + projectUrl: '', + projectDefaultBranch: 'master', + issueCount: 0, + dependencyCount: 0, + }, + ], + [ + 'issues', + { + timestamp: '2001-01-01T00:00:00Z', + projectUrl: '', + projectDefaultBranch: 'develop', + issueCount: 10, + dependencyCount: 15, + }, + ], + [ + 'no-issues', + { + timestamp: '2002-01-01T00:00:00Z', + projectUrl: '', + projectDefaultBranch: 'master', + issueCount: 0, + dependencyCount: 5, + }, + ], + ]); + }, } as FossaApi), }) + .registerApi({ + api: catalogApiRef, + deps: {}, + factory: () => + (({ + getEntities: async () => { + await new Promise(r => setTimeout(r, 1000)); + + return { + items: [ + entity('no-fossa'), + entity('zero-deps'), + entity('issues'), + entity('no-issues'), + ], + }; + }, + } as Partial) as any), + }) .addPage({ title: 'Entity Content', element: ( @@ -130,4 +197,8 @@ createDevApp() ), }) + .addPage({ + title: 'Catalog Overview', + element: , + }) .render(); diff --git a/plugins/fossa/docs/fossa-overview.png b/plugins/fossa/docs/fossa-overview.png new file mode 100644 index 0000000000..3e9482ab55 Binary files /dev/null and b/plugins/fossa/docs/fossa-overview.png differ diff --git a/plugins/fossa/package.json b/plugins/fossa/package.json index fd94c6b906..2f656c476a 100644 --- a/plugins/fossa/package.json +++ b/plugins/fossa/package.json @@ -33,12 +33,15 @@ "dependencies": { "@backstage/catalog-model": "^0.7.3", "@backstage/core": "^0.7.5", + "@backstage/errors": "^0.1.1", "@backstage/plugin-catalog-react": "^0.1.1", "@backstage/theme": "^0.2.5", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", "cross-fetch": "^3.0.6", + "luxon": "^1.26.0", + "p-limit": "^3.0.2", "react": "^16.13.1", "react-dom": "^16.13.1", "react-use": "^15.3.3" diff --git a/plugins/fossa/src/api/FossaApi.ts b/plugins/fossa/src/api/FossaApi.ts index 84406e1b1a..15e8b4473e 100644 --- a/plugins/fossa/src/api/FossaApi.ts +++ b/plugins/fossa/src/api/FossaApi.ts @@ -30,5 +30,19 @@ export const fossaApiRef = createApiRef({ }); export type FossaApi = { + /** + * Get the finding summary for a list of projects + * + * @param projectTitles a list of project titles in FOSSA + */ + getFindingSummaries( + projectTitles: Array, + ): Promise>; + + /** + * Get the finding summary of a single project. + * + * @param projectTitle the project title in FOSSA + */ getFindingSummary(projectTitle: string): Promise; }; diff --git a/plugins/fossa/src/api/FossaClient.test.ts b/plugins/fossa/src/api/FossaClient.test.ts index 820210e2b6..29e5b50fcd 100644 --- a/plugins/fossa/src/api/FossaClient.test.ts +++ b/plugins/fossa/src/api/FossaClient.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { UrlPatternDiscovery, IdentityApi } from '@backstage/core'; +import { IdentityApi, UrlPatternDiscovery } from '@backstage/core'; import { msw } from '@backstage/test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; @@ -52,138 +52,330 @@ describe('FossaClient', () => { }); }); - it('should report finding summary', async () => { - server.use( - rest.get(`${mockBaseUrl}/fossa/projects`, (req, res, ctx) => { - expect(req.url.searchParams.toString()).toBe( - 'count=1&sort=title+&title=our-service&organizationId=8736', - ); - return res( - ctx.json([ + describe('getFindingSummary', () => { + it('should report finding summary', async () => { + server.use( + rest.get(`${mockBaseUrl}/fossa/projects`, (req, res, ctx) => { + const expectedQuery = + 'count=1000&page=0&sort=title%2B&organizationId=8736&title=our-service'; + if (req.url.searchParams.toString() !== expectedQuery) { + return res( + ctx.status(500), + ctx.body( + `${req.url.searchParams.toString()} !== ${expectedQuery}`, + ), + ); + } + + return res( + ctx.json([ + { + locator: 'custom+8736/our-service', + title: 'our-service', + default_branch: 'develop', + revisions: [ + { + updatedAt: '2020-01-01T00:00:00Z', + dependency_count: 160, + unresolved_licensing_issue_count: 5, + unresolved_issue_count: 100, + }, + ], + }, + ]), + ); + }), + ); + + const summary = await client.getFindingSummary('our-service'); + + expect(summary).toEqual({ + timestamp: '2020-01-01T00:00:00Z', + issueCount: 5, + dependencyCount: 160, + projectDefaultBranch: 'develop', + projectUrl: + 'https://app.fossa.com/projects/custom%2B8736%2Four-service', + } as FindingSummary); + }); + + it('should report finding summary without licensing_issue_count', async () => { + server.use( + rest.get(`${mockBaseUrl}/fossa/projects`, (_req, res, ctx) => { + return res( + ctx.json([ + { + locator: 'custom+8736/our-service', + title: 'our-service', + default_branch: 'refs/master', + revisions: [ + { + updatedAt: '2020-01-01T00:00:00Z', + dependency_count: 160, + unresolved_issue_count: 100, + }, + ], + }, + ]), + ); + }), + ); + + const summary = await client.getFindingSummary('our-service'); + + expect(summary).toEqual({ + timestamp: '2020-01-01T00:00:00Z', + issueCount: 100, + dependencyCount: 160, + projectDefaultBranch: 'refs/master', + projectUrl: + 'https://app.fossa.com/projects/custom%2B8736%2Four-service', + } as FindingSummary); + }); + + it('should handle empty result', async () => { + server.use( + rest.get(`${mockBaseUrl}/fossa/projects`, (_req, res, ctx) => { + return res(ctx.json([])); + }), + ); + + const summary = await client.getFindingSummary('our-service'); + + expect(summary).toBeUndefined(); + }); + + it('should ignore result with invalid title', async () => { + server.use( + rest.get(`${mockBaseUrl}/fossa/projects`, (_req, res, ctx) => { + return res(ctx.json([{ title: 'our-service-2' }])); + }), + ); + + const summary = await client.getFindingSummary('our-service'); + + expect(summary).toBeUndefined(); + }); + + it('should skip organizationId', async () => { + client = new FossaClient({ discoveryApi, identityApi }); + + server.use( + rest.get(`${mockBaseUrl}/fossa/projects`, (req, res, ctx) => { + const expectedQuery = + 'count=1000&page=0&sort=title%2B&title=our-service'; + if (req.url.searchParams.toString() !== expectedQuery) { + return res( + ctx.status(500), + ctx.body( + `${req.url.searchParams.toString()} !== ${expectedQuery}`, + ), + ); + } + + return res(ctx.json([])); + }), + ); + + const summary = await client.getFindingSummary('our-service'); + + expect(summary).toBeUndefined(); + }); + + it('should handle 404 status', async () => { + server.use( + rest.get(`${mockBaseUrl}/fossa/projects`, (_req, res, ctx) => { + return res(ctx.status(404)); + }), + ); + + await expect(client.getFindingSummary('our-service')).rejects.toThrow(); + }); + }); + + describe('getFindingSummaries', () => { + it('should report finding summary', async () => { + server.use( + rest.get(`${mockBaseUrl}/fossa/projects`, (req, res, ctx) => { + const expectedQuery = + 'count=1000&page=0&sort=title%2B&organizationId=8736'; + if (req.url.searchParams.toString() !== expectedQuery) { + return res( + ctx.status(500), + ctx.body( + `${req.url.searchParams.toString()} !== ${expectedQuery}`, + ), + ); + } + + return res( + ctx.json([ + { + locator: 'custom+8736/our-service', + title: 'our-service', + default_branch: 'develop', + revisions: [ + { + updatedAt: '2020-01-01T00:00:00Z', + dependency_count: 160, + unresolved_licensing_issue_count: 5, + unresolved_issue_count: 100, + }, + ], + }, + { + locator: 'custom+8736/our-service-2', + title: 'our-service-2', + default_branch: 'develop', + revisions: [ + { + updatedAt: '2020-01-01T00:00:00Z', + dependency_count: 160, + unresolved_licensing_issue_count: 5, + unresolved_issue_count: 100, + }, + ], + }, + { + locator: 'custom+8736/our-service-3', + title: 'our-service-3', + default_branch: 'develop', + revisions: [ + { + updatedAt: '2020-01-01T00:00:00Z', + dependency_count: 160, + unresolved_licensing_issue_count: 5, + unresolved_issue_count: 100, + }, + ], + }, + ]), + ); + }), + ); + + const summary = await client.getFindingSummaries([ + 'our-service', + 'our-service-2', + ]); + + expect(summary).toEqual( + new Map([ + [ + 'our-service', { - locator: 'custom+8736/our-service', - title: 'our-service', - default_branch: 'develop', - revisions: [ - { - updatedAt: '2020-01-01T00:00:00Z', - dependency_count: 160, - unresolved_licensing_issue_count: 5, - unresolved_issue_count: 100, - }, - ], + timestamp: '2020-01-01T00:00:00Z', + issueCount: 5, + dependencyCount: 160, + projectDefaultBranch: 'develop', + projectUrl: + 'https://app.fossa.com/projects/custom%2B8736%2Four-service', }, - ]), - ); - }), - ); - - const summary = await client.getFindingSummary('our-service'); - - expect(summary).toEqual({ - timestamp: '2020-01-01T00:00:00Z', - issueCount: 5, - dependencyCount: 160, - projectDefaultBranch: 'develop', - projectUrl: 'https://app.fossa.com/projects/custom%2B8736%2Four-service', - } as FindingSummary); - }); - - it('should report finding summary without licensing_issue_count', async () => { - server.use( - rest.get(`${mockBaseUrl}/fossa/projects`, (req, res, ctx) => { - expect(req.url.searchParams.toString()).toBe( - 'count=1&sort=title+&title=our-service&organizationId=8736', - ); - return res( - ctx.json([ + ], + [ + 'our-service-2', { - locator: 'custom+8736/our-service', - title: 'our-service', - default_branch: 'refs/master', - revisions: [ - { - updatedAt: '2020-01-01T00:00:00Z', - dependency_count: 160, - unresolved_issue_count: 100, - }, - ], + timestamp: '2020-01-01T00:00:00Z', + issueCount: 5, + dependencyCount: 160, + projectDefaultBranch: 'develop', + projectUrl: + 'https://app.fossa.com/projects/custom%2B8736%2Four-service-2', }, - ]), - ); - }), - ); + ], + ]), + ); + }); - const summary = await client.getFindingSummary('our-service'); + it('should handle multiple pages', async () => { + server.use( + rest.get(`${mockBaseUrl}/fossa/projects`, (req, res, ctx) => { + const page = req.url.searchParams.get('page'); - expect(summary).toEqual({ - timestamp: '2020-01-01T00:00:00Z', - issueCount: 100, - dependencyCount: 160, - projectDefaultBranch: 'refs/master', - projectUrl: 'https://app.fossa.com/projects/custom%2B8736%2Four-service', - } as FindingSummary); - }); + if (page === '0') { + return res( + ctx.json( + [...Array(1000)].map(() => ({ + locator: 'custom+8736/our-service', + title: 'our-service-2', + default_branch: 'develop', + revisions: [ + { + updatedAt: '2020-01-01T00:00:00Z', + dependency_count: 160, + unresolved_licensing_issue_count: 5, + unresolved_issue_count: 100, + }, + ], + })), + ), + ); + } - it('should handle empty result', async () => { - server.use( - rest.get(`${mockBaseUrl}/fossa/projects`, (req, res, ctx) => { - expect(req.url.searchParams.toString()).toBe( - 'count=1&sort=title+&title=our-service&organizationId=8736', - ); - return res(ctx.json([])); - }), - ); + return res( + ctx.json([ + { + locator: 'custom+8736/our-service', + title: 'our-service', + default_branch: 'develop', + revisions: [ + { + updatedAt: '2020-01-01T00:00:00Z', + dependency_count: 160, + unresolved_licensing_issue_count: 5, + unresolved_issue_count: 100, + }, + ], + }, + ]), + ); + }), + ); - const summary = await client.getFindingSummary('our-service'); + const summary = await client.getFindingSummaries(['our-service']); - expect(summary).toBeUndefined(); - }); + expect(summary).toEqual( + new Map([ + [ + 'our-service', + { + timestamp: '2020-01-01T00:00:00Z', + issueCount: 5, + dependencyCount: 160, + projectDefaultBranch: 'develop', + projectUrl: + 'https://app.fossa.com/projects/custom%2B8736%2Four-service', + }, + ], + ]), + ); + }); - it('should ignore result with invalid title', async () => { - server.use( - rest.get(`${mockBaseUrl}/fossa/projects`, (req, res, ctx) => { - expect(req.url.searchParams.toString()).toBe( - 'count=1&sort=title+&title=our-service&organizationId=8736', - ); - return res(ctx.json([{ title: 'our-service-2' }])); - }), - ); + it('should handle empty result', async () => { + server.use( + rest.get(`${mockBaseUrl}/fossa/projects`, (_req, res, ctx) => { + return res(ctx.json([])); + }), + ); - const summary = await client.getFindingSummary('our-service'); + const summary = await client.getFindingSummaries([ + 'our-service', + 'our-service-2', + ]); - expect(summary).toBeUndefined(); - }); + expect(summary).toEqual(new Map()); + }); - it('should skip organizationId', async () => { - client = new FossaClient({ discoveryApi, identityApi }); + it('should handle 404 status', async () => { + server.use( + rest.get(`${mockBaseUrl}/fossa/projects`, (_req, res, ctx) => { + return res(ctx.status(404)); + }), + ); - server.use( - rest.get(`${mockBaseUrl}/fossa/projects`, (req, res, ctx) => { - expect(req.url.searchParams.toString()).toBe( - 'count=1&sort=title+&title=our-service', - ); - return res(ctx.status(404)); - }), - ); - - const summary = await client.getFindingSummary('our-service'); - - expect(summary).toBeUndefined(); - }); - - it('should handle 404 status', async () => { - server.use( - rest.get(`${mockBaseUrl}/fossa/projects`, (req, res, ctx) => { - expect(req.url.searchParams.toString()).toBe( - 'count=1&sort=title+&title=our-service&organizationId=8736', - ); - return res(ctx.status(404)); - }), - ); - - const summary = await client.getFindingSummary('our-service'); - - expect(summary).toBeUndefined(); + await expect( + client.getFindingSummaries(['our-service', 'our-service-2']), + ).rejects.toThrow(); + }); }); }); diff --git a/plugins/fossa/src/api/FossaClient.ts b/plugins/fossa/src/api/FossaClient.ts index 5693895901..e8f3ecb7cd 100644 --- a/plugins/fossa/src/api/FossaClient.ts +++ b/plugins/fossa/src/api/FossaClient.ts @@ -15,13 +15,28 @@ */ import { DiscoveryApi, IdentityApi } from '@backstage/core'; +import { ResponseError } from '@backstage/errors'; import fetch from 'cross-fetch'; +import pLimit from 'p-limit'; import { FindingSummary, FossaApi } from './FossaApi'; +type FossaProjectsResponse = { + title: string; + default_branch: string; + locator: string; + revisions: Array<{ + updatedAt: string; + unresolved_licensing_issue_count: number; + unresolved_issue_count: number; + dependency_count: number; + }>; +}; + export class FossaClient implements FossaApi { discoveryApi: DiscoveryApi; identityApi: IdentityApi; organizationId?: string; + private readonly limit = pLimit(5); constructor({ discoveryApi, @@ -37,42 +52,94 @@ export class FossaClient implements FossaApi { this.organizationId = organizationId; } - private async callApi(path: string): Promise { + private async callApi( + path: string, + query: Record, + ): Promise { const apiUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/fossa`; const idToken = await this.identityApi.getIdToken(); - const response = await fetch(`${apiUrl}/${path}`, { - headers: idToken ? { Authorization: `Bearer ${idToken}` } : {}, - }); - if (response.status === 200) { - return await response.json(); + const response = await fetch( + `${apiUrl}/${path}?${new URLSearchParams(query).toString()}`, + { + headers: idToken ? { Authorization: `Bearer ${idToken}` } : {}, + }, + ); + + if (!response.ok) { + throw await ResponseError.fromResponse(response); } - return undefined; + + return (await response.json()) as T; + } + + async *getProject( + projectTitles: Set, + ): AsyncIterable<{ title: string; summary: FindingSummary }> { + const pageSize = 1000; + + for (let page = 0; ; page++) { + const projects = await this.limit(() => + this.callApi('projects', { + count: pageSize, + page, + sort: 'title+', + ...(this.organizationId && { + organizationId: this.organizationId, + }), + ...(projectTitles.size === 1 && { + title: projectTitles.values().next().value, + }), + }), + ); + + for (const project of projects) { + if (projectTitles.has(project.title) && project.revisions.length > 0) { + const revision = project.revisions[0]; + yield { + title: project.title, + summary: { + timestamp: revision.updatedAt, + issueCount: + revision.unresolved_licensing_issue_count || + revision.unresolved_issue_count, + dependencyCount: revision.dependency_count, + projectDefaultBranch: project.default_branch, + projectUrl: `https://app.fossa.com/projects/${encodeURIComponent( + project.locator, + )}`, + }, + }; + } + } + + if (projects.length < pageSize) { + break; + } + } + } + + async getFindingSummaries( + projectTitles: string[], + ): Promise> { + const map = new Map(); + + if (projectTitles.length === 0) { + return map; + } + + for await (const { title, summary } of this.getProject( + new Set(projectTitles), + )) { + map.set(title, summary); + } + + return map; } async getFindingSummary( projectTitle: string, ): Promise { - const project = await this.callApi( - `projects?count=1&sort=title+&title=${projectTitle}${ - this.organizationId ? `&organizationId=${this.organizationId}` : '' - }`, - ); - - if (!project || project.length === 0 || project[0].title !== projectTitle) { - return undefined; - } - - const revision = project[0].revisions[0]; - return { - timestamp: revision.updatedAt, - issueCount: - revision.unresolved_licensing_issue_count || - revision.unresolved_issue_count, - dependencyCount: revision.dependency_count, - projectDefaultBranch: project[0].default_branch, - projectUrl: `https://app.fossa.com/projects/${encodeURIComponent( - project[0].locator, - )}`, - }; + const summaries = await this.getFindingSummaries([projectTitle]); + return summaries.get(projectTitle); } } diff --git a/plugins/fossa/src/components/FossaCard/FossaCard.test.tsx b/plugins/fossa/src/components/FossaCard/FossaCard.test.tsx new file mode 100644 index 0000000000..0761c85da5 --- /dev/null +++ b/plugins/fossa/src/components/FossaCard/FossaCard.test.tsx @@ -0,0 +1,186 @@ +/* + * 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 { Entity } from '@backstage/catalog-model'; +import { ApiProvider, ApiRegistry } from '@backstage/core'; +import { EntityProvider } from '@backstage/plugin-catalog-react'; +import { renderInTestApp } from '@backstage/test-utils'; +import React from 'react'; +import { FossaApi, fossaApiRef } from '../../api'; +import { FossaCard } from './FossaCard'; + +describe('', () => { + const fossaApi: jest.Mocked = { + getFindingSummary: jest.fn(), + getFindingSummaries: jest.fn(), + }; + let Wrapper: React.ComponentType; + + beforeEach(() => { + const apis = ApiRegistry.with(fossaApiRef, fossaApi); + + Wrapper = ({ children }: { children?: React.ReactNode }) => ( + {children} + ); + }); + + afterEach(() => jest.resetAllMocks()); + + it('shows missing annotation', async () => { + const entity: Entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'my-name', + }, + }; + + const { getByText } = await renderInTestApp( + + + + + , + ); + + expect(getByText(/Missing Annotation/i)).toBeInTheDocument(); + expect(getByText('fossa.io/project-name')).toBeInTheDocument(); + }); + + it('shows error', async () => { + const entity: Entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'my-name', + annotations: { + 'fossa.io/project-name': 'my-name', + }, + }, + }; + + fossaApi.getFindingSummary.mockRejectedValue(new Error('My Error')); + + const { getByText } = await renderInTestApp( + + + + + , + ); + + expect(getByText(/Warning: My Error/i)).toBeInTheDocument(); + }); + + it('shows empty', async () => { + const entity: Entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'my-name', + annotations: { + 'fossa.io/project-name': 'my-name', + }, + }, + }; + + fossaApi.getFindingSummary.mockResolvedValue(undefined); + + const { getByText } = await renderInTestApp( + + + + + , + ); + + expect( + getByText(/There is no Fossa project with title 'my-name'./i), + ).toBeInTheDocument(); + }); + + it('shows fossa issues', async () => { + const entity: Entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'my-name', + annotations: { + 'fossa.io/project-name': 'my-name', + }, + }, + }; + + fossaApi.getFindingSummary.mockResolvedValue({ + timestamp: '2000-01-01T00:00:00Z', + projectDefaultBranch: 'branch/default-branch', + projectUrl: 'http://…', + issueCount: 0, + dependencyCount: 10, + }); + + const { getByText } = await renderInTestApp( + + + + + , + ); + + expect(getByText(/Number of issues/i)).toBeInTheDocument(); + expect( + getByText( + (_, node) => + node?.textContent === + 'Based on 10 Dependencies on branch branch/default-branch.', + ), + ).toBeInTheDocument(); + }); + + it('warns about zero dependencies', async () => { + const entity: Entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'my-name', + annotations: { + 'fossa.io/project-name': 'my-name', + }, + }, + }; + + fossaApi.getFindingSummary.mockResolvedValue({ + timestamp: '2000-01-01T00:00:00Z', + projectDefaultBranch: 'branch/default-branch', + projectUrl: 'http://…', + issueCount: 0, + dependencyCount: 0, + }); + + const { getByText } = await renderInTestApp( + + + + + , + ); + + expect(getByText(/No Dependencies/i)).toBeInTheDocument(); + expect( + getByText(/Please check your FOSSA project settings/i), + ).toBeInTheDocument(); + }); +}); diff --git a/plugins/fossa/src/components/FossaCard/FossaCard.tsx b/plugins/fossa/src/components/FossaCard/FossaCard.tsx index 4cc27f5d03..034ba57dfc 100644 --- a/plugins/fossa/src/components/FossaCard/FossaCard.tsx +++ b/plugins/fossa/src/components/FossaCard/FossaCard.tsx @@ -17,20 +17,23 @@ import { EmptyState, InfoCard, + InfoCardVariants, MissingAnnotationEmptyState, Progress, + ResponseErrorPanel, useApi, } from '@backstage/core'; import { useEntity } from '@backstage/plugin-catalog-react'; import { Grid, Tooltip } from '@material-ui/core'; import { makeStyles } from '@material-ui/core/styles'; -import React from 'react'; +import { DateTime } from 'luxon'; +import React, { PropsWithChildren } from 'react'; import { useAsync } from 'react-use'; import { fossaApiRef } from '../../api'; import { FOSSA_PROJECT_NAME_ANNOTATION, - useProjectName, -} from '../useProjectName'; + getProjectName, +} from '../getProjectName'; const useStyles = makeStyles(theme => ({ numberError: { @@ -65,109 +68,134 @@ const useStyles = makeStyles(theme => ({ }, })); -export const FossaCard = () => { +const Card = ({ + children, + disabled, + projectUrl, + variant = 'gridItem', +}: PropsWithChildren<{ + disabled?: boolean; + projectUrl?: string; + variant?: InfoCardVariants; +}>) => { + const classes = useStyles(); + + return ( + + {children} + + ); +}; +export const FossaCard = ({ variant }: { variant?: InfoCardVariants }) => { const { entity } = useEntity(); const fossaApi = useApi(fossaApiRef); - const projectTitle = useProjectName(entity); - - const { value, loading } = useAsync( + const projectTitle = getProjectName(entity); + const { value, loading, error } = useAsync( async () => projectTitle ? fossaApi.getFindingSummary(projectTitle) : undefined, [fossaApi, projectTitle], ); - const deepLink = value - ? { - title: 'View more', - link: value.projectUrl, - } - : undefined; - const classes = useStyles(); + if (error) { + return ( + + + + ); + } + + if (loading) { + return ( + + + + ); + } + + if (!projectTitle) { + return ( + + + + ); + } + + if (!value) { + return ( + + + + ); + } + return ( - <> - + - {loading && } - - {!loading && !projectTitle && ( - - )} - - {!loading && projectTitle && !value && ( - - )} - - {value && ( - +

0 || value.dependencyCount === 0 + ? classes.numberError + : classes.numberSuccess + } > - -

0 || value.dependencyCount === 0 - ? classes.numberError - : classes.numberSuccess - } - > - {value.issueCount} -

- {value.dependencyCount > 0 && ( -

Number of issues

- )} - {value.dependencyCount === 0 && ( -

- No Dependencies. -
- Please check your FOSSA project settings. -

- )} -
+ {value.issueCount} +

+ {value.dependencyCount > 0 && ( +

Number of issues

+ )} + {value.dependencyCount === 0 && ( +

+ No Dependencies. +
+ Please check your FOSSA project settings. +

+ )} +
- - Last analyzed on{' '} - {new Date(value.timestamp).toLocaleString('en-US', { - timeZone: 'UTC', - day: 'numeric', - month: 'short', - year: 'numeric', - hour: '2-digit', - minute: '2-digit', - hour12: false, - })} - - - Based on {value.dependencyCount} Dependencies on branch{' '} - - - {value.projectDefaultBranch} - - - . - - - )} -
- + + Last analyzed on{' '} + {DateTime.fromISO(value.timestamp).toLocaleString( + DateTime.DATETIME_MED, + )} + + + Based on {value.dependencyCount} Dependencies on branch{' '} + + {value.projectDefaultBranch} + + . + + + ); }; diff --git a/plugins/fossa/src/components/FossaPage/FossaPage.test.tsx b/plugins/fossa/src/components/FossaPage/FossaPage.test.tsx new file mode 100644 index 0000000000..efab6ab091 --- /dev/null +++ b/plugins/fossa/src/components/FossaPage/FossaPage.test.tsx @@ -0,0 +1,134 @@ +/* + * 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 { Entity } from '@backstage/catalog-model'; +import { ApiProvider, ApiRegistry } from '@backstage/core'; +import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; +import { renderInTestApp } from '@backstage/test-utils'; +import React from 'react'; +import { FossaApi, fossaApiRef } from '../../api'; +import { FossaPage } from './FossaPage'; + +describe('', () => { + const catalogApi: jest.Mocked = { + addLocation: jest.fn(), + getEntities: jest.fn(), + getEntityByName: jest.fn(), + getLocationByEntity: jest.fn(), + getLocationById: jest.fn(), + getOriginLocationByEntity: jest.fn(), + removeEntityByUid: jest.fn(), + removeLocationById: jest.fn(), + }; + const fossaApi: jest.Mocked = { + getFindingSummary: jest.fn(), + getFindingSummaries: jest.fn(), + }; + let Wrapper: React.ComponentType; + + beforeEach(() => { + const apis = ApiRegistry.with(fossaApiRef, fossaApi).with( + catalogApiRef, + catalogApi, + ); + + Wrapper = ({ children }: { children?: React.ReactNode }) => ( + {children} + ); + }); + + afterEach(() => jest.resetAllMocks()); + + it('shows fossa issues', async () => { + const entity0: Entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'my-name-0', + annotations: { + 'fossa.io/project-name': 'my-name-0', + }, + }, + }; + const entity1: Entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'my-name-1', + annotations: { + 'fossa.io/project-name': 'my-name-1', + }, + }, + }; + const entity2: Entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'my-name-2', + annotations: { + 'fossa.io/project-name': 'my-name-2', + }, + }, + }; + const entity3: Entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'my-name-3', + }, + }; + + catalogApi.getEntities.mockResolvedValue({ + items: [entity0, entity1, entity2, entity3], + }); + + fossaApi.getFindingSummaries.mockResolvedValue( + new Map([ + [ + 'my-name-1', + { + timestamp: '2000-01-01T00:00:00Z', + projectDefaultBranch: 'branch/default-branch', + projectUrl: 'http://…', + issueCount: 0, + dependencyCount: 0, + }, + ], + [ + 'my-name-2', + { + timestamp: '2000-01-01T00:00:00Z', + projectDefaultBranch: 'branch/default-branch', + projectUrl: 'http://…', + issueCount: 10, + dependencyCount: 10, + }, + ], + ]), + ); + + const { getByText, getAllByText } = await renderInTestApp( + + + , + ); + + expect(getAllByText(/Not configured/i)).toHaveLength(2); + expect(getByText(/No dependencies/i)).toBeInTheDocument(); + expect(getByText(/0 Issues/i)).toBeInTheDocument(); + expect(getByText(/10 Issues/i)).toBeInTheDocument(); + }); +}); diff --git a/plugins/fossa/src/components/FossaPage/FossaPage.tsx b/plugins/fossa/src/components/FossaPage/FossaPage.tsx new file mode 100644 index 0000000000..4a5c1b9bff --- /dev/null +++ b/plugins/fossa/src/components/FossaPage/FossaPage.tsx @@ -0,0 +1,244 @@ +/* + * 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 { + Entity, + EntityName, + RELATION_OWNED_BY, +} from '@backstage/catalog-model'; +import { + Content, + Header, + Link, + Page, + StatusError, + StatusOK, + StatusWarning, + Table, + TableColumn, + TableFilter, + useApi, +} from '@backstage/core'; +import { + catalogApiRef, + EntityRefLink, + EntityRefLinks, + formatEntityRefTitle, + getEntityRelations, +} from '@backstage/plugin-catalog-react'; +import { Tooltip } from '@material-ui/core'; +import { Skeleton } from '@material-ui/lab'; +import { DateTime } from 'luxon'; +import * as React from 'react'; +import { useMemo } from 'react'; +import { useAsync } from 'react-use'; +import { FindingSummary, fossaApiRef } from '../../api'; +import { getProjectName } from '../getProjectName'; + +type FossaRow = { + entity: Entity; + resolved: { + name: string; + ownedByRelationsTitle?: string; + ownedByRelations: EntityName[]; + loading: boolean; + details?: FindingSummary; + }; +}; + +const columns: TableColumn[] = [ + { + title: 'Name', + field: 'resolved.name', + highlight: true, + width: 'auto', + render: ({ entity }) => ( + + ), + }, + { + title: 'Owner', + field: 'resolved.ownedByRelationsTitle', + render: ({ resolved }) => ( + + ), + }, + { + title: 'Status', + field: 'resolved.details.issueCount', + // We interpret missing values as '0.5' to be sorted at the end of the projects with issues. + customSort: (a, b) => + (b?.resolved?.details?.issueCount ?? 0.5) - + (a?.resolved?.details?.issueCount ?? 0.5), + render: ({ resolved }) => { + if (resolved.loading) { + return ; + } else if (!resolved.details) { + return Not configured; + } else if (resolved.details.dependencyCount === 0) { + return No dependencies; + } else if (resolved.details.issueCount > 0) { + return {resolved.details.issueCount} Issues; + } + + return {resolved.details.issueCount} Issues; + }, + }, + { + title: 'Dependencies', + field: 'resolved.details.dependencyCount', + render: ({ resolved: { loading, details } }) => { + if (loading) { + return ; + } + + return details?.dependencyCount; + }, + }, + { + title: 'Branch', + field: 'resolved.details.projectDefaultBranch', + render: ({ resolved: { loading, details } }) => { + if (loading) { + return ; + } + + return details?.projectDefaultBranch; + }, + }, + { + title: 'Last Updated', + field: 'resolved.details.timestamp', + render: ({ resolved: { loading, details } }) => { + if (loading) { + return ; + } + + return ( + details?.timestamp && ( + + + {DateTime.fromISO(details.timestamp).toRelative({ + locale: 'en', + })} + + + ) + ); + }, + }, + { + sorting: false, + render: ({ resolved: { loading, details } }) => { + if (loading) { + return ; + } + + return details && View in FOSSA; + }, + }, +]; + +const filters: TableFilter[] = [ + { column: 'Owner', type: 'multiple-select' }, + { column: 'Branch', type: 'select' }, +]; + +export const FossaPage = () => { + const catalogApi = useApi(catalogApiRef); + const fossaApi = useApi(fossaApiRef); + + // Get a list of all relevant entities + const { value: entities, loading: entitiesLoading } = useAsync(() => { + return catalogApi.getEntities({ + filter: { kind: 'Component' }, + fields: [ + 'kind', + 'metadata.namespace', + 'metadata.name', + 'metadata.annotations', + 'relations', + ], + }); + }); + + // get the project names of all entities. the idx of both lists match. + const projectNames = useMemo( + () => entities?.items?.map(getProjectName) ?? [], + [entities?.items], + ); + + // get the summary list + const { value: summaries, loading: summariesLoading } = useAsync( + async () => + await fossaApi.getFindingSummaries( + projectNames.filter(n => n !== undefined) as string[], + ), + [projectNames], + ); + + // compose the rows + const rows: FossaRow[] = useMemo( + () => + entities?.items?.map((entity, idx) => { + const projectName = projectNames[idx]; + const summary = projectName ? summaries?.get(projectName) : undefined; + const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY); + + return { + entity, + resolved: { + name: formatEntityRefTitle(entity), + ownedByRelations, + ownedByRelationsTitle: ownedByRelations + .map(r => formatEntityRefTitle(r, { defaultKind: 'group' })) + .join(', '), + loading: summariesLoading, + details: summary, + }, + }; + }) ?? [], + [projectNames, entities?.items, summaries, summariesLoading], + ); + + return ( + +
+ + + columns={columns} + data={rows} + filters={filters} + isLoading={entitiesLoading} + options={{ + pageSize: 20, + actionsColumnIndex: -1, + loadingType: 'linear', + padding: 'dense', + showEmptyDataSourceMessage: !entitiesLoading, + }} + /> + + + ); +}; diff --git a/plugins/fossa/src/components/FossaPage/index.ts b/plugins/fossa/src/components/FossaPage/index.ts new file mode 100644 index 0000000000..7d6481949e --- /dev/null +++ b/plugins/fossa/src/components/FossaPage/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export { FossaPage } from './FossaPage'; diff --git a/plugins/fossa/src/components/getProjectName.test.ts b/plugins/fossa/src/components/getProjectName.test.ts new file mode 100644 index 0000000000..93f5187890 --- /dev/null +++ b/plugins/fossa/src/components/getProjectName.test.ts @@ -0,0 +1,47 @@ +/* + * Copyright 2020 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 { Entity } from '@backstage/catalog-model'; +import { getProjectName } from './getProjectName'; + +describe('getProjectName', () => { + it('should extract the fossa project name', async () => { + const entity: Entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'test', + annotations: { + 'fossa.io/project-name': 'test', + }, + }, + }; + + expect(getProjectName(entity)).toBe('test'); + }); + + it('should return undefined', async () => { + const entity: Entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'test', + }, + }; + + expect(getProjectName(entity)).toBeUndefined(); + }); +}); diff --git a/plugins/fossa/src/components/useProjectName.ts b/plugins/fossa/src/components/getProjectName.ts similarity index 92% rename from plugins/fossa/src/components/useProjectName.ts rename to plugins/fossa/src/components/getProjectName.ts index 88379ca3fa..3f117dbbda 100644 --- a/plugins/fossa/src/components/useProjectName.ts +++ b/plugins/fossa/src/components/getProjectName.ts @@ -18,7 +18,7 @@ import { Entity } from '@backstage/catalog-model'; export const FOSSA_PROJECT_NAME_ANNOTATION = 'fossa.io/project-name'; -export const useProjectName = (entity: Entity): string | undefined => { +export const getProjectName = (entity: Entity): string | undefined => { return ( entity?.metadata.annotations?.[FOSSA_PROJECT_NAME_ANNOTATION] ?? undefined ); diff --git a/plugins/fossa/src/extensions.tsx b/plugins/fossa/src/extensions.tsx index 38573c2d1f..21e53996ba 100644 --- a/plugins/fossa/src/extensions.tsx +++ b/plugins/fossa/src/extensions.tsx @@ -14,8 +14,12 @@ * limitations under the License. */ -import { createComponentExtension } from '@backstage/core'; +import { + createComponentExtension, + createRoutableExtension, +} from '@backstage/core'; import { fossaPlugin } from './plugin'; +import { rootRoute } from './routes'; export const EntityFossaCard = fossaPlugin.provide( createComponentExtension({ @@ -24,3 +28,10 @@ export const EntityFossaCard = fossaPlugin.provide( }, }), ); + +export const FossaPage = fossaPlugin.provide( + createRoutableExtension({ + component: () => import('./components/FossaPage').then(m => m.FossaPage), + mountPoint: rootRoute, + }), +); diff --git a/plugins/fossa/src/index.ts b/plugins/fossa/src/index.ts index f1b645c29b..929983bb6a 100644 --- a/plugins/fossa/src/index.ts +++ b/plugins/fossa/src/index.ts @@ -15,4 +15,4 @@ */ export { fossaPlugin } from './plugin'; -export { EntityFossaCard } from './extensions'; +export { EntityFossaCard, FossaPage } from './extensions'; diff --git a/plugins/fossa/src/plugin.ts b/plugins/fossa/src/plugin.ts index 638be4a0bf..849769d42c 100644 --- a/plugins/fossa/src/plugin.ts +++ b/plugins/fossa/src/plugin.ts @@ -22,6 +22,7 @@ import { identityApiRef, } from '@backstage/core'; import { fossaApiRef, FossaClient } from './api'; +import { rootRoute } from './routes'; export const fossaPlugin = createPlugin({ id: 'fossa', @@ -41,4 +42,7 @@ export const fossaPlugin = createPlugin({ }), }), ], + routes: { + fossaOverview: rootRoute, + }, }); diff --git a/plugins/fossa/src/routes.ts b/plugins/fossa/src/routes.ts new file mode 100644 index 0000000000..6e8ce07b29 --- /dev/null +++ b/plugins/fossa/src/routes.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2020 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 { createRouteRef } from '@backstage/core'; + +export const rootRoute = createRouteRef({ + id: 'fossa-overview', +}); diff --git a/yarn.lock b/yarn.lock index ef3a66bb24..36734be1cb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3778,7 +3778,7 @@ react-transition-group "^4.0.0" rifm "^0.7.0" -"@material-ui/styles@^4.10.0", "@material-ui/styles@^4.11.3", "@material-ui/styles@^4.9.6": +"@material-ui/styles@^4.10.0", "@material-ui/styles@^4.11.0", "@material-ui/styles@^4.11.3", "@material-ui/styles@^4.9.6": version "4.11.3" resolved "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.3.tgz#1b8d97775a4a643b53478c895e3f2a464e8916f2" integrity sha512-HzVzCG+PpgUGMUYEJ2rTEmQYeonGh41BYfILNFb/1ueqma+p1meSdu4RX6NjxYBMhf7k+jgfHFTTz+L1SXL/Zg== @@ -5902,6 +5902,14 @@ dependencies: "@types/express" "*" +"@types/express-xml-bodyparser@^0.3.2": + version "0.3.2" + resolved "https://registry.npmjs.org/@types/express-xml-bodyparser/-/express-xml-bodyparser-0.3.2.tgz#8566883271d4a28fc57b471d06e6c08047496acb" + integrity sha512-qX01S9eZI/XY48OLI7+5GnofaFG3VyBEn0oIrY5rxkq/T0U2EBpiUfM0scILUluIR6UGBHMM5C2b8Pmir3Xrtw== + dependencies: + "@types/express" "*" + "@types/xml2js" "*" + "@types/express@*", "@types/express@4.17.7", "@types/express@^4.17.6", "@types/express@^4.17.7": version "4.17.7" resolved "https://registry.npmjs.org/@types/express/-/express-4.17.7.tgz#42045be6475636d9801369cd4418ef65cdb0dd59" @@ -5968,6 +5976,13 @@ dependencies: "@types/unist" "*" +"@types/highlightjs@^10.1.0": + version "10.1.0" + resolved "https://registry.npmjs.org/@types/highlightjs/-/highlightjs-10.1.0.tgz#bf16135e3c736db053f026a1fa8ff3aa247ef5c1" + integrity sha512-xCmqJdhDi4EqrfNDU5ZfZV2ejhszpWBkJS/jYCoAHZhQBBUGnE26l0AzHsZHoe37z4ZETFoZn8HKTIFDjRrfwA== + dependencies: + highlight.js "^10.1.0" + "@types/history@*": version "4.7.5" resolved "https://registry.npmjs.org/@types/history/-/history-4.7.5.tgz#527d20ef68571a4af02ed74350164e7a67544860" @@ -6281,9 +6296,9 @@ "@types/node" "*" "@types/morgan@^1.9.0": - version "1.9.1" - resolved "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.1.tgz#6457872df95647c1dbc6b3741e8146b71ece74bf" - integrity sha512-2j5IKrgJpEP6xw/uiVb2Xfga0W0sSVD9JP9t7EZLvpBENdB0OKgcnoKS8IsjNeNnZ/86robdZ61Orl0QCFGOXg== + version "1.9.2" + resolved "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.2.tgz#450f958a4d3fb0694a3ba012b09c8106f9a2885e" + integrity sha512-edtGMEdit146JwwIeyQeHHg9yID4WSolQPxpEorHmN3KuytuCHyn2ELNr5Uxy8SerniFbbkmgKMrGM933am5BQ== dependencies: "@types/node" "*" @@ -6901,7 +6916,7 @@ dependencies: "@types/node" "*" -"@types/xml2js@^0.4.7": +"@types/xml2js@*", "@types/xml2js@^0.4.7": version "0.4.8" resolved "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.8.tgz#84c120c864a5976d0b5cf2f930a75d850fc2b03a" integrity sha512-EyvT83ezOdec7BhDaEcsklWy7RSIdi6CNe95tmOAK0yx/Lm30C9K75snT3fYayK59ApC2oyW+rcHErdG05FHJA== @@ -6933,9 +6948,9 @@ integrity sha512-/emrKCfQMQmFCqRqqBJ0JueHBT06jBRM3e8OgnvDUcvuExONujIk2hFA5dNsN9Nt41ljGVDdChvCydATZ+KOZw== "@types/yup@^0.29.8": - version "0.29.8" - resolved "https://registry.npmjs.org/@types/yup/-/yup-0.29.8.tgz#83db15735987db9fe5a38772a0fb9500e3c5bf39" - integrity sha512-MBSp62AjB1KrSOI3gX9GekddXU5YYQAVA93+aSl78biBqoSzxg876aQY2KJK5Gnfbpqq7O2cadVX5kPAtBqIXw== + version "0.29.11" + resolved "https://registry.npmjs.org/@types/yup/-/yup-0.29.11.tgz#d654a112973f5e004bf8438122bd7e56a8e5cd7e" + integrity sha512-9cwk3c87qQKZrT251EDoibiYRILjCmxBvvcb4meofCmx1vdnNcR9gyildy5vOHASpOKMsn42CugxUvcwK5eu1g== "@types/zen-observable@^0.8.0": version "0.8.2" @@ -9990,7 +10005,7 @@ compare-versions@^3.6.0: resolved "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz#1a5689913685e5a87637b8d3ffca75514ec41d62" integrity sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA== -component-emitter@^1.2.1, component-emitter@^1.3.0: +component-emitter@^1.2.0, component-emitter@^1.2.1, component-emitter@^1.3.0: version "1.3.0" resolved "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== @@ -10294,7 +10309,7 @@ cookie@^0.4.1, cookie@~0.4.1: resolved "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== -cookiejar@^2.1.2: +cookiejar@^2.1.0, cookiejar@^2.1.2: version "2.1.2" resolved "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz#dd8a235530752f988f9a0844f3fc589e3111125c" integrity sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA== @@ -12867,6 +12882,13 @@ express-session@^1.17.1: safe-buffer "5.2.0" uid-safe "~2.1.5" +express-xml-bodyparser@^0.3.0: + version "0.3.0" + resolved "https://registry.npmjs.org/express-xml-bodyparser/-/express-xml-bodyparser-0.3.0.tgz#b1f5a98adf6c6e412c4ccba634234b82945c62be" + integrity sha1-sfWpit9sbkEsTMumNCNLgpRcYr4= + dependencies: + xml2js "^0.4.11" + express@^4.0.0, express@^4.17.0, express@^4.17.1: version "4.17.1" resolved "https://registry.npmjs.org/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" @@ -13434,7 +13456,7 @@ fork-ts-checker-webpack-plugin@4.1.6, fork-ts-checker-webpack-plugin@^4.0.5, for tapable "^1.0.0" worker-rpc "^0.1.0" -form-data@^2.3.2, form-data@^2.5.0: +form-data@^2.3.1, form-data@^2.3.2, form-data@^2.5.0: version "2.5.1" resolved "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA== @@ -13475,7 +13497,7 @@ format@^0.2.0: resolved "https://registry.npmjs.org/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b" integrity sha1-1hcBB+nv3E7TDJ3DkBbflCtctYs= -formidable@^1.2.2: +formidable@^1.2.0, formidable@^1.2.2: version "1.2.2" resolved "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz#bf69aea2972982675f00865342b982986f6b8dd9" integrity sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q== @@ -14663,15 +14685,20 @@ headers-utils@^1.2.0: integrity sha512-4/BMXcWrJErw7JpM87gF8MNEXcIMLzepYZjNRv/P9ctgupl2Ywa3u1PgHtNhSRq84bHH9Ndlkdy7bSi+bZ9I9A== helmet@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/helmet/-/helmet-4.0.0.tgz#34c187894ed001834f997c688f2b2df19846b193" - integrity sha512-HyoRKKHhWhO6+EBfgRLkuZR4/+NXc1nJB7x0bWwW89i9eoPciK0qUqyZNOA/zowpgrW9C4+J5toqMkZrpBOlkg== + version "4.4.1" + resolved "https://registry.npmjs.org/helmet/-/helmet-4.4.1.tgz#a17e1444d81d7a83ddc6e6f9bc6e2055b994efe7" + integrity sha512-G8tp0wUMI7i8wkMk2xLcEvESg5PiCitFMYgGRc/PwULB0RVhTP5GFdxOwvJwp9XVha8CuS8mnhmE8I/8dx/pbw== hex-color-regex@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== +highlight.js@^10.1.0, highlight.js@^10.6.0: + version "10.7.2" + resolved "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.2.tgz#89319b861edc66c48854ed1e6da21ea89f847360" + integrity sha512-oFLl873u4usRM9K63j4ME9u3etNF0PLiJhSQ8rdfuL51Wn3zkD6drf9ZW0dOzjnZI22YYG24z30JcmfCZjMgYg== + highlight.js@^10.1.1, highlight.js@^10.4.1, highlight.js@~10.4.0: version "10.4.1" resolved "https://registry.npmjs.org/highlight.js/-/highlight.js-10.4.1.tgz#d48fbcf4a9971c4361b3f95f302747afe19dbad0" @@ -18336,7 +18363,7 @@ meros@^1.1.2: resolved "https://registry.npmjs.org/meros/-/meros-1.1.4.tgz#c17994d3133db8b23807f62bec7f0cb276cfd948" integrity sha512-E9ZXfK9iQfG9s73ars9qvvvbSIkJZF5yOo9j4tcwM5tN8mUKfj/EKN5PzOr3ZH0y5wL7dLAHw3RVEfpQV9Q7VQ== -methods@^1.0.0, methods@^1.1.2, methods@~1.1.2: +methods@^1.0.0, methods@^1.1.1, methods@^1.1.2, methods@~1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= @@ -18449,7 +18476,7 @@ mime-types@^2.0.8, mime-types@^2.1.12, mime-types@^2.1.27, mime-types@~2.1.17, m dependencies: mime-db "1.46.0" -mime@1.6.0, mime@^1.4.0: +mime@1.6.0, mime@^1.4.0, mime@^1.4.1: version "1.6.0" resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== @@ -21585,7 +21612,7 @@ qs@6.7.0: resolved "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== -qs@^6.10.1: +qs@^6.10.1, qs@^6.5.1: version "6.10.1" resolved "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz#4931482fa8d647a5aab799c5271d2133b981fb6a" integrity sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg== @@ -22461,7 +22488,7 @@ read@1, read@~1.0.1: dependencies: mute-stream "~0.0.4" -"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6: +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -24604,6 +24631,22 @@ sucrase@^3.17.1: pirates "^4.0.1" ts-interface-checker "^0.1.9" +superagent@^3.8.3: + version "3.8.3" + resolved "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz#460ea0dbdb7d5b11bc4f78deba565f86a178e128" + integrity sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA== + dependencies: + component-emitter "^1.2.0" + cookiejar "^2.1.0" + debug "^3.1.0" + extend "^3.0.0" + form-data "^2.3.1" + formidable "^1.2.0" + methods "^1.1.1" + mime "^1.4.1" + qs "^6.5.1" + readable-stream "^2.3.5" + superagent@^6.1.0: version "6.1.0" resolved "https://registry.npmjs.org/superagent/-/superagent-6.1.0.tgz#09f08807bc41108ef164cfb4be293cebd480f4a6" @@ -24621,6 +24664,14 @@ superagent@^6.1.0: readable-stream "^3.6.0" semver "^7.3.2" +supertest@^4.0.2: + version "4.0.2" + resolved "https://registry.npmjs.org/supertest/-/supertest-4.0.2.tgz#c2234dbdd6dc79b6f15b99c8d6577b90e4ce3f36" + integrity sha512-1BAbvrOZsGA3YTCWqbmh14L0YEq0EGICX/nBnfkfVJn7SrxQV1I3pMYjSzG9y/7ZU2V9dWqyqk2POwxlb09duQ== + dependencies: + methods "^1.1.2" + superagent "^3.8.3" + supertest@^6.1.3: version "6.1.3" resolved "https://registry.npmjs.org/supertest/-/supertest-6.1.3.tgz#3f49ea964514c206c334073e8dc4e70519c7403f" @@ -26111,7 +26162,7 @@ uuid@^7.0.3: resolved "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b" integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg== -uuid@^8.0.0, uuid@^8.2.0, uuid@^8.3.0: +uuid@^8.0.0, uuid@^8.2.0, uuid@^8.3.0, uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== @@ -26849,7 +26900,7 @@ xml2js@0.4.19: sax ">=0.6.0" xmlbuilder "~9.0.1" -xml2js@^0.4.19, xml2js@^0.4.23: +xml2js@^0.4.11, xml2js@^0.4.19, xml2js@^0.4.23: version "0.4.23" resolved "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==