Skip to content

Commit

Permalink
unit tests for ProxyingSpan
Browse files Browse the repository at this point in the history
  • Loading branch information
dmarkwat committed Jan 16, 2023
1 parent b93701b commit 38d5c27
Show file tree
Hide file tree
Showing 2 changed files with 194 additions and 1 deletion.
3 changes: 2 additions & 1 deletion opencensus-shim/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ dependencies {
testImplementation("io.opencensus:opencensus-impl")
testImplementation("io.opencensus:opencensus-contrib-exemplar-util")

javaagent("io.opentelemetry.javaagent:opentelemetry-javaagent:1.19.1")
javaagent("io.opentelemetry.javaagent:opentelemetry-javaagent:1.21.0")
}

tasks.named<Test>("test") {
Expand All @@ -45,6 +45,7 @@ testing {
testTask.configure {
jvmArgs("-javaagent:${javaagent.asPath}")
environment("OTEL_TRACES_EXPORTER", "logging")
environment("OTEL_METRICS_EXPORTER", "none")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.opencensusshim;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.times;

import io.opentelemetry.api.trace.Span;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Mockito;
import org.mockito.verification.VerificationMode;

class ProxyingSpanTest {

/*
Verifies all methods on the otel Span interface are under test.
Each case is enumerated in proxyMethodsProvider() to avoid false-positives (bad reflection) and maximize
flexibility for special cases (e.g. getSpanContext() and isRecording())
*/
@Test
void verifyAllMethodsAreUnderTest() {
List<Method> methods =
proxyMethodsProvider()
.map(
pm -> {
try {
return getInterfaceMethod(
Span.class, (String) pm.get()[0], (Class<?>[]) pm.get()[1]);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
})
.collect(Collectors.toList());

assertThat(methods)
.describedAs("all interface methods are being tested")
.containsAll(allInterfaceMethods(Span.class));
assertThat(allInterfaceMethods(Span.class))
.describedAs("all tested methods are on the Span interface")
.containsAll(methods);
}

@ParameterizedTest
@MethodSource("proxyMethodsProvider")
void testit(String name, Class<?>[] params, VerificationMode timesCalled)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Method method = getInterfaceMethod(Span.class, name, params);
Span mockedSpan = Mockito.spy(Span.current());
OpenTelemetrySpanImpl span = new OpenTelemetrySpanImpl(mockedSpan);
assertProxied(span, mockedSpan, method, timesCalled);
}

static List<Method> allInterfaceMethods(Class<?> clazz) {
return Arrays.stream(clazz.getDeclaredMethods())
.filter(m -> Modifier.isPublic(m.getModifiers()) && !Modifier.isStatic(m.getModifiers()))
.collect(Collectors.toList());
}

static Stream<Arguments> proxyMethodsProvider() {
// todo fill in remaining methods
return Stream.of(
Arguments.of("end", new Class<?>[] {}, times(1)),
Arguments.of(
"end", new Class<?>[] {long.class, java.util.concurrent.TimeUnit.class}, times(1)),
Arguments.of("end", new Class<?>[] {java.time.Instant.class}, times(1)),
Arguments.of("setAttribute", new Class<?>[] {String.class, String.class}, times(1)),
Arguments.of(
"setAttribute",
new Class<?>[] {io.opentelemetry.api.common.AttributeKey.class, int.class},
times(1)),
Arguments.of(
"setAttribute",
new Class<?>[] {io.opentelemetry.api.common.AttributeKey.class, Object.class},
times(1)),
Arguments.of("setAttribute", new Class<?>[] {String.class, long.class}, times(1)),
Arguments.of("setAttribute", new Class<?>[] {String.class, double.class}, times(1)),
Arguments.of("setAttribute", new Class<?>[] {String.class, boolean.class}, times(1)),
Arguments.of(
"recordException",
new Class<?>[] {Throwable.class, io.opentelemetry.api.common.Attributes.class},
times(1)),
Arguments.of("recordException", new Class<?>[] {Throwable.class}, times(1)),
Arguments.of(
"setAllAttributes",
new Class<?>[] {io.opentelemetry.api.common.Attributes.class},
times(1)),
Arguments.of("updateName", new Class<?>[] {String.class}, times(1)),
Arguments.of(
"storeInContext", new Class<?>[] {io.opentelemetry.context.Context.class}, times(1)),
Arguments.of("addEvent", new Class<?>[] {String.class, java.time.Instant.class}, times(1)),
Arguments.of(
"addEvent",
new Class<?>[] {String.class, long.class, java.util.concurrent.TimeUnit.class},
times(1)),
Arguments.of(
"addEvent",
new Class<?>[] {
String.class, io.opentelemetry.api.common.Attributes.class, java.time.Instant.class
},
times(1)),
Arguments.of("addEvent", new Class<?>[] {String.class}, times(1)),
Arguments.of(
"addEvent",
new Class<?>[] {
String.class,
io.opentelemetry.api.common.Attributes.class,
long.class,
java.util.concurrent.TimeUnit.class
},
times(1)),
Arguments.of(
"addEvent",
new Class<?>[] {String.class, io.opentelemetry.api.common.Attributes.class},
times(1)),
Arguments.of(
"setStatus",
new Class<?>[] {io.opentelemetry.api.trace.StatusCode.class, String.class},
times(1)),
Arguments.of(
"setStatus", new Class<?>[] {io.opentelemetry.api.trace.StatusCode.class}, times(1)),
//
// special cases
//
// called never -- it's shared between OC and Otel Span types and is always true, so returns
// `true`
Arguments.of("isRecording", new Class<?>[] {}, times(0)),
// called twice: once in constructor, then once during proxy
Arguments.of("getSpanContext", new Class<?>[] {}, times(2)));
}

// gets default values for all cases, as mockito can't mock wrappers or primitives, including
// String
static Object valueLookup(Class<?> clazz) {
if (clazz == int.class || clazz == Integer.class) {
return Integer.valueOf(0).intValue();
} else if (clazz == long.class || clazz == Long.class) {
return Long.valueOf(0L).longValue();
} else if (clazz == double.class || clazz == Double.class) {
return Double.valueOf(0d).doubleValue();
} else if (clazz == char.class || clazz == Character.class) {
return Character.valueOf('\0').charValue();
} else if (clazz == boolean.class || clazz == Boolean.class) {
return Boolean.valueOf(false).booleanValue();
} else if (clazz == float.class || clazz == Float.class) {
return Float.valueOf(0f).floatValue();
} else if (clazz == String.class) {
return "";
} else if (clazz == byte.class || clazz == Byte.class) {
return Byte.valueOf((byte) 0).byteValue();
} else if (clazz == short.class || clazz == Short.class) {
return Short.valueOf((short) 0).shortValue();
} else {
return Mockito.mock(clazz);
}
}

static <T> Method getInterfaceMethod(Class<T> proxy, String name, Class<?>[] params)
throws NoSuchMethodException {
Method method = proxy.getMethod(name, params);
assertThat(method).isNotNull();
return method;
}

static <T> void assertProxied(T proxy, T delegate, Method method, VerificationMode mode)
throws InvocationTargetException, IllegalAccessException {
// Get parameters for method
Class<?>[] parameterTypes = method.getParameterTypes();
Object[] arguments = new Object[parameterTypes.length];
for (int j = 0; j < arguments.length; j++) {
arguments[j] = valueLookup(parameterTypes[j]);
}

// Invoke wrapper method
method.invoke(proxy, arguments);

// Ensure method was called on delegate exactly once with the correct arguments
method.invoke(Mockito.verify(delegate, mode), arguments);
}
}

0 comments on commit 38d5c27

Please sign in to comment.