Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
package co.elastic.apm.agent;
package co.elastic.apm.agent.testutils;

import co.elastic.test.ChildFirstURLClassLoader;
import org.apache.ivy.Ivy;
Expand All @@ -29,78 +29,40 @@
import org.apache.ivy.core.settings.IvySettings;
import org.apache.ivy.plugins.parser.xml.XmlModuleDescriptorWriter;
import org.apache.ivy.plugins.resolver.URLResolver;
import org.junit.runner.JUnitCore;
import org.junit.runner.Result;
import org.junit.runner.notification.Failure;
import org.junit.runners.BlockJUnit4ClassRunner;

import javax.annotation.Nullable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;

import static org.assertj.core.api.Assertions.assertThat;

public class TestClassWithDependencyRunner {
public abstract class AbstractTestClassWithDependencyRunner {

private WeakReference<ClassLoader> classLoader;
@Nullable
private BlockJUnit4ClassRunner testRunner;
protected final Class<?> testClass;

/**
* Downloads the dependency and all its transitive dependencies via Apache Ivy.
* Also exports a jar for the test class and all classes which reference the provided dependency.
* All downloaded and exported jar files are then loaded from class loader with child-first semantics.
* This avoids that the dependency will be loaded by the parent class loader which contains the {@code provided}-scoped maven dependency.
*/
public TestClassWithDependencyRunner(String groupId, String artifactId, String version, Class<?> testClass, Class<?>... classesReferencingDependency) throws Exception {
this(Collections.singletonList(groupId + ":" + artifactId + ":" + version), testClass, classesReferencingDependency);
}

public TestClassWithDependencyRunner(List<String> dependencies, Class<?> testClass, Class<?>... classesReferencingDependency) throws Exception {
this(dependencies, testClass.getName(), Arrays.stream(classesReferencingDependency).map(Class::getName).toArray(String[]::new));
}

public TestClassWithDependencyRunner(List<String> dependencies, String testClass, String... classesReferencingDependency) throws Exception {
protected AbstractTestClassWithDependencyRunner(List<String> dependencies, String testClass, String... classesReferencingDependency) throws Exception {
List<URL> urls = resolveArtifacts(dependencies);
List<String> classesToExport = new ArrayList<>();
classesToExport.add(testClass);
classesToExport.addAll(Arrays.asList(classesReferencingDependency));
urls.add(exportToTempJarFile(classesToExport));

URLClassLoader testClassLoader = new ChildFirstURLClassLoader(urls);
testRunner = new BlockJUnit4ClassRunner(testClassLoader.loadClass(testClass));
classLoader = new WeakReference<>(testClassLoader);
}

public void run() {
if (testRunner == null) {
throw new IllegalStateException();
}
Result result = new JUnitCore().run(testRunner);
for (Failure failure : result.getFailures()) {
System.out.println(failure);
failure.getException().printStackTrace();
}
assertThat(result.wasSuccessful()).isTrue();
}

public void assertClassLoaderIsGCed() {
testRunner = null;
System.gc();
System.gc();
System.gc();
assertThat(classLoader.get()).isNull();
this.testClass = testClassLoader.loadClass(testClass);
}

private static URL exportToTempJarFile(List<String> classes) throws IOException {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. 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 co.elastic.apm.agent.testutils;

import org.junit.runner.JUnitCore;
import org.junit.runner.Result;
import org.junit.runner.notification.Failure;
import org.junit.runners.BlockJUnit4ClassRunner;

import javax.annotation.Nullable;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

/**
* @deprecated use {@link TestClassWithDependencyRunner} and test with junit5 instead
*/
@Deprecated
public class JUnit4TestClassWithDependencyRunner extends AbstractTestClassWithDependencyRunner {

@Nullable
private final BlockJUnit4ClassRunner testRunner;

public JUnit4TestClassWithDependencyRunner(String groupId, String artifactId, String version, Class<?> testClass, Class<?>... classesReferencingDependency) throws Exception {
this(Collections.singletonList(groupId + ":" + artifactId + ":" + version), testClass, classesReferencingDependency);
}

public JUnit4TestClassWithDependencyRunner(List<String> dependencies, Class<?> testClass, Class<?>... classesReferencingDependency) throws Exception {
this(dependencies, testClass.getName(), Arrays.stream(classesReferencingDependency).map(Class::getName).toArray(String[]::new));
}

public JUnit4TestClassWithDependencyRunner(List<String> dependencies, String testClass, String... classesReferencingDependency) throws Exception {
super(dependencies, testClass, classesReferencingDependency);
testRunner = new BlockJUnit4ClassRunner(this.testClass);
}

public void run() {
if (testRunner == null) {
throw new IllegalStateException();
}
Result result = new JUnitCore().run(testRunner);
for (Failure failure : result.getFailures()) {
System.out.println(failure);
failure.getException().printStackTrace();
}
assertThat(result.wasSuccessful()).isTrue();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. 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 co.elastic.apm.agent.testutils;

import org.junit.jupiter.api.extension.ConditionEvaluationResult;
import org.junit.jupiter.api.extension.ExecutionCondition;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.platform.launcher.Launcher;
import org.junit.platform.launcher.LauncherDiscoveryRequest;
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
import org.junit.platform.launcher.core.LauncherFactory;
import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
import org.junit.platform.launcher.listeners.TestExecutionSummary;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.AnnotatedElement;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.extension.ConditionEvaluationResult.disabled;
import static org.junit.jupiter.api.extension.ConditionEvaluationResult.enabled;
import static org.junit.platform.commons.util.AnnotationUtils.findAnnotation;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;

public class TestClassWithDependencyRunner extends AbstractTestClassWithDependencyRunner {


/**
* Prevents test from running when the test class is not executed from within a {@link TestClassWithDependencyRunner}.
* <p>
* This if for example useful when attempting to test that an instrumentation is *not* active for unsupported versions
* of the instrumentation target. To test this, one would create a test that checks that the instrumentation is not active
* and run it via the {@link TestClassWithDependencyRunner} for the unsupported versions of the instrumentation target.
* <p>
* However, the test itself would also be run by maven outside the {@link TestClassWithDependencyRunner}, because it is a
* normal unit test. In this environment the test is executed with the latest version of the instrumentation target (from the pom.xml),
* which in turn would cause the test to fail because this version is actually supported.
* To prevent these "wrong" failures, this annotation can be used to disable the test outside the {@link TestClassWithDependencyRunner}.
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ExtendWith(DisableOutsideOfRunnerCondition.class)
public @interface DisableOutsideOfRunner {
/**
* @return a custom reason for disabling this outside of the dependency runner
*/
String value() default "";
}

public static class DisableOutsideOfRunnerCondition implements ExecutionCondition {

@Override
public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) {
AnnotatedElement element = context.getElement().orElse(null);
return findAnnotation(element, DisableOutsideOfRunner.class)
.map(annotation -> disabled(element + " is @DisableOutsideDependencyRunner", annotation.value()))
.orElse(enabled("@DisableOutsideDependencyRunner is not present"));
}
}

public TestClassWithDependencyRunner(List<String> dependencies, String testClass, String... classesReferencingDependency) throws Exception {
super(dependencies, testClass, classesReferencingDependency);
}

public void run() {
LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
.selectors(selectClass(testClass))
.configurationParameter("junit.jupiter.conditions.deactivate", DisableOutsideOfRunnerCondition.class.getName())
.build();
Launcher launcher = LauncherFactory.create();
SummaryGeneratingListener listener = new SummaryGeneratingListener();
launcher.registerTestExecutionListeners(listener);
launcher.execute(request);

for (TestExecutionSummary.Failure failure : listener.getSummary().getFailures()) {
System.out.println(failure);
failure.getException().printStackTrace();
}
assertThat(listener.getSummary().getTestsFailedCount()).isZero();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package co.elastic.test;

import co.elastic.apm.agent.bci.bytebuddy.CustomElementMatchers;
import co.elastic.apm.agent.testutils.JUnit4TestClassWithDependencyRunner;

import java.io.IOException;
import java.net.URL;
Expand All @@ -30,7 +31,7 @@

/**
* A Child-first class loader used for tests.
* Specifically, used within {@link co.elastic.apm.agent.TestClassWithDependencyRunner} for tests that require encapsulated
* Specifically, used within {@link JUnit4TestClassWithDependencyRunner} for tests that require encapsulated
* test classpath, for example - for testing specific library versions.
* In order for classes that are loaded by this class loader to be instrumented, it must be outside of the {@code co.elastic.apm}
* package, otherwise it may be excluded if tested through {@link CustomElementMatchers#isAgentClassLoader()}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,33 +18,22 @@
*/
package co.elastic.apm.agent.httpclient.v3;

import co.elastic.apm.agent.TestClassWithDependencyRunner;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import co.elastic.apm.agent.testutils.TestClassWithDependencyRunner;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import java.util.Arrays;
import java.util.List;

@RunWith(Parameterized.class)
public class HttpClient3VersionIT {

private final TestClassWithDependencyRunner runner;

public HttpClient3VersionIT(String dependency) throws Exception {
this.runner = new TestClassWithDependencyRunner(List.of(dependency), HttpClient3InstrumentationTest.class);
}

@Parameterized.Parameters(name = "{0}")
public static Iterable<Object[]> data() {
return Arrays.asList(new Object[][]{
{"commons-httpclient:commons-httpclient:3.0"},
{"commons-httpclient:commons-httpclient:3.1"}
});
}

@Test
public void testVersion() {
@ParameterizedTest
@ValueSource(strings = {
"3.0",
"3.1"
})
void testVersion(String version) throws Exception {
String dependency = String.format("commons-httpclient:commons-httpclient:" + version);
TestClassWithDependencyRunner runner = new TestClassWithDependencyRunner(List.of(dependency), "co.elastic.apm.agent.httpclient.v3.HttpClient3InstrumentationTest");
runner.run();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
*/
package co.elastic.apm.agent.httpclient.v4;

import co.elastic.apm.agent.TestClassWithDependencyRunner;
import co.elastic.apm.agent.testutils.JUnit4TestClassWithDependencyRunner;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
Expand All @@ -29,12 +29,12 @@
@RunWith(Parameterized.class)
public class LegacyApacheHttpClientVersionIT {

private final TestClassWithDependencyRunner runner1;
private final TestClassWithDependencyRunner runner2;
private final JUnit4TestClassWithDependencyRunner runner1;
private final JUnit4TestClassWithDependencyRunner runner2;

public LegacyApacheHttpClientVersionIT(List<String> dependencies) throws Exception {
this.runner1 = new TestClassWithDependencyRunner(dependencies, LegacyApacheHttpClientBasicHttpRequestInstrumentationTest.class);
this.runner2 = new TestClassWithDependencyRunner(dependencies, LegacyApacheHttpClientHttpUriRequestInstrumentationTest.class);
this.runner1 = new JUnit4TestClassWithDependencyRunner(dependencies, LegacyApacheHttpClientBasicHttpRequestInstrumentationTest.class);
this.runner2 = new JUnit4TestClassWithDependencyRunner(dependencies, LegacyApacheHttpClientHttpUriRequestInstrumentationTest.class);
}

@Parameterized.Parameters(name = "{0}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,17 @@
*/
package co.elastic.apm.agent.ecs_logging;

import co.elastic.apm.agent.TestClassWithDependencyRunner;
import co.elastic.apm.agent.testutils.JUnit4TestClassWithDependencyRunner;
import org.junit.jupiter.api.Test;

import java.util.List;

public class Log4j2_17_1ServiceNameInstrumentationTest {
private final TestClassWithDependencyRunner runner;
private final JUnit4TestClassWithDependencyRunner runner;

public Log4j2_17_1ServiceNameInstrumentationTest() throws Exception {
List<String> dependencies = List.of("co.elastic.logging:log4j2-ecs-layout:1.3.2");
runner = new TestClassWithDependencyRunner(dependencies, Log4j2ServiceNameInstrumentationTest.class);
runner = new JUnit4TestClassWithDependencyRunner(dependencies, Log4j2ServiceNameInstrumentationTest.class);
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
*/
package co.elastic.apm.agent.kafka;

import co.elastic.apm.agent.TestClassWithDependencyRunner;
import co.elastic.apm.agent.testutils.JUnit4TestClassWithDependencyRunner;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
Expand All @@ -27,10 +27,10 @@

@RunWith(Parameterized.class)
public class KafkaClientVersionsIT {
private final TestClassWithDependencyRunner runner;
private final JUnit4TestClassWithDependencyRunner runner;

public KafkaClientVersionsIT(String version) throws Exception {
runner = new TestClassWithDependencyRunner("org.apache.kafka", "kafka-clients", version,
runner = new JUnit4TestClassWithDependencyRunner("org.apache.kafka", "kafka-clients", version,
KafkaIT.class, KafkaIT.Consumer.class, KafkaIT.RecordIterationMode.class, KafkaIT.TestScenario.class,
KafkaIT.ConsumerRecordConsumer.class);
}
Expand Down
Loading