Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions workspaces/scorecard/app-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,12 @@ scorecard:
frequency: { minutes: 5 }
timeout: { minutes: 10 }
initialDelay: { seconds: 10 }
codeCoverage:
coverageReport:
schedule:
frequency: { minutes: 5 }
timeout: { minutes: 10 }
initialDelay: { seconds: 10 }
filecheck:
fileExistence:
options:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?xml version="1.0" ?>
<!DOCTYPE coverage SYSTEM "http://cobertura.sourceforge.net/xml/coverage-04.dtd">
<!--
Demo Cobertura coverage report for the scorecard code-coverage module.
Import into a running Backstage instance with the companion import.sh script.

Aggregate numbers baked into this file:
Lines: available=200 covered=160 missed=40 percentage=80
Branches: available=50 covered=35 missed=15 percentage=70
-->
<coverage line-rate="0.80" branch-rate="0.70" lines-covered="160" lines-valid="200" branches-covered="35" branches-valid="50" complexity="0" version="1.0" timestamp="1700000000000">
<packages>
<package name="com.example.service" line-rate="0.80" branch-rate="0.70" complexity="0">
<classes>
<class name="Application.ts" filename="src/Application.ts" line-rate="0.90" branch-rate="0.80" complexity="0">
<lines>
<line number="1" hits="1" branch="false"/>
<line number="2" hits="1" branch="false"/>
<line number="3" hits="1" branch="false"/>
<line number="4" hits="1" branch="true" condition-coverage="100% (2/2)"/>
<line number="5" hits="1" branch="false"/>
<line number="6" hits="1" branch="false"/>
<line number="7" hits="1" branch="true" condition-coverage="50% (1/2)"/>
<line number="8" hits="0" branch="false"/>
<line number="9" hits="1" branch="false"/>
<line number="10" hits="1" branch="false"/>
</lines>
</class>
<class name="Router.ts" filename="src/Router.ts" line-rate="0.75" branch-rate="0.60" complexity="0">
<lines>
<line number="1" hits="1" branch="false"/>
<line number="2" hits="1" branch="false"/>
<line number="3" hits="0" branch="false"/>
<line number="4" hits="1" branch="true" condition-coverage="50% (1/2)"/>
<line number="5" hits="1" branch="false"/>
<line number="6" hits="0" branch="false"/>
<line number="7" hits="1" branch="false"/>
<line number="8" hits="1" branch="false"/>
</lines>
</class>
<class name="Service.ts" filename="src/Service.ts" line-rate="0.70" branch-rate="0.65" complexity="0">
<lines>
<line number="1" hits="1" branch="false"/>
<line number="2" hits="1" branch="false"/>
<line number="3" hits="0" branch="false"/>
<line number="4" hits="1" branch="true" condition-coverage="100% (2/2)"/>
<line number="5" hits="1" branch="false"/>
<line number="6" hits="0" branch="false"/>
<line number="7" hits="1" branch="true" condition-coverage="0% (0/2)"/>
<line number="8" hits="1" branch="false"/>
<line number="9" hits="1" branch="false"/>
<line number="10" hits="1" branch="false"/>
</lines>
</class>
</classes>
</package>
</packages>
</coverage>
49 changes: 49 additions & 0 deletions workspaces/scorecard/examples/code-coverage/import.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/env bash
# ---------------------------------------------------------------------------
# Import demo code-coverage data into a local Backstage instance.
#
# Prerequisites:
# - A running Backstage backend at BACKSTAGE_URL (default: http://localhost:7007)
# - The code-coverage-backend plugin installed and enabled
# - The entity "component:default/code-coverage-scorecard-only" registered
# in the catalog (included in examples/all-scorecards-location.yaml)
#
# Usage:
# ./import.sh # import with defaults
# BACKSTAGE_URL=http://host:7007 ./import.sh # custom backend URL
# ENTITY_REF=component:default/my-svc ./import.sh # custom entity
# ---------------------------------------------------------------------------
set -euo pipefail

BACKSTAGE_URL="${BACKSTAGE_URL:-http://localhost:7007}"
ENTITY_REF="${ENTITY_REF:-component:default/code-coverage-scorecard-only}"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
COVERAGE_FILE="${SCRIPT_DIR}/cobertura-coverage.xml"

if [ ! -f "${COVERAGE_FILE}" ]; then

Check failure on line 23 in workspaces/scorecard/examples/code-coverage/import.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=redhat-developer_rhdh-plugins&issues=AZ_04SuqxILwZHPZa6y8&open=AZ_04SuqxILwZHPZa6y8&pullRequest=4271
echo "Error: coverage file not found: ${COVERAGE_FILE}" >&2
exit 1
fi

