Merge remote-tracking branch 'origin/master' into erikengervall/plugin-release-manager-as-a-service

This commit is contained in:
Erik Engervall
2021-04-21 21:11:02 +02:00
115 changed files with 18117 additions and 329 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core': patch
---
Sort the table filter options by name.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-import': patch
---
allow import from HTTP repositories
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-welcome': patch
---
Australian Greeting
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-common': patch
---
remove use of deprecated type HelmetOptions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': minor
---
Fix the schema / code mismatch in LDAP `set` config
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/theme': patch
---
Replace the link color in dark theme
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog': patch
---
Add low german greeting
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/core-api': patch
'@backstage/core': patch
---
Fixed a potentially confusing error being thrown about misuse of routable extensions where the error was actually something different.
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-fossa': patch
---
Add a `FossaPage` that shows the license compliance status of all components in the catalog.
See the projects `Readme` on how to use it.
+2
View File
@@ -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
+4
View File
@@ -10,6 +10,7 @@ Chai
Changesets
Chanwit
Cloudformation
Cobertura
Codecov
Codehilite
Config
@@ -22,6 +23,7 @@ Dockerize
Docusaurus
Env
Expedia
Fargate
Figma
Firekube
Fiverr
@@ -33,6 +35,7 @@ Hackathons
Heroku
Hostname
Iain
JaCoCo
JavaScript
Kaewkasi
Knex
@@ -77,6 +80,7 @@ Tolerations
WWW
Weaveworks
Webpack
XML
Zalando
Zhou
abc
+3
View File
@@ -124,6 +124,9 @@ integrations:
github:
- host: github.com
token: ${GITHUB_TOKEN}
- host: ghe.spotify.net
apiBaseUrl: https://ghe.spotify.net/api/v3
token: ${GHE_TOKEN}
### Example for how to add your GitHub Enterprise instance using the API:
# - host: ghe.example.net
# apiBaseUrl: https://ghe.example.net/api/v3
@@ -0,0 +1,16 @@
# Deploying Backstage with AWS Fargate and Aurora
There are any number of ways to deploy backstage containers on AWS. One of the
simplest from a management and ops perspective is to leverage AWS Fargate and
Aurora PostgreSQL, where both the container orchestration and database clusters
are AWS managed services.
However, to use them properly there are a large number of supporting resources
(VPCs, Security Groups, Load Balancers, Certificate, etc) required. One approach
is to use [AWS Cloud Development Kit (CDK)](https://aws.amazon.com/cdk/) to
simplify and automate the creation of the entire infrastructure stack along with
automating the build and deploy of the container.
Check out the [Backstage On AWS](https://github.com/rbogle/backstage-on-aws)
repository, for a ready to use CDK application for deploying your custom
Backstage app onto AWS ECS Fargate and Aurora.
+1 -3
View File
@@ -1,12 +1,10 @@
---
id: helm
title: Deploying Backstage with Helm
title: Deploying with Helm
description: How to deploy Backstage with Helm and Kubernetes
sidebar_label: Helm
---
# Helm charts
An example Backstage app can be deployed in Kubernetes using the
[Backstage Helm charts](https://github.com/backstage/backstage/tree/master/contrib/chart/backstage).
+3 -1
View File
@@ -30,7 +30,9 @@ There is also an example of deploying on [Heroku](heroku.md), which only
requires the first two steps.
An example of deploying Backstage with a [Helm chart](helm.md), a common pattern
in AWS, is also available.
in AWS, is also available. There is also a contrib guide to deploying Backstage
with
[AWS Fargate and Aurora PostgreSQL](https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/aws-fargate-deployment.md)
Please consider contributing other deployment guides if you get Backstage set up
on common infrastructure, it would be a great benefit to the community.
@@ -273,6 +273,20 @@ project within your organization.
Specifying this annotation may enable SonarQube related features in Backstage
for that entity.
### backstage.io/code-coverage
```yaml
# Example:
metadata:
annotations:
backstage.io/code-coverage: scm-only
```
The value of this annotation controls the code-coverage backstage plugin. If set
to `scm-only`, the plugin will only take into account files stored in source
control (e.g. ignoring generated code). If set to `enabled`, all files covered
by a coverage report will be taken into account.
## Deprecated Annotations
The following annotations are deprecated, and only listed here to aid in
+16 -12
View File
@@ -3,28 +3,32 @@ id: installation
title: Datadog RUM Installation
sidebar_label: Installation
# prettier-ignore
description: Adding Datadog RUM to Your App
description: Adding Datadog Real User Monitoring (RUM) to Your App
---
There is a basic [Datadog](https://docs.datadoghq.com/real_user_monitoring/)
Datadog Real User Monitoring (RUM) allows you to visualize and analyze the
real-time performance and user journeys of your application's individual users.
This is an option to profile and monitor the user experience of your Backstage
installation.
There is a basic [Datadog RUM](https://docs.datadoghq.com/real_user_monitoring/)
integration built into Backstage. You can enable it by adding the following to
your app configuration:
your `app-config.yaml`:
```yaml
app:
datadogRum:
clientToken: '123456789'
applicationId: qwerty
# site: # datadoghq.eu default = datadoghq.com
# env: # optional
# site: datadoghq.eu
# env: 'staging'
```
Replace the clientToken and applicationId with the ones generated for you
Datadog.
The `clientToken` and `applicationId` are generated from the Datadog RUM page
following
[these instructions](https://docs.datadoghq.com/real_user_monitoring/browser/).
optional arguments:
There are two optional arguments:
```
site: datadoghq.eu # default equals datadoghq.com
env: dev # allow to specify the environment
```
- `site`: The Datadog site of your organization; defaults to `datadoghq.com`
- `env`: The application environment for Datadog events (no default)
+5
View File
@@ -109,6 +109,11 @@
"integrations/bitbucket/discovery"
]
},
{
"type": "subcategory",
"label": "Datadog",
"ids": ["integrations/datadog-rum/installation"]
},
{
"type": "subcategory",
"label": "GitHub",
+1
View File
@@ -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",
@@ -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 = (
<EntityGithubInsightsContent />
</EntityLayout.Route>
<EntityLayout.Route path="/code-coverage" title="Code Coverage">
<EntityCodeCoverageContent />
</EntityLayout.Route>
<EntityLayout.Route path="/kafka" title="Kafka">
<EntityKafkaContent />
</EntityLayout.Route>
@@ -347,6 +352,10 @@ const websiteEntityPage = (
<EntityGithubInsightsContent />
</EntityLayout.Route>
<EntityLayout.Route path="/code-coverage" title="Code Coverage">
<EntityCodeCoverageContent />
</EntityLayout.Route>
<EntityLayout.Route path="/todos" title="TODOs">
<EntityTodoContent />
</EntityLayout.Route>
@@ -18,7 +18,7 @@ import { Config } from '@backstage/config';
import compression from 'compression';
import cors from 'cors';
import express, { Router } from 'express';
import helmet, { HelmetOptions } from 'helmet';
import helmet from 'helmet';
import * as http from 'http';
import stoppable from 'stoppable';
import { Logger } from 'winston';
@@ -195,14 +195,7 @@ export class ServiceBuilderImpl implements ServiceBuilder {
});
}
private getOptions(): {
port: number;
host: string;
logger: Logger;
corsOptions?: cors.CorsOptions;
httpsSettings?: HttpsSettings;
helmetOptions: HelmetOptions;
} {
private getOptions() {
return {
port: this.port ?? DEFAULT_PORT,
host: this.host ?? DEFAULT_HOST,
+1
View File
@@ -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",
+5
View File
@@ -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));
@@ -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,
});
}
@@ -47,12 +47,15 @@ export function createRoutableExtension<
// Validate that the routing is wired up correctly in the App.tsx
try {
useRouteRef(mountPoint);
} catch {
throw new Error(
`Routable extension component with mount point ${mountPoint} was not discovered in the app element tree. ` +
'Routable extension components may not be rendered by other components and must be ' +
'directly available as an element within the App provider component.',
);
} catch (error) {
if (error?.message.startsWith('No path for ')) {
throw new Error(
`Routable extension component with mount point ${mountPoint} was not discovered in the app element tree. ` +
'Routable extension components may not be rendered by other components and must be ' +
'directly available as an element within the App provider component.',
);
}
throw error;
}
return <InnerComponent {...props} />;
};
+1 -1
View File
@@ -363,7 +363,7 @@ export function Table<T extends object = {}>({
placeholder: 'All results',
label: filter.column,
multiple: filter.type === 'multiple-select',
items: [...extractDistinctValues(filter.column)].map(value => ({
items: [...extractDistinctValues(filter.column)].sort().map(value => ({
label: value,
value,
})),
+4 -3
View File
@@ -102,7 +102,8 @@ export const darkTheme = createTheme({
},
},
primary: {
main: '#8CB4E5',
main: '#9CC9FF',
dark: '#82BAFD',
},
secondary: {
main: '#FF88B2',
@@ -124,8 +125,8 @@ export const darkTheme = createTheme({
errorText: '#CA001B',
infoText: '#004e8a',
warningText: '#000000',
linkHover: '#2196F3',
link: '#0A6EBE',
linkHover: '#82BAFD',
link: '#9CC9FF',
gold: yellow.A700,
navigation: {
background: '#424242',
@@ -88,7 +88,7 @@ describe('readLdapConfig', () => {
filter: 'f',
paged: true,
},
set: [{ path: 'p', value: 'v' }],
set: { p: 'v' },
map: {
rdn: 'u',
name: 'v',
@@ -107,7 +107,7 @@ describe('readLdapConfig', () => {
filter: 'f',
paged: true,
},
set: [{ path: 'p', value: 'v' }],
set: { p: 'v' },
map: {
rdn: 'u',
name: 'v',
@@ -136,7 +136,7 @@ describe('readLdapConfig', () => {
filter: 'f',
paged: true,
},
set: [{ path: 'p', value: 'v' }],
set: { p: 'v' },
map: {
rdn: 'u',
name: 'v',
@@ -155,7 +155,7 @@ describe('readLdapConfig', () => {
filter: 'f',
paged: true,
},
set: [{ path: 'p', value: 'v' }],
set: { p: 'v' },
map: {
rdn: 'u',
name: 'v',
@@ -182,9 +182,9 @@ describe('readLdapConfig', () => {
dn: 'udn',
options: {
filter: `
(|
(cn=foo bar)
(cn=bar)
(|
(cn=foo bar)
(cn=bar)
)
`,
},
@@ -57,7 +57,7 @@ export type UserConfig = {
// default is scope "one" and attributes "*" and "+".
options: SearchOptions;
// JSON paths (on a.b.c form) and hard coded values to set on those paths
set?: { path: string; value: JsonValue }[];
set?: { [path: string]: JsonValue };
// Mappings from well known entity fields, to LDAP attribute names
map: {
// The name of the attribute that holds the relative distinguished name of
@@ -94,7 +94,7 @@ export type GroupConfig = {
// Only the scope, filter, attributes, and paged fields are supported.
options: SearchOptions;
// JSON paths (on a.b.c form) and hard coded values to set on those paths
set?: { path: string; value: JsonValue }[];
set?: { [path: string]: JsonValue };
// Mappings from well known entity fields, to LDAP attribute names
map: {
// The name of the attribute that holds the relative distinguished name of
@@ -189,15 +189,12 @@ export function readLdapConfig(config: Config): LdapProviderConfig[] {
}
function readSetConfig(
c: Config[] | undefined,
): { path: string; value: JsonValue }[] | undefined {
c: Config | undefined,
): { [path: string]: JsonValue } | undefined {
if (!c) {
return undefined;
}
return c.map(entry => ({
path: entry.getString('path'),
value: entry.get('value'),
}));
return Object.fromEntries(c.keys().map(path => [path, c.get(path)]));
}
function readUserMapConfig(
@@ -244,7 +241,7 @@ export function readLdapConfig(config: Config): LdapProviderConfig[] {
return {
dn: c.getString('dn'),
options: readOptionsConfig(c.getOptionalConfig('options')),
set: readSetConfig(c.getOptionalConfigArray('set')),
set: readSetConfig(c.getOptionalConfig('set')),
map: readUserMapConfig(c.getOptionalConfig('map')),
};
}
@@ -255,7 +252,7 @@ export function readLdapConfig(config: Config): LdapProviderConfig[] {
return {
dn: c.getString('dn'),
options: readOptionsConfig(c.getOptionalConfig('options')),
set: readSetConfig(c.getOptionalConfigArray('set')),
set: readSetConfig(c.getOptionalConfig('set')),
map: readGroupMapConfig(c.getOptionalConfig('map')),
};
}
@@ -15,6 +15,7 @@
*/
import { GroupEntity, UserEntity } from '@backstage/catalog-model';
import { SearchEntry } from 'ldapjs';
import lodashSet from 'lodash/set';
import { buildOrgHierarchy } from '../util/org';
import { LdapClient } from './client';
@@ -25,7 +26,6 @@ import {
LDAP_UUID_ANNOTATION,
} from './constants';
import { LdapVendor } from './vendors';
import { SearchEntry } from 'ldapjs';
/**
* Reads users out of an LDAP provider.
@@ -63,7 +63,7 @@ export async function readLdapUsers(
};
if (set) {
for (const { path, value } of set) {
for (const [path, value] of Object.entries(set)) {
lodashSet(entity, path, value);
}
}
@@ -142,7 +142,7 @@ export async function readLdapGroups(
};
if (set) {
for (const { path, value } of set) {
for (const [path, value] of Object.entries(set)) {
lodashSet(entity, path, value);
}
}
@@ -127,7 +127,9 @@ describe('<StepInitAnalyzeUrl />', () => {
expect(catalogImportApi.analyzeUrl).toBeCalledTimes(0);
expect(onAnalysisFn).toBeCalledTimes(0);
expect(errorApi.post).toBeCalledTimes(0);
expect(getByText('Must start with https://.')).toBeInTheDocument();
expect(
getByText('Must start with http:// or https://.'),
).toBeInTheDocument();
});
it('should analyze single location', async () => {
@@ -132,8 +132,8 @@ export const StepInitAnalyzeUrl = ({
validate: {
httpsValidator: (value: any) =>
(typeof value === 'string' &&
value.match(/^https:\/\//) !== null) ||
'Must start with https://.',
value.match(/^http[s]?:\/\//) !== null) ||
'Must start with http:// or https://.',
},
})}
required
@@ -11,6 +11,7 @@
"Armenian": "Բարի կէսօր:",
"Assamese": "শুভ আবেলি",
"Asturian": "Bones tardes",
"Australian": "G'day",
"Azerbaijani": "Günortanız xeyir",
"Bashkir": "хәйерле көн",
"Basque": "Arratsalde on",
@@ -68,6 +69,7 @@
"Latvian": "Labdien",
"Limburgish": "Gojemiddig",
"Lithuanian": "Laba diena",
"Low German": "Moin",
"Lozi": "Ki musihali",
"Lule Sámi": "Buorre biejvve",
"Luxembourgish": "Gudde Mëtteg",
@@ -10,6 +10,7 @@
"Asante": "Maadwó",
"Asturian": "Bona nuechi",
"Ateso": "Akwar najokan",
"Australian": "G'day",
"Aymara": "Winas tartis",
"Azerbaijani": "Axşamınız xeyir",
"Basque": "Arratsalde on",
@@ -56,6 +57,7 @@
"Kurdish": "Evarbash",
"Latvian": "Labvakar",
"Lithuanian": "Labas vakaras",
"Low German": "Moin",
"Luganda": "Osiibye otya nno",
"Luo": "Oimore",
"Luxembourgish": "Gudden Owend",
@@ -4,6 +4,7 @@
"Albanian": "Mirëmëngjes",
"Arabic": "صباح الخير",
"Armenian": "Paree looys",
"Australian": "G'day",
"Azerbaijani": "Sabahınız xeyir",
"Bengali": "shuprobhat",
"Bulgarian": "Dobro utro",
@@ -32,6 +33,7 @@
"Kazakh": "Kayırlı tan",
"Kurdish": "Beyanî baş",
"Lithuanian": "Labas rytas",
"Low German": "Moin",
"Macedonian": "Добро утро",
"Malay": "Selamat pagi",
"Maltese": "Bonġu",
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
};
+154
View File
@@ -0,0 +1,154 @@
# code-coverage-backend
This is the backend part of the `code-coverage` plugin. It takes care of processing various coverage formats and standardizing them into a single json format, used by the frontend.
## Configuring your entity
In order to use this plugin, you must set the `backstage.io/code-coverage` annotation.
```yaml
metadata:
annotations:
backstage.io/code-coverage: enabled
```
There's a feature to only include files that are in VCS in the coverage report, this is helpful to not count generated files for example. To enable this set the `backstage.io/code-coverage` annotation to `scm-only`.
```yaml
metadata:
annotations:
backstage.io/code-coverage: scm-only
```
Note: It may be required to set the [`backstage.io/source-location` annotation](https://backstage.io/docs/features/software-catalog/well-known-annotations#backstageiosource-location), however this should generally not be needed.
## API
### Adding a Cobertura report
POST a Cobertura XML file to `/report`
Example:
```json
// curl -X POST -H "Content-Type:text/xml" -d @cobertura.xml "localhost:7000/api/code-coverage/report?entity=component:default/entity-name&coverageType=cobertura"
{
"links": [
{
"href": "http://localhost:7000/api/code-coverage/report?entity=component:default/entity-name",
"rel": "coverage"
}
]
}
```
### Adding a JaCoCo report
POST a JaCoCo XML file to `/report`
Example:
```json
// curl -X POST -H "Content-Type:text/xml" -d @jacoco.xml "localhost:7000/api/code-coverage/report?entity=component:default/entity-name&coverageType=jacoco"
{
"links": [
{
"href": "http://localhost:7000/api/code-coverage/report?entity=component:default/entity-name",
"rel": "coverage"
}
]
}
```
### Reading json coverage
GET `/report`
Example:
```json
// curl localhost:7000/api/code-coverage/report?entity=component:default/entity-name
{
"aggregate": {
"branch": {
"available": 0,
"covered": 0,
"missed": 0,
"percentage": 0
},
"line": {
"available": 5,
"covered": 4,
"missed": 1,
"percentage": 80
}
},
"entity": {
"kind": "Component",
"name": "entity-name",
"namespace": "default"
},
"files": [
{
"branchHits": {},
"filename": "main.go",
"lineHits": {
"117": 12,
"142": 8,
"34": 8,
"42": 0,
"58": 6
}
}
]
}
```
### Coverage history
GET `/history`
Example
```json
// curl localhost:7000/api/code-coverage/history?entity=component:default/entity-name
{
"entity": {
"kind": "Component",
"name": "entity-name",
"namespace": "default"
},
"history": [
{
"branch": {
"available": 0,
"covered": 0,
"missed": 0,
"percentage": 0
},
"line": {
"available": 299,
"covered": 116,
"missed": 183,
"percentage": 38.8
},
"timestamp": 1615490766141
},
{
"branch": {
"available": 0,
"covered": 0,
"missed": 0,
"percentage": 0
},
"line": {
"available": 299,
"covered": 116,
"missed": 183,
"percentage": 38.8
},
"timestamp": 1615406307929
}
]
}
```
@@ -0,0 +1,65 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// @ts-check
/**
* @param {import('knex').Knex} knex
*/
exports.up = async function up(knex) {
// Note for the reader: the knex increments types automatically make it a
// primary column, whether you like it or not. That's why the id column is
// not marked as primary as one might have expected; it's only used for
// lookups by ID. Because, SQLite and MySQL don't return RETURNING on
// inserts ... so we want a manually generated key for lookups (an uuid),
// and also an index for ordering guarantees :)
await knex.schema.createTable('code_coverage', table => {
table.comment('The table of code coverage');
table
.bigIncrements('index')
.notNullable()
.comment('An insert counter to ensure ordering');
table.uuid('id').notNullable().comment('The ID of the code coverage');
table
.text('entity')
.notNullable()
.comment('The entity ref that this code coverage applies to');
table
.text('coverage')
.notNullable()
.comment('The coverage json as a string');
table
.dateTime('created_at')
.defaultTo(knex.fn.now())
.notNullable()
.comment('The timestamp when this entry was created');
table.index('index', 'code_coverage_index_idx');
table.index('id', 'code_coverage_id_idx');
table.index('entity', 'code_coverage_entity_idx');
});
};
/**
* @param {import('knex').Knex} knex
*/
exports.down = async function down(knex) {
await knex.schema.alterTable('code_coverage', table => {
table.dropIndex([], 'code_coverage_index_idx');
table.dropIndex([], 'code_coverage_id_idx');
table.dropIndex([], 'code_coverage_entity_idx');
});
await knex.schema.dropTable('code_coverage');
};
@@ -0,0 +1,49 @@
{
"name": "@backstage/plugin-code-coverage-backend",
"version": "0.1.1",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
},
"scripts": {
"start": "backstage-cli backend:dev",
"build": "backstage-cli backend:build",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"prepack": "backstage-cli prepack",
"postpack": "backstage-cli postpack",
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^0.6.2",
"@backstage/catalog-client": "^0.3.10",
"@backstage/catalog-model": "^0.7.7",
"@backstage/config": "^0.1.4",
"@backstage/errors": "^0.1.1",
"@backstage/integration": "^0.5.1",
"@types/express": "^4.17.6",
"cross-fetch": "^3.0.6",
"express": "^4.17.1",
"express-promise-router": "^4.1.0",
"express-xml-bodyparser": "^0.3.0",
"knex": "^0.95.1",
"uuid": "^8.3.2",
"winston": "^3.2.1",
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.6.8",
"@types/express-xml-bodyparser": "^0.3.2",
"@types/supertest": "^2.0.8",
"msw": "^0.21.2",
"supertest": "^4.0.2",
"xml2js": "^0.4.23"
},
"files": [
"dist"
]
}
@@ -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';
+33
View File
@@ -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);
});
@@ -0,0 +1,198 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { SingleConnectionDatabaseManager } from '@backstage/backend-common';
import { stringifyEntityRef } from '@backstage/catalog-model';
import { ConfigReader } from '@backstage/config';
import {
CodeCoverageDatabase,
CodeCoverageStore,
} from './CodeCoverageDatabase';
import { JsonCodeCoverage } from './types';
const db = SingleConnectionDatabaseManager.fromConfig(
new ConfigReader({
backend: {
database: {
client: 'sqlite3',
connection: ':memory:',
},
},
}),
).forPlugin('code-coverage');
const coverage: Array<JsonCodeCoverage> = [
{
metadata: {
generationTime: 1234567890,
vcs: {
location: 'local',
type: 'local',
},
},
entity: {
kind: 'Component',
name: 'test-entity',
namespace: 'default',
},
files: [
{
filename: 'src/main.py',
lineHits: {
'10': 5,
'11': 4,
'12': 4,
},
branchHits: {},
},
],
},
{
metadata: {
generationTime: 2345678901,
vcs: {
location: 'local',
type: 'local',
},
},
entity: {
kind: 'Component',
name: 'test-entity',
namespace: 'default',
},
files: [
{
filename: 'src/main.py',
lineHits: {
'10': 5,
'11': 4,
'12': 4,
'22': 0,
'23': 0,
'24': 0,
'30': 1,
},
branchHits: {
'10': {
available: 2,
covered: 1,
missed: 1,
},
},
},
],
},
];
let database: CodeCoverageStore;
describe('CodeCoverageDatabase', () => {
beforeAll(async () => {
const client = await db.getClient();
database = await CodeCoverageDatabase.create(client);
await database.insertCodeCoverage(coverage[0]);
await database.insertCodeCoverage(coverage[1]);
});
describe('insertCodeCoverage', () => {
it('can insert code coverage', async () => {
const ncov = {
metadata: {
generationTime: 3456789012,
vcs: {
location: 'local',
type: 'local',
},
},
entity: {
kind: 'Component',
name: 'test-entity-for-insert',
namespace: 'default',
},
files: [
{
filename: 'src/main.py',
lineHits: {
'10': 5,
'11': 4,
'12': 4,
},
branchHits: {},
},
],
};
const { codeCoverageId } = await database.insertCodeCoverage(ncov);
expect(codeCoverageId.length).not.toBe(0);
});
});
describe('getCodeCoverage', () => {
it("can get coverage that's in the database", async () => {
const cov = await database.getCodeCoverage(
stringifyEntityRef({
name: 'test-entity',
kind: 'Component',
namespace: 'default',
}),
);
expect(cov).toEqual(coverage[1]);
});
});
describe('getHistory', () => {
it("can get history that's in the database", async () => {
const cov = await database.getHistory(
stringifyEntityRef({
name: 'test-entity',
kind: 'Component',
namespace: 'default',
}),
5,
);
expect(cov.history.length).toEqual(2);
expect(cov.history).toEqual([
{
branch: {
available: 2,
covered: 1,
missed: 1,
percentage: 50,
},
line: {
available: 7,
covered: 4,
missed: 3,
percentage: 57.14,
},
timestamp: 2345678901,
},
{
branch: {
available: 0,
covered: 0,
missed: 0,
percentage: 0,
},
line: {
available: 3,
covered: 3,
missed: 0,
percentage: 100,
},
timestamp: 1234567890,
},
]);
});
});
});
@@ -0,0 +1,117 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { resolvePackagePath } from '@backstage/backend-common';
import { NotFoundError } from '@backstage/errors';
import { parseEntityName, stringifyEntityRef } from '@backstage/catalog-model';
import { Knex } from 'knex';
import { v4 as uuid } from 'uuid';
import { aggregateCoverage } from './CoverageUtils';
import { JsonCodeCoverage, JsonCoverageHistory } from './types';
export type RawDbCoverageRow = {
id: string;
entity: string;
coverage: string;
};
export interface CodeCoverageStore {
insertCodeCoverage(
coverage: JsonCodeCoverage,
): Promise<{ codeCoverageId: string }>;
getCodeCoverage(entity: string): Promise<JsonCodeCoverage>;
getHistory(entity: string, limit: number): Promise<JsonCoverageHistory>;
}
const migrationsDir = resolvePackagePath(
'@backstage/plugin-code-coverage-backend',
'migrations',
);
export class CodeCoverageDatabase implements CodeCoverageStore {
static async create(knex: Knex): Promise<CodeCoverageStore> {
await knex.migrate.latest({
directory: migrationsDir,
});
return new CodeCoverageDatabase(knex);
}
constructor(private readonly db: Knex) {}
async insertCodeCoverage(
coverage: JsonCodeCoverage,
): Promise<{ codeCoverageId: string }> {
const codeCoverageId = uuid();
const entity = stringifyEntityRef({
kind: coverage.entity.kind,
namespace: coverage.entity.namespace,
name: coverage.entity.name,
});
await this.db<RawDbCoverageRow>('code_coverage').insert({
id: codeCoverageId,
entity: entity,
coverage: JSON.stringify(coverage),
});
return { codeCoverageId };
}
async getCodeCoverage(entity: string): Promise<JsonCodeCoverage> {
const [result] = await this.db<RawDbCoverageRow>('code_coverage')
.where({ entity: entity })
.orderBy('index', 'desc')
.limit(1)
.select();
if (!result) {
throw new NotFoundError(
`No coverage for entity '${JSON.stringify(entity)}' found`,
);
}
try {
return JSON.parse(result.coverage);
} catch (error) {
throw new Error(`Failed to parse coverage for '${entity}', ${error}`);
}
}
async getHistory(
entity: string,
limit: number,
): Promise<JsonCoverageHistory> {
const res = await this.db<RawDbCoverageRow>('code_coverage')
.where({ entity: entity })
.orderBy('index', 'desc')
.limit(limit)
.select();
const history = res
.map(r => JSON.parse(r.coverage))
.map(c => aggregateCoverage(c));
const entityName = parseEntityName(entity);
return {
entity: {
name: entityName.name,
kind: entityName.kind,
namespace: entityName.namespace,
},
history: history,
};
}
}
@@ -0,0 +1,326 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import fs from 'fs';
import { Readable } from 'stream';
import {
calculatePercentage,
aggregateCoverage,
CoverageUtils,
} from './CoverageUtils';
import { Entity } from '@backstage/catalog-model';
import { parseString } from 'xml2js';
describe('calculatePercentage', () => {
[
[100, 25, 25],
[100, 100, 100],
[133, 13, 9.77],
[0, 0, 0],
].forEach(([a, c, e]) => {
it(`${c}/${a} === ${e}`, () => {
expect(calculatePercentage(a, c)).toEqual(e);
});
});
});
/* eslint-disable no-restricted-syntax */
describe('aggregateCoverage', () => {
[
{
file: 'jacoco-jsoncoverage-files-1.json',
expected: {
branch: {
available: 68,
covered: 48,
missed: 20,
percentage: 70.59,
},
line: {
available: 162,
covered: 105,
missed: 57,
percentage: 64.81,
},
timestamp: 1234567890,
},
},
{
file: 'cobertura-jsoncoverage-files-1.json',
expected: {
branch: {
available: 0,
covered: 0,
missed: 0,
percentage: 0,
},
line: {
available: 146,
covered: 145,
missed: 1,
percentage: 99.32,
},
timestamp: 1234567890,
},
},
{
file: 'cobertura-jsoncoverage-files-2.json',
expected: {
branch: {
available: 466,
covered: 380,
missed: 86,
percentage: 81.55,
},
line: {
available: 2632,
covered: 2079,
missed: 553,
percentage: 78.99,
},
timestamp: 1234567890,
},
},
{
file: 'cobertura-jsoncoverage-files-3.json',
expected: {
branch: {
available: 73,
covered: 49,
missed: 24,
percentage: 67.12,
},
line: {
available: 110,
covered: 91,
missed: 19,
percentage: 82.73,
},
timestamp: 1234567890,
},
},
{
file: 'cobertura-jsoncoverage-files-4.json',
expected: {
branch: {
available: 0,
covered: 0,
missed: 0,
percentage: 0,
},
line: {
available: 325,
covered: 0,
missed: 325,
percentage: 0,
},
timestamp: 1234567890,
},
},
{
file: 'cobertura-jsoncoverage-files-5.json',
expected: {
branch: {
available: 38,
covered: 21,
missed: 17,
percentage: 55.26,
},
line: {
available: 175,
covered: 124,
missed: 51,
percentage: 70.86,
},
timestamp: 1234567890,
},
},
].forEach(td => {
it(`processes ${td.file}`, () => {
const json = JSON.parse(
fs.readFileSync(`${__dirname}/__fixtures__/${td.file}`).toString(),
);
const aggregate = aggregateCoverage({
metadata: {
generationTime: 1234567890,
vcs: {
location: 'foo',
type: 'foo',
},
},
entity: {
kind: 'foo',
name: 'foo',
namespace: 'default',
},
files: json,
});
expect(aggregate).toEqual(td.expected);
});
});
});
describe('CodeCoverageUtils', () => {
const scmFilesFixture = fs
.readFileSync(`${__dirname}/__fixtures__/cobertura-sourcefiles-1.txt`)
.toString()
.split('\n')
.map(f => {
return { path: f };
});
const scmIntegraions = {
byUrl: jest.fn().mockReturnValue({
type: 'local',
title: 'local',
}),
};
const scmTree = {
files: jest
.fn()
.mockReturnValue(new Promise((r, _e) => r(scmFilesFixture))),
};
const urlReader = {
readTree: jest.fn().mockReturnValue(new Promise((r, _e) => r(scmTree))),
};
const utils = new CoverageUtils(scmIntegraions, urlReader);
describe('validateRequestBody', () => {
it('rejects missing content type', () => {
let err: Error | null = null;
try {
const mockRequest: Partial<Request> = {
// @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<Request> = {
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<Request> = {
headers: {
// @ts-ignore
'content-type': 'text/xml',
},
// @ts-ignore
body: Readable.from(
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?><report name="example"></report>',
),
};
// @ts-ignore
const data: Readable = utils.validateRequestBody(mockRequest as Request);
expect(data.read()).toContain('<?xml');
});
});
describe('processCoveragePayload', () => {
const mockRequest: Partial<Request> = {
headers: {
// @ts-ignore
'content-type': 'text/xml',
},
// @ts-ignore
body: Readable.from(
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?><report name="example"></report>',
),
};
it('ignores scm if annotation is not set', async () => {
const entity: Entity = {
kind: 'Component',
metadata: {
name: 'test-entity',
namespace: 'test',
},
apiVersion: 'backstage.io/v1alpha1',
};
const {
scmFiles,
sourceLocation,
vcs,
body, // @ts-ignore
} = await utils.processCoveragePayload(entity, mockRequest);
let data;
// in normal flow the express app will already have done this through the middleware
parseString((body as Readable).read(), (_e, r) => {
data = r;
});
expect(scmFiles.length).toBe(0);
expect(vcs).toBeUndefined();
expect(sourceLocation).toBeUndefined();
expect(data).toEqual({
report: {
$: {
name: 'example',
},
},
});
});
it('populates scm data if annotation is set', async () => {
const entity: Entity = {
kind: 'Component',
metadata: {
name: 'test-entity',
namespace: 'test',
annotations: {
'backstage.io/code-coverage': 'scm-only',
'backstage.io/source-location':
'url:https://github.com/example/test/',
},
},
apiVersion: 'backstage.io/v1alpha1',
};
const {
scmFiles,
sourceLocation,
vcs, // @ts-ignore
} = await utils.processCoveragePayload(entity, mockRequest);
expect(scmFiles.length).toBe(scmFiles.length);
expect(vcs).not.toBeUndefined();
expect(sourceLocation).not.toBeUndefined();
});
});
});
@@ -0,0 +1,175 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Request } from 'express';
import { UrlReader } from '@backstage/backend-common';
import { InputError, NotFoundError } from '@backstage/errors';
import {
Entity,
getEntitySourceLocation,
stringifyEntityRef,
} from '@backstage/catalog-model';
import { ScmIntegration, ScmIntegrations } from '@backstage/integration';
import { AggregateCoverage, FileEntry, JsonCodeCoverage } from './types';
export const calculatePercentage = (
available: number,
covered: number,
): number => {
if (available === 0) {
return 0;
}
return parseFloat(((covered / available) * 100).toFixed(2));
};
export const aggregateCoverage = (c: JsonCodeCoverage): AggregateCoverage => {
let availableLine = 0;
let coveredLine = 0;
let availableBranch = 0;
let coveredBranch = 0;
c.files.forEach(f => {
availableLine += Object.keys(f.lineHits).length;
coveredLine += Object.values(f.lineHits).filter(l => l > 0).length;
availableBranch += Object.keys(f.branchHits)
.map(b => parseInt(b, 10))
.map((b: number) => f.branchHits[b].available)
.filter(Boolean)
.reduce((acc, curr) => acc + curr, 0);
coveredBranch += Object.keys(f.branchHits)
.map(b => parseInt(b, 10))
.map((b: number) => f.branchHits[b].covered)
.filter(Boolean)
.reduce((acc, curr) => acc + curr, 0);
});
return {
timestamp: c.metadata.generationTime,
branch: {
available: availableBranch,
covered: coveredBranch,
missed: availableBranch - coveredBranch,
percentage: calculatePercentage(availableBranch, coveredBranch),
},
line: {
available: availableLine,
covered: coveredLine,
missed: availableLine - coveredLine,
percentage: calculatePercentage(availableLine, coveredLine),
},
};
};
export class CoverageUtils {
constructor(
readonly scm: Partial<ScmIntegrations>,
readonly urlReader: Partial<UrlReader>,
) {}
async processCoveragePayload(
entity: Entity,
req: Request,
): Promise<{
sourceLocation?: string;
vcs?: ScmIntegration;
scmFiles: string[];
body: {};
}> {
const enforceScmFiles =
entity.metadata.annotations?.['backstage.io/code-coverage'] ===
'scm-only' || false;
let sourceLocation: string | undefined = undefined;
let vcs: ScmIntegration | undefined = undefined;
let scmFiles: string[] = [];
if (enforceScmFiles) {
try {
const sl = getEntitySourceLocation(entity);
sourceLocation = sl.target;
} catch (e: unknown) {
// TODO: logging
}
if (!sourceLocation) {
throw new InputError(
`No "backstage.io/source-location" annotation on entity ${stringifyEntityRef(
entity,
)}`,
);
}
vcs = this.scm.byUrl?.(sourceLocation);
if (!vcs) {
throw new InputError(`Unable to determine SCM from ${sourceLocation}`);
}
const scmTree = await this.urlReader.readTree?.(sourceLocation);
if (!scmTree) {
throw new NotFoundError(`Unable to read tree from ${sourceLocation}`);
}
scmFiles = (await scmTree.files()).map(f => f.path);
}
const body = this.validateRequestBody(req);
if (Object.keys(body).length === 0) {
throw new InputError('Unable to parse body');
}
return {
sourceLocation,
vcs,
scmFiles,
body,
};
}
async buildCoverage(
entity: Entity,
sourceLocation: string | undefined,
vcs: ScmIntegration | undefined,
files: FileEntry[],
): Promise<JsonCodeCoverage> {
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;
}
}
@@ -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": {}
}
]
@@ -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
}
}
}
]
@@ -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": {}
}
]
@@ -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
}
}
}
]
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,479 @@
<?xml version="1.0"?>
<coverage
line-rate="0.98" lines-valid="312" lines-covered="306" branches-valid="12" branches-covered="12" branch-rate="1.00" complexity="0" version="1.0" timestamp="1567499510503">
<packages>
<package name="com.example" line-rate="0.98" branch-rate="1.00" complexity="0">
<classes>
<class
name="com.example.Main" filename="com/example/Main.scala" line-rate="0.97" branch-rate="1.00" complexity="0">
<methods>
<method
name="com.example/Main/methodOne" signature="()V" line-rate="1.00" branch-rate="1.00" complexity="0">
<lines>
<line number="203" hits="1" branch="false"/>
<line number="202" hits="1" branch="false"/>
<line number="204" hits="1" branch="false"/>
<line number="203" hits="1" branch="false"/>
<line number="202" hits="1" branch="false"/>
<line number="204" hits="1" branch="false"/>
<line number="203" hits="1" branch="false"/>
</lines>
</method>
<method
name="com.example/Main/methodTwo" signature="()V" line-rate="1.00" branch-rate="1.00" complexity="0">
<lines>
<line number="164" hits="1" branch="false"/>
<line number="169" hits="1" branch="false"/>
<line number="170" hits="1" branch="false"/>
<line number="167" hits="1" branch="false"/>
<line number="168" hits="1" branch="false"/>
<line number="163" hits="1" branch="false"/>
<line number="163" hits="1" branch="false"/>
<line number="168" hits="1" branch="false"/>
<line number="169" hits="1" branch="false"/>
<line number="163" hits="1" branch="false"/>
<line number="170" hits="1" branch="false"/>
<line number="167" hits="1" branch="false"/>
<line number="166" hits="1" branch="false"/>
<line number="160" hits="1" branch="false"/>
<line number="168" hits="1" branch="false"/>
<line number="163" hits="1" branch="false"/>
<line number="169" hits="1" branch="false"/>
<line number="164" hits="1" branch="false"/>
</lines>
</method>
<method
name="com.example/Main/methodThree" signature="()V" line-rate="1.00" branch-rate="1.00" complexity="0">
<lines>
<line number="97" hits="1" branch="false"/>
<line number="104" hits="1" branch="false"/>
<line number="99" hits="1" branch="false"/>
<line number="95" hits="1" branch="false"/>
<line number="100" hits="1" branch="false"/>
<line number="104" hits="1" branch="false"/>
<line number="97" hits="1" branch="false"/>
<line number="105" hits="1" branch="false"/>
<line number="97" hits="1" branch="false"/>
<line number="97" hits="1" branch="false"/>
<line number="105" hits="1" branch="false"/>
<line number="100" hits="1" branch="false"/>
<line number="103" hits="1" branch="false"/>
</lines>
</method>
<method
name="com.example/Main/&lt;none&gt;" signature="()V" line-rate="1.00" branch-rate="1.00" complexity="0">
<lines>
<line number="30" hits="1" branch="false"/>
<line number="26" hits="1" branch="false"/>
<line number="28" hits="1" branch="false"/>
<line number="27" hits="1" branch="false"/>
<line number="30" hits="1" branch="false"/>
<line number="26" hits="1" branch="false"/>
<line number="27" hits="1" branch="false"/>
<line number="28" hits="1" branch="false"/>
</lines>
</method>
<method
name="com.example/Main/main" signature="()V" line-rate="1.00" branch-rate="1.00" complexity="0">
<lines>
<line number="56" hits="1" branch="false"/>
<line number="73" hits="1" branch="false"/>
<line number="77" hits="1" branch="false"/>
<line number="75" hits="1" branch="false"/>
<line number="44" hits="1" branch="false"/>
<line number="60" hits="1" branch="false"/>
<line number="35" hits="1" branch="false"/>
<line number="75" hits="1" branch="false"/>
<line number="65" hits="1" branch="false"/>
<line number="50" hits="1" branch="false"/>
<line number="37" hits="1" branch="false"/>
<line number="77" hits="1" branch="false"/>
<line number="37" hits="1" branch="false"/>
<line number="52" hits="1" branch="false"/>
<line number="65" hits="1" branch="false"/>
<line number="42" hits="1" branch="false"/>
<line number="58" hits="1" branch="false"/>
<line number="68" hits="1" branch="false"/>
<line number="33" hits="1" branch="false"/>
<line number="75" hits="1" branch="false"/>
<line number="48" hits="1" branch="false"/>
<line number="64" hits="1" branch="false"/>
<line number="61" hits="1" branch="false"/>
<line number="75" hits="1" branch="false"/>
<line number="33" hits="1" branch="false"/>
<line number="46" hits="1" branch="false"/>
<line number="53" hits="1" branch="false"/>
<line number="36" hits="1" branch="false"/>
<line number="65" hits="1" branch="false"/>
<line number="75" hits="1" branch="false"/>
<line number="38" hits="1" branch="false"/>
<line number="55" hits="1" branch="false"/>
<line number="68" hits="1" branch="false"/>
</lines>
</method>
</methods>
<lines>
<line number="203" hits="1" branch="false"/>
<line number="135" hits="1" branch="false"/>
<line number="149" hits="1" branch="false"/>
<line number="126" hits="1" branch="false"/>
<line number="164" hits="1" branch="false"/>
<line number="169" hits="1" branch="false"/>
<line number="183" hits="1" branch="false"/>
<line number="56" hits="1" branch="false"/>
<line number="138" hits="1" branch="false"/>
<line number="143" hits="1" branch="false"/>
<line number="188" hits="1" branch="false"/>
<line number="170" hits="1" branch="false"/>
<line number="112" hits="1" branch="false"/>
<line number="127" hits="1" branch="false"/>
<line number="86" hits="1" branch="false"/>
<line number="97" hits="1" branch="false"/>
<line number="104" hits="1" branch="false"/>
<line number="73" hits="1" branch="false"/>
<line number="77" hits="1" branch="false"/>
<line number="167" hits="1" branch="false"/>
<line number="210" hits="1" branch="false"/>
<line number="88" hits="1" branch="false"/>
<line number="99" hits="1" branch="false"/>
<line number="75" hits="1" branch="false"/>
<line number="30" hits="1" branch="false"/>
<line number="84" hits="1" branch="false"/>
<line number="44" hits="1" branch="false"/>
<line number="60" hits="1" branch="false"/>
<line number="182" hits="1" branch="false"/>
<line number="124" hits="1" branch="false"/>
<line number="125" hits="1" branch="false"/>
<line number="168" hits="1" branch="false"/>
<line number="146" hits="1" branch="false"/>
<line number="114" hits="1" branch="false"/>
<line number="163" hits="1" branch="false"/>
<line number="35" hits="1" branch="false"/>
<line number="114" hits="1" branch="false"/>
<line number="88" hits="1" branch="false"/>
<line number="75" hits="1" branch="false"/>
<line number="65" hits="1" branch="false"/>
<line number="85" hits="1" branch="false"/>
<line number="50" hits="1" branch="false"/>
<line number="26" hits="1" branch="false"/>
<line number="95" hits="1" branch="false"/>
<line number="28" hits="1" branch="false"/>
<line number="37" hits="1" branch="false"/>
<line number="202" hits="1" branch="false"/>
<line number="210" hits="1" branch="false"/>
<line number="181" hits="1" branch="false"/>
<line number="163" hits="1" branch="false"/>
<line number="168" hits="1" branch="false"/>
<line number="85" hits="1" branch="false"/>
<line number="169" hits="1" branch="false"/>
<line number="183" hits="1" branch="false"/>
<line number="148" hits="1" branch="false"/>
<line number="126" hits="1" branch="false"/>
<line number="163" hits="1" branch="false"/>
<line number="135" hits="1" branch="false"/>
<line number="100" hits="1" branch="false"/>
<line number="88" hits="1" branch="false"/>
<line number="114" hits="1" branch="false"/>
<line number="204" hits="1" branch="false"/>
<line number="127" hits="1" branch="false"/>
<line number="140" hits="1" branch="false"/>
<line number="104" hits="1" branch="false"/>
<line number="97" hits="1" branch="false"/>
<line number="77" hits="1" branch="false"/>
<line number="86" hits="1" branch="false"/>
<line number="188" hits="1" branch="false"/>
<line number="27" hits="1" branch="false"/>
<line number="37" hits="1" branch="false"/>
<line number="170" hits="1" branch="false"/>
<line number="210" hits="1" branch="false"/>
<line number="52" hits="1" branch="false"/>
<line number="65" hits="1" branch="false"/>
<line number="115" hits="1" branch="false"/>
<line number="145" hits="1" branch="false"/>
<line number="167" hits="1" branch="false"/>
<line number="42" hits="1" branch="false"/>
<line number="136" hits="1" branch="false"/>
<line number="30" hits="1" branch="false"/>
<line number="124" hits="1" branch="false"/>
<line number="125" hits="1" branch="false"/>
<line number="105" hits="1" branch="false"/>
<line number="88" hits="1" branch="false"/>
<line number="84" hits="1" branch="false"/>
<line number="58" hits="1" branch="false"/>
<line number="68" hits="1" branch="false"/>
<line number="97" hits="1" branch="false"/>
<line number="33" hits="1" branch="false"/>
<line number="75" hits="1" branch="false"/>
<line number="26" hits="1" branch="false"/>
<line number="48" hits="1" branch="false"/>
<line number="64" hits="1" branch="false"/>
<line number="203" hits="1" branch="false"/>
<line number="187" hits="1" branch="false"/>
<line number="179" hits="1" branch="false"/>
<line number="166" hits="1" branch="false"/>
<line number="97" hits="1" branch="false"/>
<line number="127" hits="1" branch="false"/>
<line number="138" hits="1" branch="false"/>
<line number="210" hits="1" branch="false"/>
<line number="188" hits="1" branch="false"/>
<line number="202" hits="1" branch="false"/>
<line number="160" hits="1" branch="false"/>
<line number="168" hits="1" branch="false"/>
<line number="178" hits="1" branch="false"/>
<line number="105" hits="1" branch="false"/>
<line number="112" hits="1" branch="false"/>
<line number="135" hits="1" branch="false"/>
<line number="147" hits="1" branch="false"/>
<line number="100" hits="1" branch="false"/>
<line number="85" hits="1" branch="false"/>
<line number="88" hits="1" branch="false"/>
<line number="114" hits="1" branch="false"/>
<line number="204" hits="1" branch="false"/>
<line number="124" hits="1" branch="false"/>
<line number="183" hits="1" branch="false"/>
<line number="61" hits="1" branch="false"/>
<line number="75" hits="1" branch="false"/>
<line number="163" hits="1" branch="false"/>
<line number="169" hits="1" branch="false"/>
<line number="33" hits="1" branch="false"/>
<line number="46" hits="1" branch="false"/>
<line number="86" hits="1" branch="false"/>
<line number="53" hits="1" branch="false"/>
<line number="88" hits="1" branch="false"/>
<line number="27" hits="1" branch="false"/>
<line number="36" hits="1" branch="false"/>
<line number="126" hits="1" branch="false"/>
<line number="139" hits="1" branch="false"/>
<line number="150" hits="1" branch="false"/>
<line number="115" hits="1" branch="false"/>
<line number="65" hits="1" branch="false"/>
<line number="75" hits="1" branch="false"/>
<line number="164" hits="1" branch="false"/>
<line number="103" hits="1" branch="false"/>
<line number="38" hits="1" branch="false"/>
<line number="83" hits="1" branch="false"/>
<line number="87" hits="1" branch="false"/>
<line number="55" hits="1" branch="false"/>
<line number="68" hits="1" branch="false"/>
<line number="28" hits="1" branch="false"/>
<line number="210" hits="1" branch="false"/>
<line number="203" hits="1" branch="false"/>
</lines>
</class>
<class
name="com.example.Other" filename="com/example/Other.scala" line-rate="0.99" branch-rate="1.00" complexity="0">
<methods>
<method
name="com.example/Other/methodOne" signature="()V" line-rate="1.00" branch-rate="1.00" complexity="0">
<lines>
<line number="42" hits="1" branch="false"/>
<line number="50" hits="1" branch="false"/>
<line number="46" hits="1" branch="false"/>
<line number="52" hits="1" branch="false"/>
<line number="50" hits="1" branch="false"/>
<line number="44" hits="1" branch="false"/>
<line number="49" hits="1" branch="false"/>
<line number="50" hits="1" branch="false"/>
<line number="50" hits="1" branch="false"/>
<line number="42" hits="1" branch="false"/>
<line number="50" hits="1" branch="false"/>
<line number="52" hits="1" branch="false"/>
<line number="45" hits="1" branch="false"/>
<line number="49" hits="1" branch="false"/>
</lines>
</method>
<method
name="com.example/Other/methodTwo" signature="()V" line-rate="1.00" branch-rate="1.00" complexity="0">
<lines>
<line number="133" hits="1" branch="false"/>
<line number="133" hits="1" branch="false"/>
<line number="134" hits="1" branch="false"/>
<line number="132" hits="1" branch="false"/>
</lines>
</method>
</methods>
<lines>
<line number="91" hits="1" branch="false"/>
<line number="93" hits="1" branch="false"/>
<line number="68" hits="1" branch="false"/>
<line number="42" hits="1" branch="false"/>
<line number="79" hits="1" branch="false"/>
<line number="50" hits="1" branch="false"/>
<line number="117" hits="1" branch="false"/>
<line number="59" hits="1" branch="false"/>
<line number="21" hits="1" branch="false"/>
<line number="31" hits="1" branch="false"/>
<line number="111" hits="1" branch="false"/>
<line number="105" hits="1" branch="false"/>
<line number="61" hits="1" branch="false"/>
<line number="46" hits="1" branch="false"/>
<line number="25" hits="1" branch="false"/>
<line number="52" hits="1" branch="false"/>
<line number="32" hits="1" branch="false"/>
<line number="144" hits="1" branch="false"/>
<line number="146" hits="1" branch="false"/>
<line number="129" hits="1" branch="false"/>
<line number="147" hits="1" branch="false"/>
<line number="109" hits="1" branch="false"/>
<line number="93" hits="1" branch="false"/>
<line number="117" hits="1" branch="false"/>
<line number="104" hits="1" branch="false"/>
<line number="145" hits="1" branch="false"/>
<line number="133" hits="1" branch="false"/>
<line number="142" hits="1" branch="false"/>
<line number="50" hits="1" branch="false"/>
<line number="61" hits="1" branch="false"/>
<line number="129" hits="1" branch="false"/>
<line number="146" hits="1" branch="false"/>
<line number="44" hits="1" branch="false"/>
<line number="119" hits="1" branch="false"/>
<line number="74" hits="1" branch="false"/>
<line number="143" hits="1" branch="false"/>
<line number="146" hits="1" branch="false"/>
<line number="32" hits="1" branch="false"/>
<line number="111" hits="1" branch="false"/>
<line number="139" hits="1" branch="false"/>
<line number="92" hits="1" branch="false"/>
<line number="70" hits="1" branch="false"/>
<line number="103" hits="1" branch="false"/>
<line number="105" hits="1" branch="false"/>
<line number="140" hits="1" branch="false"/>
<line number="115" hits="1" branch="false"/>
<line number="93" hits="1" branch="false"/>
<line number="124" hits="1" branch="false"/>
<line number="103" hits="1" branch="false"/>
<line number="71" hits="1" branch="false"/>
<line number="86" hits="1" branch="false"/>
<line number="27" hits="1" branch="false"/>
<line number="33" hits="1" branch="false"/>
<line number="49" hits="1" branch="false"/>
<line number="59" hits="1" branch="false"/>
<line number="64" hits="1" branch="false"/>
<line number="129" hits="1" branch="false"/>
<line number="110" hits="1" branch="false"/>
<line number="50" hits="1" branch="false"/>
<line number="29" hits="1" branch="false"/>
<line number="37" hits="0" branch="false"/>
<line number="66" hits="1" branch="false"/>
<line number="72" hits="1" branch="false"/>
<line number="87" hits="1" branch="false"/>
<line number="59" hits="1" branch="false"/>
<line number="20" hits="1" branch="false"/>
<line number="149" hits="1" branch="false"/>
<line number="25" hits="1" branch="false"/>
<line number="146" hits="1" branch="false"/>
<line number="140" hits="1" branch="false"/>
<line number="113" hits="1" branch="false"/>
<line number="144" hits="1" branch="false"/>
<line number="123" hits="1" branch="false"/>
<line number="129" hits="1" branch="false"/>
<line number="105" hits="1" branch="false"/>
<line number="59" hits="1" branch="false"/>
<line number="94" hits="1" branch="false"/>
<line number="50" hits="1" branch="false"/>
<line number="72" hits="1" branch="false"/>
<line number="85" hits="1" branch="false"/>
<line number="144" hits="1" branch="false"/>
<line number="147" hits="1" branch="false"/>
<line number="65" hits="1" branch="false"/>
<line number="133" hits="1" branch="false"/>
<line number="142" hits="1" branch="false"/>
<line number="116" hits="1" branch="false"/>
<line number="104" hits="1" branch="false"/>
<line number="123" hits="1" branch="false"/>
<line number="110" hits="1" branch="false"/>
<line number="143" hits="1" branch="false"/>
<line number="42" hits="1" branch="false"/>
<line number="50" hits="1" branch="false"/>
<line number="23" hits="1" branch="false"/>
<line number="103" hits="1" branch="false"/>
<line number="119" hits="1" branch="false"/>
<line number="129" hits="1" branch="false"/>
<line number="74" hits="1" branch="false"/>
<line number="92" hits="1" branch="false"/>
<line number="145" hits="1" branch="false"/>
<line number="31" hits="1" branch="false"/>
<line number="105" hits="1" branch="false"/>
<line number="111" hits="1" branch="false"/>
<line number="61" hits="1" branch="false"/>
<line number="70" hits="1" branch="false"/>
<line number="139" hits="1" branch="false"/>
<line number="52" hits="1" branch="false"/>
<line number="86" hits="1" branch="false"/>
<line number="92" hits="1" branch="false"/>
<line number="33" hits="1" branch="false"/>
<line number="45" hits="1" branch="false"/>
<line number="26" hits="1" branch="false"/>
<line number="61" hits="1" branch="false"/>
<line number="70" hits="1" branch="false"/>
<line number="103" hits="1" branch="false"/>
<line number="34" hits="1" branch="false"/>
<line number="29" hits="1" branch="false"/>
<line number="129" hits="1" branch="false"/>
<line number="134" hits="1" branch="false"/>
<line number="149" hits="1" branch="false"/>
<line number="142" hits="1" branch="false"/>
<line number="145" hits="1" branch="false"/>
<line number="103" hits="1" branch="false"/>
<line number="115" hits="1" branch="false"/>
<line number="92" hits="1" branch="false"/>
<line number="113" hits="1" branch="false"/>
<line number="144" hits="1" branch="false"/>
<line number="70" hits="1" branch="false"/>
<line number="73" hits="1" branch="false"/>
<line number="138" hits="1" branch="false"/>
<line number="143" hits="1" branch="false"/>
<line number="146" hits="1" branch="false"/>
<line number="105" hits="1" branch="false"/>
<line number="129" hits="1" branch="false"/>
<line number="49" hits="1" branch="false"/>
<line number="59" hits="1" branch="false"/>
<line number="123" hits="1" branch="false"/>
<line number="132" hits="1" branch="false"/>
<line number="34" hits="1" branch="false"/>
<line number="86" hits="1" branch="false"/>
<line number="144" hits="1" branch="false"/>
<line number="27" hits="1" branch="false"/>
<line number="110" hits="1" branch="false"/>
<line number="116" hits="1" branch="false"/>
<line number="64" hits="1" branch="false"/>
<line number="94" hits="1" branch="false"/>
<line number="71" hits="1" branch="false"/>
<line number="103" hits="1" branch="false"/>
<line number="147" hits="1" branch="false"/>
<line number="142" hits="1" branch="false"/>
</lines>
</class>
</classes>
</package>
<package name="com.example.utils" line-rate="1.00" branch-rate="1.00" complexity="0">
<classes>
<class
name="com.example.utils.Util" filename="com/example/utils/Util.scala" line-rate="1.00" branch-rate="1.00" complexity="0">
<methods>
<method
name="com.example.utils/DateFormatUtils/utilMethod" signature="()V" line-rate="1.00" branch-rate="1.00" complexity="0">
<lines>
<line number="13" hits="1" branch="false"/>
</lines>
</method>
<method
name="com.example.utils/DateFormatUtils/utilFunc" signature="()V" line-rate="1.00" branch-rate="1.00" complexity="0">
<lines>
<line number="8" hits="1" branch="false"/>
<line number="7" hits="1" branch="false"/>
<line number="8" hits="1" branch="false"/>
</lines>
</method>
</methods>
<lines>
<line number="8" hits="1" branch="false"/>
<line number="7" hits="1" branch="false"/>
<line number="13" hits="1" branch="false"/>
<line number="8" hits="1" branch="false"/>
</lines>
</class>
</classes>
</package>
</packages>
</coverage>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,357 @@
<?xml version="1.0" ?>
<coverage lines-valid="524" lines-covered="455" line-rate="0.8683" branches-valid="653" branches-covered="465" branch-rate="0.7121" timestamp="1575309673957" complexity="0" version="0.1">
<packages>
<package name="src" line-rate="1" branch-rate="1">
<classes>
<class name="index.ts" filename="src/index.ts" line-rate="1" branch-rate="1">
<methods>
</methods>
<lines>
</lines>
</class>
</classes>
</package>
<package name="src.atoms.BoxShadow" line-rate="1" branch-rate="1">
<classes>
<class name="box-shadow.ts" filename="src/atoms/BoxShadow/box-shadow.ts" line-rate="1" branch-rate="1">
<methods>
</methods>
<lines>
<line number="4" hits="5" branch="false"/>
</lines>
</class>
<class name="index.ts" filename="src/atoms/BoxShadow/index.ts" line-rate="1" branch-rate="1">
<methods>
</methods>
<lines>
</lines>
</class>
</classes>
</package>
<package name="src.atoms.Button" line-rate="0.4706" branch-rate="0.7646999999999999">
<classes>
<class name="button.tsx" filename="src/atoms/Button/button.tsx" line-rate="0.4706" branch-rate="0.7646999999999999">
<methods>
<method name="ButtonComponent" hits="41" signature="()V">
<lines>
<line number="17" hits="41"/>
</lines>
</method>
<method name="(anonymous_1)" hits="0" signature="()V">
<lines>
<line number="34" hits="0"/>
</lines>
</method>
<method name="(anonymous_2)" hits="41" signature="()V">
<lines>
<line number="70" hits="41"/>
</lines>
</method>
<method name="(anonymous_3)" hits="41" signature="()V">
<lines>
<line number="72" hits="41"/>
</lines>
</method>
<method name="(anonymous_4)" hits="41" signature="()V">
<lines>
<line number="74" hits="41"/>
</lines>
</method>
</methods>
<lines>
<line number="31" hits="41" branch="false"/>
<line number="32" hits="41" branch="false"/>
<line number="34" hits="41" branch="false"/>
<line number="35" hits="0" branch="true" condition-coverage="0% (0/4)"/>
<line number="36" hits="0" branch="false"/>
<line number="37" hits="0" branch="false"/>
<line number="38" hits="0" branch="false"/>
<line number="39" hits="0" branch="false"/>
<line number="40" hits="0" branch="false"/>
<line number="41" hits="0" branch="false"/>
<line number="42" hits="0" branch="false"/>
<line number="43" hits="0" branch="false"/>
<line number="47" hits="41" branch="false"/>
<line number="68" hits="10" branch="false"/>
<line number="70" hits="41" branch="true" condition-coverage="100% (1/1)"/>
<line number="72" hits="41" branch="true" condition-coverage="100% (3/3)"/>
<line number="75" hits="41" branch="true" condition-coverage="100% (2/2)"/>
</lines>
</class>
<class name="index.ts" filename="src/atoms/Button/index.ts" line-rate="1" branch-rate="1">
<methods>
</methods>
<lines>
</lines>
</class>
</classes>
</package>
<package name="src.atoms.Container" line-rate="1" branch-rate="1">
<classes>
<class name="components.ts" filename="src/atoms/Container/components.ts" line-rate="1" branch-rate="1">
<methods>
<method name="(anonymous_0)" hits="35" signature="()V">
<lines>
<line number="5" hits="35"/>
</lines>
</method>
<method name="(anonymous_1)" hits="35" signature="()V">
<lines>
<line number="19" hits="35"/>
</lines>
</method>
<method name="(anonymous_2)" hits="35" signature="()V">
<lines>
<line number="20" hits="35"/>
</lines>
</method>
<method name="(anonymous_3)" hits="35" signature="()V">
<lines>
<line number="23" hits="35"/>
</lines>
</method>
</methods>
<lines>
<line number="4" hits="13" branch="false"/>
<line number="5" hits="35" branch="false"/>
<line number="13" hits="13" branch="false"/>
<line number="19" hits="35" branch="false"/>
<line number="20" hits="35" branch="false"/>
<line number="23" hits="35" branch="false"/>
</lines>
</class>
<class name="container.tsx" filename="src/atoms/Container/container.tsx" line-rate="1" branch-rate="1">
<methods>
<method name="Container" hits="35" signature="()V">
<lines>
<line number="12" hits="35"/>
</lines>
</method>
</methods>
<lines>
<line number="20" hits="35" branch="false"/>
</lines>
</class>
<class name="index.ts" filename="src/atoms/Container/index.ts" line-rate="1" branch-rate="1">
<methods>
</methods>
<lines>
</lines>
</class>
</classes>
</package>
<package name="src.atoms.DownArrow" line-rate="1" branch-rate="1">
<classes>
<class name="down-arrow.tsx" filename="src/atoms/DownArrow/down-arrow.tsx" line-rate="1" branch-rate="1">
<methods>
<method name="DownArrow" hits="28" signature="()V">
<lines>
<line number="3" hits="28"/>
</lines>
</method>
</methods>
<lines>
<line number="4" hits="28" branch="false"/>
</lines>
</class>
<class name="index.ts" filename="src/atoms/DownArrow/index.ts" line-rate="1" branch-rate="1">
<methods>
</methods>
<lines>
</lines>
</class>
</classes>
</package>
<package name="src.molecules.Hero" line-rate="0.8695999999999999" branch-rate="0.7236">
<classes>
<class name="components.ts" filename="src/molecules/Hero/components.ts" line-rate="0.8769" branch-rate="0.7143">
<methods>
<method name="(anonymous_0)" hits="10" signature="()V">
<lines>
<line number="26" hits="10"/>
</lines>
</method>
<method name="(anonymous_1)" hits="10" signature="()V">
<lines>
<line number="28" hits="10"/>
</lines>
</method>
<method name="(anonymous_2)" hits="10" signature="()V">
<lines>
<line number="94" hits="10"/>
</lines>
</method>
<method name="(anonymous_3)" hits="10" signature="()V">
<lines>
<line number="103" hits="10"/>
</lines>
</method>
<method name="(anonymous_4)" hits="10" signature="()V">
<lines>
<line number="121" hits="10"/>
</lines>
</method>
<method name="(anonymous_5)" hits="10" signature="()V">
<lines>
<line number="182" hits="10"/>
</lines>
</method>
<method name="(anonymous_6)" hits="5" signature="()V">
<lines>
<line number="203" hits="5"/>
</lines>
</method>
<method name="(anonymous_7)" hits="5" signature="()V">
<lines>
<line number="218" hits="5"/>
</lines>
</method>
<method name="(anonymous_8)" hits="5" signature="()V">
<lines>
<line number="236" hits="5"/>
</lines>
</method>
<method name="(anonymous_9)" hits="5" signature="()V">
<lines>
<line number="241" hits="5"/>
</lines>
</method>
<method name="(anonymous_10)" hits="4" signature="()V">
<lines>
<line number="249" hits="4"/>
</lines>
</method>
<method name="(anonymous_11)" hits="4" signature="()V">
<lines>
<line number="254" hits="4"/>
</lines>
</method>
<method name="(anonymous_12)" hits="4" signature="()V">
<lines>
<line number="264" hits="4"/>
</lines>
</method>
<method name="(anonymous_13)" hits="4" signature="()V">
<lines>
<line number="277" hits="4"/>
</lines>
</method>
<method name="(anonymous_14)" hits="4" signature="()V">
<lines>
<line number="282" hits="4"/>
</lines>
</method>
<method name="(anonymous_15)" hits="4" signature="()V">
<lines>
<line number="294" hits="4"/>
</lines>
</method>
<method name="(anonymous_16)" hits="3" signature="()V">
<lines>
<line number="299" hits="3"/>
</lines>
</method>
</methods>
<lines>
<line number="25" hits="2" branch="false"/>
<line number="26" hits="10" branch="false"/>
<line number="29" hits="10" branch="true" condition-coverage="100% (2/2)"/>
<line number="37" hits="3" branch="false"/>
<line number="39" hits="3" branch="true" condition-coverage="100% (2/2)"/>
<line number="40" hits="3" branch="false"/>
<line number="42" hits="3" branch="true" condition-coverage="100% (2/2)"/>
<line number="43" hits="1" branch="true" condition-coverage="75% (3/4)"/>
<line number="44" hits="1" branch="false"/>
<line number="45" hits="0" branch="true" condition-coverage="0% (0/4)"/>
<line number="46" hits="0" branch="false"/>
<line number="48" hits="1" branch="true" condition-coverage="50% (1/2)"/>
<line number="52" hits="0" branch="false"/>
<line number="53" hits="1" branch="true" condition-coverage="50% (1/2)"/>
<line number="57" hits="0" branch="false"/>
<line number="59" hits="1" branch="true" condition-coverage="50% (1/2)"/>
<line number="60" hits="1" branch="true" condition-coverage="50% (1/2)"/>
<line number="63" hits="3" branch="false"/>
<line number="95" hits="10" branch="true" condition-coverage="50% (1/2)"/>
<line number="103" hits="10" branch="false"/>
<line number="112" hits="2" branch="false"/>
<line number="122" hits="10" branch="true" condition-coverage="50% (2/4)"/>
<line number="125" hits="9" branch="false"/>
<line number="134" hits="0" branch="false"/>
<line number="151" hits="1" branch="false"/>
<line number="170" hits="2" branch="false"/>
<line number="181" hits="2" branch="false"/>
<line number="182" hits="10" branch="true" condition-coverage="100% (2/2)"/>
<line number="194" hits="2" branch="false"/>
<line number="204" hits="5" branch="true" condition-coverage="80% (4/5)"/>
<line number="207" hits="3" branch="false"/>
<line number="209" hits="0" branch="false"/>
<line number="211" hits="1" branch="false"/>
<line number="213" hits="1" branch="false"/>
<line number="219" hits="5" branch="true" condition-coverage="66.66666666666666% (4/6)"/>
<line number="222" hits="3" branch="false"/>
<line number="224" hits="0" branch="false"/>
<line number="226" hits="0" branch="false"/>
<line number="228" hits="1" branch="false"/>
<line number="230" hits="1" branch="false"/>
<line number="236" hits="5" branch="true" condition-coverage="100% (1/1)"/>
<line number="241" hits="5" branch="true" condition-coverage="100% (1/1)"/>
<line number="245" hits="2" branch="false"/>
<line number="249" hits="4" branch="true" condition-coverage="50% (1/2)"/>
<line number="255" hits="4" branch="true" condition-coverage="100% (3/3)"/>
<line number="258" hits="3" branch="false"/>
<line number="260" hits="1" branch="false"/>
<line number="265" hits="4" branch="true" condition-coverage="100% (4/4)"/>
<line number="268" hits="2" branch="false"/>
<line number="270" hits="1" branch="false"/>
<line number="272" hits="1" branch="false"/>
<line number="277" hits="4" branch="true" condition-coverage="100% (1/1)"/>
<line number="283" hits="4" branch="true" condition-coverage="100% (4/4)"/>
<line number="286" hits="2" branch="false"/>
<line number="288" hits="1" branch="false"/>
<line number="290" hits="1" branch="false"/>
<line number="294" hits="4" branch="true" condition-coverage="50% (1/2)"/>
<line number="298" hits="2" branch="false"/>
<line number="299" hits="3" branch="true" condition-coverage="50% (1/2)"/>
<line number="304" hits="2" branch="false"/>
<line number="311" hits="2" branch="false"/>
<line number="319" hits="2" branch="false"/>
<line number="329" hits="2" branch="false"/>
<line number="361" hits="2" branch="false"/>
<line number="371" hits="2" branch="false"/>
</lines>
</class>
<class name="hero.tsx" filename="src/molecules/Hero/hero.tsx" line-rate="0.75" branch-rate="0.76">
<methods>
<method name="Hero" hits="10" signature="()V">
<lines>
<line number="40" hits="10"/>
</lines>
</method>
<method name="(anonymous_1)" hits="0" signature="()V">
<lines>
<line number="72" hits="0"/>
</lines>
</method>
<method name="(anonymous_2)" hits="3" signature="()V">
<lines>
<line number="127" hits="3"/>
</lines>
</method>
</methods>
<lines>
<line number="56" hits="10" branch="false"/>
<line number="58" hits="10" branch="false"/>
<line number="73" hits="0" branch="true" condition-coverage="0% (0/2)"/>
<line number="128" hits="3" branch="false"/>
</lines>
</class>
<class name="index.ts" filename="src/molecules/Hero/index.ts" line-rate="1" branch-rate="1">
<methods>
</methods>
<lines>
</lines>
</class>
</classes>
</package>
</packages>
</coverage>
@@ -0,0 +1,348 @@
<?xml version="1.0" ?>
<coverage line-rate="0.0" lines-covered="0" lines-valid="325" version="0.1">
<package line-rate="0.0" name="Sources">
<classes>
<class filename="Sources/TDSOptions.m" line-rate="0.0" name="TDSOptions.m">
<lines>
<line hits="0" number="32" />
<line hits="0" number="33" />
<line hits="0" number="34" />
<line hits="0" number="35" />
<line hits="0" number="36" />
<line hits="0" number="37" />
<line hits="0" number="38" />
<line hits="0" number="45" />
<line hits="0" number="46" />
<line hits="0" number="47" />
<line hits="0" number="48" />
<line hits="0" number="49" />
<line hits="0" number="50" />
<line hits="0" number="51" />
<line hits="0" number="52" />
<line hits="0" number="53" />
<line hits="0" number="54" />
<line hits="0" number="55" />
<line hits="0" number="56" />
<line hits="0" number="57" />
<line hits="0" number="58" />
<line hits="0" number="61" />
<line hits="0" number="62" />
<line hits="0" number="63" />
<line hits="0" number="66" />
<line hits="0" number="67" />
<line hits="0" number="68" />
<line hits="0" number="69" />
<line hits="0" number="70" />
<line hits="0" number="71" />
<line hits="0" number="72" />
<line hits="0" number="73" />
<line hits="0" number="74" />
<line hits="0" number="75" />
<line hits="0" number="76" />
<line hits="0" number="77" />
</lines>
</class>
<class filename="Sources/TDSUserLock.m" line-rate="0.0" name="TDSUserLock.m">
<lines>
<line hits="0" number="17" />
<line hits="0" number="18" />
<line hits="0" number="19" />
<line hits="0" number="20" />
<line hits="0" number="21" />
<line hits="0" number="22" />
<line hits="0" number="23" />
<line hits="0" number="24" />
<line hits="0" number="25" />
<line hits="0" number="26" />
<line hits="0" number="27" />
<line hits="0" number="28" />
<line hits="0" number="29" />
<line hits="0" number="30" />
<line hits="0" number="31" />
<line hits="0" number="32" />
<line hits="0" number="33" />
<line hits="0" number="34" />
<line hits="0" number="35" />
<line hits="0" number="36" />
<line hits="0" number="37" />
<line hits="0" number="38" />
<line hits="0" number="39" />
<line hits="0" number="40" />
<line hits="0" number="43" />
<line hits="0" number="44" />
<line hits="0" number="45" />
<line hits="0" number="46" />
<line hits="0" number="47" />
<line hits="0" number="48" />
<line hits="0" number="49" />
<line hits="0" number="58" />
<line hits="0" number="59" />
<line hits="0" number="60" />
<line hits="0" number="61" />
<line hits="0" number="62" />
<line hits="0" number="63" />
<line hits="0" number="64" />
<line hits="0" number="65" />
<line hits="0" number="66" />
<line hits="0" number="67" />
<line hits="0" number="68" />
<line hits="0" number="69" />
<line hits="0" number="70" />
</lines>
</class>
<class filename="Sources/TDSUserManager.m" line-rate="0.0" name="TDSUserManager.m">
<lines>
<line hits="0" number="24" />
<line hits="0" number="25" />
<line hits="0" number="26" />
<line hits="0" number="27" />
<line hits="0" number="28" />
<line hits="0" number="29" />
<line hits="0" number="30" />
<line hits="0" number="31" />
<line hits="0" number="34" />
<line hits="0" number="35" />
<line hits="0" number="36" />
<line hits="0" number="37" />
<line hits="0" number="38" />
<line hits="0" number="39" />
<line hits="0" number="40" />
<line hits="0" number="49" />
<line hits="0" number="50" />
<line hits="0" number="51" />
<line hits="0" number="52" />
<line hits="0" number="53" />
<line hits="0" number="54" />
<line hits="0" number="55" />
<line hits="0" number="56" />
<line hits="0" number="57" />
<line hits="0" number="58" />
<line hits="0" number="59" />
<line hits="0" number="60" />
<line hits="0" number="61" />
<line hits="0" number="62" />
<line hits="0" number="63" />
<line hits="0" number="64" />
<line hits="0" number="65" />
<line hits="0" number="66" />
<line hits="0" number="67" />
<line hits="0" number="68" />
<line hits="0" number="69" />
<line hits="0" number="70" />
<line hits="0" number="74" />
<line hits="0" number="75" />
<line hits="0" number="76" />
<line hits="0" number="77" />
<line hits="0" number="78" />
<line hits="0" number="79" />
<line hits="0" number="80" />
<line hits="0" number="81" />
<line hits="0" number="82" />
<line hits="0" number="83" />
<line hits="0" number="84" />
<line hits="0" number="85" />
<line hits="0" number="86" />
<line hits="0" number="87" />
<line hits="0" number="88" />
<line hits="0" number="89" />
<line hits="0" number="90" />
<line hits="0" number="91" />
<line hits="0" number="92" />
<line hits="0" number="93" />
<line hits="0" number="94" />
<line hits="0" number="101" />
<line hits="0" number="102" />
<line hits="0" number="103" />
<line hits="0" number="104" />
<line hits="0" number="105" />
<line hits="0" number="106" />
<line hits="0" number="107" />
<line hits="0" number="108" />
<line hits="0" number="109" />
<line hits="0" number="110" />
<line hits="0" number="111" />
<line hits="0" number="112" />
<line hits="0" number="113" />
<line hits="0" number="114" />
<line hits="0" number="115" />
<line hits="0" number="116" />
<line hits="0" number="117" />
<line hits="0" number="118" />
<line hits="0" number="119" />
<line hits="0" number="120" />
<line hits="0" number="121" />
<line hits="0" number="122" />
<line hits="0" number="123" />
<line hits="0" number="124" />
<line hits="0" number="125" />
<line hits="0" number="126" />
<line hits="0" number="127" />
<line hits="0" number="128" />
<line hits="0" number="129" />
<line hits="0" number="130" />
<line hits="0" number="131" />
<line hits="0" number="136" />
<line hits="0" number="137" />
<line hits="0" number="138" />
<line hits="0" number="139" />
<line hits="0" number="140" />
<line hits="0" number="141" />
<line hits="0" number="142" />
<line hits="0" number="143" />
<line hits="0" number="144" />
<line hits="0" number="145" />
<line hits="0" number="146" />
<line hits="0" number="147" />
<line hits="0" number="148" />
<line hits="0" number="149" />
<line hits="0" number="150" />
<line hits="0" number="151" />
<line hits="0" number="152" />
<line hits="0" number="153" />
<line hits="0" number="158" />
<line hits="0" number="159" />
<line hits="0" number="160" />
<line hits="0" number="161" />
<line hits="0" number="162" />
<line hits="0" number="163" />
<line hits="0" number="164" />
<line hits="0" number="165" />
<line hits="0" number="166" />
<line hits="0" number="167" />
<line hits="0" number="168" />
<line hits="0" number="169" />
<line hits="0" number="170" />
<line hits="0" number="171" />
<line hits="0" number="172" />
<line hits="0" number="173" />
<line hits="0" number="174" />
<line hits="0" number="175" />
<line hits="0" number="176" />
<line hits="0" number="177" />
<line hits="0" number="178" />
<line hits="0" number="179" />
<line hits="0" number="180" />
<line hits="0" number="181" />
<line hits="0" number="182" />
<line hits="0" number="183" />
<line hits="0" number="184" />
<line hits="0" number="189" />
<line hits="0" number="190" />
<line hits="0" number="191" />
<line hits="0" number="192" />
<line hits="0" number="193" />
<line hits="0" number="194" />
<line hits="0" number="195" />
<line hits="0" number="196" />
<line hits="0" number="197" />
<line hits="0" number="198" />
<line hits="0" number="199" />
<line hits="0" number="200" />
<line hits="0" number="201" />
<line hits="0" number="202" />
<line hits="0" number="203" />
<line hits="0" number="204" />
<line hits="0" number="205" />
<line hits="0" number="206" />
<line hits="0" number="207" />
<line hits="0" number="208" />
<line hits="0" number="209" />
<line hits="0" number="210" />
<line hits="0" number="211" />
<line hits="0" number="212" />
<line hits="0" number="213" />
<line hits="0" number="214" />
<line hits="0" number="215" />
<line hits="0" number="216" />
<line hits="0" number="217" />
<line hits="0" number="218" />
<line hits="0" number="219" />
<line hits="0" number="220" />
<line hits="0" number="221" />
<line hits="0" number="222" />
<line hits="0" number="223" />
<line hits="0" number="224" />
<line hits="0" number="225" />
<line hits="0" number="226" />
<line hits="0" number="227" />
<line hits="0" number="228" />
<line hits="0" number="229" />
<line hits="0" number="230" />
<line hits="0" number="231" />
<line hits="0" number="232" />
<line hits="0" number="233" />
<line hits="0" number="234" />
<line hits="0" number="235" />
<line hits="0" number="236" />
<line hits="0" number="239" />
<line hits="0" number="240" />
<line hits="0" number="241" />
<line hits="0" number="242" />
<line hits="0" number="243" />
<line hits="0" number="244" />
<line hits="0" number="245" />
<line hits="0" number="246" />
<line hits="0" number="247" />
<line hits="0" number="248" />
<line hits="0" number="249" />
<line hits="0" number="252" />
<line hits="0" number="253" />
<line hits="0" number="254" />
<line hits="0" number="255" />
<line hits="0" number="256" />
<line hits="0" number="257" />
<line hits="0" number="258" />
<line hits="0" number="259" />
<line hits="0" number="260" />
<line hits="0" number="261" />
<line hits="0" number="262" />
<line hits="0" number="263" />
<line hits="0" number="264" />
<line hits="0" number="265" />
<line hits="0" number="268" />
<line hits="0" number="269" />
<line hits="0" number="270" />
<line hits="0" number="271" />
<line hits="0" number="272" />
<line hits="0" number="273" />
<line hits="0" number="274" />
<line hits="0" number="275" />
<line hits="0" number="276" />
<line hits="0" number="277" />
<line hits="0" number="278" />
<line hits="0" number="279" />
<line hits="0" number="280" />
<line hits="0" number="281" />
<line hits="0" number="282" />
<line hits="0" number="283" />
<line hits="0" number="284" />
<line hits="0" number="287" />
<line hits="0" number="288" />
<line hits="0" number="289" />
<line hits="0" number="290" />
<line hits="0" number="291" />
<line hits="0" number="292" />
<line hits="0" number="293" />
<line hits="0" number="294" />
<line hits="0" number="295" />
<line hits="0" number="296" />
</lines>
</class>
<class filename="Sources/TDSUtil.m" line-rate="0.0" name="TDSUtil.m">
<lines>
<line hits="0" number="8" />
<line hits="0" number="9" />
<line hits="0" number="10" />
<line hits="0" number="11" />
<line hits="0" number="12" />
<line hits="0" number="13" />
<line hits="0" number="16" />
<line hits="0" number="17" />
<line hits="0" number="18" />
<line hits="0" number="19" />
<line hits="0" number="20" />
</lines>
</class>
</classes>
</package>
</coverage>
@@ -0,0 +1,441 @@
<?xml version="1.0" ?>
<coverage branch-rate="0.7602339181286549" branches-covered="130" branches-valid="171" complexity="0" line-rate="0.8738738738738738" lines-covered="582" lines-valid="666" timestamp="1575668573" version="2.0.3">
<sources>
<source>.</source>
</sources>
<packages>
<package branch-rate="0.75" complexity="0" line-rate="0.9672131147540983" name="src.graphql">
<classes>
<class branch-rate="0.0" complexity="0" filename="src/graphql/queries.ts" line-rate="1.0" name="src.graphql.queries.ts">
<methods/>
<lines>
<line branch="false" hits="3" number="1"/>
<line branch="false" hits="3" number="3"/>
<line branch="false" hits="3" number="50"/>
<line branch="false" hits="3" number="65"/>
<line branch="false" hits="3" number="80"/>
<line branch="false" hits="3" number="92"/>
<line branch="false" hits="3" number="138"/>
</lines>
</class>
<class branch-rate="0.8" complexity="0" filename="src/graphql/resolvers.ts" line-rate="0.974025974025974" name="src.graphql.resolvers.ts">
<methods>
<method branch-rate="1.0" line-rate="1.0" name="(anonymous_12)" signature="">
<lines>
<line branch="false" hits="17" number="22"/>
</lines>
</method>
<method branch-rate="1.0" line-rate="1.0" name="(anonymous_13)" signature="">
<lines>
<line branch="false" hits="10" number="28"/>
</lines>
</method>
<method branch-rate="1.0" line-rate="1.0" name="(anonymous_14)" signature="">
<lines>
<line branch="false" hits="12" number="36"/>
</lines>
</method>
<method branch-rate="1.0" line-rate="1.0" name="(anonymous_15)" signature="">
<lines>
<line branch="false" hits="4" number="27"/>
</lines>
</method>
<method branch-rate="1.0" line-rate="1.0" name="(anonymous_16)" signature="">
<lines>
<line branch="false" hits="4" number="35"/>
</lines>
</method>
<method branch-rate="1.0" line-rate="1.0" name="(anonymous_17)" signature="">
<lines>
<line branch="false" hits="9" number="43"/>
</lines>
</method>
<method branch-rate="1.0" line-rate="1.0" name="(anonymous_18)" signature="">
<lines>
<line branch="false" hits="11" number="51"/>
</lines>
</method>
<method branch-rate="1.0" line-rate="1.0" name="(anonymous_19)" signature="">
<lines>
<line branch="false" hits="4" number="56"/>
</lines>
</method>
<method branch-rate="1.0" line-rate="1.0" name="(anonymous_20)" signature="">
<lines>
<line branch="false" hits="4" number="61"/>
</lines>
</method>
<method branch-rate="1.0" line-rate="1.0" name="(anonymous_21)" signature="">
<lines>
<line branch="false" hits="4" number="50"/>
</lines>
</method>
<method branch-rate="1.0" line-rate="1.0" name="(anonymous_22)" signature="">
<lines>
<line branch="false" hits="4" number="55"/>
</lines>
</method>
<method branch-rate="1.0" line-rate="1.0" name="(anonymous_23)" signature="">
<lines>
<line branch="false" hits="4" number="60"/>
</lines>
</method>
<method branch-rate="1.0" line-rate="1.0" name="(anonymous_24)" signature="">
<lines>
<line branch="false" hits="7" number="128"/>
</lines>
</method>
<method branch-rate="1.0" line-rate="1.0" name="(anonymous_25)" signature="">
<lines>
<line branch="false" hits="4" number="139"/>
</lines>
</method>
<method branch-rate="1.0" line-rate="1.0" name="(anonymous_26)" signature="">
<lines>
<line branch="false" hits="4" number="145"/>
</lines>
</method>
<method branch-rate="1.0" line-rate="1.0" name="(anonymous_28)" signature="">
<lines>
<line branch="false" hits="4" number="160"/>
</lines>
</method>
<method branch-rate="1.0" line-rate="1.0" name="(anonymous_30)" signature="">
<lines>
<line branch="false" hits="3" number="177"/>
</lines>
</method>
<method branch-rate="1.0" line-rate="1.0" name="(anonymous_31)" signature="">
<lines>
<line branch="false" hits="9" number="185"/>
</lines>
</method>
<method branch-rate="1.0" line-rate="1.0" name="(anonymous_32)" signature="">
<lines>
<line branch="false" hits="8" number="70"/>
</lines>
</method>
<method branch-rate="1.0" line-rate="1.0" name="(anonymous_33)" signature="">
<lines>
<line branch="false" hits="4" number="78"/>
</lines>
</method>
<method branch-rate="1.0" line-rate="1.0" name="(anonymous_34)" signature="">
<lines>
<line branch="false" hits="4" number="81"/>
</lines>
</method>
<method branch-rate="1.0" line-rate="1.0" name="(anonymous_35)" signature="">
<lines>
<line branch="false" hits="4" number="84"/>
</lines>
</method>
<method branch-rate="1.0" line-rate="1.0" name="(anonymous_36)" signature="">
<lines>
<line branch="false" hits="4" number="97"/>
</lines>
</method>
<method branch-rate="1.0" line-rate="1.0" name="(anonymous_37)" signature="">
<lines>
<line branch="false" hits="4" number="97"/>
</lines>
</method>
<method branch-rate="1.0" line-rate="1.0" name="(anonymous_38)" signature="">
<lines>
<line branch="false" hits="4" number="103"/>
</lines>
</method>
<method branch-rate="1.0" line-rate="1.0" name="(anonymous_39)" signature="">
<lines>
<line branch="false" hits="4" number="114"/>
</lines>
</method>
<method branch-rate="1.0" line-rate="1.0" name="(anonymous_40)" signature="">
<lines>
<line branch="false" hits="4" number="144"/>
</lines>
</method>
<method branch-rate="1.0" line-rate="1.0" name="(anonymous_41)" signature="">
<lines>
<line branch="false" hits="4" number="159"/>
</lines>
</method>
<method branch-rate="1.0" line-rate="1.0" name="(anonymous_42)" signature="">
<lines>
<line branch="false" hits="4" number="176"/>
</lines>
</method>
<method branch-rate="1.0" line-rate="1.0" name="(anonymous_43)" signature="">
<lines>
<line branch="false" hits="4" number="184"/>
</lines>
</method>
</methods>
<lines>
<line branch="false" hits="4" number="1"/>
<line branch="false" hits="4" number="2"/>
<line branch="false" hits="4" number="3"/>
<line branch="false" hits="4" number="5"/>
<line branch="false" hits="4" number="6"/>
<line branch="false" hits="4" number="7"/>
<line branch="false" hits="4" number="8"/>
<line branch="false" hits="4" number="9"/>
<line branch="false" hits="4" number="10"/>
<line branch="false" hits="4" number="14"/>
<line branch="false" hits="4" number="21"/>
<line branch="false" hits="17" number="24"/>
<line branch="false" hits="4" number="27"/>
<line branch="false" hits="4" number="28"/>
<line branch="false" hits="10" number="32"/>
<line branch="false" hits="4" number="35"/>
<line branch="false" hits="4" number="36"/>
<line branch="false" hits="12" number="37"/>
<line branch="false" hits="4" number="42"/>
<line branch="false" hits="9" number="45"/>
<line branch="false" hits="9" number="47"/>
<line branch="false" hits="4" number="50"/>
<line branch="false" hits="4" number="51"/>
<line branch="false" hits="11" number="52"/>
<line branch="false" hits="4" number="55"/>
<line branch="false" hits="4" number="56"/>
<line branch="false" hits="4" number="57"/>
<line branch="false" hits="4" number="60"/>
<line branch="false" hits="4" number="61"/>
<line branch="false" hits="4" number="62"/>
<line branch="false" hits="4" number="67"/>
<line branch="false" hits="4" number="70"/>
<line branch="false" hits="8" number="73"/>
<line branch="false" hits="4" number="78"/>
<line branch="false" hits="4" number="81"/>
<line branch="false" hits="4" number="83"/>
<line branch="false" hits="4" number="85"/>
<line branch="true" condition-coverage="100% (2/2)" hits="4" number="86"/>
<line branch="false" hits="4" number="87"/>
<line branch="false" hits="4" number="88"/>
<line branch="false" hits="4" number="94"/>
<line branch="false" hits="4" number="95"/>
<line branch="false" hits="4" number="97"/>
<line branch="false" hits="4" number="98"/>
<line branch="false" hits="4" number="103"/>
<line branch="false" hits="4" number="104"/>
<line branch="false" hits="4" number="107"/>
<line branch="false" hits="4" number="114"/>
<line branch="false" hits="4" number="115"/>
<line branch="false" hits="4" number="118"/>
<line branch="true" condition-coverage="50% (1/2)" hits="4" number="119"/>
<line branch="false" hits="0" number="120"/>
<line branch="false" hits="4" number="124"/>
<line branch="false" hits="7" number="130"/>
<line branch="false" hits="7" number="132"/>
<line branch="false" hits="7" number="134"/>
<line branch="false" hits="7" number="136"/>
<line branch="false" hits="4" number="140"/>
<line branch="false" hits="4" number="144"/>
<line branch="false" hits="4" number="145"/>
<line branch="false" hits="4" number="151"/>
<line branch="false" hits="4" number="159"/>
<line branch="false" hits="4" number="160"/>
<line branch="false" hits="4" number="165"/>
<line branch="true" condition-coverage="100% (2/2)" hits="4" number="166"/>
<line branch="true" condition-coverage="100% (2/2)" hits="4" number="167"/>
<line branch="false" hits="1" number="168"/>
<line branch="false" hits="4" number="169"/>
<line branch="false" hits="4" number="176"/>
<line branch="false" hits="4" number="177"/>
<line branch="false" hits="3" number="178"/>
<line branch="false" hits="4" number="184"/>
<line branch="false" hits="4" number="185"/>
<line branch="false" hits="9" number="186"/>
<line branch="false" hits="9" number="187"/>
<line branch="true" condition-coverage="50% (1/2)" hits="2" number="193"/>
<line branch="false" hits="0" number="194"/>
</lines>
</class>
<class branch-rate="0.7272727272727273" complexity="0" filename="src/graphql/router.ts" line-rate="0.9444444444444444" name="src.graphql.router.ts">
<methods>
<method branch-rate="1.0" line-rate="1.0" name="handleApolloClientError" signature="">
<lines>
<line branch="false" hits="1" number="21"/>
</lines>
</method>
<method branch-rate="1.0" line-rate="1.0" name="(anonymous_8)" signature="">
<lines>
<line branch="false" hits="1" number="22"/>
</lines>
</method>
<method branch-rate="1.0" line-rate="1.0" name="graphqlRoute" signature="">
<lines>
<line branch="false" hits="18" number="43"/>
</lines>
</method>
<method branch-rate="1.0" line-rate="1.0" name="(anonymous_10)" signature="">
<lines>
<line branch="false" hits="11" number="53"/>
</lines>
</method>
<method branch-rate="1.0" line-rate="1.0" name="(anonymous_11)" signature="">
<lines>
<line branch="false" hits="11" number="53"/>
</lines>
</method>
</methods>
<lines>
<line branch="false" hits="3" number="12"/>
<line branch="false" hits="3" number="14"/>
<line branch="false" hits="1" number="22"/>
<line branch="true" condition-coverage="100% (3/3)" hits="1" number="23"/>
<line branch="true" condition-coverage="50% (1/2)" hits="0" number="26"/>
<line branch="true" condition-coverage="50% (1/2)" hits="1" number="32"/>
<line branch="false" hits="1" number="33"/>
<line branch="false" hits="1" number="34"/>
<line branch="false" hits="0" number="37"/>
<line branch="false" hits="3" number="43"/>
<line branch="false" hits="18" number="53"/>
<line branch="false" hits="11" number="54"/>
<line branch="true" condition-coverage="100% (2/2)" hits="0" number="55"/>
<line branch="false" hits="11" number="57"/>
<line branch="false" hits="11" number="58"/>
<line branch="false" hits="11" number="63"/>
<line branch="false" hits="11" number="64"/>
<line branch="true" condition-coverage="50% (1/2)" hits="10" number="65"/>
<line branch="false" hits="10" number="66"/>
<line branch="false" hits="1" number="68"/>
</lines>
</class>
<class branch-rate="0.0" complexity="0" filename="src/graphql/schema.ts" line-rate="1.0" name="src.graphql.schema.ts">
<methods>
<method branch-rate="1.0" line-rate="1.0" name="getGraphqlSchema" signature="">
<lines>
<line branch="false" hits="4" number="9"/>
</lines>
</method>
</methods>
<lines>
<line branch="false" hits="3" number="1"/>
<line branch="false" hits="3" number="2"/>
<line branch="false" hits="3" number="4"/>
<line branch="false" hits="3" number="5"/>
<line branch="false" hits="3" number="9"/>
<line branch="true" condition-coverage="0% (0/1)" hits="0" number="11"/>
<line branch="false" hits="4" number="13"/>
<line branch="false" hits="4" number="15"/>
<line branch="false" hits="4" number="16"/>
<line branch="false" hits="4" number="17"/>
<line branch="false" hits="4" number="19"/>
</lines>
</class>
<class branch-rate="0.8333333333333334" complexity="0" filename="src/graphql/transforms.ts" line-rate="0.9" name="src.graphql.transforms.ts">
<methods>
<method branch-rate="1.0" line-rate="1.0" name="(anonymous_0)" signature="">
<lines>
<line branch="false" hits="6" number="9"/>
</lines>
</method>
<method branch-rate="1.0" line-rate="1.0" name="(anonymous_1)" signature="">
<lines>
<line branch="false" hits="1" number="20"/>
</lines>
</method>
</methods>
<lines>
<line branch="false" hits="4" number="9"/>
<line branch="true" condition-coverage="100% (2/2)" hits="6" number="13"/>
<line branch="false" hits="2" number="14"/>
<line branch="true" condition-coverage="100% (2/2)" hits="6" number="15"/>
<line branch="false" hits="1" number="16"/>
<line branch="false" hits="6" number="17"/>
<line branch="false" hits="4" number="20"/>
<line branch="true" condition-coverage="50% (1/2)" hits="1" number="23"/>
<line branch="false" hits="0" number="24"/>
<line branch="false" hits="1" number="25"/>
</lines>
</class>
</classes>
</package>
<package branch-rate="0.0" complexity="0" line-rate="0.0" name="src.workers">
<classes>
<class branch-rate="0.0" complexity="0" filename="src/workers/primary-worker.js" line-rate="0.0" name="src.workers.primary-worker.js">
<methods>
<method branch-rate="0.0" line-rate="0.0" name="doWork" signature="">
<lines>
<line branch="false" hits="0" number="16"/>
</lines>
</method>
<method branch-rate="0.0" line-rate="0.0" name="(anonymous_9)" signature="">
<lines>
<line branch="false" hits="0" number="25"/>
</lines>
</method>
<method branch-rate="0.0" line-rate="0.0" name="(anonymous_10)" signature="">
<lines>
<line branch="false" hits="0" number="49"/>
</lines>
</method>
</methods>
<lines>
<line branch="false" hits="0" number="2"/>
<line branch="false" hits="0" number="3"/>
<line branch="false" hits="0" number="4"/>
<line branch="false" hits="0" number="5"/>
<line branch="false" hits="0" number="6"/>
<line branch="false" hits="0" number="8"/>
<line branch="false" hits="0" number="16"/>
<line branch="true" condition-coverage="0% (0/1)" hits="0" number="17"/>
<line branch="true" condition-coverage="0% (0/2)" hits="0" number="21"/>
<line branch="false" hits="0" number="22"/>
<line branch="false" hits="0" number="23"/>
<line branch="false" hits="0" number="24"/>
<line branch="false" hits="0" number="25"/>
<line branch="false" hits="0" number="27"/>
<line branch="false" hits="0" number="28"/>
<line branch="false" hits="0" number="31"/>
<line branch="false" hits="0" number="33"/>
<line branch="false" hits="0" number="34"/>
<line branch="false" hits="0" number="37"/>
<line branch="false" hits="0" number="38"/>
<line branch="false" hits="0" number="48"/>
<line branch="true" condition-coverage="0% (0/2)" hits="0" number="49"/>
<line branch="false" hits="0" number="51"/>
<line branch="false" hits="0" number="53"/>
</lines>
</class>
<class branch-rate="0.0" complexity="0" filename="src/workers/secondary-worker.js" line-rate="0.0" name="src.workers.secondary-worker.js">
<methods>
<method branch-rate="0.0" line-rate="0.0" name="doWork" signature="">
<lines>
<line branch="false" hits="0" number="4"/>
</lines>
</method>
<method branch-rate="0.0" line-rate="0.0" name="(anonymous_9)" signature="">
<lines>
<line branch="false" hits="0" number="13"/>
</lines>
</method>
</methods>
<lines>
<line branch="false" hits="0" number="2"/>
<line branch="false" hits="0" number="4"/>
<line branch="true" condition-coverage="0% (0/1)" hits="0" number="5"/>
<line branch="true" condition-coverage="0% (0/2)" hits="0" number="9"/>
<line branch="false" hits="0" number="10"/>
<line branch="false" hits="0" number="11"/>
<line branch="false" hits="0" number="12"/>
<line branch="false" hits="0" number="13"/>
<line branch="false" hits="0" number="14"/>
<line branch="false" hits="0" number="15"/>
<line branch="false" hits="0" number="16"/>
<line branch="false" hits="0" number="18"/>
<line branch="false" hits="0" number="19"/>
<line branch="false" hits="0" number="22"/>
<line branch="false" hits="0" number="23"/>
<line branch="true" condition-coverage="0% (0/2)" hits="0" number="24"/>
<line branch="false" hits="0" number="25"/>
<line branch="false" hits="0" number="26"/>
<line branch="false" hits="0" number="27"/>
<line branch="false" hits="0" number="29"/>
</lines>
</class>
</classes>
</package>
</packages>
</coverage>
@@ -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
}
}
}
]
@@ -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
@@ -0,0 +1,282 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<report name="example">
<sessioninfo id="743a5357362a-f32e3def" start="1575926125817" dump="1575926133882"/>
<package name="com/example">
<sourcefile name="Main.kt">
<line nr="28" mi="6" ci="0" mb="0" cb="0"/>
<line nr="31" mi="7" ci="0" mb="0" cb="0"/>
<line nr="36" mi="3" ci="0" mb="0" cb="0"/>
<line nr="37" mi="12" ci="0" mb="0" cb="0"/>
<line nr="38" mi="2" ci="0" mb="0" cb="0"/>
<line nr="39" mi="2" ci="0" mb="0" cb="0"/>
<line nr="40" mi="5" ci="0" mb="0" cb="0"/>
<line nr="41" mi="5" ci="0" mb="0" cb="0"/>
<line nr="42" mi="3" ci="0" mb="0" cb="0"/>
<line nr="43" mi="3" ci="0" mb="0" cb="0"/>
<line nr="44" mi="3" ci="0" mb="0" cb="0"/>
<line nr="45" mi="3" ci="0" mb="0" cb="0"/>
<line nr="46" mi="3" ci="0" mb="0" cb="0"/>
<line nr="47" mi="4" ci="0" mb="0" cb="0"/>
<line nr="48" mi="4" ci="0" mb="0" cb="0"/>
<line nr="49" mi="4" ci="0" mb="0" cb="0"/>
<line nr="50" mi="1" ci="0" mb="0" cb="0"/>
<line nr="52" mi="8" ci="0" mb="0" cb="0"/>
<line nr="53" mi="19" ci="0" mb="0" cb="0"/>
<line nr="56" mi="1" ci="0" mb="0" cb="0"/>
<line nr="57" mi="8" ci="0" mb="0" cb="0"/>
<line nr="58" mi="1" ci="0" mb="0" cb="0"/>
<line nr="59" mi="24" ci="0" mb="0" cb="0"/>
<line nr="60" mi="2" ci="0" mb="0" cb="0"/>
<line nr="62" mi="1" ci="0" mb="0" cb="0"/>
<line nr="65" mi="5" ci="0" mb="0" cb="0"/>
<line nr="66" mi="5" ci="0" mb="0" cb="0"/>
<line nr="67" mi="3" ci="0" mb="0" cb="0"/>
<line nr="69" mi="1" ci="0" mb="0" cb="0"/>
<line nr="70" mi="1" ci="0" mb="0" cb="0"/>
<line nr="71" mi="1" ci="0" mb="0" cb="0"/>
<line nr="72" mi="7" ci="0" mb="0" cb="0"/>
<line nr="74" mi="2" ci="0" mb="0" cb="0"/>
<line nr="76" mi="3" ci="0" mb="0" cb="0"/>
<line nr="77" mi="7" ci="0" mb="0" cb="0"/>
<line nr="78" mi="5" ci="0" mb="0" cb="0"/>
<line nr="79" mi="5" ci="0" mb="0" cb="0"/>
<line nr="81" mi="1" ci="0" mb="0" cb="0"/>
<line nr="82" mi="1" ci="0" mb="0" cb="0"/>
<line nr="83" mi="2" ci="0" mb="0" cb="0"/>
<line nr="84" mi="9" ci="0" mb="0" cb="0"/>
<line nr="85" mi="3" ci="0" mb="0" cb="0"/>
<line nr="86" mi="1" ci="0" mb="0" cb="0"/>
<line nr="87" mi="3" ci="0" mb="0" cb="0"/>
<line nr="88" mi="1" ci="0" mb="0" cb="0"/>
<line nr="89" mi="6" ci="0" mb="0" cb="0"/>
<line nr="92" mi="6" ci="0" mb="0" cb="0"/>
<line nr="93" mi="1" ci="0" mb="0" cb="0"/>
<counter type="INSTRUCTION" missed="219" covered="0"/>
<counter type="LINE" missed="48" covered="0"/>
<counter type="COMPLEXITY" missed="8" covered="0"/>
<counter type="METHOD" missed="8" covered="0"/>
<counter type="CLASS" missed="6" covered="0"/>
</sourcefile>
<sourcefile name="Parser.kt">
<line nr="5" mi="0" ci="9" mb="0" cb="0"/>
<line nr="10" mi="0" ci="7" mb="0" cb="0"/>
<line nr="12" mi="1" ci="8" mb="1" cb="1"/>
<line nr="13" mi="0" ci="2" mb="0" cb="0"/>
<line nr="14" mi="0" ci="3" mb="0" cb="0"/>
<line nr="16" mi="2" ci="28" mb="2" cb="2"/>
<line nr="17" mi="0" ci="1" mb="0" cb="0"/>
<line nr="19" mi="0" ci="4" mb="0" cb="0"/>
<line nr="20" mi="0" ci="16" mb="0" cb="0"/>
<line nr="21" mi="0" ci="18" mb="0" cb="0"/>
<line nr="22" mi="0" ci="12" mb="0" cb="0"/>
<line nr="23" mi="0" ci="5" mb="0" cb="0"/>
<line nr="24" mi="0" ci="14" mb="0" cb="2"/>
<line nr="25" mi="1" ci="22" mb="2" cb="6"/>
<line nr="26" mi="0" ci="9" mb="0" cb="0"/>
<line nr="28" mi="0" ci="7" mb="0" cb="0"/>
<line nr="29" mi="0" ci="1" mb="0" cb="0"/>
<line nr="32" mi="0" ci="2" mb="0" cb="0"/>
<line nr="34" mi="0" ci="13" mb="0" cb="0"/>
<line nr="36" mi="2" ci="0" mb="0" cb="0"/>
<line nr="37" mi="9" ci="0" mb="0" cb="0"/>
<line nr="38" mi="37" ci="0" mb="4" cb="0"/>
<line nr="39" mi="3" ci="0" mb="0" cb="0"/>
<line nr="40" mi="1" ci="0" mb="0" cb="0"/>
<line nr="43" mi="0" ci="7" mb="1" cb="1"/>
<line nr="44" mi="20" ci="0" mb="2" cb="0"/>
<line nr="46" mi="0" ci="2" mb="0" cb="0"/>
<line nr="49" mi="0" ci="7" mb="0" cb="0"/>
<line nr="50" mi="0" ci="1" mb="0" cb="0"/>
<line nr="53" mi="0" ci="5" mb="0" cb="0"/>
<line nr="54" mi="0" ci="3" mb="0" cb="0"/>
<line nr="55" mi="0" ci="18" mb="0" cb="4"/>
<line nr="56" mi="0" ci="3" mb="0" cb="0"/>
<line nr="57" mi="0" ci="1" mb="0" cb="0"/>
<line nr="58" mi="0" ci="3" mb="0" cb="0"/>
<line nr="59" mi="0" ci="3" mb="0" cb="0"/>
<line nr="60" mi="0" ci="5" mb="0" cb="0"/>
<line nr="62" mi="0" ci="1" mb="0" cb="0"/>
<line nr="64" mi="0" ci="6" mb="0" cb="0"/>
<line nr="65" mi="0" ci="2" mb="0" cb="0"/>
<line nr="69" mi="2" ci="5" mb="0" cb="0"/>
<line nr="70" mi="17" ci="14" mb="3" cb="1"/>
<line nr="71" mi="1" ci="8" mb="0" cb="0"/>
<line nr="72" mi="0" ci="20" mb="0" cb="0"/>
<line nr="73" mi="0" ci="16" mb="0" cb="2"/>
<line nr="74" mi="0" ci="22" mb="0" cb="0"/>
<line nr="75" mi="2" ci="3" mb="1" cb="1"/>
<line nr="76" mi="0" ci="2" mb="0" cb="0"/>
<line nr="77" mi="0" ci="19" mb="0" cb="2"/>
<line nr="78" mi="0" ci="1" mb="0" cb="0"/>
<line nr="79" mi="0" ci="13" mb="0" cb="2"/>
<line nr="80" mi="0" ci="5" mb="0" cb="0"/>
<line nr="81" mi="0" ci="4" mb="0" cb="0"/>
<line nr="83" mi="0" ci="4" mb="0" cb="0"/>
<line nr="84" mi="0" ci="12" mb="0" cb="0"/>
<line nr="85" mi="0" ci="10" mb="0" cb="2"/>
<line nr="86" mi="0" ci="7" mb="0" cb="0"/>
<line nr="87" mi="0" ci="9" mb="0" cb="0"/>
<line nr="94" mi="0" ci="10" mb="0" cb="2"/>
<line nr="95" mi="0" ci="5" mb="0" cb="0"/>
<line nr="96" mi="0" ci="4" mb="0" cb="0"/>
<line nr="98" mi="0" ci="9" mb="0" cb="0"/>
<line nr="99" mi="0" ci="12" mb="0" cb="0"/>
<line nr="100" mi="0" ci="10" mb="0" cb="2"/>
<line nr="101" mi="0" ci="7" mb="0" cb="0"/>
<line nr="102" mi="0" ci="4" mb="0" cb="0"/>
<line nr="103" mi="0" ci="9" mb="0" cb="0"/>
<line nr="104" mi="0" ci="10" mb="0" cb="2"/>
<line nr="105" mi="0" ci="5" mb="0" cb="0"/>
<line nr="106" mi="0" ci="10" mb="0" cb="0"/>
<line nr="107" mi="0" ci="4" mb="0" cb="0"/>
<line nr="108" mi="0" ci="3" mb="0" cb="2"/>
<line nr="109" mi="0" ci="2" mb="0" cb="0"/>
<line nr="110" mi="0" ci="5" mb="0" cb="0"/>
<line nr="111" mi="0" ci="2" mb="0" cb="0"/>
<line nr="113" mi="0" ci="1" mb="0" cb="0"/>
<line nr="114" mi="0" ci="7" mb="0" cb="0"/>
<line nr="116" mi="0" ci="3" mb="0" cb="0"/>
<line nr="117" mi="0" ci="12" mb="0" cb="0"/>
<line nr="118" mi="0" ci="26" mb="0" cb="0"/>
<line nr="119" mi="0" ci="10" mb="0" cb="2"/>
<line nr="120" mi="0" ci="7" mb="0" cb="0"/>
<line nr="122" mi="0" ci="3" mb="0" cb="0"/>
<line nr="123" mi="0" ci="9" mb="0" cb="0"/>
<line nr="124" mi="0" ci="14" mb="0" cb="2"/>
<line nr="125" mi="0" ci="2" mb="0" cb="0"/>
<line nr="126" mi="0" ci="3" mb="0" cb="0"/>
<counter type="INSTRUCTION" missed="98" covered="642"/>
<counter type="BRANCH" missed="16" covered="38"/>
<counter type="LINE" missed="6" covered="81"/>
<counter type="COMPLEXITY" missed="15" covered="20"/>
<counter type="METHOD" missed="3" covered="5"/>
<counter type="CLASS" missed="2" covered="2"/>
</sourcefile>
</package>
<package name="com/example/models">
<sourcefile name="Thing.kt">
<line nr="9" mi="0" ci="8" mb="0" cb="0"/>
<line nr="10" mi="4" ci="3" mb="0" cb="0"/>
<line nr="11" mi="3" ci="0" mb="0" cb="0"/>
<line nr="14" mi="0" ci="8" mb="0" cb="2"/>
<line nr="15" mi="9" ci="3" mb="1" cb="1"/>
<line nr="16" mi="0" ci="1" mb="0" cb="0"/>
<line nr="19" mi="0" ci="8" mb="0" cb="0"/>
<line nr="20" mi="4" ci="3" mb="0" cb="0"/>
<line nr="21" mi="3" ci="0" mb="0" cb="0"/>
<line nr="24" mi="5" ci="3" mb="1" cb="1"/>
<line nr="25" mi="9" ci="3" mb="1" cb="1"/>
<line nr="26" mi="0" ci="1" mb="0" cb="0"/>
<line nr="29" mi="0" ci="11" mb="0" cb="0"/>
<line nr="30" mi="0" ci="3" mb="0" cb="0"/>
<line nr="31" mi="4" ci="3" mb="0" cb="0"/>
<line nr="32" mi="4" ci="3" mb="0" cb="0"/>
<line nr="35" mi="0" ci="12" mb="0" cb="2"/>
<line nr="36" mi="0" ci="12" mb="0" cb="2"/>
<line nr="37" mi="0" ci="1" mb="0" cb="0"/>
<line nr="40" mi="0" ci="5" mb="0" cb="0"/>
<line nr="41" mi="4" ci="3" mb="0" cb="0"/>
<line nr="44" mi="9" ci="3" mb="1" cb="1"/>
<line nr="45" mi="0" ci="1" mb="0" cb="0"/>
<line nr="48" mi="0" ci="12" mb="0" cb="0"/>
<line nr="49" mi="0" ci="3" mb="0" cb="0"/>
<line nr="50" mi="0" ci="3" mb="0" cb="0"/>
<line nr="51" mi="3" ci="0" mb="0" cb="0"/>
<counter type="INSTRUCTION" missed="61" covered="119"/>
<counter type="BRANCH" missed="4" covered="10"/>
<counter type="LINE" missed="3" covered="24"/>
<counter type="COMPLEXITY" missed="12" covered="16"/>
<counter type="METHOD" missed="8" covered="13"/>
<counter type="CLASS" missed="0" covered="5"/>
</sourcefile>
</package>
<package name="com/example/generated">
<sourcefile name="Untracked.kt">
<line nr="12" mi="8" ci="0" mb="0" cb="0"/>
<line nr="17" mi="6" ci="0" mb="0" cb="0"/>
<line nr="20" mi="8" ci="0" mb="0" cb="0"/>
<line nr="21" mi="2" ci="0" mb="0" cb="0"/>
<line nr="22" mi="3" ci="0" mb="0" cb="0"/>
<line nr="23" mi="4" ci="0" mb="0" cb="0"/>
<line nr="24" mi="13" ci="0" mb="0" cb="0"/>
<line nr="27" mi="7" ci="0" mb="0" cb="0"/>
<line nr="28" mi="5" ci="0" mb="4" cb="0"/>
<line nr="29" mi="19" ci="0" mb="0" cb="0"/>
<line nr="30" mi="4" ci="0" mb="0" cb="0"/>
<line nr="31" mi="3" ci="0" mb="0" cb="0"/>
<line nr="32" mi="1" ci="0" mb="0" cb="0"/>
<line nr="33" mi="1" ci="0" mb="0" cb="0"/>
<line nr="34" mi="2" ci="0" mb="0" cb="0"/>
<line nr="35" mi="2" ci="0" mb="0" cb="0"/>
<line nr="39" mi="23" ci="0" mb="0" cb="0"/>
<line nr="40" mi="3" ci="0" mb="0" cb="0"/>
<line nr="41" mi="3" ci="0" mb="0" cb="0"/>
<line nr="42" mi="1" ci="0" mb="0" cb="0"/>
<line nr="43" mi="5" ci="0" mb="0" cb="0"/>
<line nr="44" mi="3" ci="0" mb="2" cb="0"/>
<line nr="45" mi="21" ci="0" mb="0" cb="0"/>
<line nr="46" mi="13" ci="0" mb="0" cb="0"/>
<line nr="48" mi="1" ci="0" mb="0" cb="0"/>
<line nr="51" mi="9" ci="0" mb="0" cb="0"/>
<line nr="52" mi="2" ci="0" mb="0" cb="0"/>
<line nr="53" mi="3" ci="0" mb="0" cb="0"/>
<line nr="54" mi="4" ci="0" mb="0" cb="0"/>
<line nr="55" mi="13" ci="0" mb="0" cb="0"/>
<line nr="58" mi="7" ci="0" mb="0" cb="0"/>
<line nr="59" mi="5" ci="0" mb="4" cb="0"/>
<line nr="60" mi="19" ci="0" mb="0" cb="0"/>
<line nr="61" mi="4" ci="0" mb="0" cb="0"/>
<line nr="62" mi="3" ci="0" mb="0" cb="0"/>
<line nr="63" mi="1" ci="0" mb="0" cb="0"/>
<line nr="64" mi="1" ci="0" mb="0" cb="0"/>
<line nr="65" mi="2" ci="0" mb="0" cb="0"/>
<line nr="66" mi="2" ci="0" mb="0" cb="0"/>
<line nr="70" mi="23" ci="0" mb="0" cb="0"/>
<line nr="71" mi="1" ci="0" mb="0" cb="0"/>
<line nr="72" mi="2" ci="0" mb="0" cb="0"/>
<line nr="73" mi="3" ci="0" mb="0" cb="0"/>
<line nr="74" mi="1" ci="0" mb="0" cb="0"/>
<line nr="75" mi="5" ci="0" mb="0" cb="0"/>
<line nr="76" mi="3" ci="0" mb="2" cb="0"/>
<line nr="77" mi="21" ci="0" mb="0" cb="0"/>
<line nr="78" mi="13" ci="0" mb="0" cb="0"/>
<line nr="80" mi="1" ci="0" mb="0" cb="0"/>
<line nr="82" mi="1" ci="0" mb="0" cb="0"/>
<line nr="84" mi="15" ci="0" mb="0" cb="0"/>
<line nr="87" mi="1" ci="5" mb="0" cb="0"/>
<line nr="89" mi="0" ci="5" mb="0" cb="0"/>
<line nr="90" mi="0" ci="6" mb="0" cb="0"/>
<line nr="91" mi="0" ci="8" mb="0" cb="0"/>
<line nr="92" mi="0" ci="8" mb="0" cb="0"/>
<line nr="93" mi="0" ci="8" mb="0" cb="0"/>
<line nr="94" mi="0" ci="8" mb="0" cb="0"/>
<line nr="95" mi="0" ci="8" mb="0" cb="0"/>
<line nr="96" mi="0" ci="8" mb="0" cb="0"/>
<line nr="97" mi="0" ci="8" mb="0" cb="0"/>
<line nr="98" mi="0" ci="8" mb="0" cb="0"/>
<line nr="99" mi="0" ci="8" mb="0" cb="0"/>
<line nr="100" mi="0" ci="9" mb="0" cb="0"/>
<line nr="101" mi="0" ci="8" mb="0" cb="0"/>
<line nr="102" mi="0" ci="8" mb="0" cb="0"/>
<line nr="103" mi="0" ci="7" mb="0" cb="0"/>
<line nr="107" mi="2" ci="8" mb="0" cb="0"/>
<line nr="108" mi="0" ci="8" mb="0" cb="0"/>
<line nr="109" mi="0" ci="8" mb="0" cb="0"/>
<line nr="110" mi="0" ci="3" mb="0" cb="0"/>
<line nr="113" mi="2" ci="9" mb="0" cb="0"/>
<line nr="114" mi="0" ci="8" mb="0" cb="0"/>
<line nr="115" mi="0" ci="8" mb="0" cb="0"/>
<line nr="116" mi="0" ci="8" mb="0" cb="0"/>
<line nr="117" mi="0" ci="8" mb="0" cb="0"/>
<line nr="118" mi="0" ci="8" mb="0" cb="0"/>
<line nr="119" mi="0" ci="8" mb="0" cb="0"/>
<line nr="120" mi="0" ci="3" mb="0" cb="0"/>
<counter type="INSTRUCTION" missed="345" covered="212"/>
<counter type="BRANCH" missed="12" covered="0"/>
<counter type="LINE" missed="51" covered="28"/>
<counter type="COMPLEXITY" missed="19" covered="1"/>
<counter type="METHOD" missed="13" covered="1"/>
<counter type="CLASS" missed="9" covered="1"/>
</sourcefile>
</package>
</report>
@@ -0,0 +1,20 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { FileEntry } from '../types';
export interface Converter {
convert(xml: unknown, scmFiles: Array<string>): Array<FileEntry>;
}
@@ -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);
});
});
});
@@ -0,0 +1,126 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { BranchHit, FileEntry } from '../types';
import { CoberturaXML, InnerClass, LineHit } from './types';
import { Logger } from 'winston';
import { Converter } from './Converter';
export class Cobertura implements Converter {
constructor(readonly logger: Logger) {
this.logger = logger;
}
/**
* convert cobertura into shared json coverage format
*
* @param xml cobertura xml object
* @param scmFiles list of files that are commited to SCM
*/
convert(xml: CoberturaXML, scmFiles: string[]): FileEntry[] {
const ppc = xml.coverage.packages
?.flatMap(p => p.package)
.filter(Boolean)
.flatMap(p => p.classes);
const pc = xml.coverage.package?.filter(Boolean).flatMap(p => p.classes);
const classes = [ppc, pc]
.flat()
.filter(Boolean)
.flatMap(c => c.class)
.filter(Boolean);
const jscov: Array<FileEntry> = [];
classes.forEach(c => {
const packageAndFilename = c.$.filename;
const lines = this.extractLines(c);
const lineHits: Record<number, number> = {};
const branchHits: Record<number, BranchHit> = {};
lines.forEach(l => {
if (!lineHits[l.number]) {
lineHits[l.number] = 0;
}
lineHits[l.number] += l.hits;
if (l.branch && l['condition-coverage']) {
const bh = this.parseBranch(l['condition-coverage']);
if (bh) {
branchHits[l.number] = bh;
}
}
});
const currentFile = scmFiles.find(f => f.endsWith(packageAndFilename));
this.logger.debug(`matched ${packageAndFilename} to ${currentFile}`);
if (
scmFiles.length === 0 ||
(Object.keys(lineHits).length > 0 && currentFile)
) {
jscov.push({
filename: currentFile || packageAndFilename,
branchHits: branchHits,
lineHits: lineHits,
});
}
});
return jscov;
}
/**
* Parses branch coverage information from condition-coverage
*
* @param condition condition-coverage value from line coverage
*/
private parseBranch(condition: string): BranchHit | null {
const pattern = /[0-9\.]+\%\s\(([0-9]+)\/([0-9]+)\)/;
const match = condition.match(pattern);
if (!match) {
return null;
}
const covered = parseInt(match[1], 10);
const available = parseInt(match[2], 10);
return {
covered: covered,
missed: available - covered,
available: available,
};
}
/**
* Extract line hits from a class coverage entry
*
* @param clz class coverage information
*/
private extractLines(clz: InnerClass): Array<LineHit> {
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;
}
}
@@ -0,0 +1,19 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './Converter';
export { Cobertura } from './cobertura';
export { Jacoco } from './jacoco';
@@ -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());
});
});
@@ -0,0 +1,96 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { BranchHit, FileEntry } from '../types';
import { JacocoSourceFile, JacocoXML } from './types';
import { Logger } from 'winston';
import { Converter } from './Converter';
type ParsedLine = {
number: number;
missed_instructions: number;
covered_instructions: number;
missed_branches: number;
covered_branches: number;
};
export class Jacoco implements Converter {
constructor(readonly logger: Logger) {
this.logger = logger;
}
/**
* Converts jacoco into shared json coverage format
*
* @param xml jacoco xml object
* @param scmFiles list of files that are committed to SCM
*/
convert(xml: JacocoXML, scmFiles: Array<string>): Array<FileEntry> {
const jscov: Array<FileEntry> = [];
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<number, number> = {};
const branchHits: Record<number, BranchHit> = {};
lines.forEach(l => {
if (!lineHits[l.number]) {
lineHits[l.number] = 0;
}
lineHits[l.number] += l.covered_instructions;
const ab = l.covered_branches + l.missed_branches;
if (ab > 0) {
branchHits[l.number] = {
covered: l.covered_branches,
missed: l.missed_branches,
available: ab,
};
}
});
const packageAndFilename = `${packageName}/${fileName}`;
const currentFile = scmFiles.find(f => f.endsWith(packageAndFilename));
this.logger.debug(`matched ${packageAndFilename} to ${currentFile}`);
if (Object.keys(lineHits).length > 0 && currentFile) {
jscov.push({
filename: currentFile,
branchHits: branchHits,
lineHits: lineHits,
});
}
});
});
return jscov;
}
private extractLines(sourcefile: JacocoSourceFile): ParsedLine[] {
const parsed: ParsedLine[] = [];
sourcefile.line.forEach(l => {
parsed.push({
number: parseInt(l.$.nr, 10),
missed_instructions: parseInt(l.$.mi, 10),
covered_instructions: parseInt(l.$.ci, 10),
missed_branches: parseInt(l.$.mb, 10),
covered_branches: parseInt(l.$.cb, 10),
});
});
return parsed;
}
}
@@ -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>;
package: Array<InnerPackage>;
};
export type Package = {
package: Array<InnerPackage>;
};
export type InnerPackage = {
classes: Array<Class>;
};
export type Class = {
class: Array<InnerClass>;
};
export type InnerClass = {
$: {
filename: string;
};
lines: Array<Line>;
methods: Array<Method>;
};
export type Method = {
method: Array<InnerMethod>;
};
export type InnerMethod = {
lines: Array<Line>;
};
export type Line = {
line: Array<InnerLine>;
};
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;
};
};
@@ -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<PluginEndpointDiscovery> = {
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' });
});
});
});
@@ -0,0 +1,234 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import express from 'express';
import Router from 'express-promise-router';
import { Logger } from 'winston';
import xmlparser from 'express-xml-bodyparser';
import { CatalogClient } from '@backstage/catalog-client';
import {
errorHandler,
PluginDatabaseManager,
PluginEndpointDiscovery,
UrlReader,
} from '@backstage/backend-common';
import { InputError, NotFoundError } from '@backstage/errors';
import { Config } from '@backstage/config';
import { ScmIntegrations } from '@backstage/integration';
import { CodeCoverageDatabase } from './CodeCoverageDatabase';
import { aggregateCoverage, CoverageUtils } from './CoverageUtils';
import { Cobertura } from './converter/cobertura';
import { Jacoco } from './converter/jacoco';
import { Converter } from './converter';
import {
EntityRef,
getEntitySourceLocation,
parseEntityName,
} from '@backstage/catalog-model';
export interface RouterOptions {
config: Config;
discovery: PluginEndpointDiscovery;
database: PluginDatabaseManager;
urlReader: UrlReader;
logger: Logger;
}
export interface CodeCoverageApi {
name: string;
}
export const makeRouter = async (
options: RouterOptions,
): Promise<express.Router> => {
const { config, logger, discovery, database, urlReader } = options;
const codeCoverageDatabase = await CodeCoverageDatabase.create(
await database.getClient(),
);
const codecovUrl = await discovery.getExternalBaseUrl('code-coverage');
const catalogApi = new CatalogClient({ discoveryApi: discovery });
const scm = ScmIntegrations.fromConfig(config);
const router = Router();
router.use(xmlparser());
router.use(express.json());
const utils = new CoverageUtils(scm, urlReader);
router.get('/health', async (_req, res) => {
res.status(200).json({ status: 'ok' });
});
/**
* /report?entity=component:default/mycomponent
*/
router.get('/report', async (req, res) => {
const { entity } = req.query;
const entityName = parseEntityName(entity as EntityRef);
const entityLookup = await catalogApi.getEntityByName(entityName);
if (!entityLookup) {
throw new NotFoundError(`No entity found matching ${entity}`);
}
const stored = await codeCoverageDatabase.getCodeCoverage(entity as string);
const aggregate = aggregateCoverage(stored);
res.status(200).json({
...stored,
aggregate: {
line: aggregate.line,
branch: aggregate.branch,
},
});
});
/**
* /history?entity=component:default/mycomponent
*/
router.get('/history', async (req, res) => {
const { entity } = req.query;
const entityName = parseEntityName(entity as EntityRef);
const entityLookup = await catalogApi.getEntityByName(entityName);
if (!entityLookup) {
throw new NotFoundError(`No entity found matching ${entity}`);
}
const { limit } = req.query;
const history = await codeCoverageDatabase.getHistory(
entity as string,
parseInt(limit?.toString() || '10', 10),
);
res.status(200).json(history);
});
/**
* /file-content?entity=component:default/mycomponent&path=src/some-file.go
*/
router.get('/file-content', async (req, res) => {
const { entity, path } = req.query;
const entityName = parseEntityName(entity as EntityRef);
const entityLookup = await catalogApi.getEntityByName(entityName);
if (!entityLookup) {
throw new NotFoundError(`No entity found matching ${entity}`);
}
if (!path) {
throw new InputError('Need path query parameter');
}
const sourceLocation = getEntitySourceLocation(entityLookup);
if (!sourceLocation) {
throw new InputError(
`No "backstage.io/source-location" annotation on entity ${entity}`,
);
}
const vcs = scm.byUrl(sourceLocation.target);
if (!vcs) {
throw new InputError(`Unable to determine SCM from ${sourceLocation}`);
}
const scmTree = await urlReader.readTree(sourceLocation.target);
const scmFile = (await scmTree.files()).find(f => f.path === path);
if (!scmFile) {
res.status(404).json({
message: "Couldn't find file in SCM",
file: path,
scm: vcs.title,
});
return;
}
const content = await scmFile?.content();
if (!content) {
res.status(400).json({
message: "Couldn't process content of file in SCM",
file: path,
scm: vcs.title,
});
return;
}
const data = content.toString();
res.status(200).contentType('text/plain').send(data);
});
/**
* /report?entity=component:default/mycomponent&coverageType=cobertura
*/
router.post('/report', async (req, res) => {
const { entity, coverageType } = req.query;
const entityName = parseEntityName(entity as EntityRef);
const entityLookup = await catalogApi.getEntityByName(entityName);
if (!entityLookup) {
throw new NotFoundError(`No entity found matching ${entity}`);
}
let converter: Converter;
if (!coverageType) {
throw new InputError('Need coverageType query parameter');
} else if (coverageType === 'jacoco') {
converter = new Jacoco(logger);
} else if (coverageType === 'cobertura') {
converter = new Cobertura(logger);
} else {
throw new InputError(`Unsupported coverage type '${coverageType}`);
}
const {
sourceLocation,
vcs,
scmFiles,
body,
} = await utils.processCoveragePayload(entityLookup, req);
const files = converter.convert(body, scmFiles);
if (!files || files.length === 0) {
throw new InputError(`Unable to parse body as ${coverageType}`);
}
const coverage = await utils.buildCoverage(
entityLookup,
sourceLocation,
vcs,
files,
);
await codeCoverageDatabase.insertCodeCoverage(coverage);
res.status(201).json({
links: [
{
rel: 'coverage',
href: `${codecovUrl}/report?entity=${entity}`,
},
],
});
});
router.use(errorHandler());
return router;
};
export async function createRouter(
options: RouterOptions,
): Promise<express.Router> {
const logger = options.logger;
logger.info('Initializing Code Coverage backend');
return makeRouter(options);
}
@@ -0,0 +1,74 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
createServiceBuilder,
loadBackendConfig,
SingleHostDiscovery,
UrlReaders,
useHotMemoize,
} from '@backstage/backend-common';
import { Server } from 'http';
import knexFactory from 'knex';
import { Logger } from 'winston';
import { createRouter } from './router';
export interface ServerOptions {
port: number;
enableCors: boolean;
logger: Logger;
}
export async function startStandaloneServer(
options: ServerOptions,
): Promise<Server> {
const logger = options.logger.child({ service: 'code-coverage-backend' });
const config = await loadBackendConfig({ logger, argv: process.argv });
const db = useHotMemoize(module, () => {
const knex = knexFactory({
client: 'sqlite3',
connection: ':memory:',
useNullAsDefault: true,
});
knex.client.pool.on('createSuccess', (_eventId: any, resource: any) => {
resource.run('PRAGMA foreign_keys = ON', () => {});
});
return knex;
});
logger.debug('Starting application server...');
const router = await createRouter({
database: { getClient: async () => db },
config,
discovery: SingleHostDiscovery.fromConfig(config),
urlReader: UrlReaders.default({ logger, config }),
logger,
});
const service = createServiceBuilder(module)
.enableCors({ origin: 'http://localhost:3000' })
.addRouter('/code-coverage', router);
return await service.start().catch(err => {
logger.error(err);
process.exit(1);
});
}
module.hot?.accept();
@@ -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<FileEntry>;
};
export type JsonCoverageHistory = {
entity: EntityName;
history: Array<AggregateCoverage>;
};
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<number, number>;
branchHits: Record<number, BranchHit>;
};
export type AggregateCoverage = {
timestamp: number;
line: {
available: number;
covered: number;
missed: number;
percentage: number;
};
branch: {
available: number;
covered: number;
missed: number;
percentage: number;
};
};
@@ -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 {};
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
};
+23
View File
@@ -0,0 +1,23 @@
# code-coverage
This is the frontend part of the code-coverage plugin. It displays code coverage summaries for your entities.
## Configuring your entity
In order to use this plugin, you must set the `backstage.io/code-coverage` annotation on entities for which coverage ingestion has been enabled.
```yaml
metadata:
annotations:
backstage.io/code-coverage: enabled
```
There's a feature to only include files that are in VCS in the coverage report, this is helpful to not count generated files for example. To enable this set the `backstage.io/code-coverage` annotation to `scm-only`.
```yaml
metadata:
annotations:
backstage.io/code-coverage: scm-only
```
Note: It may be required to set the [`backstage.io/source-location` annotation](https://backstage.io/docs/features/software-catalog/well-known-annotations#backstageiosource-location), however this should generally not be needed.
+26
View File
@@ -0,0 +1,26 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { createDevApp } from '@backstage/dev-utils';
import { codeCoveragePlugin, EntityCodeCoverageContent } from '../src/plugin';
createDevApp()
.registerPlugin(codeCoveragePlugin)
.addPage({
element: <EntityCodeCoverageContent />,
title: 'Root Page',
})
.render();
+58
View File
@@ -0,0 +1,58 @@
{
"name": "@backstage/plugin-code-coverage",
"version": "0.1.1",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"scripts": {
"build": "backstage-cli plugin:build",
"start": "backstage-cli plugin:serve",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"diff": "backstage-cli plugin:diff",
"prepack": "backstage-cli prepack",
"postpack": "backstage-cli postpack",
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/catalog-model": "^0.7.7",
"@backstage/config": "^0.1.4",
"@backstage/core": "^0.7.5",
"@backstage/errors": "^0.1.1",
"@backstage/plugin-catalog-react": "^0.1.4",
"@backstage/theme": "^0.2.5",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/styles": "^4.11.0",
"@material-ui/lab": "4.0.0-alpha.45",
"highlight.js": "^10.6.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-router": "6.0.0-beta.0",
"react-router-dom": "6.0.0-beta.0",
"react-use": "^15.3.3",
"recharts": "^1.8.5"
},
"devDependencies": {
"@backstage/cli": "^0.6.8",
"@backstage/dev-utils": "^0.1.13",
"@backstage/test-utils": "^0.1.10",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
"@testing-library/user-event": "^12.0.7",
"@types/highlightjs": "^10.1.0",
"@types/jest": "^26.0.7",
"@types/node": "^14.14.32",
"@types/recharts": "^1.8.15",
"cross-fetch": "^3.0.6",
"msw": "^0.21.2"
},
"files": [
"dist"
]
}
+93
View File
@@ -0,0 +1,93 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { EntityName, stringifyEntityRef } from '@backstage/catalog-model';
import { createApiRef, DiscoveryApi } from '@backstage/core';
import { ResponseError } from '@backstage/errors';
import { JsonCodeCoverage, JsonCoverageHistory } from './types';
export type CodeCoverageApi = {
discovery: DiscoveryApi;
getCoverageForEntity: (entity: EntityName) => Promise<JsonCodeCoverage>;
getFileContentFromEntity: (
entity: EntityName,
filePath: string,
) => Promise<string>;
getCoverageHistoryForEntity: (
entity: EntityName,
limit?: number,
) => Promise<JsonCoverageHistory>;
};
export const codeCoverageApiRef = createApiRef<CodeCoverageApi>({
id: 'plugin.code-coverage.service',
description: 'Used by the code coverage plugin to make requests',
});
export class CodeCoverageRestApi implements CodeCoverageApi {
url: string = '';
constructor(public discovery: DiscoveryApi) {}
private async fetch<T = unknown | string | JsonCoverageHistory>(
path: string,
init?: RequestInit,
): Promise<T | string> {
if (!this.url) {
this.url = await this.discovery.getBaseUrl('code-coverage');
}
const resp = await fetch(`${this.url}${path}`, init);
if (!resp.ok) {
throw await ResponseError.fromResponse(resp);
}
if (resp.headers.get('content-type')?.includes('application/json')) {
return await resp.json();
}
return await resp.text();
}
async getCoverageForEntity(
entityName: EntityName,
): Promise<JsonCodeCoverage> {
const entity = encodeURIComponent(stringifyEntityRef(entityName));
return (await this.fetch<JsonCodeCoverage>(
`/report?entity=${entity}`,
)) as JsonCodeCoverage;
}
async getFileContentFromEntity(
entityName: EntityName,
filePath: string,
): Promise<string> {
const entity = encodeURIComponent(stringifyEntityRef(entityName));
return await this.fetch<string>(
`/file-content?entity=${entity}&path=${encodeURI(filePath)}`,
);
}
async getCoverageHistoryForEntity(
entityName: EntityName,
limit?: number,
): Promise<JsonCoverageHistory> {
const entity = encodeURIComponent(stringifyEntityRef(entityName));
const hasValidLimit = limit && limit > 0;
return (await this.fetch<JsonCoverageHistory>(
`/history?entity=${entity}${
hasValidLimit ? `&limit=${encodeURIComponent(String(limit))}` : ''
}`,
)) as JsonCoverageHistory;
}
}
@@ -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 (
<Page themeId="tool">
<Content>
<ContentHeader title="Code coverage" />
<CoverageHistoryChart />
<FileExplorer />
</Content>
</Page>
);
};
@@ -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';
@@ -0,0 +1,166 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Progress, ResponseErrorPanel, useApi } from '@backstage/core';
import { useEntity } from '@backstage/plugin-catalog-react';
import { BackstageTheme } from '@backstage/theme';
import {
Box,
Card,
CardContent,
CardHeader,
makeStyles,
Typography,
} from '@material-ui/core';
import TrendingDownIcon from '@material-ui/icons/TrendingDown';
import TrendingFlatIcon from '@material-ui/icons/TrendingFlat';
import TrendingUpIcon from '@material-ui/icons/TrendingUp';
import { Alert } from '@material-ui/lab';
import { ClassNameMap } from '@material-ui/styles';
import React from 'react';
import { useAsync } from 'react-use';
import {
CartesianGrid,
Legend,
Line,
LineChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from 'recharts';
import { codeCoverageApiRef } from '../../api';
type Coverage = 'line' | 'branch';
const useStyles = makeStyles<BackstageTheme>(theme => ({
trendDown: {
color: theme.palette.status.warning,
},
trendUp: {
color: theme.palette.status.ok,
},
}));
const getTrendIcon = (trend: number, classes: ClassNameMap) => {
switch (true) {
case trend > 0:
return <TrendingUpIcon className={classes.trendUp} />;
case trend < 0:
return <TrendingDownIcon className={classes.trendDown} />;
case trend === 0:
default:
return <TrendingFlatIcon />;
}
};
export const CoverageHistoryChart = () => {
const { entity } = useEntity();
const codeCoverageApi = useApi(codeCoverageApiRef);
const {
loading: loadingHistory,
error: errorHistory,
value: valueHistory,
} = useAsync(
async () =>
await codeCoverageApi.getCoverageHistoryForEntity({
kind: entity.kind,
namespace: entity.metadata.namespace || 'default',
name: entity.metadata.name,
}),
);
const classes = useStyles();
if (loadingHistory) {
return <Progress />;
}
if (errorHistory) {
return <ResponseErrorPanel error={errorHistory} />;
} else if (!valueHistory) {
return <Alert severity="warning">No history found.</Alert>;
}
if (!valueHistory.history.length) {
return (
<Card>
<CardHeader title="History" />
<CardContent>No coverage history found</CardContent>
</Card>
);
}
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 (
<Card>
<CardHeader title="History" />
<CardContent>
<Box px={6} display="flex">
<Box display="flex" mr={4}>
{getTrendIcon(lineTrend, classes)}
<Typography>
Current line: {latestCoverage.line.percentage}%<br />(
{Math.floor(lineTrend)}% change over {valueHistory.history.length}{' '}
builds)
</Typography>
</Box>
<Box display="flex">
{getTrendIcon(branchTrend, classes)}
<Typography>
Current branch: {latestCoverage.branch.percentage}%<br />(
{Math.floor(branchTrend)}% change over{' '}
{valueHistory.history.length} builds)
</Typography>
</Box>
</Box>
<ResponsiveContainer width="100%" height={300}>
<LineChart
data={valueHistory.history}
margin={{ right: 48, top: 32 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="timestamp" />
<YAxis dataKey="line.percentage" />
<YAxis dataKey="branch.percentage" />
<Tooltip />
<Legend />
<Line
type="monotone"
dataKey="branch.percentage"
stroke="#8884d8"
/>
<Line type="monotone" dataKey="line.percentage" stroke="#82ca9d" />
</LineChart>
</ResponsiveContainer>
</CardContent>
</Card>
);
};
@@ -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';
@@ -0,0 +1,100 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { makeStyles } from '@material-ui/core';
const useStyles = makeStyles(theme => ({
lineNumberCell: {
color: `${theme.palette.grey[500]}`,
fontSize: '90%',
borderRight: `1px solid ${theme.palette.grey[500]}`,
paddingRight: theme.spacing(1),
textAlign: 'right',
},
hitCountCell: {
width: '50px',
borderRight: `1px solid ${theme.palette.grey[500]}`,
textAlign: 'center',
color: 'white', // need to enforce this color since it needs to stand out against colored background
paddingLeft: theme.spacing(1),
paddingRight: theme.spacing(1),
},
countRoundedRectangle: {
borderRadius: '45px',
fontSize: '90%',
padding: '1px 3px 1px 3px',
width: '50px',
},
hitCountRoundedRectangle: {
backgroundColor: `${theme.palette.success.main}`,
},
notHitCountRoundedRectangle: {
backgroundColor: `${theme.palette.error.main}`,
},
codeLine: {
paddingLeft: `${theme.spacing(1)}`,
whiteSpace: 'pre',
fontSize: '90%',
},
hitCodeLine: {
backgroundColor: `${theme.palette.success.main}`,
},
notHitCodeLine: {
backgroundColor: `${theme.palette.error.main}`,
},
}));
type CodeRowProps = {
lineNumber: number;
lineContent: string;
lineHits?: number | null;
};
export const CodeRow = ({
lineNumber,
lineContent,
lineHits = null,
}: CodeRowProps) => {
const classes = useStyles();
const hitCountRoundedRectangleClass = [classes.countRoundedRectangle];
const lineContentClass = [classes.codeLine];
let hitRoundedRectangle = null;
if (lineHits !== null) {
if (lineHits > 0) {
hitCountRoundedRectangleClass.push(classes.hitCountRoundedRectangle);
lineContentClass.push(classes.hitCodeLine);
} else {
hitCountRoundedRectangleClass.push(classes.notHitCountRoundedRectangle);
lineContentClass.push(classes.notHitCodeLine);
}
hitRoundedRectangle = (
<div className={hitCountRoundedRectangleClass.join(' ')}>{lineHits}</div>
);
}
return (
<tr>
<td className={classes.lineNumberCell}>{lineNumber}</td>
<td className={classes.hitCountCell}>{hitRoundedRectangle}</td>
<td
className={lineContentClass.join(' ')}
dangerouslySetInnerHTML={{ __html: lineContent }}
/>
</tr>
);
};
@@ -0,0 +1,132 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Progress, ResponseErrorPanel, useApi } from '@backstage/core';
import { useEntity } from '@backstage/plugin-catalog-react';
import { makeStyles, Paper } from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import React from 'react';
import { useAsync } from 'react-use';
import { codeCoverageApiRef } from '../../api';
import { FileEntry } from '../../types';
import { CodeRow } from './CodeRow';
import { highlightLines } from './Highlighter';
type Props = {
filename: string;
coverage: FileEntry;
};
const useStyles = makeStyles(theme => ({
paper: {
margin: 'auto',
top: '2em',
width: '80%',
border: '2px solid #000',
boxShadow: theme.shadows[5],
padding: theme.spacing(2, 4, 3),
overflow: 'scroll',
},
coverageFileViewTable: {
borderSpacing: '0px',
width: '80%',
marginTop: theme.spacing(2),
},
}));
type FormattedLinesProps = {
highlightedLines: string[];
lineHits: Record<number, number>;
};
const FormattedLines = ({
highlightedLines,
lineHits,
}: FormattedLinesProps) => {
return (
<>
{highlightedLines.map((lineContent, idx) => {
const line = idx + 1;
return (
<CodeRow
key={line}
lineNumber={line}
lineContent={lineContent}
lineHits={lineHits[line]}
/>
);
})}
</>
);
};
export const FileContent = ({ filename, coverage }: Props) => {
const { entity } = useEntity();
const codeCoverageApi = useApi(codeCoverageApiRef);
const { loading, error, value } = useAsync(
async () =>
await codeCoverageApi.getFileContentFromEntity(
{
kind: entity.kind,
namespace: entity.metadata.namespace || 'default',
name: entity.metadata.name,
},
filename,
),
[entity],
);
const classes = useStyles();
if (loading) {
return <Progress />;
}
if (error) {
return <ResponseErrorPanel error={error} />;
}
if (!value) {
return (
<Alert severity="warning">
Unable to retrieve file content for {filename}
</Alert>
);
}
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<string, number>, next: [string, number]) => {
acc[next[0]] = next[1];
return acc;
},
{},
);
return (
<Paper variant="outlined" className={classes.paper}>
<table className={classes.coverageFileViewTable}>
<tbody>
<FormattedLines
highlightedLines={highlightedLines}
lineHits={lineHits}
/>
</tbody>
</table>
</Paper>
);
};
@@ -0,0 +1,285 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
Progress,
ResponseErrorPanel,
Table,
TableColumn,
useApi,
} from '@backstage/core';
import { useEntity } from '@backstage/plugin-catalog-react';
import {
Box,
Card,
CardContent,
CardHeader,
Modal,
Tooltip,
} from '@material-ui/core';
import DescriptionIcon from '@material-ui/icons/Description';
import { Alert } from '@material-ui/lab';
import React, { Fragment, useEffect, useState } from 'react';
import { useAsync } from 'react-use';
import { codeCoverageApiRef } from '../../api';
import { FileEntry } from '../../types';
import { FileContent } from './FileContent';
type FileStructureObject = Record<string, any>;
type CoverageTableRow = {
filename?: string;
files: CoverageTableRow[];
coverage: number;
missing: number;
tracked: number;
path: string;
tableData?: { id: number };
};
const buildFileStructure = (row: CoverageTableRow) => {
const dataGroupedByPath: FileStructureObject = row.files.reduce(
(acc: FileStructureObject, cur: CoverageTableRow) => {
let path = cur.filename;
if (row.path) {
path = path?.split(`${row.path}/`)[1];
}
const pathArray = path?.split('/');
if (!pathArray) {
return acc;
}
if (!acc.hasOwnProperty(pathArray[0])) {
acc[pathArray[0]] = [];
}
acc[pathArray[0]].push(cur);
return acc;
},
{},
);
row.files = Object.keys(dataGroupedByPath).map(pathGroup => {
return buildFileStructure({
path: pathGroup,
files: dataGroupedByPath.hasOwnProperty('files')
? dataGroupedByPath.files
: dataGroupedByPath[pathGroup],
coverage:
dataGroupedByPath[pathGroup].reduce(
(acc: number, cur: CoverageTableRow) => acc + cur.coverage,
0,
) / dataGroupedByPath[pathGroup].length,
missing: dataGroupedByPath[pathGroup].reduce(
(acc: number, cur: CoverageTableRow) => acc + cur.missing,
0,
),
tracked: dataGroupedByPath[pathGroup].reduce(
(acc: number, cur: CoverageTableRow) => acc + cur.tracked,
0,
),
});
});
return row;
};
const formatInitialData = (value: any) => {
return buildFileStructure({
path: '',
coverage: value.aggregate.line.percentage,
missing: value.aggregate.line.missed,
tracked: value.aggregate.line.available,
files: value.files.map((fc: FileEntry) => {
return {
path: '',
filename: fc.filename,
coverage: Math.floor(
(Object.values(fc.lineHits).filter((hits: number) => hits > 0)
.length /
Object.values(fc.lineHits).length) *
100,
),
missing: Object.values(fc.lineHits).filter(hits => !hits).length,
tracked: Object.values(fc.lineHits).length,
};
}),
});
};
export const FileExplorer = () => {
const { entity } = useEntity();
const [curData, setCurData] = useState<CoverageTableRow | undefined>();
const [tableData, setTableData] = useState<CoverageTableRow[] | undefined>();
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 <Progress />;
} else if (error) {
return <ResponseErrorPanel error={error} />;
}
if (!value) {
return (
<Alert severity="warning">No code coverage found for ${entity}</Alert>
);
}
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<CoverageTableRow>[] = [
{
title: 'Path',
type: 'string',
field: 'path',
render: (row: CoverageTableRow) => {
if (row.files?.length) {
return (
<div
role="button"
tabIndex={row.tableData!.id}
style={{ color: 'lightblue', cursor: 'pointer' }}
onKeyDown={() => {
setCurPath(`${curPath}/${row.path}`);
moveDownIntoPath(row.path);
}}
onClick={() => {
setCurPath(`${curPath}/${row.path}`);
moveDownIntoPath(row.path);
}}
>
{row.path}
</div>
);
}
return (
<Box display="flex" alignItems="center">
{row.path}
<Tooltip title="View file content">
<DescriptionIcon
fontSize="small"
style={{ color: 'lightblue', cursor: 'pointer' }}
onClick={() => {
setCurFile(`${curPath.slice(1)}/${row.path}`);
setModalOpen(true);
}}
/>
</Tooltip>
</Box>
);
},
},
{
title: 'Coverage',
type: 'numeric',
field: 'coverage',
render: (row: CoverageTableRow) => `${row.coverage}%`,
},
{
title: 'Missing lines',
type: 'numeric',
field: 'missing',
},
{
title: 'Tracked lines',
type: 'numeric',
field: 'tracked',
},
];
const pathArray = curPath.split('/');
const lastPathElementIndex = pathArray.length - 1;
const fileCoverage = value.files.find((f: FileEntry) =>
f.filename.endsWith(curFile),
);
if (!fileCoverage) {
return null;
}
return (
<Card>
<CardHeader title="Explore Files" />
<CardContent>
<Box mb={2} display="flex">
{pathArray.map((pathElement, idx) => (
<Fragment key={pathElement || 'root'}>
<div
role="button"
tabIndex={idx}
style={{
color: `${idx !== lastPathElementIndex && 'lightblue'}`,
cursor: `${idx !== lastPathElementIndex && 'pointer'}`,
}}
onKeyDown={() => moveUpIntoPath(pathElement)}
onClick={() => moveUpIntoPath(pathElement)}
>
{pathElement || 'root'}
</div>
<div>{'\u00A0/\u00A0'}</div>
</Fragment>
))}
</Box>
<Table
emptyContent={<>No files found</>}
data={tableData || []}
columns={columns}
/>
<Modal
open={modalOpen}
onClick={event => event.stopPropagation()}
onClose={() => setModalOpen(false)}
style={{ overflow: 'scroll' }}
>
<FileContent filename={curFile} coverage={fileCoverage} />
</Modal>
</CardContent>
</Card>
);
};
@@ -0,0 +1,53 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import 'highlight.js/styles/atom-one-dark.css';
import highlight from 'highlight.js';
/*
* Given a file extension, repo name, and array of code lines, return a Promise resolving
* to an array of formatted lines with html/css formatting.
*
* @param fileExtension The extension of the source file
* @param lines The source code lines
*
* @returns Promise of formatted lines
*
* @see http://highlightjs.readthedocs.io/en/latest/api.html#highlight-name-value-ignore-illegals-continuation
*/
export const highlightLines = (fileExtension: string, lines: Array<string>) => {
const formattedLines: Array<string> = [];
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;
};
@@ -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';
@@ -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 (
<MissingAnnotationEmptyState annotation="backstage.io/code-coverage" />
);
}
return <CodeCoveragePage />;
};
+21
View File
@@ -0,0 +1,21 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { codeCoveragePlugin, EntityCodeCoverageContent } from './plugin';
export {
Router,
isCodeCoverageAvailable,
isCodeCoverageAvailable as isPluginApplicableToEntity,
} from './components/Router';
+22
View File
@@ -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();
});
});
+45
View File
@@ -0,0 +1,45 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
createApiFactory,
createPlugin,
createRoutableExtension,
discoveryApiRef,
} from '@backstage/core';
import { codeCoverageApiRef, CodeCoverageRestApi } from './api';
import { rootRouteRef } from './routes';
export const codeCoveragePlugin = createPlugin({
id: 'code-coverage',
routes: {
root: rootRouteRef,
},
apis: [
createApiFactory({
api: codeCoverageApiRef,
deps: { discoveryApi: discoveryApiRef },
factory: ({ discoveryApi }) => new CodeCoverageRestApi(discoveryApi),
}),
],
});
export const EntityCodeCoverageContent = codeCoveragePlugin.provide(
createRoutableExtension({
component: () => import('./components/Router').then(m => m.Router),
mountPoint: rootRouteRef,
}),
);
+20
View File
@@ -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',
});
+17
View File
@@ -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';
+71
View File
@@ -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<FileEntry>;
};
export type JsonCoverageHistory = {
entity: EntityName;
history: Array<AggregateCoverage>;
};
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<number, number>;
branchHits: Record<number, BranchHit>;
};
export type AggregateCoverage = {
timestamp: number;
line: {
available: number;
covered: number;
missed: number;
percentage: number;
};
branch: {
available: number;
covered: number;
missed: number;
percentage: number;
};
};
+48
View File
@@ -75,3 +75,51 @@ spec:
owner: CNCF
lifecycle: experimental
```
## Other Components
### FOSSA Overview Page
The plugin provides an optional page that can be used to check the license compliance of all components.
![FOSSA Overview](./docs/fossa-overview.png)
Add it to your Backstage application:
1. Install the FOSSA Plugin (see [Getting Started](#getting-started)).
2. Register the page:
```tsx
// packages/app/src/App.tsx
import { FossaPage } from '@backstage/plugin-fossa';
// ...
const routes = (
<FlatRoutes>
// ...
<Route path="/fossa" element={<FossaPage />} />
</FlatRoutes>
);
```
3. (Optional) Add a Sidebar Icon:
```tsx
// packages/app/src/components/Root/Root.tsx
// ...
export const Root = ({ children }: PropsWithChildren<{}>) => (
<SidebarPage>
<Sidebar>
// ...
<SidebarItem icon={CheckCircleIcon} to="fossa" text="FOSSA" />
// ...
</Sidebar>
{children}
</SidebarPage>
);
```
+76 -5
View File
@@ -14,15 +14,20 @@
* limitations under the License.
*/
import { Entity } from '@backstage/catalog-model';
import { Entity, RELATION_OWNED_BY } from '@backstage/catalog-model';
import { Content, Header, Page } from '@backstage/core';
import { createDevApp } from '@backstage/dev-utils';
import { EntityProvider } from '@backstage/plugin-catalog-react';
import {
CatalogApi,
catalogApiRef,
EntityProvider,
} from '@backstage/plugin-catalog-react';
import { Grid } from '@material-ui/core';
import React from 'react';
import { EntityFossaCard } from '../src';
import { FossaApi, fossaApiRef } from '../src/api';
import { FOSSA_PROJECT_NAME_ANNOTATION } from '../src/components/useProjectName';
import { EntityFossaCard, fossaPlugin } from '../src';
import { FindingSummary, FossaApi, fossaApiRef } from '../src/api';
import { FossaPage } from '../src/components/FossaPage';
import { FOSSA_PROJECT_NAME_ANNOTATION } from '../src/components/getProjectName';
const entity = (name?: string) =>
({
@@ -34,9 +39,16 @@ const entity = (name?: string) =>
},
name: name,
},
relations: [
{
type: RELATION_OWNED_BY,
target: { kind: 'Group', namespace: 'default', name },
},
],
} as Entity);
createDevApp()
.registerPlugin(fossaPlugin)
.registerApi({
api: fossaApiRef,
deps: {},
@@ -81,8 +93,63 @@ createDevApp()
return undefined;
}
},
getFindingSummaries: async () => {
await new Promise(r => setTimeout(r, 1000));
return new Map<string, FindingSummary>([
[
'zero-deps',
{
timestamp: '2000-01-01T00:00:00Z',
projectUrl: '',
projectDefaultBranch: 'master',
issueCount: 0,
dependencyCount: 0,
},
],
[
'issues',
{
timestamp: '2001-01-01T00:00:00Z',
projectUrl: '',
projectDefaultBranch: 'develop',
issueCount: 10,
dependencyCount: 15,
},
],
[
'no-issues',
{
timestamp: '2002-01-01T00:00:00Z',
projectUrl: '',
projectDefaultBranch: 'master',
issueCount: 0,
dependencyCount: 5,
},
],
]);
},
} as FossaApi),
})
.registerApi({
api: catalogApiRef,
deps: {},
factory: () =>
(({
getEntities: async () => {
await new Promise(r => setTimeout(r, 1000));
return {
items: [
entity('no-fossa'),
entity('zero-deps'),
entity('issues'),
entity('no-issues'),
],
};
},
} as Partial<CatalogApi>) as any),
})
.addPage({
title: 'Entity Content',
element: (
@@ -130,4 +197,8 @@ createDevApp()
</Page>
),
})
.addPage({
title: 'Catalog Overview',
element: <FossaPage />,
})
.render();
Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

+3
View File
@@ -33,12 +33,15 @@
"dependencies": {
"@backstage/catalog-model": "^0.7.3",
"@backstage/core": "^0.7.5",
"@backstage/errors": "^0.1.1",
"@backstage/plugin-catalog-react": "^0.1.1",
"@backstage/theme": "^0.2.5",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
"cross-fetch": "^3.0.6",
"luxon": "^1.26.0",
"p-limit": "^3.0.2",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-use": "^15.3.3"

Some files were not shown because too many files have changed in this diff Show More