diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 444eed8d2..0adbe5189 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,7 +4,6 @@ opentelemetry-instrumentation-alpha = "2.21.0-alpha" opentelemetry-semconv = "1.37.0" opentelemetry-semconv-alpha = "1.37.0-alpha" opentelemetry-contrib = "1.51.0-alpha" -mockito = "5.20.0" junit = "6.0.1" byteBuddy = "1.18.1" okhttp = "5.3.0" @@ -63,8 +62,6 @@ androidx-test-core = "androidx.test:core:1.7.0" androidx-test-rules = "androidx.test:rules:1.7.0" androidx-test-runner = "androidx.test:runner:1.7.0" androidx-junit = "androidx.test.ext:junit:1.3.0" -mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" } -mockito-junit-jupiter = { module = "org.mockito:mockito-junit-jupiter", version.ref = "mockito" } mockk = "io.mockk:mockk:1.14.6" junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" } junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" } @@ -92,7 +89,7 @@ androidx-junit-ktx = { group = "androidx.test.ext", name = "junit-ktx", version. kover-plugin = { module = "org.jetbrains.kotlinx.kover:org.jetbrains.kotlinx.kover.gradle.plugin", version.ref = "koverGradlePlugin" } [bundles] -mocking = ["mockito-core", "mockito-junit-jupiter", "mockk"] +mocking = ["mockk"] junit = ["junit-jupiter-api", "junit-jupiter-engine", "junit-vintage-engine"] [plugins] diff --git a/instrumentation/okhttp3/testing/src/androidTest/java/io/opentelemetry/instrumentation/library/okhttp/v3_0/InstrumentationTest.java b/instrumentation/okhttp3/testing/src/androidTest/java/io/opentelemetry/instrumentation/library/okhttp/v3_0/InstrumentationTest.java deleted file mode 100644 index c8f21e586..000000000 --- a/instrumentation/okhttp3/testing/src/androidTest/java/io/opentelemetry/instrumentation/library/okhttp/v3_0/InstrumentationTest.java +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.library.okhttp.v3_0; - -import static org.junit.Assert.assertEquals; - -import androidx.annotation.NonNull; -import io.opentelemetry.android.test.common.OpenTelemetryRumRule; -import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.api.trace.Span; -import io.opentelemetry.api.trace.SpanContext; -import io.opentelemetry.context.Scope; -import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter; -import io.opentelemetry.sdk.OpenTelemetrySdk; -import io.opentelemetry.sdk.trace.SdkTracerProvider; -import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; -import java.io.IOException; -import java.util.concurrent.CountDownLatch; -import mockwebserver3.MockResponse; -import mockwebserver3.MockWebServer; -import okhttp3.Call; -import okhttp3.Callback; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; -import org.junit.After; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; - -public class InstrumentationTest { - private MockWebServer server; - - @Rule public OpenTelemetryRumRule openTelemetryRumRule = new OpenTelemetryRumRule(); - - @Before - public void setUp() throws IOException { - server = new MockWebServer(); - server.start(); - } - - @After - public void tearDown() throws IOException { - server.close(); - } - - @Test - public void okhttpTraces() throws IOException { - server.enqueue(new MockResponse.Builder().code(200).build()); - - Span span = openTelemetryRumRule.getSpan(); - - try (Scope ignored = span.makeCurrent()) { - OkHttpClient client = - new OkHttpClient.Builder() - .addInterceptor( - chain -> { - SpanContext currentSpan = Span.current().getSpanContext(); - assertEquals( - span.getSpanContext().getTraceId(), - currentSpan.getTraceId()); - return chain.proceed(chain.request()); - }) - .build(); - createCall(client, "/test/").execute().close(); - } - - span.end(); - - assertEquals(2, openTelemetryRumRule.inMemorySpanExporter.getFinishedSpanItems().size()); - } - - @Test - public void okhttpTraces_with_callback() throws InterruptedException { - CountDownLatch lock = new CountDownLatch(1); - Span span = openTelemetryRumRule.getSpan(); - - try (Scope ignored = span.makeCurrent()) { - server.enqueue(new MockResponse.Builder().code(200).build()); - - OkHttpClient client = - new OkHttpClient.Builder() - .addInterceptor( - chain -> { - SpanContext currentSpan = Span.current().getSpanContext(); - // Verify context propagation. - assertEquals( - span.getSpanContext().getTraceId(), - currentSpan.getTraceId()); - return chain.proceed(chain.request()); - }) - .build(); - createCall(client, "/test/") - .enqueue( - new Callback() { - @Override - public void onFailure(@NonNull Call call, @NonNull IOException e) {} - - @Override - public void onResponse( - @NonNull Call call, @NonNull Response response) { - // Verify that the original caller's context is the current one - // here. - assertEquals(span, Span.current()); - lock.countDown(); - } - }); - } - - lock.await(); - span.end(); - - assertEquals(2, openTelemetryRumRule.inMemorySpanExporter.getFinishedSpanItems().size()); - } - - @Test - public void avoidCreatingSpansForInternalOkhttpRequests() throws InterruptedException { - // NOTE: For some reason this test always passes when running all the tests in this file at - // once, - // so it should be run isolated to actually get it to fail when it's expected to fail. - OtlpHttpSpanExporter exporter = - OtlpHttpSpanExporter.builder().setEndpoint(server.url("").toString()).build(); - OpenTelemetry openTelemetry = - OpenTelemetrySdk.builder() - .setTracerProvider( - SdkTracerProvider.builder() - .addSpanProcessor(SimpleSpanProcessor.create(exporter)) - .build()) - .build(); - - server.enqueue(new MockResponse.Builder().code(200).build()); - - // This span should trigger 1 export okhttp call, which is the only okhttp call expected - // for this test case. - openTelemetry - .tracerBuilder("Some Scope") - .build() - .spanBuilder("Some Span") - .startSpan() - .end(); - - // Wait for unwanted extra okhttp requests. - int loop = 0; - while (loop < 10) { - Thread.sleep(100); - // Stop waiting if we get at least one unwanted request. - if (server.getRequestCount() > 1) { - break; - } - loop++; - } - - assertEquals(1, server.getRequestCount()); - } - - private Call createCall(OkHttpClient client, String urlPath) { - Request request = new Request.Builder().url(server.url(urlPath)).build(); - return client.newCall(request); - } -} diff --git a/instrumentation/okhttp3/testing/src/androidTest/java/io/opentelemetry/instrumentation/library/okhttp/v3_0/InstrumentationTest.kt b/instrumentation/okhttp3/testing/src/androidTest/java/io/opentelemetry/instrumentation/library/okhttp/v3_0/InstrumentationTest.kt new file mode 100644 index 000000000..9c3c43b39 --- /dev/null +++ b/instrumentation/okhttp3/testing/src/androidTest/java/io/opentelemetry/instrumentation/library/okhttp/v3_0/InstrumentationTest.kt @@ -0,0 +1,185 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +@file:Suppress("ktlint:standard:package-name") + +package io.opentelemetry.instrumentation.library.okhttp.v3_0 + +import io.opentelemetry.android.test.common.OpenTelemetryRumRule +import io.opentelemetry.api.OpenTelemetry +import io.opentelemetry.api.trace.Span +import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter +import io.opentelemetry.sdk.OpenTelemetrySdk +import io.opentelemetry.sdk.trace.SdkTracerProvider +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor +import mockwebserver3.MockResponse +import mockwebserver3.MockWebServer +import okhttp3.Call +import okhttp3.Callback +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import org.assertj.core.api.Assertions.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.io.IOException +import java.util.concurrent.CountDownLatch + +class InstrumentationTest { + private lateinit var server: MockWebServer + + @get:Rule + internal var openTelemetryRumRule: OpenTelemetryRumRule = OpenTelemetryRumRule() + + @Before + @Throws(IOException::class) + fun setUp() { + server = MockWebServer() + server.start() + } + + @After + @Throws(IOException::class) + fun tearDown() { + server.close() + } + + @Test + @Throws(IOException::class) + fun okhttpTraces() { + server.enqueue(MockResponse.Builder().code(200).build()) + + val lock = CountDownLatch(1) + val span = openTelemetryRumRule.getSpan() + + span.makeCurrent().use { ignored -> + val client = + OkHttpClient + .Builder() + .addInterceptor( + Interceptor { chain: Interceptor.Chain -> + val currentSpan = Span.current().spanContext + assertThat(span.spanContext.traceId) + .isEqualTo(currentSpan.traceId) + lock.countDown() + chain.proceed(chain.request()) + }, + ).build() + createCall(client, "/test/").execute().close() + } + lock.await() + span.end() + + assertThat( + openTelemetryRumRule.inMemorySpanExporter.finishedSpanItems.size + .toLong(), + ).isEqualTo(2) + } + + @Test + @Throws(InterruptedException::class) + fun okhttpTraces_with_callback() { + val lock = CountDownLatch(2) + val span = openTelemetryRumRule.getSpan() + + span.makeCurrent().use { ignored -> + server.enqueue(MockResponse.Builder().code(200).build()) + val client = + OkHttpClient + .Builder() + .addInterceptor( + Interceptor { chain: Interceptor.Chain -> + val currentSpan = Span.current().spanContext + // Verify context propagation. + assertThat(span.spanContext.traceId) + .isEqualTo(currentSpan.traceId) + lock.countDown() + chain.proceed(chain.request()) + }, + ).build() + createCall(client, "/test/") + .enqueue( + object : Callback { + override fun onFailure( + call: Call, + e: IOException, + ) { + } + + override fun onResponse( + call: Call, + response: Response, + ) { + // Verify that the original caller's context is the current one + // here. + assertThat(span).isEqualTo(Span.current()) + lock.countDown() + } + }, + ) + } + lock.await() + span.end() + + assertThat( + openTelemetryRumRule.inMemorySpanExporter.finishedSpanItems.size + .toLong(), + ).isEqualTo(2) + } + + @Test + @Throws(InterruptedException::class) + fun avoidCreatingSpansForInternalOkhttpRequests() { + // NOTE: For some reason this test always passes when running all the tests in this file at + // once, + // so it should be run isolated to actually get it to fail when it's expected to fail. + val exporter = + OtlpHttpSpanExporter.builder().setEndpoint(server.url("").toString()).build() + val openTelemetry: OpenTelemetry = + OpenTelemetrySdk + .builder() + .setTracerProvider( + SdkTracerProvider + .builder() + .addSpanProcessor(SimpleSpanProcessor.create(exporter)) + .build(), + ).build() + + server.enqueue(MockResponse.Builder().code(200).build()) + + // This span should trigger 1 export okhttp call, which is the only okhttp call expected + // for this test case. + openTelemetry + .tracerBuilder("Some Scope") + .build() + .spanBuilder("Some Span") + .startSpan() + .end() + + // Wait for unwanted extra okhttp requests. + var loop = 0 + while (loop < 10) { + Thread.sleep(100) + // Stop waiting if we get at least one unwanted request. + if (server.requestCount > 1) { + break + } + loop++ + } + + assertThat(server.requestCount.toLong()).isEqualTo(1) + } + + private fun createCall( + client: OkHttpClient, + urlPath: String, + ): Call { + val request = Request.Builder().url(server.url(urlPath)).build() + return client.newCall(request) + } +} diff --git a/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderListenerTest.java b/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderListenerTest.java deleted file mode 100644 index 9d63b6ac8..000000000 --- a/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderListenerTest.java +++ /dev/null @@ -1,222 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.android.instrumentation.slowrendering; - -import static android.view.FrameMetrics.DRAW_DURATION; -import static android.view.FrameMetrics.FIRST_DRAW_FRAME; -import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; -import static org.junit.Assert.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.when; - -import android.app.Activity; -import android.app.Application; -import android.content.ComponentName; -import android.os.Build; -import android.os.Handler; -import android.view.FrameMetrics; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.api.trace.Tracer; -import io.opentelemetry.sdk.testing.junit4.OpenTelemetryRule; -import io.opentelemetry.sdk.trace.data.SpanData; -import java.time.Duration; -import java.util.List; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Answers; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; -import org.robolectric.annotation.Config; - -@RunWith(AndroidJUnit4.class) -@Config(sdk = Build.VERSION_CODES.N) -public class SlowRenderListenerTest { - - private static final AttributeKey COUNT_KEY = AttributeKey.longKey("count"); - - @Rule public OpenTelemetryRule otelTesting = OpenTelemetryRule.create(); - @Rule public MockitoRule mocks = MockitoJUnit.rule(); - - @Mock Handler frameMetricsHandler; - @Mock Application application; - - @Mock(answer = Answers.RETURNS_DEEP_STUBS) - Activity activity; - - @Mock FrameMetrics frameMetrics; - @Mock JankReporter jankReporter; - ScheduledExecutorService executorService; - - @Captor ArgumentCaptor activityListenerCaptor; - - @Before - public void setup() { - executorService = Executors.newSingleThreadScheduledExecutor(); - ComponentName componentName = new ComponentName("io.otel", "Komponent"); - when(activity.getComponentName()).thenReturn(componentName); - } - - @Test - public void add() { - SlowRenderListener testInstance = - new SlowRenderListener( - jankReporter, executorService, frameMetricsHandler, Duration.ZERO); - - testInstance.onActivityResumed(activity); - - verify(activity.getWindow()) - .addOnFrameMetricsAvailableListener( - activityListenerCaptor.capture(), eq(frameMetricsHandler)); - assertEquals("io.otel/Komponent", activityListenerCaptor.getValue().getActivityName()); - } - - @Test - public void removeBeforeAddOk() { - SlowRenderListener testInstance = - new SlowRenderListener( - jankReporter, executorService, frameMetricsHandler, Duration.ZERO); - - testInstance.onActivityPaused(activity); - - verifyNoInteractions(activity); - assertThat(otelTesting.getSpans()).hasSize(0); - } - - @Test - public void addAndRemove() { - SlowRenderListener testInstance = - new SlowRenderListener( - jankReporter, executorService, frameMetricsHandler, Duration.ZERO); - - testInstance.onActivityResumed(activity); - testInstance.onActivityPaused(activity); - - verify(activity.getWindow()) - .addOnFrameMetricsAvailableListener( - activityListenerCaptor.capture(), eq(frameMetricsHandler)); - verify(activity.getWindow()) - .removeOnFrameMetricsAvailableListener(activityListenerCaptor.getValue()); - - assertThat(otelTesting.getSpans()).hasSize(0); - } - - @Test - public void removeWithMetrics() { - Tracer tracer = otelTesting.getOpenTelemetry().getTracer("testTracer"); - jankReporter = new SpanBasedJankReporter(tracer); - SlowRenderListener testInstance = - new SlowRenderListener( - jankReporter, executorService, frameMetricsHandler, Duration.ZERO); - - testInstance.onActivityResumed(activity); - - verify(activity.getWindow()) - .addOnFrameMetricsAvailableListener(activityListenerCaptor.capture(), any()); - PerActivityListener listener = activityListenerCaptor.getValue(); - for (long duration : makeSomeDurations()) { - when(frameMetrics.getMetric(DRAW_DURATION)).thenReturn(duration); - listener.onFrameMetricsAvailable(null, frameMetrics, 0); - } - - testInstance.onActivityPaused(activity); - - List spans = otelTesting.getSpans(); - assertSpanContent(spans); - } - - @Test - public void start() { - ScheduledExecutorService exec = mock(ScheduledExecutorService.class); - - doAnswer( - invocation -> { - Runnable runnable = invocation.getArgument(0); - runnable.run(); // just call it immediately - return null; - }) - .when(exec) - .scheduleWithFixedDelay(any(), eq(1001L), eq(1001L), eq(TimeUnit.MILLISECONDS)); - - Tracer tracer = otelTesting.getOpenTelemetry().getTracer("testTracer"); - jankReporter = new SpanBasedJankReporter(tracer); - SlowRenderListener testInstance = - new SlowRenderListener( - jankReporter, exec, frameMetricsHandler, Duration.ofMillis(1001)); - - testInstance.onActivityResumed(activity); - - verify(activity.getWindow()) - .addOnFrameMetricsAvailableListener(activityListenerCaptor.capture(), any()); - PerActivityListener listener = activityListenerCaptor.getValue(); - for (long duration : makeSomeDurations()) { - when(frameMetrics.getMetric(DRAW_DURATION)).thenReturn(duration); - listener.onFrameMetricsAvailable(null, frameMetrics, 0); - } - - testInstance.start(); - - List spans = otelTesting.getSpans(); - assertSpanContent(spans); - } - - @Test - public void activityListenerSkipsFirstFrame() { - PerActivityListener listener = new PerActivityListener(activity); - when(frameMetrics.getMetric(FIRST_DRAW_FRAME)).thenReturn(1L); - listener.onFrameMetricsAvailable(null, frameMetrics, 99); - verify(frameMetrics, never()).getMetric(DRAW_DURATION); - } - - private static void assertSpanContent(List spans) { - assertThat(spans) - .hasSize(2) - .satisfiesExactly( - span -> - assertThat(span) - .hasName("slowRenders") - .endsAt(span.getStartEpochNanos()) - .hasAttribute(COUNT_KEY, 3L) - .hasAttribute( - AttributeKey.stringKey("activity.name"), - "io.otel/Komponent"), - span -> - assertThat(span) - .hasName("frozenRenders") - .endsAt(span.getStartEpochNanos()) - .hasAttribute(COUNT_KEY, 1L) - .hasAttribute( - AttributeKey.stringKey("activity.name"), - "io.otel/Komponent")); - } - - private List makeSomeDurations() { - return Stream.of( - 5L, 11L, 101L, // slow - 701L, // frozen - 17L, // slow - 17L, // slow - 16L, 11L) - .map(TimeUnit.MILLISECONDS::toNanos) - .collect(Collectors.toList()); - } -} diff --git a/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderListenerTest.kt b/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderListenerTest.kt new file mode 100644 index 000000000..b10d1505b --- /dev/null +++ b/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderListenerTest.kt @@ -0,0 +1,270 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.instrumentation.slowrendering + +import android.app.Activity +import android.content.ComponentName +import android.os.Build +import android.os.Handler +import android.view.FrameMetrics +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.mockk.CapturingSlot +import io.mockk.confirmVerified +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.impl.annotations.RelaxedMockK +import io.mockk.junit4.MockKRule +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import io.opentelemetry.api.common.AttributeKey +import io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions +import io.opentelemetry.sdk.testing.junit4.OpenTelemetryRule +import io.opentelemetry.sdk.trace.data.SpanData +import kotlinx.coroutines.Runnable +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.ThrowingConsumer +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config +import java.time.Duration +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.TimeUnit + +@RunWith(AndroidJUnit4::class) +@Config(sdk = [Build.VERSION_CODES.N]) +class SlowRenderListenerTest { + @get:Rule + var otelTesting: OpenTelemetryRule = OpenTelemetryRule.create() + + @get:Rule + var mocks = MockKRule(this) + + @MockK + lateinit var frameMetricsHandler: Handler + + @RelaxedMockK + lateinit var activity: Activity + + @RelaxedMockK + lateinit var frameMetrics: FrameMetrics + + @RelaxedMockK + internal lateinit var jankReporter: JankReporter + private lateinit var executorService: ScheduledExecutorService + + internal lateinit var activityListenerCaptor: CapturingSlot + + @Before + fun setup() { + activityListenerCaptor = slot() + executorService = Executors.newSingleThreadScheduledExecutor() + val componentName = ComponentName("io.otel", "Komponent") + every { activity.componentName } returns componentName + } + + @Test + fun add() { + val testInstance = + SlowRenderListener( + jankReporter, + executorService, + frameMetricsHandler, + Duration.ZERO, + ) + + testInstance.onActivityResumed(activity) + + verify { + activity.window.addOnFrameMetricsAvailableListener( + capture(activityListenerCaptor), + eq(frameMetricsHandler), + ) + } + + assertThat(activityListenerCaptor.captured.getActivityName()) + .isEqualTo("io.otel/Komponent") + } + + @Test + fun removeBeforeAddOk() { + val testInstance = + SlowRenderListener( + jankReporter, + executorService, + frameMetricsHandler, + Duration.ZERO, + ) + + testInstance.onActivityPaused(activity) + + confirmVerified(activity) + assertThat(otelTesting.spans).hasSize(0) + } + + @Test + fun addAndRemove() { + val testInstance = + SlowRenderListener( + jankReporter, + executorService, + frameMetricsHandler, + Duration.ZERO, + ) + + testInstance.onActivityResumed(activity) + testInstance.onActivityPaused(activity) + + verify { + activity.window.addOnFrameMetricsAvailableListener( + capture(activityListenerCaptor), + eq(frameMetricsHandler), + ) + } + verify { activity.window.removeOnFrameMetricsAvailableListener(eq(activityListenerCaptor.captured)) } + + assertThat(otelTesting.spans).hasSize(0) + } + + @Test + fun removeWithMetrics() { + val tracer = otelTesting.openTelemetry.getTracer("testTracer") + jankReporter = SpanBasedJankReporter(tracer) + val testInstance = + SlowRenderListener( + jankReporter, + executorService, + frameMetricsHandler, + Duration.ZERO, + ) + + testInstance.onActivityResumed(activity) + + verify { + activity.window.addOnFrameMetricsAvailableListener( + capture(activityListenerCaptor), + any(), + ) + } + val listener = activityListenerCaptor.captured + for (duration in makeSomeDurations()) { + every { frameMetrics.getMetric(FrameMetrics.DRAW_DURATION) } returns duration + listener.onFrameMetricsAvailable(null, frameMetrics, 0) + } + + testInstance.onActivityPaused(activity) + + val spans = otelTesting.spans + assertSpanContent(spans) + } + + @Test + fun start() { + val exec = mockk(relaxed = true) + every { + exec.scheduleWithFixedDelay( + any(), + eq(1001L), + eq(1001L), + eq(TimeUnit.MILLISECONDS), + ) + } answers { + val runnable = invocation.args[0] as Runnable + runnable.run() // just call it immediately + null + } + + val tracer = otelTesting.openTelemetry.getTracer("testTracer") + jankReporter = SpanBasedJankReporter(tracer) + val testInstance = + SlowRenderListener( + jankReporter, + exec, + frameMetricsHandler, + Duration.ofMillis(1001), + ) + + testInstance.onActivityResumed(activity) + + verify { + activity.window.addOnFrameMetricsAvailableListener( + capture(activityListenerCaptor), + any(), + ) + } + val listener = activityListenerCaptor.captured + for (duration in makeSomeDurations()) { + every { frameMetrics.getMetric(FrameMetrics.DRAW_DURATION) } returns duration + listener.onFrameMetricsAvailable(null, frameMetrics, 0) + } + + testInstance.start() + + val spans = otelTesting.spans + assertSpanContent(spans) + } + + @Test + fun activityListenerSkipsFirstFrame() { + val listener = PerActivityListener(activity) + every { frameMetrics.getMetric(FrameMetrics.FIRST_DRAW_FRAME) } returns 1L + listener.onFrameMetricsAvailable(null, frameMetrics, 99) + every { frameMetrics.getMetric(FrameMetrics.DRAW_DURATION) } + verify(exactly = 0) { frameMetrics.getMetric(FrameMetrics.DRAW_DURATION) } + } + + private fun makeSomeDurations(): List = + listOf( + 5L, + 11L, + 101L, // slow + 701L, // frozen + 17L, // slow + 17L, // slow + 16L, + 11L, + ).map { duration -> + TimeUnit.MILLISECONDS.toNanos( + duration, + ) + } + + companion object { + private val COUNT_KEY: AttributeKey = AttributeKey.longKey("count") + + private fun assertSpanContent(spans: MutableList) { + assertThat(spans) + .hasSize(2) + .satisfiesExactly( + ThrowingConsumer { span -> + OpenTelemetryAssertions + .assertThat(span) + .hasName("slowRenders") + .endsAt(span!!.startEpochNanos) + .hasAttribute(COUNT_KEY, 3L) + .hasAttribute( + AttributeKey.stringKey("activity.name"), + "io.otel/Komponent", + ) + }, + ThrowingConsumer { span -> + OpenTelemetryAssertions + .assertThat(span) + .hasName("frozenRenders") + .endsAt(span!!.startEpochNanos) + .hasAttribute(COUNT_KEY, 1L) + .hasAttribute( + AttributeKey.stringKey("activity.name"), + "io.otel/Komponent", + ) + }, + ) + } + } +}