ENCODED_ENTITY=$(printf '%s' "${ENTITY_REF}" | sed 's/:/%3A/g; s/\//%2F/g')
URL="${BACKSTAGE_URL}/api/code-coverage/report?entity=${ENCODED_ENTITY}&coverageType=cobertura"

echo "Importing code-coverage data..."
echo " Backend: ${BACKSTAGE_URL}"
echo " Entity: ${ENTITY_REF}"
echo " File: ${COVERAGE_FILE}"
echo ""

HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST "${URL}" \
-H 'Content-Type: text/xml' \
${SERVICE_TOKEN:+-H "Authorization: Bearer ${SERVICE_TOKEN}"} \
--data-binary "@${COVERAGE_FILE}")

if [ "${HTTP_CODE}" -ge 200 ] && [ "${HTTP_CODE}" -lt 300 ]; then

Check failure on line 43 in workspaces/scorecard/examples/code-coverage/import.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=redhat-developer_rhdh-plugins&issues=AZ_04SuqxILwZHPZa6y9&open=AZ_04SuqxILwZHPZa6y9&pullRequest=4271

Check failure on line 43 in workspaces/scorecard/examples/code-coverage/import.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=redhat-developer_rhdh-plugins&issues=AZ_04SuqxILwZHPZa6y-&open=AZ_04SuqxILwZHPZa6y-&pullRequest=4271
echo "Success (HTTP ${HTTP_CODE}): coverage data imported for ${ENTITY_REF}"
else
echo "Error (HTTP ${HTTP_CODE}): failed to import coverage data" >&2
echo "Verify the code-coverage-backend plugin is running and the entity is registered." >&2
exit 1
fi
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
mockMetricsApi,
mockApiResponse,
mockSonarqubeScorecardResponse,
mockCodeCoverageScorecardResponse,
} from './utils/apiUtils';
import { CatalogPage } from './pages/CatalogPage';
import { ScorecardPage } from './pages/ScorecardPage';
Expand All @@ -48,6 +49,7 @@
sonarqubeScorecardResponse,
sonarqubeFailedQualityGateResponse,
fileCheckScorecardResponse,
codeCoverageScorecardResponse,
githubCustomAggregatedResponse,
gitHubPartiallyAggregatedResponse,
gitHubWeightedPartiallyAggregatedResponse,
Expand Down Expand Up @@ -423,6 +425,87 @@
});
});

test.describe('Code Coverage Entity Scorecards', () => {
test.skip(

Check warning on line 429 in workspaces/scorecard/packages/app-legacy/e2e-tests/scorecard.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this unit test or explain why it is ignored.

See more on https://sonarcloud.io/project/issues?id=redhat-developer_rhdh-plugins&issues=AZ_04TC0xILwZHPZa6y_&open=AZ_04TC0xILwZHPZa6y_&pullRequest=4271
process.env.APP_MODE === 'nfs',
'Legacy-only: NFS uses grouped metric cards',
);

test('Verify all code coverage metrics display correctly', async ({}, testInfo) => {
await mockCodeCoverageScorecardResponse(
page,
codeCoverageScorecardResponse,
);

await catalogPage.openCatalog();
await catalogPage.openComponent('code-coverage-scorecard-only');
await page.getByText('Scorecard', { exact: true }).click();

const expectedTitles = [
'Code coverage (Lines)',
'Code coverage - Tracked lines of code',
'Code coverage - Covered lines of code',
'Code coverage - Missed lines of code',
'Code coverage (Branches)',
'Code coverage - Tracked branches',
'Code coverage - Covered branches',
'Code coverage - Missed branches',
];

for (const title of expectedTitles) {
await expect(
page.getByText(title, { exact: true }).first(),
).toBeVisible({ timeout: 10000 });
}

await runAccessibilityTests(page, testInfo);
});

test('Verify code coverage metric values', async () => {
await mockCodeCoverageScorecardResponse(
page,
codeCoverageScorecardResponse,
);

await catalogPage.openCatalog();
await catalogPage.openComponent('code-coverage-scorecard-only');
await page.getByText('Scorecard', { exact: true }).click();

await expect(
page.getByText('Code coverage (Lines)', { exact: true }),
).toBeVisible({ timeout: 10000 });

const expectedValues: Record<string, string> = {
'Code coverage (Lines)': '80',
'Code coverage - Tracked lines of code': '200',
'Code coverage - Covered lines of code': '160',
'Code coverage - Missed lines of code': '40',
'Code coverage (Branches)': '70',
'Code coverage - Tracked branches': '50',
'Code coverage - Covered branches': '35',
'Code coverage - Missed branches': '15',
};

for (const [title, value] of Object.entries(expectedValues)) {
const card = page
.locator('[role="article"]')
.filter({ hasText: title })
.first();
await expect(card).toContainText(value);
}
});

test('Verify empty state for code-coverage entity with no metrics', async () => {
await mockCodeCoverageScorecardResponse(page, emptyScorecardResponse);

await catalogPage.openCatalog();
await catalogPage.openComponent('code-coverage-scorecard-only');
await page.getByText('Scorecard', { exact: true }).click();

await expect(page.getByText(translations.emptyState.title)).toBeVisible();
});
});

