From e7d2fb93f0a96cb53ae80348eea66b0ae7235f49 Mon Sep 17 00:00:00 2001 From: alde Date: Wed, 24 Feb 2021 16:48:38 -0500 Subject: [PATCH] Create code-coverage plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In order to use this plugin, you must set the `backstage.io/code-coverage` annotation on your entity. ```yaml backstage.io/code-coverage: enabled ``` There's a feature to only include files that are in SCM in the coverage report, this is helpful to not count generated files for example. To enable this set the `backstage.io/code-coverage` annotation to `scm-only`. ```yaml backstage.io/code-coverage: scm-only ``` The backend plugin provides API endpoints for submitting code-coverage reports. Currently jacoco and cobertura are supported. These reports are normalized to a json format that is stored in the database. ```json // curl -X POST -H "Content-Type:text/xml" -d @cobertura.xml "localhost:7000/api/code-coverage/Component/default/entity-name?coverageType=cobertura" { "links": [ { "href": "http://localhost:7000/api/code-coverage/Component/default/entity-name", "rel": "coverage" } ] } ``` It also provides some additional API endpoints: * Viewing the latest report * Viewing a more condensed history of code coverage values * Retrieving file contents from source-control, used by the UI Provides a graph of code coverage change over time, as well as a file view where you can see the highlighted lines. Co-authored-by: nissayeva Signed-off-by: alde Signed-off-by: Fredrik Adelöw --- .github/CODEOWNERS | 2 + packages/app/package.json | 1 + .../app/src/components/catalog/EntityPage.tsx | 9 + packages/backend/package.json | 1 + packages/backend/src/index.ts | 5 + packages/backend/src/plugins/codecoverage.ts | 28 + plugins/code-coverage-backend/.eslintrc.js | 3 + plugins/code-coverage-backend/README.md | 150 + .../migrations/20210302_init.js | 50 + plugins/code-coverage-backend/package.json | 50 + plugins/code-coverage-backend/src/index.ts | 17 + plugins/code-coverage-backend/src/run.ts | 33 + .../src/service/CodeCoverageDatabase.test.ts | 199 + .../src/service/CodeCoverageDatabase.ts | 112 + .../src/service/CoverageUtils.test.ts | 325 + .../src/service/CoverageUtils.ts | 168 + .../cobertura-jsoncoverage-files-1.json | 166 + .../cobertura-jsoncoverage-files-2.json | 3952 ++++++++++++ .../cobertura-jsoncoverage-files-3.json | 297 + .../cobertura-jsoncoverage-files-4.json | 351 + .../cobertura-jsoncoverage-files-5.json | 325 + .../__fixtures__/cobertura-sourcefiles-1.txt | 19 + .../__fixtures__/cobertura-sourcefiles-2.txt | 49 + .../__fixtures__/cobertura-sourcefiles-3.txt | 29 + .../__fixtures__/cobertura-sourcefiles-4.txt | 20 + .../__fixtures__/cobertura-sourcefiles-5.txt | 26 + .../__fixtures__/cobertura-testdata-1.xml | 479 ++ .../__fixtures__/cobertura-testdata-2.xml | 5721 +++++++++++++++++ .../__fixtures__/cobertura-testdata-3.xml | 357 + .../__fixtures__/cobertura-testdata-4.xml | 348 + .../__fixtures__/cobertura-testdata-5.xml | 441 ++ .../jacoco-jsoncoverage-files-1.json | 319 + .../__fixtures__/jacoco-sourcefiles-1.txt | 7 + .../__fixtures__/jacoco-testdata-1.xml | 282 + .../src/service/converter/cobertura.test.ts | 56 + .../src/service/converter/cobertura.ts | 126 + .../src/service/converter/index.ts | 23 + .../src/service/converter/jacoco.test.ts | 50 + .../src/service/converter/jacoco.ts | 96 + .../src/service/converter/types.ts | 92 + .../src/service/jsoncoverage-types.ts | 71 + .../src/service/router.test.ts | 79 + .../src/service/router.ts | 229 + .../src/service/standaloneServer.ts | 64 + .../code-coverage-backend/src/setupTests.ts | 17 + plugins/code-coverage/.eslintrc.js | 3 + plugins/code-coverage/README.md | 19 + plugins/code-coverage/dev/index.tsx | 26 + plugins/code-coverage/package.json | 57 + plugins/code-coverage/src/api.ts | 102 + .../CodeCoveragePage/CodeCoveragePage.tsx | 31 + .../src/components/CodeCoveragePage/index.ts | 16 + .../CoverageHistoryChart.tsx | 169 + .../components/CoverageHistoryChart/index.ts | 17 + .../components/FileExplorer/CoverageRow.tsx | 103 + .../components/FileExplorer/FileContent.tsx | 137 + .../components/FileExplorer/FileExplorer.tsx | 280 + .../components/FileExplorer/Highlighter.ts | 54 + .../src/components/FileExplorer/index.ts | 17 + .../code-coverage/src/components/Router.tsx | 34 + plugins/code-coverage/src/dev/index.tsx | 28 + plugins/code-coverage/src/index.ts | 23 + plugins/code-coverage/src/plugin.test.ts | 22 + plugins/code-coverage/src/plugin.ts | 45 + plugins/code-coverage/src/routes.ts | 20 + plugins/code-coverage/src/setupTests.ts | 17 + yarn.lock | 280 +- 67 files changed, 16733 insertions(+), 11 deletions(-) create mode 100644 packages/backend/src/plugins/codecoverage.ts create mode 100644 plugins/code-coverage-backend/.eslintrc.js create mode 100644 plugins/code-coverage-backend/README.md create mode 100644 plugins/code-coverage-backend/migrations/20210302_init.js create mode 100644 plugins/code-coverage-backend/package.json create mode 100644 plugins/code-coverage-backend/src/index.ts create mode 100644 plugins/code-coverage-backend/src/run.ts create mode 100644 plugins/code-coverage-backend/src/service/CodeCoverageDatabase.test.ts create mode 100644 plugins/code-coverage-backend/src/service/CodeCoverageDatabase.ts create mode 100644 plugins/code-coverage-backend/src/service/CoverageUtils.test.ts create mode 100644 plugins/code-coverage-backend/src/service/CoverageUtils.ts create mode 100644 plugins/code-coverage-backend/src/service/__fixtures__/cobertura-jsoncoverage-files-1.json create mode 100644 plugins/code-coverage-backend/src/service/__fixtures__/cobertura-jsoncoverage-files-2.json create mode 100644 plugins/code-coverage-backend/src/service/__fixtures__/cobertura-jsoncoverage-files-3.json create mode 100644 plugins/code-coverage-backend/src/service/__fixtures__/cobertura-jsoncoverage-files-4.json create mode 100644 plugins/code-coverage-backend/src/service/__fixtures__/cobertura-jsoncoverage-files-5.json create mode 100644 plugins/code-coverage-backend/src/service/__fixtures__/cobertura-sourcefiles-1.txt create mode 100644 plugins/code-coverage-backend/src/service/__fixtures__/cobertura-sourcefiles-2.txt create mode 100644 plugins/code-coverage-backend/src/service/__fixtures__/cobertura-sourcefiles-3.txt create mode 100644 plugins/code-coverage-backend/src/service/__fixtures__/cobertura-sourcefiles-4.txt create mode 100644 plugins/code-coverage-backend/src/service/__fixtures__/cobertura-sourcefiles-5.txt create mode 100644 plugins/code-coverage-backend/src/service/__fixtures__/cobertura-testdata-1.xml create mode 100644 plugins/code-coverage-backend/src/service/__fixtures__/cobertura-testdata-2.xml create mode 100644 plugins/code-coverage-backend/src/service/__fixtures__/cobertura-testdata-3.xml create mode 100644 plugins/code-coverage-backend/src/service/__fixtures__/cobertura-testdata-4.xml create mode 100644 plugins/code-coverage-backend/src/service/__fixtures__/cobertura-testdata-5.xml create mode 100644 plugins/code-coverage-backend/src/service/__fixtures__/jacoco-jsoncoverage-files-1.json create mode 100644 plugins/code-coverage-backend/src/service/__fixtures__/jacoco-sourcefiles-1.txt create mode 100644 plugins/code-coverage-backend/src/service/__fixtures__/jacoco-testdata-1.xml create mode 100644 plugins/code-coverage-backend/src/service/converter/cobertura.test.ts create mode 100644 plugins/code-coverage-backend/src/service/converter/cobertura.ts create mode 100644 plugins/code-coverage-backend/src/service/converter/index.ts create mode 100644 plugins/code-coverage-backend/src/service/converter/jacoco.test.ts create mode 100644 plugins/code-coverage-backend/src/service/converter/jacoco.ts create mode 100644 plugins/code-coverage-backend/src/service/converter/types.ts create mode 100644 plugins/code-coverage-backend/src/service/jsoncoverage-types.ts create mode 100644 plugins/code-coverage-backend/src/service/router.test.ts create mode 100644 plugins/code-coverage-backend/src/service/router.ts create mode 100644 plugins/code-coverage-backend/src/service/standaloneServer.ts create mode 100644 plugins/code-coverage-backend/src/setupTests.ts create mode 100644 plugins/code-coverage/.eslintrc.js create mode 100644 plugins/code-coverage/README.md create mode 100644 plugins/code-coverage/dev/index.tsx create mode 100644 plugins/code-coverage/package.json create mode 100644 plugins/code-coverage/src/api.ts create mode 100644 plugins/code-coverage/src/components/CodeCoveragePage/CodeCoveragePage.tsx create mode 100644 plugins/code-coverage/src/components/CodeCoveragePage/index.ts create mode 100644 plugins/code-coverage/src/components/CoverageHistoryChart/CoverageHistoryChart.tsx create mode 100644 plugins/code-coverage/src/components/CoverageHistoryChart/index.ts create mode 100644 plugins/code-coverage/src/components/FileExplorer/CoverageRow.tsx create mode 100644 plugins/code-coverage/src/components/FileExplorer/FileContent.tsx create mode 100644 plugins/code-coverage/src/components/FileExplorer/FileExplorer.tsx create mode 100644 plugins/code-coverage/src/components/FileExplorer/Highlighter.ts create mode 100644 plugins/code-coverage/src/components/FileExplorer/index.ts create mode 100644 plugins/code-coverage/src/components/Router.tsx create mode 100644 plugins/code-coverage/src/dev/index.tsx create mode 100644 plugins/code-coverage/src/index.ts create mode 100644 plugins/code-coverage/src/plugin.test.ts create mode 100644 plugins/code-coverage/src/plugin.ts create mode 100644 plugins/code-coverage/src/routes.ts create mode 100644 plugins/code-coverage/src/setupTests.ts 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/packages/app/package.json b/packages/app/package.json index 4d2dbe660a..95e14766c3 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/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/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..2e2f5ba91f --- /dev/null +++ b/plugins/code-coverage-backend/README.md @@ -0,0 +1,150 @@ +# code-coverage + +This is the backend part of the code-coverage plugin. It takes care of processing various coverage formats and standardizing them into a single json format, used by the frontend. + +## Configuring your entity + +In order to use this plugin, you must set the `backstage.io/code-coverage` annotation. + +```yaml +backstage.io/code-coverage: enabled +``` + +There's a feature to only include files that are in VCS in the coverage report, this is helpful to not count generated files for example. To enable this set the `backstage.io/code-coverage` annotation to `scm-only`. + +```yaml +backstage.io/code-coverage: scm-only +``` + +Note: This requires the [`backstage.io/source-location` annotation](https://backstage.io/docs/features/software-catalog/well-known-annotations#backstageiosource-location) to be set. + +## API + +### Adding cobertura report + +POST a cobertura xml to `/:kind/:namespace/:name?coverageType=cobertura` + +Example: + +```json +// curl -X POST -H "Content-Type:text/xml" -d @cobertura.xml "localhost:7000/api/code-coverage/Component/default/entity-name?coverageType=cobertura" +{ + "links": [ + { + "href": "http://localhost:7000/api/code-coverage/Component/default/entity-name", + "rel": "coverage" + } + ] +} +``` + +### Adding jacoco report + +POST a jacoco xml to `/:kind/:namespace/:name?coverageType=jacoco` + +Example: + +```json +// curl -X POST -H "Content-Type:text/xml" -d @jacoco.xml "localhost:7000/api/code-coverage/Component/default/entity-name?coverageType=jacoco" +{ + "links": [ + { + "href": "http://localhost:7000/api/code-coverage/Component/default/entity-name", + "rel": "coverage" + } + ] +} +``` + +### Reading json coverage + +GET `/:kind/:namespace/:name` + +Example: + +```json +// curl localhost:7000/api/code-coverage/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 `/:kind/:namespace/:name/history` + +Example + +```json +// curl localhost:7000/api/code-coverage/Component/default/entity-name/history +{ + "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..0edd455e7d --- /dev/null +++ b/plugins/code-coverage-backend/migrations/20210302_init.js @@ -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. + */ + +// @ts-check + +/** + * @param {import('knex').Knex} knex + */ +exports.up = async function up(knex) { + await knex.schema.createTable('code_coverage', table => { + table.comment('The table of code coverage'); + table + .uuid('id') + .primary() + .notNullable() + .comment('The ID of the code coverage'); + table.text('entity_name').notNullable().comment('entity name'); + table.text('entity_kind').notNullable().comment('entity kind'); + table.text('entity_namespace').notNullable().comment('entity namespace'); + 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'); + }); +}; + +/** + * @param {import('knex').Knex} knex + */ +exports.down = async function down(knex) { + 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..79de777dfa --- /dev/null +++ b/plugins/code-coverage-backend/package.json @@ -0,0 +1,50 @@ +{ + "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.5.4", + "@backstage/catalog-client": "^0.3.6", + "@backstage/catalog-model": "^0.7.2", + "@backstage/config": "^0.1.3", + "@backstage/errors": "^0.1.1", + "@backstage/integration": "^0.5.0", + "@backstage/plugin-catalog-backend": "^0.6.5", + "@types/express": "^4.17.6", + "cross-fetch": "^3.0.6", + "express": "^4.17.1", + "express-promise-router": "^3.0.3", + "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.2", + "@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..af734dd358 --- /dev/null +++ b/plugins/code-coverage-backend/src/service/CodeCoverageDatabase.test.ts @@ -0,0 +1,199 @@ +/* + * 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 { + getVoidLogger, + SingleConnectionDatabaseManager, +} from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { + CodeCoverageDatabase, + CodeCoverageStore, +} from './CodeCoverageDatabase'; +import { JsonCodeCoverage } from './jsoncoverage-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, getVoidLogger()); + database.insertCodeCoverage(coverage[0]); + await new Promise(r => setTimeout(r, 1000)); + 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({ + 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( + { + 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..44109e8e2e --- /dev/null +++ b/plugins/code-coverage-backend/src/service/CodeCoverageDatabase.ts @@ -0,0 +1,112 @@ +/* + * 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 { EntityName } from '@backstage/catalog-model'; +import { Knex } from 'knex'; +import { v4 as uuid } from 'uuid'; +import { Logger } from 'winston'; +import { aggregateCoverage } from './CoverageUtils'; +import { JsonCodeCoverage, JsonCoverageHistory } from './jsoncoverage-types'; + +export type RawDbCoverageRow = { + id: string; + entity_name: string; + entity_namespace: string; + entity_kind: string; + coverage: string; +}; +export interface CodeCoverageStore { + insertCodeCoverage( + coverage: JsonCodeCoverage, + ): Promise<{ codeCoverageId: string }>; + getCodeCoverage(entity: EntityName): Promise; + getHistory(entity: EntityName, limit: number): Promise; +} + +const migrationsDir = resolvePackagePath( + '@backstage/plugin-code-coverage-backend', + 'migrations', +); +export class CodeCoverageDatabase implements CodeCoverageStore { + static async create(knex: Knex, logger: Logger): Promise { + await knex.migrate.latest({ + directory: migrationsDir, + }); + return new CodeCoverageDatabase(knex, logger); + } + + constructor(private readonly db: Knex, private readonly logger: Logger) {} + + async insertCodeCoverage( + coverage: JsonCodeCoverage, + ): Promise<{ codeCoverageId: string }> { + const codeCoverageId = uuid(); + await this.db('code_coverage').insert({ + id: codeCoverageId, + entity_name: coverage.entity.name, + entity_kind: coverage.entity.kind, + entity_namespace: coverage.entity.namespace, + coverage: JSON.stringify(coverage), + }); + return { codeCoverageId }; + } + async getCodeCoverage(entity: EntityName): Promise { + const [result] = await this.db('code_coverage') + .where({ + entity_name: entity.name, + entity_kind: entity.kind, + entity_namespace: entity.namespace, + }) + .orderBy('created_at', '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: EntityName, + limit: number, + ): Promise { + const res = await this.db('code_coverage') + .where({ + entity_name: entity.name, + entity_kind: entity.kind, + entity_namespace: entity.namespace, + }) + .orderBy('created_at', 'desc') + .limit(limit) + .select(); + + const history = res + .map(r => JSON.parse(r.coverage)) + .map(c => aggregateCoverage(c)); + + return { + entity, + 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..5a5cffbced --- /dev/null +++ b/plugins/code-coverage-backend/src/service/CoverageUtils.test.ts @@ -0,0 +1,325 @@ +/* + * 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': '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..0ed1561003 --- /dev/null +++ b/plugins/code-coverage-backend/src/service/CoverageUtils.ts @@ -0,0 +1,168 @@ +/* + * 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 } from '@backstage/catalog-model'; +import { ScmIntegration, ScmIntegrations } from '@backstage/integration'; +import { + AggregateCoverage, + FileEntry, + JsonCodeCoverage, +} from './jsoncoverage-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) { + sourceLocation = + entity.metadata.annotations?.['backstage.io/source-location']; + if (!sourceLocation) { + throw new InputError( + `No "backstage.io/source-location" annotation on entity ${entity.kind}/${entity.metadata.namespace}/${entity.metadata.name}`, + ); + } + + 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/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..72eeb25b56 --- /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 '../jsoncoverage-types'; +import { CoberturaXML, InnerClass, LineHit } from './types'; +import { Logger } from 'winston'; +import { Converter } from '.'; + +export class Cobertura extends Converter { + constructor(readonly logger: Logger) { + super(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.info(`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..083af10f68 --- /dev/null +++ b/plugins/code-coverage-backend/src/service/converter/index.ts @@ -0,0 +1,23 @@ +/* + * 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 { Logger } from 'winston'; +import { FileEntry } from '../jsoncoverage-types'; + +export abstract class Converter { + constructor(readonly logger: Logger) {} + + abstract convert(xml: any, scmFiles: Array): Array; +} 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..fe60f9378c --- /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 '../jsoncoverage-types'; +import { JacocoSourceFile, JacocoXML } from './types'; +import { Logger } from 'winston'; +import { Converter } from '.'; + +type ParsedLine = { + number: number; + missed_instructions: number; + covered_instructions: number; + missed_branches: number; + covered_branches: number; +}; + +export class Jacoco extends Converter { + constructor(readonly logger: Logger) { + super(logger); + } + + /** + * Converts jacoco into shared json coverage format + * + * @param xml jacoco xml object + * @param scmFiles list of files that are commited 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.info(`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/jsoncoverage-types.ts b/plugins/code-coverage-backend/src/service/jsoncoverage-types.ts new file mode 100644 index 0000000000..f1ef3dd694 --- /dev/null +++ b/plugins/code-coverage-backend/src/service/jsoncoverage-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/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..3f3dcf8a1e --- /dev/null +++ b/plugins/code-coverage-backend/src/service/router.ts @@ -0,0 +1,229 @@ +/* + * 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'; + +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(), + logger, + ); + 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' }); + }); + + router.get('/:kind/:namespace/:name', async (req, res) => { + const { kind, namespace, name } = req.params; + const entity = await catalogApi.getEntityByName({ kind, namespace, name }); + if (!entity) { + throw new NotFoundError( + `No entity found matching ${kind}/${namespace}/${name}`, + ); + } + const stored = await codeCoverageDatabase.getCodeCoverage({ + kind, + namespace, + name, + }); + + const aggregate = aggregateCoverage(stored); + + res.status(200).json({ + ...stored, + aggregate: { + line: aggregate.line, + branch: aggregate.branch, + }, + }); + }); + + router.get('/:kind/:namespace/:name/history', async (req, res) => { + const { kind, namespace, name } = req.params; + const entity = await catalogApi.getEntityByName({ kind, namespace, name }); + if (!entity) { + throw new NotFoundError( + `No entity found matching ${kind}/${namespace}/${name}`, + ); + } + const { limit } = req.query; + const history = await codeCoverageDatabase.getHistory( + { + kind, + namespace, + name, + }, + parseInt(limit?.toString() || '10', 10), + ); + + res.status(200).json(history); + }); + + router.get('/:kind/:namespace/:name/file-content', async (req, res) => { + const { kind, namespace, name } = req.params; + const entity = await catalogApi.getEntityByName({ kind, namespace, name }); + if (!entity) { + throw new NotFoundError( + `No entity found matching ${kind}/${namespace}/${name}`, + ); + } + const { path } = req.query; + if (!path) { + throw new InputError('Need path query parameter'); + } + + const sourceLocation = + entity.metadata.annotations?.['backstage.io/source-location']; + if (!sourceLocation) { + throw new InputError( + `No "backstage.io/source-location" annotation on entity ${entity.kind}/${entity.metadata.namespace}/${entity.metadata.name}`, + ); + } + + const vcs = scm.byUrl(sourceLocation); + if (!vcs) { + throw new InputError(`Unable to determine SCM from ${sourceLocation}`); + } + + const scmTree = await urlReader.readTree(sourceLocation); + const scmFile = (await scmTree.files()).find(f => f.path === path); + if (!scmFile) { + res.status(400).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); + }); + + router.post('/:kind/:namespace/:name/', async (req, res) => { + const { kind, namespace, name } = req.params; + const { coverageType } = req.query; + 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 NotFoundError(`unsupported coverage type '${coverageType}`); + } + const entity = await catalogApi.getEntityByName({ kind, namespace, name }); + if (!entity) { + throw new NotFoundError( + `No entity found matching ${kind}/${namespace}/${name}`, + ); + } + const { + sourceLocation, + vcs, + scmFiles, + body, + } = await utils.processCoveragePayload(entity, 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( + entity, + sourceLocation, + vcs, + files, + ); + await codeCoverageDatabase.insertCodeCoverage(coverage); + + res.status(201).json({ + links: [ + { + rel: 'coverage', + href: `${codecovUrl}/${kind}/${namespace}/${name}`, + }, + ], + }); + }); + + 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..8a3c4eaea1 --- /dev/null +++ b/plugins/code-coverage-backend/src/service/standaloneServer.ts @@ -0,0 +1,64 @@ +/* + * 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 { Logger } from 'winston'; +import { createRouter } from './router'; +import { DatabaseManager } from '@backstage/plugin-catalog-backend'; + +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, () => + DatabaseManager.createInMemoryDatabaseConnection(), + ); + + logger.debug('Starting application server...'); + const router = await createRouter({ + database: { getClient: () => 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/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..b93ad7a091 --- /dev/null +++ b/plugins/code-coverage/README.md @@ -0,0 +1,19 @@ +# code-coverage + +This is the frontend part of the code-coverage plugin. It takes care of processing various coverage formats and standardizing them into a single json format, used by the frontend. + +## Configuring your entity + +In order to use this plugin, you must set the `backstage.io/code-coverage` annotation. + +```yaml +backstage.io/code-coverage: enabled +``` + +There's a feature to only include files that are in VCS in the coverage report, this is helpful to not count generated files for example. To enable this set the `backstage.io/code-coverage` annotation to `scm-only`. + +```yaml +backstage.io/code-coverage: scm-only +``` + +Note: This requires the [`backstage.io/source-location` annotation](https://backstage.io/docs/features/software-catalog/well-known-annotations#backstageiosource-location) to be set. diff --git a/plugins/code-coverage/dev/index.tsx b/plugins/code-coverage/dev/index.tsx new file mode 100644 index 0000000000..9eabafb976 --- /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, CodeCoveragePage } 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..75be2cab92 --- /dev/null +++ b/plugins/code-coverage/package.json @@ -0,0 +1,57 @@ +{ + "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.2", + "@backstage/config": "^0.1.3", + "@backstage/core": "^0.6.2", + "@backstage/core-api": "^0.2.12", + "@backstage/dev-utils": "^0.1.11", + "@backstage/plugin-catalog-react": "^0.1.0", + "@backstage/theme": "^0.2.3", + "@material-ui/core": "^4.11.0", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.45", + "@types/highlightjs": "^10.1.0", + "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.1", + "@backstage/test-utils": "^0.1.7", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^10.4.1", + "@testing-library/user-event": "^12.0.7", + "@types/jest": "^26.0.7", + "@types/node": "^12.0.0", + "@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..da18bde267 --- /dev/null +++ b/plugins/code-coverage/src/api.ts @@ -0,0 +1,102 @@ +/* + * 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 { createApiRef } from '@backstage/core'; +import { Config } from '@backstage/config'; +import { EntityName } from '@backstage/catalog-model'; + +export class FetchError extends Error { + get name(): string { + return this.constructor.name; + } + + static async forResponse(resp: Response): Promise { + return new FetchError( + `Request failed with status code ${ + resp.status + }.\nReason: ${await resp.text()}`, + ); + } +} + +export type CodeCoverageApi = { + url: string; + 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 { + static fromConfig(config: Config) { + return new CodeCoverageRestApi(config.getString('backend.baseUrl')); + } + + constructor(public url: string) {} + + private async fetch( + input: string, + init?: RequestInit, + ): Promise { + const resp = await fetch(`${this.url}${input}`, init); + if (!resp.ok) throw await FetchError.forResponse(resp); + if (resp.headers.get('content-type')?.includes('application/json')) { + return await resp.json(); + } + return await resp.text(); + } + + async getCoverageForEntity({ + kind, + namespace, + name, + }: EntityName): Promise { + return await this.fetch( + `/api/code-coverage/${kind}/${namespace}/${name}`, + ); + } + + async getFileContentFromEntity( + { kind, namespace, name }: EntityName, + filePath: string, + ): Promise { + return await this.fetch( + `/api/code-coverage/${kind}/${namespace}/${name}/file-content?path=${filePath}`, + ); + } + + async getCoverageHistoryForEntity( + { kind, namespace, name }: EntityName, + limit?: number, + ): Promise { + const hasValidLimit = limit && limit > 0; + return await this.fetch( + `/api/code-coverage/${kind}/${namespace}/${name}/history${ + hasValidLimit ? `?limit=${limit}` : '' + }`, + ); + } +} 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..a7bfeceb19 --- /dev/null +++ b/plugins/code-coverage/src/components/CoverageHistoryChart/CoverageHistoryChart.tsx @@ -0,0 +1,169 @@ +/* + * 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 { + Box, + Card, + CardContent, + MenuItem, + Select, + Typography, +} from '@material-ui/core'; +import { + LineChart, + XAxis, + YAxis, + Tooltip, + Legend, + Line, + CartesianGrid, + ResponsiveContainer, +} from 'recharts'; +import { useAsync } from 'react-use'; +import { useApi } from '@backstage/core-api'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { codeCoverageApiRef } from '../../api'; +import { Progress } from '@backstage/core'; +import { Alert } from '@material-ui/lab'; +import TrendingDownIcon from '@material-ui/icons/TrendingDown'; +import TrendingUpIcon from '@material-ui/icons/TrendingUp'; +import TrendingFlatIcon from '@material-ui/icons/TrendingFlat'; + +type Coverage = 'line' | 'branch'; + +const getTrendIcon = (trend: number) => { + 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, + }), + ); + + if (loadingHistory) { + return ; + } else if (errorHistory) { + return {errorHistory.message}; + } + + if (!valueHistory.history.length) { + return ( + + + + + History + + 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 ( + + + + + History + + + + + {getTrendIcon(lineTrend)} + + Current line: {latestCoverage.line.percentage}%
( + {Math.floor(lineTrend)}% change over{' '} + {valueHistory.history.length} builds) +
+
+ + {getTrendIcon(branchTrend)} + + 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/CoverageRow.tsx b/plugins/code-coverage/src/components/FileExplorer/CoverageRow.tsx new file mode 100644 index 0000000000..a4eabf715d --- /dev/null +++ b/plugins/code-coverage/src/components/FileExplorer/CoverageRow.tsx @@ -0,0 +1,103 @@ +/* + * 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, { FC } from 'react'; + +// styles +import { makeStyles } from '@material-ui/core'; +import { red, green } from '@material-ui/core/colors'; + +const useStyles = makeStyles(theme => ({ + lineNumberCell: { + color: `${theme.palette.grey[500]}`, + fontSize: '90%', + borderRight: `1px solid ${theme.palette.grey[500]}`, + paddingRight: '8px', + 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: `${green[500]}`, + }, + notHitCountRoundedRectangle: { + backgroundColor: `${red[500]}`, + }, + codeLine: { + paddingLeft: `${theme.spacing(1)}`, + whiteSpace: 'pre', + fontSize: '90%', + }, + hitCodeLine: { + backgroundColor: `${green[100]}`, + }, + notHitCodeLine: { + backgroundColor: `${red[100]}`, + }, +})); + +type CodeRowProps = { + lineNumber: number; + lineContent: string; + lineHits?: number | null; +}; +const CodeRow: FC = ({ + 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} + + + ); +}; + +export default CodeRow; 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..7b1bc2c070 --- /dev/null +++ b/plugins/code-coverage/src/components/FileExplorer/FileContent.tsx @@ -0,0 +1,137 @@ +/* + * 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, { FC, useEffect } from 'react'; +import { useApi } from '@backstage/core-api'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { useAsync } from 'react-use'; +import { codeCoverageApiRef } from '../../api'; +import { Progress } from '@backstage/core'; +import { Alert } from '@material-ui/lab'; +import { makeStyles, Paper } from '@material-ui/core'; +import { highlightLines } from './Highlighter'; +import { FileCoverage } from './FileExplorer'; +import CoverageRow from './CoverageRow'; + +type Props = { + filename: string; + coverage: FileCoverage; +}; + +const getContent = async (value: any) => { + let blob = value; + if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) { + blob = new Blob([value], { type: 'application/octet-stream' }); + } + return value; +}; + +const useStyles = makeStyles(theme => ({ + paper: { + margin: 'auto', + top: '2em', + width: '80%', + backgroundColor: theme.palette.background.paper, + 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: FC = ({ + 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, + ), + ); + + useEffect(() => { + if (value) getContent(value); + }, [value]); + + const classes = useStyles(); + + if (loading) { + return ; + } else if (error) { + return {error.message}; + } + + 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..11baa419f4 --- /dev/null +++ b/plugins/code-coverage/src/components/FileExplorer/FileExplorer.tsx @@ -0,0 +1,280 @@ +/* + * 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 { useApi } from '@backstage/core-api'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { Progress, Table, TableColumn } from '@backstage/core'; +import { + Box, + Card, + CardContent, + Modal, + Tooltip, + Typography, +} from '@material-ui/core'; +import React, { Fragment, useEffect, useState } from 'react'; +import { useAsync } from 'react-use'; +import { codeCoverageApiRef } from '../../api'; +import { Alert } from '@material-ui/lab'; +import DescriptionIcon from '@material-ui/icons/Description'; +import { FileContent } from './FileContent'; + +export type FileCoverage = { + filename: string; + branchHits: { [branch: number]: number }; + lineHits: { [line: number]: number }; +}; + +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: FileCoverage) => { + 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 {error.message}; + } + + 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: FileCoverage) => + f.filename.endsWith(curFile), + ); + + return ( + + + + + Explore Files + + + {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..fda1a11f11 --- /dev/null +++ b/plugins/code-coverage/src/components/FileExplorer/Highlighter.ts @@ -0,0 +1,54 @@ +/* + * 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 {String} fileExtension The extension of the source file + * @param {String} repo The name of the code repository + * @param {Array} lines The source code lines + * + * @returns {Promise>} 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/dev/index.tsx b/plugins/code-coverage/src/dev/index.tsx new file mode 100644 index 0000000000..5c944b097a --- /dev/null +++ b/plugins/code-coverage/src/dev/index.tsx @@ -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 { createDevApp } from '@backstage/dev-utils'; +import { codeCoveragePlugin } from '../plugin'; +import { codeCoverageApiRef, CodeCoverageRestApi } from '../api'; + +createDevApp() + .registerPlugin(codeCoveragePlugin) + .registerApi({ + api: codeCoverageApiRef, + deps: {}, + factory: () => new CodeCoverageRestApi('http://localhost:3000'), + }) + .render(); diff --git a/plugins/code-coverage/src/index.ts b/plugins/code-coverage/src/index.ts new file mode 100644 index 0000000000..84010e64ac --- /dev/null +++ b/plugins/code-coverage/src/index.ts @@ -0,0 +1,23 @@ +/* + * 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'; + +// export { createBackendRouter } from './backend/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..fa5f31d9cc --- /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 { + configApiRef, + createApiFactory, + createPlugin, + createRoutableExtension, +} 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: { configApi: configApiRef }, + factory: ({ configApi }) => CodeCoverageRestApi.fromConfig(configApi), + }), + ], +}); + +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/yarn.lock b/yarn.lock index a9b95da3ec..baf771b351 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1623,6 +1623,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.10.3": + version "7.13.10" + resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.13.10.tgz#47d42a57b6095f4468da440388fdbad8bebf0d7d" + integrity sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.10.4", "@babel/template@^7.12.13", "@babel/template@^7.3.3": version "7.12.13" resolved "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz#530265be8a2589dbb37523844c5bcb55947fb327" @@ -1679,6 +1686,138 @@ lodash "^4.17.19" to-fast-properties "^2.0.0" +"@backstage/backend-common@^0.5.4": + version "0.5.6" + resolved "https://registry.npmjs.org/@backstage/backend-common/-/backend-common-0.5.6.tgz#e49840eaa6738f6697319ccdfff04ce40cc54b72" + integrity sha512-BzKlcTZLAVDpH87fi2TCWIFO9DfVdaPIIZ8gZSorCuLLErG4X7CS/zQiujJZ/zD5HGtO2QB57fuRkwjBMhJhhw== + dependencies: + "@backstage/cli-common" "^0.1.1" + "@backstage/config" "^0.1.2" + "@backstage/config-loader" "^0.5.1" + "@backstage/integration" "^0.5.1" + "@octokit/rest" "^18.0.12" + "@types/cors" "^2.8.6" + "@types/dockerode" "^3.2.1" + "@types/express" "^4.17.6" + archiver "^5.0.2" + compression "^1.7.4" + concat-stream "^2.0.0" + cors "^2.8.5" + cross-fetch "^3.0.6" + dockerode "^3.2.1" + express "^4.17.1" + express-promise-router "^3.0.3" + fs-extra "^9.0.1" + git-url-parse "^11.4.4" + helmet "^4.0.0" + isomorphic-git "^1.8.0" + knex "^0.95.1" + lodash "^4.17.15" + logform "^2.1.1" + minimatch "^3.0.4" + minimist "^1.2.5" + morgan "^1.10.0" + selfsigned "^1.10.7" + stoppable "^1.1.0" + tar "^6.0.5" + unzipper "^0.10.11" + winston "^3.2.1" + +"@backstage/config-loader@^0.5.1": + version "0.5.1" + resolved "https://registry.npmjs.org/@backstage/config-loader/-/config-loader-0.5.1.tgz#a80f2047e209d2f41b17ad715c632ab142385b39" + integrity sha512-PmwXERs5BIf77sUIg3Fhp/elkR6ELj0czmpUgP4cuEdtazi0WcmdjCUC2DjgsKJWvYggsL1o372QKHh/M5AMTQ== + dependencies: + "@backstage/cli-common" "^0.1.1" + "@backstage/config" "^0.1.1" + ajv "^7.0.3" + fs-extra "^9.0.0" + json-schema "^0.2.5" + json-schema-merge-allof "^0.7.0" + typescript-json-schema "^0.47.0" + yaml "^1.9.2" + yup "^0.29.3" + +"@backstage/core@^0.6.2": + version "0.6.3" + resolved "https://registry.npmjs.org/@backstage/core/-/core-0.6.3.tgz#574ca46ab59e18af07fcf3a248c4428d4dfcc302" + integrity sha512-OSpnvotyCkb6fL02T1Z5TLcogRlOzVIH8YBfNlKeo/Xo/JnlVp1uSpQxCWjAW/lzGK5gp6oh3ezFRjm69mTwSA== + dependencies: + "@backstage/config" "^0.1.3" + "@backstage/core-api" "^0.2.11" + "@backstage/theme" "^0.2.3" + "@material-ui/core" "^4.11.0" + "@material-ui/icons" "^4.9.1" + "@material-ui/lab" "4.0.0-alpha.45" + "@testing-library/react-hooks" "^3.4.2" + "@types/dagre" "^0.7.44" + "@types/prop-types" "^15.7.3" + "@types/react" "^16.9" + "@types/react-sparklines" "^1.7.0" + "@types/react-text-truncate" "^0.14.0" + classnames "^2.2.6" + clsx "^1.1.0" + d3-selection "^2.0.0" + d3-shape "^2.0.0" + d3-zoom "^2.0.0" + dagre "^0.8.5" + immer "^8.0.1" + lodash "^4.17.15" + material-table "^1.69.1" + prop-types "^15.7.2" + qs "^6.9.4" + rc-progress "^3.0.0" + react "^16.12.0" + react-dom "^16.12.0" + react-helmet "6.1.0" + react-hook-form "^6.6.0" + react-markdown "^5.0.2" + react-router "6.0.0-beta.0" + react-router-dom "6.0.0-beta.0" + react-sparklines "^1.7.0" + react-syntax-highlighter "^13.5.1" + react-text-truncate "^0.16.0" + react-use "^15.3.3" + remark-gfm "^1.0.0" + zen-observable "^0.8.15" + +"@backstage/plugin-catalog-backend@^0.6.5": + version "0.6.7" + resolved "https://registry.npmjs.org/@backstage/plugin-catalog-backend/-/plugin-catalog-backend-0.6.7.tgz#97d0ed6db6aeb04e09ed0fee05e76bacf66cc179" + integrity sha512-Ey9PXyUjmi8IxQNSXBhF6Sl2SmZ/iXCFHXnffH8XCdBFTZS8HDRE2tSOlIIU4XQKd6lJVuAGTekas2DkaS9/lA== + dependencies: + "@azure/msal-node" "^1.0.0-beta.3" + "@backstage/backend-common" "^0.6.0" + "@backstage/catalog-model" "^0.7.4" + "@backstage/config" "^0.1.4" + "@backstage/errors" "^0.1.1" + "@backstage/integration" "^0.5.1" + "@backstage/plugin-search-backend-node" "^0.1.2" + "@backstage/search-common" "^0.1.1" + "@octokit/graphql" "^4.5.8" + "@types/express" "^4.17.6" + "@types/ldapjs" "^1.0.9" + aws-sdk "^2.840.0" + codeowners-utils "^1.0.2" + core-js "^3.6.5" + cross-fetch "^3.0.6" + express "^4.17.1" + express-promise-router "^4.1.0" + fs-extra "^9.0.0" + git-url-parse "^11.4.4" + glob "^7.1.6" + knex "^0.95.1" + ldapjs "^2.2.0" + lodash "^4.17.15" + morgan "^1.10.0" + p-limit "^3.0.2" + qs "^6.9.4" + uuid "^8.0.0" + winston "^3.2.1" + yaml "^1.9.2" + yn "^4.0.0" + yup "^0.29.3" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -5520,6 +5659,20 @@ lz-string "^1.4.4" pretty-format "^26.6.2" +"@testing-library/dom@^7.22.3": + version "7.30.3" + resolved "https://registry.npmjs.org/@testing-library/dom/-/dom-7.30.3.tgz#779ea9bbb92d63302461800a388a5a890ac22519" + integrity sha512-7JhIg2MW6WPwyikH2iL3o7z+FTVgSOd2jqCwTAHqK7Qal2gRRYiUQyURAxtbK9VXm/UTyG9bRihv8C5Tznr2zw== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/runtime" "^7.12.5" + "@types/aria-query" "^4.2.0" + aria-query "^4.2.2" + chalk "^4.1.0" + dom-accessibility-api "^0.5.4" + lz-string "^1.4.4" + pretty-format "^26.6.2" + "@testing-library/jest-dom@^5.10.1": version "5.11.9" resolved "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.11.9.tgz#e6b3cd687021f89f261bd53cbe367041fbd3e975" @@ -5550,6 +5703,14 @@ "@babel/runtime" "^7.5.4" "@types/testing-library__react-hooks" "^3.4.0" +"@testing-library/react@^10.4.1": + version "10.4.9" + resolved "https://registry.npmjs.org/@testing-library/react/-/react-10.4.9.tgz#9faa29c6a1a217bf8bbb96a28bd29d7a847ca150" + integrity sha512-pHZKkqUy0tmiD81afs8xfiuseXfU/N7rAX3iKjeZYje86t9VaB0LrxYVa+OOsvkrveX5jCK3IjajVn2MbePvqA== + dependencies: + "@babel/runtime" "^7.10.3" + "@testing-library/dom" "^7.22.3" + "@testing-library/react@^11.2.5": version "11.2.6" resolved "https://registry.npmjs.org/@testing-library/react/-/react-11.2.6.tgz#586a23adc63615985d85be0c903f374dab19200b" @@ -5902,6 +6063,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 +6137,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" @@ -6313,6 +6489,11 @@ resolved "https://registry.npmjs.org/@types/node/-/node-10.17.35.tgz#58058f29b870e6ae57b20e4f6e928f02b7129f56" integrity sha512-gXx7jAWpMddu0f7a+L+txMplp3FnHl53OhQIF9puXKq3hDGY/GjH+MF04oWnV/adPSCrbtHumDCFwzq2VhltWA== +"@types/node@^12.0.0": + version "12.20.10" + resolved "https://registry.npmjs.org/@types/node/-/node-12.20.10.tgz#4dcb8a85a8f1211acafb88d72fafc7e3d2685583" + integrity sha512-TxCmnSSppKBBOzYzPR2BR25YlX5Oay8z2XGwFBInuA/Co0V9xJhLlW4kjbxKtgeNo3NOMbQP1A5Rc03y+XecPw== + "@types/node@^12.7.1": version "12.12.58" resolved "https://registry.npmjs.org/@types/node/-/node-12.12.58.tgz#46dae9b2b9ee5992818c8f7cee01ff4ce03ab44c" @@ -6901,7 +7082,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== @@ -9990,7 +10171,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 +10475,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== @@ -12844,6 +13025,15 @@ expect@^26.6.2: jest-message-util "^26.6.2" jest-regex-util "^26.0.0" +express-promise-router@^3.0.3: + version "3.0.3" + resolved "https://registry.npmjs.org/express-promise-router/-/express-promise-router-3.0.3.tgz#5e6d22a5a3f013d71833172fe8d7ab780c3f6b70" + integrity sha1-Xm0ipaPwE9cYMxcv6NereAw/a3A= + dependencies: + is-promise "^2.1.0" + lodash.flattendeep "^4.0.0" + methods "^1.0.0" + express-promise-router@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/express-promise-router/-/express-promise-router-4.1.0.tgz#79160e145c27610ba411bceb0552a36f11dbab4f" @@ -12867,6 +13057,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 +13631,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 +13672,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== @@ -14672,6 +14869,11 @@ hex-color-regex@^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" @@ -15120,6 +15322,11 @@ immer@8.0.1: resolved "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz#9c73db683e2b3975c424fb0572af5889877ae656" integrity sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA== +immer@^8.0.1: + version "8.0.4" + resolved "https://registry.npmjs.org/immer/-/immer-8.0.4.tgz#3a21605a4e2dded852fb2afd208ad50969737b7a" + integrity sha512-jMfL18P+/6P6epANRvRk6q8t+3gGhqsJ9EuJ25AXE+9bNTYtssvzeYbEd0mXRYWCmmXSIbnlpz6vd6iJlmGGGQ== + immer@^9.0.1: version "9.0.1" resolved "https://registry.npmjs.org/immer/-/immer-9.0.1.tgz#1116368e051f9a0fd188c5136b6efb74ed69c57f" @@ -18336,7 +18543,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 +18656,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,6 +21792,13 @@ qs@6.7.0: resolved "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== +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== + dependencies: + side-channel "^1.0.4" + qs@^6.5.2, qs@^6.6.0, qs@^6.7.0, qs@^6.9.1, qs@^6.9.4, qs@^6.9.6: version "6.9.6" resolved "https://registry.npmjs.org/qs/-/qs-6.9.6.tgz#26ed3c8243a431b2924aca84cc90471f35d5a0ee" @@ -22160,7 +22374,7 @@ react-string-replace@^0.4.1: dependencies: lodash "^4.17.4" -react-syntax-highlighter@^13.5.0: +react-syntax-highlighter@^13.5.0, react-syntax-highlighter@^13.5.1: version "13.5.3" resolved "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-13.5.3.tgz#9712850f883a3e19eb858cf93fad7bb357eea9c6" integrity sha512-crPaF+QGPeHNIblxxCdf2Lg936NAHKhNhuMzRL3F9ct6aYXL3NcZtCL0Rms9+qVo6Y1EQLdXGypBNSbPL/r+qg== @@ -22454,7 +22668,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== @@ -23634,6 +23848,15 @@ side-channel@^1.0.2: es-abstract "^1.17.0-next.1" object-inspect "^1.7.0" +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + sigmund@~1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" @@ -24588,6 +24811,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" @@ -24605,6 +24844,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" @@ -25586,6 +25833,17 @@ typedarray@^0.0.6: resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= +typescript-json-schema@^0.47.0: + version "0.47.0" + resolved "https://registry.npmjs.org/typescript-json-schema/-/typescript-json-schema-0.47.0.tgz#84dde5460b127c6774da81bf70b23c7e04857b13" + integrity sha512-A6NVwSOTSsNDHfaqDcDeKwwyXEeKqBHoAr20jcetnYj4e8C6zVFofAVhAuwsBXCRYiWEE/lyHrcxpsSpbIk0Mg== + dependencies: + "@types/json-schema" "^7.0.6" + glob "^7.1.6" + json-stable-stringify "^1.0.1" + typescript "^4.1.3" + yargs "^16.2.0" + typescript-json-schema@^0.49.0: version "0.49.0" resolved "https://registry.npmjs.org/typescript-json-schema/-/typescript-json-schema-0.49.0.tgz#442f6347ca85fb0d9811f217fb0d6537b68734b3" @@ -26095,7 +26353,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== @@ -26833,7 +27091,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==