diff --git a/.github/workflows/fapi-oidc-conformance-test.yml b/.github/workflows/fapi-oidc-conformance-test.yml
index eab7fd2b22..1a47e0419f 100644
--- a/.github/workflows/fapi-oidc-conformance-test.yml
+++ b/.github/workflows/fapi-oidc-conformance-test.yml
@@ -253,13 +253,34 @@ jobs:
name: test-logs
path: ./*log.txt
- - name: Archive Jacoco results
+ - name: Archive Jacoco exec
uses: actions/upload-artifact@v4
if: always()
with:
name: jacoco-exec
path: ./jacoco.exec
+ - name: Build Jacoco Report Generator
+ run: |
+ cd ./product-is/modules/integration/tests-common/jacoco-report-generator
+ mvn clean install
+ ARTIFACT_ID=$(mvn help:evaluate -Dexpression=project.artifactId -q -DforceStdout)
+ VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)
+ JAR_NAME="${ARTIFACT_ID}-${VERSION}.jar"
+ echo "JAR_NAME=${JAR_NAME}" >> $GITHUB_ENV
+ echo "Report generator jar name: $JAR_NAME"
+
+ - name: Generate Jacoco Report
+ run: |
+ java -jar ./product-is/modules/integration/tests-common/jacoco-report-generator/target/${{ env.JAR_NAME }} ./jacoco.exec ./product-is/oidc-fapi-conformance-tests/${{ env.PRODUCT_IS_DIR }}/repository/deployment/server/webapps ./product-is/oidc-fapi-conformance-tests/${{ env.PRODUCT_IS_DIR }}/repository/components/plugins ./product-is/oidc-fapi-conformance-tests/${{ env.PRODUCT_IS_DIR }}/repository/components/dropins ./product-is/oidc-fapi-conformance-tests/${{ env.PRODUCT_IS_DIR }}/lib/runtimes/cxf3
+
+ - name: Archive Jacoco report
+ uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: jacoco-xml
+ path: ./report/jacoco.xml
+
- name: Send Email
if: always()
run: |
diff --git a/.github/workflows/oidc-conformance-test.yml b/.github/workflows/oidc-conformance-test.yml
index 3aaf8f72d3..bd456fc860 100644
--- a/.github/workflows/oidc-conformance-test.yml
+++ b/.github/workflows/oidc-conformance-test.yml
@@ -206,13 +206,34 @@ jobs:
name: test-logs
path: ./*log.txt
- - name: Archive Jacoco results
- uses: actions/upload-artifact@v3
+ - name: Archive Jacoco exec
+ uses: actions/upload-artifact@v4
if: always()
with:
name: jacoco-exec
path: ./jacoco.exec
+ - name: Build Jacoco Report Generator
+ run: |
+ cd ./product-is/modules/integration/tests-common/jacoco-report-generator
+ mvn clean install
+ ARTIFACT_ID=$(mvn help:evaluate -Dexpression=project.artifactId -q -DforceStdout)
+ VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)
+ JAR_NAME="${ARTIFACT_ID}-${VERSION}.jar"
+ echo "JAR_NAME=${JAR_NAME}" >> $GITHUB_ENV
+ echo "Report generator jar name: $JAR_NAME"
+
+ - name: Generate Jacoco Report
+ run: |
+ java -jar ./product-is/modules/integration/tests-common/jacoco-report-generator/target/${{ env.JAR_NAME }} ./jacoco.exec ./product-is/oidc-conformance-tests/${{ env.PRODUCT_IS_DIR }}/repository/deployment/server/webapps ./product-is/oidc-conformance-tests/${{ env.PRODUCT_IS_DIR }}/repository/components/plugins ./product-is/oidc-conformance-tests/${{ env.PRODUCT_IS_DIR }}/repository/components/dropins ./product-is/oidc-conformance-tests/${{ env.PRODUCT_IS_DIR }}/lib/runtimes/cxf3
+
+ - name: Archive Jacoco report
+ uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: jacoco-xml
+ path: ./report/jacoco.xml
+
- name: Send Email
if: always()
run: |
diff --git a/modules/integration/tests-common/jacoco-report-generator/pom.xml b/modules/integration/tests-common/jacoco-report-generator/pom.xml
new file mode 100644
index 0000000000..80e0bd3952
--- /dev/null
+++ b/modules/integration/tests-common/jacoco-report-generator/pom.xml
@@ -0,0 +1,95 @@
+
+
+
+
+ 4.0.0
+
+ org.wso2.is
+ identity-integration-tests
+ 7.0.1-SNAPSHOT
+ ../../pom.xml
+
+
+ jacoco-report-generator
+
+
+ 11
+ 11
+ UTF-8
+ 0.8.12
+ 4.0.1
+ 3.2.4
+
+
+
+
+ org.jacoco
+ org.jacoco.core
+ ${org.jacoco.version}
+
+
+ org.jacoco
+ org.jacoco.report
+ ${org.jacoco.version}
+
+
+ org.codehaus.plexus
+ plexus-utils
+ ${plexus-utils.version}
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ ${maven-shade-plugin.version}
+
+
+ package
+
+ shade
+
+
+
+
+ org.jacoco:org.jacoco.core
+ org.jacoco:org.jacoco.report
+ org.codehaus.plexus:plexus-utils
+ org.ow2.asm:asm
+ org.ow2.asm:asm-tree
+ org.ow2.asm:asm-commons
+
+
+
+
+ org.wso2.carbon.identity.jacoco.ReportGenerator
+
+
+
+
+
+
+
+
+
diff --git a/modules/integration/tests-common/jacoco-report-generator/src/main/java/org/wso2/carbon/identity/jacoco/ReportGenerator.java b/modules/integration/tests-common/jacoco-report-generator/src/main/java/org/wso2/carbon/identity/jacoco/ReportGenerator.java
new file mode 100644
index 0000000000..044092d778
--- /dev/null
+++ b/modules/integration/tests-common/jacoco-report-generator/src/main/java/org/wso2/carbon/identity/jacoco/ReportGenerator.java
@@ -0,0 +1,284 @@
+/*
+ * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you 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.
+ */
+
+package org.wso2.carbon.identity.jacoco;
+
+import org.codehaus.plexus.util.DirectoryScanner;
+import org.codehaus.plexus.util.FileUtils;
+import org.jacoco.core.analysis.Analyzer;
+import org.jacoco.core.analysis.CoverageBuilder;
+import org.jacoco.core.analysis.IBundleCoverage;
+import org.jacoco.core.tools.ExecFileLoader;
+import org.jacoco.report.DirectorySourceFileLocator;
+import org.jacoco.report.IReportVisitor;
+import org.jacoco.report.xml.XMLFormatter;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+/**
+ * This will create an XML report projects based on a single execution data store called jacoco.exec.
+ */
+public class ReportGenerator {
+
+ private static final String CLASS_FILE_PATTERN = "**/*.class";
+ private static final int BUFFER_SIZE = 1024;
+ private static final String INVALID_EXTENSION_ERROR = "Invalid extension: %s is invalid";
+ private static final String EXTRACTION_ERROR = "Error on archive extraction";
+
+ private final String title;
+ private final File executionDataFile;
+ private final Set classDirectories;
+ private final File xmlReport;
+ private final File tempDirectory;
+
+ private ExecFileLoader execFileLoader;
+
+ /**
+ * Starts the report generation process
+ *
+ * @param args Arguments to the report generation.
+ * [ ...]
+ * @throws IOException
+ */
+ public static void main(final String[] args) throws IOException {
+
+ if (args.length < 2) {
+ System.err.println("Usage: java -jar ReportGenerator.jar " +
+ "[ ...]");
+ System.exit(1);
+ }
+
+ File executionDataFile = new File(args[0]);
+ Set classDirectories = new HashSet<>();
+ for (int i = 1; i < args.length; i++) {
+ classDirectories.add(new File(args[i]));
+ }
+
+ try {
+ final ReportGenerator generator = new ReportGenerator(executionDataFile, classDirectories);
+ generator.create();
+ } catch (Exception e) {
+ System.err.println("Error while creating report: " + e.getMessage());
+ }
+ }
+
+ /**
+ * Create a new generator based for the given project.
+ *
+ * @param executionDataFile the execution data file
+ * @param classDirectories the set of class directories
+ */
+ public ReportGenerator(File executionDataFile, Set classDirectories) {
+
+ this.title = "Jacoco Coverage Report";
+ this.executionDataFile = executionDataFile;
+ this.classDirectories = classDirectories;
+ this.xmlReport = new File("./report/jacoco.xml");
+ this.tempDirectory = new File("./tmp");
+
+ // Create report directory if it does not exist
+ File reportDir = this.xmlReport.getParentFile();
+ if (!reportDir.exists() && !reportDir.mkdirs()) {
+ throw new RuntimeException("Failed to create report directory: " + reportDir.getAbsolutePath());
+ }
+ }
+
+ /**
+ * Create the report.
+ *
+ * @throws IOException - Throws if report creation fails
+ */
+ public void create() throws IOException {
+
+ // Read the jacoco.exec file. Multiple data files could be merged at this point
+ loadExecutionData();
+
+ // Run the structure analyzer on a single class folder to build up the coverage model. The process would be
+ // similar if your classes were in a jar file. Typically, you would create a bundle for each class folder and
+ // each jar you want in your report. If you have more than one bundle you will need to add a grouping node to
+ // your report.
+ final IBundleCoverage bundleCoverage = analyzeStructure();
+
+ createReport(bundleCoverage);
+ }
+
+ private void loadExecutionData() throws IOException {
+
+ execFileLoader = new ExecFileLoader();
+ execFileLoader.load(executionDataFile);
+ }
+
+ private IBundleCoverage analyzeStructure() throws IOException {
+
+ final CoverageBuilder coverageBuilder = new CoverageBuilder();
+ final Analyzer analyzer = new Analyzer(execFileLoader.getExecutionDataStore(), coverageBuilder);
+
+ List jarFilesToAnalyze = new ArrayList<>();
+ List classFilesToAnalyze = new ArrayList<>();
+
+ for (File classDirectory : classDirectories) {
+ // Jar files to analyze
+ File[] files =
+ classDirectory.listFiles((dir, name)
+ -> name.startsWith("org.wso2.carbon") && !name.contains(".stub_"));
+ if (files != null) {
+ jarFilesToAnalyze.addAll(Arrays.asList(files));
+ }
+
+ // Class files to analyze
+ files = classDirectory.listFiles((dir, name) -> name.endsWith(".class"));
+ if (files != null) {
+ classFilesToAnalyze.addAll(Arrays.asList(files));
+ }
+ }
+
+ String[] includes = {CLASS_FILE_PATTERN};
+ String[] excludes = {"-*.stub*", "-*.stub_", "-*.stub_4.0.0", "-*.stub-"};
+
+ for (final File jarFile : jarFilesToAnalyze) {
+ String extractedDir = extractJarFile(jarFile.getAbsolutePath(), tempDirectory);
+ String[] classFiles = scanDirectory(extractedDir, includes, excludes);
+
+ for (String classFile : classFiles) {
+ analyzer.analyzeAll(new File(extractedDir + File.separator + classFile));
+ }
+ FileUtils.forceDelete(new File(extractedDir));
+ }
+
+ for (final File classFile : classFilesToAnalyze) {
+ analyzer.analyzeAll(classFile);
+ }
+
+ return coverageBuilder.getBundle(title);
+ }
+
+ private void createReport(final IBundleCoverage bundleCoverage) throws IOException {
+
+ // Create a concrete report visitor based on some supplied configuration. In this case we use the defaults
+ try (FileOutputStream fos = new FileOutputStream(xmlReport)) {
+ final XMLFormatter xmlFormatter = new XMLFormatter();
+ final IReportVisitor visitor = xmlFormatter.createVisitor(fos);
+
+ // Initialize the report with all the execution and session information. At this point the report doesn't
+ // know about the structure of the report being created
+ visitor.visitInfo(execFileLoader.getSessionInfoStore().getInfos(),
+ execFileLoader.getExecutionDataStore().getContents());
+
+ // Populate the report structure with the bundle coverage information.
+ // Call visitGroup if you need groups in your report.
+ visitor.visitBundle(bundleCoverage,
+ new DirectorySourceFileLocator(null, "utf-8", 4));
+
+ // Signal end of structure information to allow report to write all information out.
+ visitor.visitEnd();
+ }
+ }
+
+ /**
+ * Extract jar files given at jar file path
+ *
+ * @param jarFilePath - Jar file path
+ * @param tempDir - Temporary directory to extract jar file
+ * @return - Jar file extracted directory.
+ * @throws IOException - Throws if jar extraction fails
+ */
+ private synchronized String extractJarFile(String jarFilePath, File tempDir) throws IOException {
+
+ if (!jarFilePath.endsWith(".war") && !jarFilePath.endsWith(".jar")) {
+ throw new IllegalArgumentException(String.format(INVALID_EXTENSION_ERROR, jarFilePath));
+ }
+
+ String jarFileName = new File(jarFilePath).getName();
+ String tempExtractedDir = new File(tempDir, jarFileName.substring(0, jarFileName.lastIndexOf('.'))).getPath();
+
+ try {
+ extractFile(jarFilePath, tempExtractedDir);
+ } catch (IOException e) {
+ throw new IOException("Could not extract the file " + jarFileName, e);
+ }
+ return tempExtractedDir;
+ }
+
+ /**
+ * Scan given directory for include and exclude patterns.
+ *
+ * @param jarExtractedDir - Path to check for given include/exclude pattern
+ * @param includes - Include pattern array
+ * @param excludes - Exclude class pattern array
+ * @return - Included files
+ */
+ private String[] scanDirectory(String jarExtractedDir, String[] includes, String[] excludes) {
+
+ DirectoryScanner ds = new DirectoryScanner();
+
+ ds.setIncludes(includes);
+ ds.setExcludes(excludes);
+ ds.setBasedir(new File(jarExtractedDir));
+ ds.setCaseSensitive(true);
+
+ ds.scan();
+ return ds.getIncludedFiles();
+ }
+
+ /**
+ * Extract the given archive file to the given directory
+ *
+ * @param sourceFilePath - Path to the archive file
+ * @param extractedDir - Path to the directory to extract the archive file
+ * @throws IOException - Throws if extraction fails
+ */
+ private void extractFile(String sourceFilePath, String extractedDir) throws IOException {
+
+ byte[] buf = new byte[BUFFER_SIZE];
+ try (ZipInputStream zipInputStream = new ZipInputStream(new FileInputStream(sourceFilePath))) {
+ ZipEntry zipEntry;
+ while ((zipEntry = zipInputStream.getNextEntry()) != null) {
+ File newFile = new File(extractedDir, zipEntry.getName());
+ if (zipEntry.isDirectory()) {
+ if (!newFile.isDirectory() && !newFile.mkdirs()) {
+ throw new IOException("Failed to create directory " + newFile);
+ }
+ } else {
+ File parent = newFile.getParentFile();
+ if (!parent.isDirectory() && !parent.mkdirs()) {
+ throw new IOException("Failed to create directory " + parent);
+ }
+ try (FileOutputStream fos = new FileOutputStream(newFile)) {
+ int len;
+ while ((len = zipInputStream.read(buf)) > 0) {
+ fos.write(buf, 0, len);
+ }
+ }
+ }
+ zipInputStream.closeEntry();
+ }
+ } catch (IOException e) {
+ throw new IOException(EXTRACTION_ERROR, e);
+ }
+ }
+}
diff --git a/modules/integration/tests-common/pom.xml b/modules/integration/tests-common/pom.xml
index 4069a8798d..3e92089cd4 100644
--- a/modules/integration/tests-common/pom.xml
+++ b/modules/integration/tests-common/pom.xml
@@ -33,6 +33,7 @@
ui-pages
integration-test-utils
extensions
+ jacoco-report-generator