Skip to content

Commit

Permalink
fix code coverage calculation
Browse files Browse the repository at this point in the history
Signed-off-by: Subhobrata Dey <[email protected]>
  • Loading branch information
sbcd90 committed Apr 12, 2024
1 parent 41d5a2e commit 743e538
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 14 deletions.
62 changes: 49 additions & 13 deletions build-tools/opensearchplugin-coverage.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,26 @@
* SPDX-License-Identifier: Apache-2.0
*/

apply plugin: 'jacoco'

/**
* OpenSearch Plugin build tools don't work with the Gradle Jacoco Plugin to report coverage out of the box.
* ES Plugin build tools don't work with the Gradle Jacoco Plugin to report coverage out of the box.
* https://github.com/elastic/elasticsearch/issues/28867.
*
* This code sets up coverage reporting manually for OpenSearch plugin tests. This is complicated because:
* 1. The OpenSearch integTest Task doesn't implement Gradle's JavaForkOptions so we have to manually start the jacoco agent with the test JVM
* This code sets up coverage reporting manually for ES plugin tests. This is complicated because:
* 1. The ES integTest Task doesn't implement Gradle's JavaForkOptions so we have to manually start the jacoco agent with the test JVM
* 2. The cluster nodes are stopped using 'kill -9' which means jacoco can't dump it's execution output to a file on VM shutdown
* 3. The Java Security Manager prevents JMX from writing execution output to the file.
*
* To workaround these we start the cluster with jmx enabled and then use Jacoco's JMX MBean to get the execution data before the
* cluster is stopped and dump it to a file. Luckily our current security policy seems to allow this. This will also probably
* break if there are multiple nodes in the integTestCluster. But for now... it sorta works.
* break if there are multiple nodes in the integTestCluster. But for now... it sorta works.
*/
apply plugin: 'jacoco'

// Get gradle to generate the required jvm agent arg for us using a dummy tasks of type Test. Unfortunately Elastic's
// testing tasks don't derive from Test so the jacoco plugin can't do this automatically.
def jacocoDir = "${buildDir}/jacoco"

task dummyTest(type: Test) {
enabled = false
workingDir = file("/") // Force absolute path to jacoco agent jar
Expand All @@ -31,19 +33,53 @@ task dummyTest(type: Test) {
}
}

task dummyIntegTest(type: Test) {
enabled = false
workingDir = file("/") // Force absolute path to jacoco agent jar
jacoco {
destinationFile = file("${jacocoDir}/integTest.exec")
destinationFile.parentFile.mkdirs()
jmx = true
}
}
task dummyIntegTestRunner(type: Test) {
enabled = false
workingDir = file("/") // Force absolute path to jacoco agent jar
jacoco {
destinationFile = file("${jacocoDir}/integTestRunner.exec")
destinationFile.parentFile.mkdirs()
jmx = true
}
}

integTest {
systemProperty 'jacoco.dir', "${jacocoDir}"
}

jacocoTestReport {
dependsOn test
executionData dummyTest.jacoco.destinationFile
getSourceDirectories().from(sourceSets.main.allSource)
getClassDirectories().from(sourceSets.main.output)
dependsOn integTest, test
executionData.from dummyTest.jacoco.destinationFile, dummyIntegTest.jacoco.destinationFile, dummyIntegTestRunner.jacoco.destinationFile
sourceDirectories.from = "src/main/java"
classDirectories.from = sourceSets.main.output
reports {
html.required = true // human readable
csv.required = true
xml.required = true // for coverlay
}
}

project.gradle.projectsEvaluated {
jacocoTestReport.dependsOn test
}

check.dependsOn jacocoTestReport
allprojects {
afterEvaluate {
jacocoTestReport.dependsOn integTest

testClusters.integTest {
jvmArgs " ${dummyIntegTest.jacoco.getAsJvmArg()}".replace('javaagent:', 'javaagent:/')
systemProperty 'com.sun.management.jmxremote', "true"
systemProperty 'com.sun.management.jmxremote.authenticate', "false"
systemProperty 'com.sun.management.jmxremote.port', "7777"
systemProperty 'com.sun.management.jmxremote.ssl', "false"
systemProperty 'java.rmi.server.hostname', "127.0.0.1"
}
}
}
6 changes: 5 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ apply plugin: 'opensearch.opensearchplugin'
apply plugin: 'opensearch.testclusters'
apply plugin: 'opensearch.java-rest-test'
apply plugin: 'opensearch.pluginzip'
apply from: 'build-tools/opensearchplugin-coverage.gradle'
apply from: 'gradle/formatting.gradle'

ext {
Expand Down Expand Up @@ -306,6 +305,11 @@ testClusters.integTest {
plugins.add(firstPlugin)
}
}
def usingRemoteCluster = System.properties.containsKey('tests.rest.cluster') || System.properties.containsKey('tests.cluster')
def usingMultiNode = project.properties.containsKey('numNodes')
if (!usingRemoteCluster && !usingMultiNode) {
apply from: 'build-tools/opensearchplugin-coverage.gradle'
}

run {
doFirst {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/
package org.opensearch.securityanalytics;

import java.nio.file.Files;
import java.util.Set;
import java.util.ArrayList;
import java.util.function.BiConsumer;
Expand All @@ -16,6 +17,7 @@
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.http.message.BasicHeader;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.After;
import org.junit.Before;
Expand Down Expand Up @@ -69,6 +71,12 @@
import org.opensearch.test.rest.OpenSearchRestTestCase;


import javax.management.MBeanServerInvocationHandler;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
Expand Down Expand Up @@ -1786,4 +1794,43 @@ public String getMatchAllSearchRequestString(int num) {
" }\n" +
"}";
}

/**
* We need to be able to dump the jacoco coverage before cluster is shut down.
* The new internal testing framework removed some of the gradle tasks we were listening to
* to choose a good time to do it. This will dump the executionData to file after each test.
* TODO: This is also currently just overwriting integTest.exec with the updated execData without
* resetting after writing each time. This can be improved to either write an exec file per test
* or by letting jacoco append to the file
*/
public interface IProxy {
byte[] getExecutionData(boolean reset);

void dump(boolean reset);

void reset();
}


@AfterClass
public static void dumpCoverage() throws IOException, MalformedObjectNameException {
// jacoco.dir is set in esplugin-coverage.gradle, if it doesn't exist we don't
// want to collect coverage so we can return early
String jacocoBuildPath = System.getProperty("jacoco.dir");
if (Strings.isNullOrEmpty(jacocoBuildPath)) {
return;
}

String serverUrl = "service:jmx:rmi:///jndi/rmi://127.0.0.1:7777/jmxrmi";
try (JMXConnector connector = JMXConnectorFactory.connect(new JMXServiceURL(serverUrl))) {
IProxy proxy = MBeanServerInvocationHandler.newProxyInstance(
connector.getMBeanServerConnection(), new ObjectName("org.jacoco:type=Runtime"), IProxy.class,
false);

Path path = org.opensearch.common.io.PathUtils.get(jacocoBuildPath + "/integTestRunner.exec");
Files.write(path, proxy.getExecutionData(false));
} catch (Exception ex) {
throw new RuntimeException("Failed to dump coverage: " + ex);
}
}
}

0 comments on commit 743e538

Please sign in to comment.