mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2026-09-09 18:03:21 +00:00
AAE-50678 Fix: report test coverage to SonarCloud (#12181),
* fix: enable LCOV coverage reporting for SonarCloud Add lcov reporter to all karma configs, upload coverage artifacts from unit test matrix jobs, and add a SonarCloud scan job that merges coverage reports and runs the sonar-scanner with proper LCOV paths. Co-authored-by: eromano <1030050+eromano@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix: pass secrets to unit-test-workflow and set SONAR_HOST_URL for SonarCloud Co-authored-by: eromano <1030050+eromano@users.noreply.github.com> * fix: add test outputs to nx.json so NX caches and restores coverage reports Co-authored-by: eromano <1030050+eromano@users.noreply.github.com> * test: add unit tests for Chart model to verify coverage reporting * fix: use find to locate lcov.info in downloaded artifacts for SonarCloud coverage Co-authored-by: eromano <1030050+eromano@users.noreply.github.com> * test: add fake file with unit test to verify coverage reporting Co-authored-by: eromano <1030050+eromano@users.noreply.github.com> * ci: add full SonarCloud scan workflow on develop push Co-authored-by: eromano <1030050+eromano@users.noreply.github.com> * test: remove fake coverage-canary file and its spec Co-authored-by: eromano <1030050+eromano@users.noreply.github.com> * fix: replace secrets inherit with explicit SONAR_TOKEN in workflow call Co-authored-by: eromano <1030050+eromano@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: eromano <1030050+eromano@users.noreply.github.com> Co-authored-by: Eugenio Romano <eromano@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
eromano
Copilot Autofix powered by AI
Eugenio Romano
parent
f915c813f0
commit
7b8d616a9f
@@ -254,6 +254,8 @@ jobs:
|
||||
name: "Unit Tests"
|
||||
needs: [setup]
|
||||
uses: ./.github/workflows/unit-test-workflow.yml
|
||||
secrets:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
with:
|
||||
base_ref: ${{ github.base_ref || 'develop' }}
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
name: "SonarCloud Full Scan (develop)"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
workflow_dispatch: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
full-unit-tests-and-sonar-scan:
|
||||
name: "Full Unit Tests + SonarCloud Scan"
|
||||
uses: ./.github/workflows/unit-test-workflow.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
full: true
|
||||
@@ -2,12 +2,21 @@ name: "Unit Tests Workflow"
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
secrets:
|
||||
SONAR_TOKEN:
|
||||
description: 'Token for SonarCloud analysis'
|
||||
required: false
|
||||
inputs:
|
||||
base_ref:
|
||||
description: 'Base branch for affected calculation'
|
||||
required: false
|
||||
type: string
|
||||
default: 'develop'
|
||||
full:
|
||||
description: 'Run the full (non-affected) test suite for every project instead of only affected ones'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
generate-affected-matrix:
|
||||
@@ -30,9 +39,15 @@ jobs:
|
||||
id: set-matrix
|
||||
env:
|
||||
BASE_REF: ${{ inputs.base_ref }}
|
||||
FULL_RUN: ${{ inputs.full }}
|
||||
run: |
|
||||
echo "Base ref is $BASE_REF"
|
||||
AFFECTED_UNIT=$(pnpm nx show projects --affected --target=test --base=origin/$BASE_REF --head=HEAD --select=projects --plain --exclude=cli,stories,eslint-angular)
|
||||
if [ "$FULL_RUN" == "true" ]; then
|
||||
echo "Running full (non-affected) test suite"
|
||||
AFFECTED_UNIT=$(pnpm nx show projects --target=test --select=projects --plain --exclude=cli,stories,eslint-angular)
|
||||
else
|
||||
echo "Base ref is $BASE_REF"
|
||||
AFFECTED_UNIT=$(pnpm nx show projects --affected --target=test --base=origin/$BASE_REF --head=HEAD --select=projects --plain --exclude=cli,stories,eslint-angular)
|
||||
fi
|
||||
echo "Affected projects for UNIT: $AFFECTED_UNIT"
|
||||
|
||||
if [ -z "$AFFECTED_UNIT" ]; then
|
||||
@@ -74,8 +89,56 @@ jobs:
|
||||
NODE_OPTIONS: "--max-old-space-size=5120"
|
||||
run: |
|
||||
xvfb-run --auto-servernum pnpm nx run ${{ matrix.project }}:test
|
||||
- name: Upload coverage report
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: coverage-${{ matrix.project }}
|
||||
path: coverage/${{ matrix.project }}/lcov.info
|
||||
if-no-files-found: ignore
|
||||
retention-days: 1
|
||||
- name: Save nx cache
|
||||
if: ${{ success() }}
|
||||
uses: ./.github/actions/save-nx-cache
|
||||
with:
|
||||
cache-suffix: test-${{ matrix.project }}
|
||||
|
||||
sonarcloud:
|
||||
name: "SonarCloud Scan"
|
||||
runs-on: ubuntu-latest
|
||||
needs: [generate-affected-matrix, unit-tests]
|
||||
if: ${{ needs.generate-affected-matrix.outputs.hasProjects == 'true' && always() && needs.unit-tests.result != 'cancelled' }}
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
pattern: coverage-*
|
||||
path: coverage-reports
|
||||
- name: Merge coverage reports
|
||||
run: |
|
||||
mkdir -p coverage
|
||||
echo "Artifact structure:"
|
||||
find coverage-reports -type f -name 'lcov.info' 2>/dev/null || true
|
||||
for dir in coverage-reports/coverage-*/; do
|
||||
project_name=$(basename "$dir" | sed 's/^coverage-//')
|
||||
lcov_file=$(find "$dir" -name 'lcov.info' -type f | head -1)
|
||||
if [ -n "$lcov_file" ]; then
|
||||
mkdir -p "coverage/${project_name}"
|
||||
cp "$lcov_file" "coverage/${project_name}/lcov.info"
|
||||
echo "Copied coverage for ${project_name}"
|
||||
fi
|
||||
done
|
||||
echo "Coverage files found:"
|
||||
find coverage -name 'lcov.info' -type f
|
||||
- name: SonarCloud Scan
|
||||
uses: SonarSource/sonarqube-scan-action@aa494459d7c39c106cc77b166de8b4250a32bb97 # v5.1.0
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
SONAR_HOST_URL: https://sonarcloud.io
|
||||
|
||||
@@ -56,7 +56,7 @@ module.exports = function (config) {
|
||||
coverageReporter: {
|
||||
dir: join(__dirname, '../../coverage/content-services'),
|
||||
subdir: '.',
|
||||
reporters: [{ type: 'html' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }],
|
||||
reporters: [{ type: 'html' }, { type: 'lcov' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }],
|
||||
check: {
|
||||
global: {
|
||||
statements: 75,
|
||||
|
||||
@@ -69,7 +69,7 @@ module.exports = function (config) {
|
||||
coverageReporter: {
|
||||
dir: join(__dirname, '../../coverage/core'),
|
||||
subdir: '.',
|
||||
reporters: [{ type: 'html' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }],
|
||||
reporters: [{ type: 'html' }, { type: 'lcov' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }],
|
||||
check: {
|
||||
global: {
|
||||
statements: 75,
|
||||
|
||||
@@ -23,7 +23,7 @@ module.exports = function (config) {
|
||||
coverageReporter: {
|
||||
dir: join(__dirname, '../../coverage/extensions'),
|
||||
subdir: '.',
|
||||
reporters: [{ type: 'html' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }],
|
||||
reporters: [{ type: 'html' }, { type: 'lcov' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }],
|
||||
check: {
|
||||
global: {
|
||||
statements: 75,
|
||||
|
||||
@@ -44,7 +44,7 @@ module.exports = function (config) {
|
||||
coverageReporter: {
|
||||
dir: join(__dirname, '../../coverage/insights'),
|
||||
subdir: '.',
|
||||
reporters: [{ type: 'html' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }],
|
||||
reporters: [{ type: 'html' }, { type: 'lcov' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }],
|
||||
check: {
|
||||
global: {
|
||||
statements: 75,
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2026 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* 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 { Chart } from './chart.model';
|
||||
|
||||
describe('Chart Model', () => {
|
||||
describe('constructor', () => {
|
||||
it('should create with default values when no argument is provided', () => {
|
||||
const chart = new Chart();
|
||||
expect(chart.labels).toEqual([]);
|
||||
expect(chart.data).toEqual([]);
|
||||
expect(chart.datasets).toEqual([]);
|
||||
expect(chart.showDetails).toBe(false);
|
||||
});
|
||||
|
||||
it('should populate properties from input object', () => {
|
||||
const chart = new Chart({
|
||||
id: '1',
|
||||
title: 'Test Chart',
|
||||
titleKey: 'KEY',
|
||||
labels: ['a', 'b'],
|
||||
data: [1, 2],
|
||||
datasets: [{ data: [1] }],
|
||||
showDetails: true,
|
||||
detailsTable: { key: 'value' },
|
||||
options: { responsive: true }
|
||||
});
|
||||
|
||||
expect(chart.id).toBe('1');
|
||||
expect(chart.title).toBe('Test Chart');
|
||||
expect(chart.titleKey).toBe('KEY');
|
||||
expect(chart.labels).toEqual(['a', 'b']);
|
||||
expect(chart.data).toEqual([1, 2]);
|
||||
expect(chart.datasets).toEqual([{ data: [1] }]);
|
||||
expect(chart.showDetails).toBe(true);
|
||||
expect(chart.detailsTable).toEqual({ key: 'value' });
|
||||
expect(chart.options).toEqual({ responsive: true });
|
||||
});
|
||||
|
||||
it('should convert type and set icon for pieChart', () => {
|
||||
const chart = new Chart({ type: 'pieChart' });
|
||||
expect(chart.type).toBe('pie');
|
||||
expect(chart.icon).toBe('pie_chart');
|
||||
});
|
||||
|
||||
it('should convert type and set icon for barChart', () => {
|
||||
const chart = new Chart({ type: 'barChart' });
|
||||
expect(chart.type).toBe('bar');
|
||||
expect(chart.icon).toBe('equalizer');
|
||||
});
|
||||
|
||||
it('should convert type and set icon for line', () => {
|
||||
const chart = new Chart({ type: 'line' });
|
||||
expect(chart.type).toBe('line');
|
||||
expect(chart.icon).toBe('show_chart');
|
||||
});
|
||||
|
||||
it('should convert type and set icon for table', () => {
|
||||
const chart = new Chart({ type: 'table' });
|
||||
expect(chart.type).toBe('table');
|
||||
expect(chart.icon).toBe('web');
|
||||
});
|
||||
|
||||
it('should convert type and set icon for multiBarChart', () => {
|
||||
const chart = new Chart({ type: 'multiBarChart' });
|
||||
expect(chart.type).toBe('multiBar');
|
||||
expect(chart.icon).toBe('poll');
|
||||
});
|
||||
|
||||
it('should convert type and set icon for processDefinitionHeatMap', () => {
|
||||
const chart = new Chart({ type: 'processDefinitionHeatMap' });
|
||||
expect(chart.type).toBe('HeatMap');
|
||||
expect(chart.icon).toBe('share');
|
||||
});
|
||||
|
||||
it('should convert type and set icon for masterDetailTable', () => {
|
||||
const chart = new Chart({ type: 'masterDetailTable' });
|
||||
expect(chart.type).toBe('masterDetailTable');
|
||||
expect(chart.icon).toBe('subtitles');
|
||||
});
|
||||
|
||||
it('should default to table type for unknown types', () => {
|
||||
const chart = new Chart({ type: 'unknown' });
|
||||
expect(chart.type).toBe('table');
|
||||
expect(chart.icon).toBe('web');
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasData', () => {
|
||||
it('should return true when data is not empty', () => {
|
||||
const chart = new Chart({ data: [1, 2, 3] });
|
||||
expect(chart.hasData()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when data is empty', () => {
|
||||
const chart = new Chart({ data: [] });
|
||||
expect(chart.hasData()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when no data is provided', () => {
|
||||
const chart = new Chart();
|
||||
expect(chart.hasData()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasDatasets', () => {
|
||||
it('should return true when datasets is not empty', () => {
|
||||
const chart = new Chart({ datasets: [{ data: [1] }] });
|
||||
expect(chart.hasDatasets()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when datasets is empty', () => {
|
||||
const chart = new Chart({ datasets: [] });
|
||||
expect(chart.hasDatasets()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasZeroValues', () => {
|
||||
it('should return true when all data values are zero', () => {
|
||||
const chart = new Chart({ data: [0, 0, 0] });
|
||||
expect(chart.hasZeroValues()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when at least one value is non-zero', () => {
|
||||
const chart = new Chart({ data: [0, 1, 0] });
|
||||
expect(chart.hasZeroValues()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when data is empty', () => {
|
||||
const chart = new Chart({ data: [] });
|
||||
expect(chart.hasZeroValues()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -48,7 +48,7 @@ module.exports = function (config) {
|
||||
coverageReporter: {
|
||||
dir: join(__dirname, '../../coverage/process-services-cloud'),
|
||||
subdir: '.',
|
||||
reporters: [{ type: 'html' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }],
|
||||
reporters: [{ type: 'html' }, { type: 'lcov' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }],
|
||||
check: {
|
||||
global: {
|
||||
statements: 75,
|
||||
|
||||
@@ -43,7 +43,7 @@ module.exports = function (config) {
|
||||
coverageReporter: {
|
||||
dir: join(__dirname, '../../coverage/process-services'),
|
||||
subdir: '.',
|
||||
reporters: [{ type: 'html' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }],
|
||||
reporters: [{ type: 'html' }, { type: 'lcov' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }],
|
||||
check: {
|
||||
global: {
|
||||
statements: 75,
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
"cache": true
|
||||
},
|
||||
"test": {
|
||||
"cache": true
|
||||
"cache": true,
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectName}"]
|
||||
},
|
||||
"stylelint": {
|
||||
"cache": true
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
sonar.organization=alfresco
|
||||
sonar.projectKey=Alfresco_alfresco-ng2-components
|
||||
|
||||
sonar.sources=lib
|
||||
sonar.tests=lib
|
||||
sonar.test.inclusions=**/*.spec.ts
|
||||
sonar.exclusions=**/node_modules/**,**/dist/**,**/*.spec.ts,**/*.mock.ts,**/mock/**,**/mocks/**,**/testing/**,**/stories/**
|
||||
|
||||
sonar.javascript.lcov.reportPaths=coverage/core/lcov.info,coverage/content-services/lcov.info,coverage/extensions/lcov.info,coverage/insights/lcov.info,coverage/process-services/lcov.info,coverage/process-services-cloud/lcov.info
|
||||
|
||||
sonar.sourceEncoding=UTF-8
|
||||
Reference in New Issue
Block a user