test.describe('Homepage aggregated scorecards', () => {
test('Verify missing permission on all default homepage scorecard widgets', async () => {
await mockHomepageAggregationsPermissionDenied(page);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,23 @@ export async function mockApiTextResponse(
});
}

const CODE_COVERAGE_SCORECARD_API_ROUTE =
'**/api/scorecard/metrics/catalog/Component/default/code-coverage-scorecard-only';

export async function mockCodeCoverageScorecardResponse(
page: Page,
responseData: object,
status = 200,
) {
await page.route(CODE_COVERAGE_SCORECARD_API_ROUTE, async route => {
await route.fulfill({
status,
contentType: 'application/json',
body: JSON.stringify(responseData),
});
});
}

const SONARQUBE_SCORECARD_API_ROUTE =
'**/api/scorecard/metrics/catalog/Component/default/sonarqube-scorecard-only';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -860,6 +860,82 @@ export const jiraEntitiesDrillDownNoDataResponse = {
},
};

// Code coverage scorecard responses — 8 metrics matching CodeCoverageMetricProvider
export const codeCoverageScorecardResponse = [
sonarqubeNumberMetric(
'codeCoverage.linePercentage',
'Code coverage (Lines)',
'Percentage of lines covered by tests.',
80,
[
{ key: 'success', expression: '>80' },
{ key: 'warning', expression: '50-80' },
{ key: 'error', expression: '<50' },
],
'warning',
),
sonarqubeNumberMetric(
'codeCoverage.lineAvailable',
'Code coverage - Tracked lines of code',
'Total number of lines tracked for code coverage.',
200,
[],
'success',
),
sonarqubeNumberMetric(
'codeCoverage.lineCovered',
'Code coverage - Covered lines of code',
'Number of lines covered by tests.',
160,
[],
'success',
),
sonarqubeNumberMetric(
'codeCoverage.lineMissed',
'Code coverage - Missed lines of code',
'Number of lines not covered by tests.',
40,
[],
'success',
),
sonarqubeNumberMetric(
'codeCoverage.branchPercentage',
'Code coverage (Branches)',
'Percentage of branches covered by tests.',
70,
[
{ key: 'success', expression: '>80' },
{ key: 'warning', expression: '50-80' },
{ key: 'error', expression: '<50' },
],
'warning',
),
sonarqubeNumberMetric(
'codeCoverage.branchAvailable',
'Code coverage - Tracked branches',
'Total number of branches tracked for code coverage.',
50,
[],
'success',
),
sonarqubeNumberMetric(
'codeCoverage.branchCovered',
'Code coverage - Covered branches',
'Number of branches covered by tests.',
35,
[],
'success',
),
sonarqubeNumberMetric(
'codeCoverage.branchMissed',
'Code coverage - Missed branches',
'Number of branches not covered by tests.',
15,
[],
'success',
),
];

export const fileCheckScorecardResponse = [
{
id: 'filecheck.readme',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,53 @@ spec:
lifecycle: production
```

## Importing data

This module reads coverage reports from the
[code-coverage-backend](https://github.com/backstage/community-plugins/tree/main/workspaces/code-coverage/plugins/code-coverage-backend)
plugin. You must first import coverage data into that plugin before the
scorecard can display metrics.

### Cobertura XML

```bash
curl -X POST \
'http://localhost:7007/api/code-coverage/report?entity=component:default/my-service&coverageType=cobertura' \
-H 'Content-Type: text/xml' \
-H "Authorization: Bearer $SERVICE_TOKEN" \
--data-binary @coverage/cobertura-coverage.xml
```

### LCOV

```bash
curl -X POST \
'http://localhost:7007/api/code-coverage/report?entity=component:default/my-service&coverageType=lcov' \
-H 'Content-Type: text/plain' \
-H "Authorization: Bearer $SERVICE_TOKEN" \
--data-binary @coverage/lcov.info
```

### Query parameters

| Parameter | Description |
| -------------- | ---------------------------------------------------- |
| `entity` | Full entity ref, e.g. `component:default/my-service` |
| `coverageType` | Format of the coverage file: `cobertura` or `lcov` |

### Demo data

The `examples/code-coverage/` directory in this workspace contains a
ready-to-use Cobertura XML report and an import script:

```bash
cd workspaces/scorecard
./examples/code-coverage/import.sh
```

The script imports the demo report for the `code-coverage-scorecard-only`
entity. See the script source for details and customization.

## Installation

Add the module to your backend:
Expand Down
Loading