diff --git a/instrumentation/rxjava/rxjava-3-common/library/src/test/groovy/RxJava3AsyncOperationEndStrategyTest.groovy b/instrumentation/rxjava/rxjava-3-common/library/src/test/groovy/RxJava3AsyncOperationEndStrategyTest.groovy deleted file mode 100644 index c3c174396b8e..000000000000 --- a/instrumentation/rxjava/rxjava-3-common/library/src/test/groovy/RxJava3AsyncOperationEndStrategyTest.groovy +++ /dev/null @@ -1,1045 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.api.trace.Span -import io.opentelemetry.context.Context -import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter -import io.opentelemetry.instrumentation.rxjava.v3.common.RxJava3AsyncOperationEndStrategy -import io.reactivex.rxjava3.core.Completable -import io.reactivex.rxjava3.core.Flowable -import io.reactivex.rxjava3.core.Maybe -import io.reactivex.rxjava3.core.Observable -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.observers.TestObserver -import io.reactivex.rxjava3.parallel.ParallelFlowable -import io.reactivex.rxjava3.processors.ReplayProcessor -import io.reactivex.rxjava3.processors.UnicastProcessor -import io.reactivex.rxjava3.subjects.CompletableSubject -import io.reactivex.rxjava3.subjects.MaybeSubject -import io.reactivex.rxjava3.subjects.ReplaySubject -import io.reactivex.rxjava3.subjects.SingleSubject -import io.reactivex.rxjava3.subjects.UnicastSubject -import io.reactivex.rxjava3.subscribers.TestSubscriber -import org.reactivestreams.Publisher -import org.reactivestreams.Subscriber -import org.reactivestreams.Subscription -import spock.lang.Specification - -class RxJava3AsyncOperationEndStrategyTest extends Specification { - String request = "request" - String response = "response" - - Instrumenter instrumenter - - Context context - - Span span - - def underTest = RxJava3AsyncOperationEndStrategy.create() - - def underTestWithExperimentalAttributes = RxJava3AsyncOperationEndStrategy.builder() - .setCaptureExperimentalSpanAttributes(true) - .build() - - void setup() { - instrumenter = Mock() - context = Mock() - span = Mock() - span.storeInContext(_) >> { callRealMethod() } - } - - static class CompletableTest extends RxJava3AsyncOperationEndStrategyTest { - def "is supported"() { - expect: - underTest.supports(Completable) - } - - def "ends span on already completed"() { - given: - def observer = new TestObserver() - - when: - def result = (Completable) underTest.end(instrumenter, context, request, Completable.complete(), String) - result.subscribe(observer) - - then: - 1 * instrumenter.end(context, request, null, null) - observer.assertComplete() - } - - def "ends span on already errored"() { - given: - def exception = new IllegalStateException() - def observer = new TestObserver() - - when: - def result = (Completable) underTest.end(instrumenter, context, request, Completable.error(exception), String) - result.subscribe(observer) - - then: - 1 * instrumenter.end(context, request, null, exception) - observer.assertError(exception) - } - - def "ends span when completed"() { - given: - def source = CompletableSubject.create() - def observer = new TestObserver() - - when: - def result = (Completable) underTest.end(instrumenter, context, request, source, String) - result.subscribe(observer) - - then: - 0 * instrumenter._ - - when: - source.onComplete() - - then: - 1 * instrumenter.end(context, request, null, null) - observer.assertComplete() - } - - def "ends span when errored"() { - given: - def exception = new IllegalStateException() - def source = CompletableSubject.create() - def observer = new TestObserver() - - when: - def result = (Completable) underTest.end(instrumenter, context, request, source, String) - result.subscribe(observer) - - then: - 0 * instrumenter._ - - when: - source.onError(exception) - - then: - 1 * instrumenter.end(context, request, null, exception) - observer.assertError(exception) - } - - def "ends span when cancelled"() { - given: - def source = CompletableSubject.create() - def observer = new TestObserver() - def context = span.storeInContext(Context.root()) - - when: - def result = (Completable) underTest.end(instrumenter, context, request, source, String) - result.subscribe(observer) - - then: - 0 * instrumenter._ - 0 * span._ - - when: - observer.dispose() - - then: - 1 * instrumenter.end(context, request, null, null) - 0 * span.setAttribute(_) - } - - def "ends span when cancelled and capturing experimental span attributes"() { - given: - def source = CompletableSubject.create() - def observer = new TestObserver() - def context = span.storeInContext(Context.root()) - - when: - def result = (Completable) underTestWithExperimentalAttributes.end(instrumenter, context, request, source, String) - result.subscribe(observer) - - then: - 0 * instrumenter._ - 0 * span._ - - when: - observer.dispose() - - then: - 1 * instrumenter.end(context, request, null, null) - 1 * span.setAttribute({ it.getKey() == "rxjava.canceled" }, true) - } - - def "ends span once for multiple subscribers"() { - given: - def source = CompletableSubject.create() - def observer1 = new TestObserver() - def observer2 = new TestObserver() - def observer3 = new TestObserver() - - when: - def result = (Completable) underTest.end(instrumenter, context, request, source, String) - result.subscribe(observer1) - result.subscribe(observer2) - result.subscribe(observer3) - - then: - 0 * instrumenter._ - - when: - source.onComplete() - - then: - 1 * instrumenter.end(context, request, null, null) - observer1.assertComplete() - observer2.assertComplete() - observer3.assertComplete() - } - } - - static class MaybeTest extends RxJava3AsyncOperationEndStrategyTest { - def "is supported"() { - expect: - underTest.supports(Maybe) - } - - def "ends span on already completed"() { - given: - def observer = new TestObserver() - - when: - def result = (Maybe) underTest.end(instrumenter, context, request, Maybe.just(response), String) - result.subscribe(observer) - - then: - 1 * instrumenter.end(context, request, response, null) - observer.assertComplete() - } - - def "ends span on already empty"() { - given: - def observer = new TestObserver() - - when: - def result = (Maybe) underTest.end(instrumenter, context, request, Maybe.empty(), String) - result.subscribe(observer) - - then: - 1 * instrumenter.end(context, request, null, null) - observer.assertComplete() - } - - def "ends span on already errored"() { - given: - def exception = new IllegalStateException() - def observer = new TestObserver() - - when: - def result = (Maybe) underTest.end(instrumenter, context, request, Maybe.error(exception), String) - result.subscribe(observer) - - then: - 1 * instrumenter.end(context, request, null, exception) - observer.assertError(exception) - } - - def "ends span when completed"() { - given: - def source = MaybeSubject.create() - def observer = new TestObserver() - - when: - def result = (Maybe) underTest.end(instrumenter, context, request, source, String) - result.subscribe(observer) - - then: - 0 * instrumenter._ - - when: - source.onSuccess(response) - - then: - 1 * instrumenter.end(context, request, response, null) - observer.assertComplete() - } - - def "ends span when empty"() { - given: - def source = MaybeSubject.create() - def observer = new TestObserver() - - when: - def result = (Maybe) underTest.end(instrumenter, context, request, source, String) - result.subscribe(observer) - - then: - 0 * instrumenter._ - - when: - source.onComplete() - - then: - 1 * instrumenter.end(context, request, null, null) - observer.assertComplete() - } - - def "ends span when errored"() { - given: - def exception = new IllegalStateException() - def source = MaybeSubject.create() - def observer = new TestObserver() - - when: - def result = (Maybe) underTest.end(instrumenter, context, request, source, String) - result.subscribe(observer) - - then: - 0 * instrumenter._ - - when: - source.onError(exception) - - then: - 1 * instrumenter.end(context, request, null, exception) - observer.assertError(exception) - } - - def "ends span when cancelled"() { - given: - def source = MaybeSubject.create() - def observer = new TestObserver() - def context = span.storeInContext(Context.root()) - - when: - def result = (Maybe) underTest.end(instrumenter, context, request, source, String) - result.subscribe(observer) - - then: - 0 * instrumenter._ - - when: - observer.dispose() - - then: - 1 * instrumenter.end(context, request, null, null) - 0 * span.setAttribute(_) - } - - def "ends span when cancelled and capturing experimental span attributes"() { - given: - def source = MaybeSubject.create() - def observer = new TestObserver() - def context = span.storeInContext(Context.root()) - - when: - def result = (Maybe) underTestWithExperimentalAttributes.end(instrumenter, context, request, source, String) - result.subscribe(observer) - - then: - 0 * instrumenter._ - 0 * span._ - - when: - observer.dispose() - - then: - 1 * instrumenter.end(context, request, null, null) - 1 * span.setAttribute({ it.getKey() == "rxjava.canceled" }, true) - } - - def "ends span once for multiple subscribers"() { - given: - def source = MaybeSubject.create() - def observer1 = new TestObserver() - def observer2 = new TestObserver() - def observer3 = new TestObserver() - - when: - def result = (Maybe) underTest.end(instrumenter, context, request, source, String) - result.subscribe(observer1) - result.subscribe(observer2) - result.subscribe(observer3) - - then: - 0 * instrumenter._ - - when: - source.onSuccess(response) - - then: - 1 * instrumenter.end(context, request, response, null) - observer1.assertValue(response) - observer1.assertComplete() - observer2.assertValue(response) - observer2.assertComplete() - observer3.assertValue(response) - observer3.assertComplete() - } - } - - static class SingleTest extends RxJava3AsyncOperationEndStrategyTest { - def "is supported"() { - expect: - underTest.supports(Single) - } - - def "ends span on already completed"() { - given: - def observer = new TestObserver() - - when: - def result = (Single) underTest.end(instrumenter, context, request, Single.just(response), String) - result.subscribe(observer) - - then: - 1 * instrumenter.end(context, request, response, null) - observer.assertComplete() - } - - def "ends span on already errored"() { - given: - def exception = new IllegalStateException() - def observer = new TestObserver() - - when: - def result = (Single) underTest.end(instrumenter, context, request, Single.error(exception), String) - result.subscribe(observer) - - then: - 1 * instrumenter.end(context, request, null, exception) - observer.assertError(exception) - } - - def "ends span when completed"() { - given: - def source = SingleSubject.create() - def observer = new TestObserver() - - when: - def result = (Single) underTest.end(instrumenter, context, request, source, String) - result.subscribe(observer) - - then: - 0 * instrumenter._ - - when: - source.onSuccess(response) - - then: - 1 * instrumenter.end(context, request, response, null) - observer.assertComplete() - } - - def "ends span when errored"() { - given: - def exception = new IllegalStateException() - def source = SingleSubject.create() - def observer = new TestObserver() - - when: - def result = (Single) underTest.end(instrumenter, context, request, source, String) - result.subscribe(observer) - - then: - 0 * instrumenter._ - - when: - source.onError(exception) - - then: - 1 * instrumenter.end(context, request, null, exception) - observer.assertError(exception) - } - - def "ends span when cancelled"() { - given: - def source = SingleSubject.create() - def observer = new TestObserver() - def context = span.storeInContext(Context.root()) - - when: - def result = (Single) underTest.end(instrumenter, context, request, source, String) - result.subscribe(observer) - - then: - 0 * instrumenter._ - - when: - observer.dispose() - - then: - 1 * instrumenter.end(context, request, null, null) - 0 * span.setAttribute(_) - } - - def "ends span when cancelled and capturing experimental span attributes"() { - given: - def source = SingleSubject.create() - def observer = new TestObserver() - def context = span.storeInContext(Context.root()) - - when: - def result = (Single) underTestWithExperimentalAttributes.end(instrumenter, context, request, source, String) - result.subscribe(observer) - - then: - 0 * instrumenter._ - 0 * span._ - - when: - observer.dispose() - - then: - 1 * instrumenter.end(context, request, null, null) - 1 * span.setAttribute({ it.getKey() == "rxjava.canceled" }, true) - } - - def "ends span once for multiple subscribers"() { - given: - def source = SingleSubject.create() - def observer1 = new TestObserver() - def observer2 = new TestObserver() - def observer3 = new TestObserver() - - when: - def result = (Single) underTest.end(instrumenter, context, request, source, String) - result.subscribe(observer1) - result.subscribe(observer2) - result.subscribe(observer3) - - then: - 0 * instrumenter._ - - when: - source.onSuccess(response) - - then: - 1 * instrumenter.end(context, request, response, null) - observer1.assertValue(response) - observer1.assertComplete() - observer2.assertValue(response) - observer2.assertComplete() - observer3.assertValue(response) - observer3.assertComplete() - } - } - - static class ObservableTest extends RxJava3AsyncOperationEndStrategyTest { - def "is supported"() { - expect: - underTest.supports(Observable) - } - - def "ends span on already completed"() { - given: - def observer = new TestObserver() - - when: - def result = (Observable) underTest.end(instrumenter, context, request, Observable.just(response), String) - result.subscribe(observer) - - then: - 1 * instrumenter.end(context, request, null, null) - observer.assertComplete() - } - - def "ends span on already errored"() { - given: - def exception = new IllegalStateException() - def observer = new TestObserver() - - when: - def result = (Observable) underTest.end(instrumenter, context, request, Observable.error(exception), String) - result.subscribe(observer) - - then: - 1 * instrumenter.end(context, request, null, exception) - observer.assertError(exception) - } - - def "ends span when completed"() { - given: - def source = UnicastSubject.create() - def observer = new TestObserver() - - when: - def result = (Observable) underTest.end(instrumenter, context, request, source, String) - result.subscribe(observer) - - then: - 0 * instrumenter._ - - when: - source.onComplete() - - then: - 1 * instrumenter.end(context, request, null, null) - observer.assertComplete() - } - - def "ends span when errored"() { - given: - def exception = new IllegalStateException() - def source = UnicastSubject.create() - def observer = new TestObserver() - - when: - def result = (Observable) underTest.end(instrumenter, context, request, source, String) - result.subscribe(observer) - - then: - 0 * instrumenter._ - - when: - source.onError(exception) - - then: - 1 * instrumenter.end(context, request, null, exception) - observer.assertError(exception) - } - - def "ends span when cancelled"() { - given: - def source = UnicastSubject.create() - def observer = new TestObserver() - def context = span.storeInContext(Context.root()) - - when: - def result = (Observable) underTest.end(instrumenter, context, request, source, String) - result.subscribe(observer) - - then: - 0 * instrumenter._ - - when: - observer.dispose() - - then: - 1 * instrumenter.end(context, request, null, null) - 0 * span.setAttribute(_) - } - - def "ends span when cancelled and capturing experimental span attributes"() { - given: - def source = UnicastSubject.create() - def observer = new TestObserver() - def context = span.storeInContext(Context.root()) - - when: - def result = (Observable) underTestWithExperimentalAttributes.end(instrumenter, context, request, source, String) - result.subscribe(observer) - - then: - 0 * instrumenter._ - 0 * span._ - - when: - observer.dispose() - - then: - 1 * instrumenter.end(context, request, null, null) - 1 * span.setAttribute({ it.getKey() == "rxjava.canceled" }, true) - } - - def "ends span once for multiple subscribers"() { - given: - def source = ReplaySubject.create() - def observer1 = new TestObserver() - def observer2 = new TestObserver() - def observer3 = new TestObserver() - - when: - def result = (Observable) underTest.end(instrumenter, context, request, source, String) - result.subscribe(observer1) - result.subscribe(observer2) - result.subscribe(observer3) - - then: - 0 * instrumenter._ - - when: - source.onComplete() - - then: - 1 * instrumenter.end(context, request, null, null) - observer1.assertComplete() - observer2.assertComplete() - observer3.assertComplete() - } - } - - static class FlowableTest extends RxJava3AsyncOperationEndStrategyTest { - def "is supported"() { - expect: - underTest.supports(Flowable) - } - - def "ends span on already completed"() { - given: - def observer = new TestSubscriber() - - when: - def result = (Flowable) underTest.end(instrumenter, context, request, Flowable.just(response), String) - result.subscribe(observer) - - then: - 1 * instrumenter.end(context, request, null, null) - observer.assertComplete() - } - - def "ends span on already errored"() { - given: - def exception = new IllegalStateException() - def observer = new TestSubscriber() - - when: - def result = (Flowable) underTest.end(instrumenter, context, request, Flowable.error(exception), String) - result.subscribe(observer) - - then: - 1 * instrumenter.end(context, request, null, exception) - observer.assertError(exception) - } - - def "ends span when completed"() { - given: - def source = UnicastProcessor.create() - def observer = new TestSubscriber() - - when: - def result = (Flowable) underTest.end(instrumenter, context, request, source, String) - result.subscribe(observer) - - then: - 0 * instrumenter._ - - when: - source.onComplete() - - then: - 1 * instrumenter.end(context, request, null, null) - observer.assertComplete() - } - - def "ends span when errored"() { - given: - def exception = new IllegalStateException() - def source = UnicastProcessor.create() - def observer = new TestSubscriber() - - when: - def result = (Flowable) underTest.end(instrumenter, context, request, source, String) - result.subscribe(observer) - - then: - 0 * instrumenter._ - - when: - source.onError(exception) - - then: - 1 * instrumenter.end(context, request, null, exception) - observer.assertError(exception) - } - - def "ends span when cancelled"() { - given: - def source = UnicastProcessor.create() - def observer = new TestSubscriber() - def context = span.storeInContext(Context.root()) - - when: - def result = (Flowable) underTest.end(instrumenter, context, request, source, String) - result.subscribe(observer) - - then: - 0 * instrumenter._ - - when: - observer.cancel() - - then: - 1 * instrumenter.end(context, request, null, null) - 0 * span.setAttribute(_) - } - - def "ends span when cancelled and capturing experimental span attributes"() { - given: - def source = UnicastProcessor.create() - def observer = new TestSubscriber() - def context = span.storeInContext(Context.root()) - - when: - def result = (Flowable) underTestWithExperimentalAttributes.end(instrumenter, context, request, source, String) - result.subscribe(observer) - - then: - 0 * instrumenter._ - 0 * span._ - - when: - observer.cancel() - - then: - 1 * instrumenter.end(context, request, null, null) - 1 * span.setAttribute({ it.getKey() == "rxjava.canceled" }, true) - } - - def "ends span once for multiple subscribers"() { - given: - def source = ReplayProcessor.create() - def observer1 = new TestSubscriber() - def observer2 = new TestSubscriber() - def observer3 = new TestSubscriber() - - when: - def result = (Flowable) underTest.end(instrumenter, context, request, source, String) - result.subscribe(observer1) - result.subscribe(observer2) - result.subscribe(observer3) - - then: - 0 * instrumenter._ - - when: - source.onComplete() - - then: - 1 * instrumenter.end(context, request, null, null) - observer1.assertComplete() - observer2.assertComplete() - observer3.assertComplete() - } - } - - static class ParallelFlowableTest extends RxJava3AsyncOperationEndStrategyTest { - def "is supported"() { - expect: - underTest.supports(ParallelFlowable) - } - - def "ends span on already completed"() { - given: - def observer = new TestSubscriber() - - when: - def result = (ParallelFlowable) underTest.end(instrumenter, context, request, Flowable.just(response).parallel(), String) - result.sequential().subscribe(observer) - - then: - observer.assertComplete() - 1 * instrumenter.end(context, request, null, null) - } - - def "ends span on already errored"() { - given: - def exception = new IllegalStateException() - def observer = new TestSubscriber() - - when: - def result = (ParallelFlowable) underTest.end(instrumenter, context, request, Flowable.error(exception).parallel(), String) - result.sequential().subscribe(observer) - - then: - observer.assertError(exception) - 1 * instrumenter.end(context, request, null, exception) - } - - def "ends span when completed"() { - given: - def source = UnicastProcessor.create() - def observer = new TestSubscriber() - - when: - def result = (ParallelFlowable) underTest.end(instrumenter, context, request, source.parallel(), String) - result.sequential().subscribe(observer) - - then: - 0 * instrumenter._ - - when: - source.onComplete() - - then: - observer.assertComplete() - 1 * instrumenter.end(context, request, null, null) - } - - def "ends span when errored"() { - given: - def exception = new IllegalStateException() - def source = UnicastProcessor.create() - def observer = new TestSubscriber() - - when: - def result = (ParallelFlowable) underTest.end(instrumenter, context, request, source.parallel(), String) - result.sequential().subscribe(observer) - - then: - 0 * instrumenter._ - - when: - source.onError(exception) - - then: - observer.assertError(exception) - 1 * instrumenter.end(context, request, null, exception) - } - - def "ends span when cancelled"() { - given: - def source = UnicastProcessor.create() - def observer = new TestSubscriber() - def context = span.storeInContext(Context.root()) - - when: - def result = (ParallelFlowable) underTest.end(instrumenter, context, request, source.parallel(), String) - result.sequential().subscribe(observer) - - then: - 0 * instrumenter._ - - when: - observer.cancel() - - then: - 1 * instrumenter.end(context, request, null, null) - 0 * span.setAttribute(_) - } - - def "ends span when cancelled and capturing experimental span attributes"() { - given: - def source = UnicastProcessor.create() - def observer = new TestSubscriber() - def context = span.storeInContext(Context.root()) - - when: - def result = (ParallelFlowable) underTestWithExperimentalAttributes.end(instrumenter, context, request, source.parallel(), String) - result.sequential().subscribe(observer) - - then: - 0 * instrumenter._ - 0 * span._ - - when: - observer.cancel() - - then: - 1 * instrumenter.end(context, request, null, null) - 1 * span.setAttribute({ it.getKey() == "rxjava.canceled" }, true) - } - } - - static class PublisherTest extends RxJava3AsyncOperationEndStrategyTest { - def "is supported"() { - expect: - underTest.supports(Publisher) - } - - def "ends span when completed"() { - given: - def source = new CustomPublisher() - def observer = new TestSubscriber() - - when: - def result = (Flowable) underTest.end(instrumenter, context, request, source, String) - result.subscribe(observer) - - then: - 0 * instrumenter._ - - when: - source.onComplete() - - then: - 1 * instrumenter.end(context, request, null, null) - observer.assertComplete() - } - - def "ends span when errored"() { - given: - def exception = new IllegalStateException() - def source = new CustomPublisher() - def observer = new TestSubscriber() - - when: - def result = (Flowable) underTest.end(instrumenter, context, request, source, String) - result.subscribe(observer) - - then: - 0 * instrumenter._ - - when: - source.onError(exception) - - then: - 1 * instrumenter.end(context, request, null, exception) - observer.assertError(exception) - } - - def "ends span when cancelled"() { - given: - def source = new CustomPublisher() - def observer = new TestSubscriber() - def context = span.storeInContext(Context.root()) - - when: - def result = (Flowable) underTest.end(instrumenter, context, request, source, String) - result.subscribe(observer) - - then: - 0 * instrumenter._ - - when: - observer.cancel() - - then: - 1 * instrumenter.end(context, request, null, null) - 0 * span.setAttribute(_) - } - - def "ends span when cancelled and capturing experimental span attributes"() { - given: - def source = new CustomPublisher() - def observer = new TestSubscriber() - def context = span.storeInContext(Context.root()) - - when: - def result = (Flowable) underTestWithExperimentalAttributes.end(instrumenter, context, request, source, String) - result.subscribe(observer) - - then: - 0 * instrumenter._ - 0 * span._ - - when: - observer.cancel() - - then: - 1 * instrumenter.end(context, request, null, null) - 1 * span.setAttribute({ it.getKey() == "rxjava.canceled" }, true) - } - } - - static class CustomPublisher implements Publisher, Subscription { - Subscriber subscriber - - @Override - void subscribe(Subscriber subscriber) { - this.subscriber = subscriber - subscriber.onSubscribe(this) - } - - def onComplete() { - this.subscriber.onComplete() - } - - def onError(Throwable exception) { - this.subscriber.onError(exception) - } - - @Override - void request(long l) {} - - @Override - void cancel() {} - } -} diff --git a/instrumentation/rxjava/rxjava-3-common/library/src/test/java/RxJava3AsyncOperationEndStrategyTest.java b/instrumentation/rxjava/rxjava-3-common/library/src/test/java/RxJava3AsyncOperationEndStrategyTest.java new file mode 100644 index 000000000000..f3fbd8144e23 --- /dev/null +++ b/instrumentation/rxjava/rxjava-3-common/library/src/test/java/RxJava3AsyncOperationEndStrategyTest.java @@ -0,0 +1,951 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +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 io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.annotation.support.async.AsyncOperationEndStrategy; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.rxjava.v3.common.RxJava3AsyncOperationEndStrategy; +import io.reactivex.rxjava3.core.Completable; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Maybe; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.parallel.ParallelFlowable; +import io.reactivex.rxjava3.processors.ReplayProcessor; +import io.reactivex.rxjava3.processors.UnicastProcessor; +import io.reactivex.rxjava3.subjects.CompletableSubject; +import io.reactivex.rxjava3.subjects.MaybeSubject; +import io.reactivex.rxjava3.subjects.ReplaySubject; +import io.reactivex.rxjava3.subjects.SingleSubject; +import io.reactivex.rxjava3.subjects.UnicastSubject; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +@ExtendWith(MockitoExtension.class) +public class RxJava3AsyncOperationEndStrategyTest { + private static final AttributeKey CANCELED_ATTRIBUTE_KEY = + AttributeKey.booleanKey("rxjava.canceled"); + @Mock Instrumenter instrumenter; + @Mock Span span; + private final AsyncOperationEndStrategy underTest = RxJava3AsyncOperationEndStrategy.create(); + private final RxJava3AsyncOperationEndStrategy underTestWithExperimentalAttributes = + RxJava3AsyncOperationEndStrategy.builder().setCaptureExperimentalSpanAttributes(true).build(); + + @Nested + class CompletableTest { + @Test + void supported() { + assertThat(underTest.supports(Completable.class)).isTrue(); + } + + @Test + void endsSpanOnAlreadyCompleted() { + Completable result = + (Completable) + underTest.end( + instrumenter, Context.root(), "request", Completable.complete(), String.class); + TestObserver observer = result.test(); + + observer.assertComplete(); + verify(instrumenter).end(Context.root(), "request", null, null); + } + + @Test + void endsSpanOnAlreadyErrored() { + IllegalStateException exception = new IllegalStateException(); + + Completable result = + (Completable) + underTest.end( + instrumenter, + Context.root(), + "request", + Completable.error(exception), + String.class); + TestObserver observer = result.test(); + + observer.assertError(exception); + verify(instrumenter).end(Context.root(), "request", null, exception); + } + + @Test + void endsSpanWhenCompleted() { + CompletableSubject source = CompletableSubject.create(); + + Completable result = + (Completable) + underTest.end(instrumenter, Context.root(), "request", source, String.class); + TestObserver observer = result.test(); + + verifyNoInteractions(instrumenter); + + source.onComplete(); + observer.assertComplete(); + verify(instrumenter).end(Context.root(), "request", null, null); + } + + @Test + void endsSpanWhenErrored() { + IllegalStateException exception = new IllegalStateException(); + CompletableSubject source = CompletableSubject.create(); + + Completable result = + (Completable) + underTest.end(instrumenter, Context.root(), "request", source, String.class); + TestObserver observer = result.test(); + + verifyNoInteractions(instrumenter); + + source.onError(exception); + observer.assertError(exception); + verify(instrumenter).end(Context.root(), "request", null, exception); + } + + @Test + void endsWhenCancelled() { + when(span.storeInContext(any())).thenCallRealMethod(); + CompletableSubject source = CompletableSubject.create(); + Context context = Context.root().with(span); + + Completable result = + (Completable) underTest.end(instrumenter, context, "request", source, String.class); + TestObserver observer = result.test(); + + verifyNoInteractions(instrumenter); + + observer.dispose(); + verify(instrumenter).end(context, "request", null, null); + verify(span, never()).setAttribute(CANCELED_ATTRIBUTE_KEY, true); + } + + @Test + void endsSpanWhenCancelledExperimentalAttribute() { + when(span.storeInContext(any())).thenCallRealMethod(); + CompletableSubject source = CompletableSubject.create(); + Context context = Context.root().with(span); + + Completable result = + (Completable) + underTestWithExperimentalAttributes.end( + instrumenter, context, "request", source, String.class); + TestObserver observer = result.test(); + + verifyNoInteractions(instrumenter); + verify(span, never()).setAttribute(CANCELED_ATTRIBUTE_KEY, true); + + observer.dispose(); + + verify(instrumenter).end(context, "request", null, null); + verify(span).setAttribute(CANCELED_ATTRIBUTE_KEY, true); + } + + @Test + void endsSpanOnceForMultipleSubscribers() { + CompletableSubject source = CompletableSubject.create(); + TestObserver observer1 = new TestObserver<>(); + TestObserver observer2 = new TestObserver<>(); + TestObserver observer3 = new TestObserver<>(); + + Completable result = + (Completable) + underTest.end(instrumenter, Context.root(), "request", source, String.class); + result.subscribe(observer1); + result.subscribe(observer2); + result.subscribe(observer3); + + verifyNoInteractions(instrumenter); + + source.onComplete(); + observer1.assertComplete(); + observer2.assertComplete(); + observer3.assertComplete(); + verify(instrumenter).end(Context.root(), "request", null, null); + } + } + + @Nested + class MaybeTest { + @Test + void supported() { + assertThat(underTest.supports(Maybe.class)).isTrue(); + } + + @Test + void endsSpanOnAlreadyCompleted() { + Maybe result = + (Maybe) + underTest.end( + instrumenter, Context.root(), "request", Maybe.just("response"), String.class); + TestObserver observer = result.test(); + + observer.assertComplete(); + verify(instrumenter).end(Context.root(), "request", "response", null); + } + + @Test + void endsSpanOnAlreadyEmpty() { + Maybe result = + (Maybe) + underTest.end(instrumenter, Context.root(), "request", Maybe.empty(), String.class); + TestObserver observer = result.test(); + + observer.assertComplete(); + verify(instrumenter).end(Context.root(), "request", null, null); + } + + @Test + void endsSpanOnAlreadyErrored() { + IllegalStateException exception = new IllegalStateException(); + + Maybe result = + (Maybe) + underTest.end( + instrumenter, Context.root(), "request", Maybe.error(exception), String.class); + TestObserver observer = result.test(); + + observer.assertError(exception); + verify(instrumenter).end(Context.root(), "request", null, exception); + } + + @Test + void endsSpanWhenCompleted() { + MaybeSubject source = MaybeSubject.create(); + + Maybe result = + (Maybe) underTest.end(instrumenter, Context.root(), "request", source, String.class); + TestObserver observer = result.test(); + + verifyNoInteractions(instrumenter); + + source.onSuccess("response"); + observer.assertComplete(); + verify(instrumenter).end(Context.root(), "request", "response", null); + } + + @Test + void endsSpanWhenEmpty() { + MaybeSubject source = MaybeSubject.create(); + + Maybe result = + (Maybe) underTest.end(instrumenter, Context.root(), "request", source, String.class); + TestObserver observer = result.test(); + + verifyNoInteractions(instrumenter); + + source.onComplete(); + observer.assertComplete(); + verify(instrumenter).end(Context.root(), "request", null, null); + } + + @Test + void endsSpanWhenErrored() { + IllegalStateException exception = new IllegalStateException(); + MaybeSubject source = MaybeSubject.create(); + + Maybe result = + (Maybe) underTest.end(instrumenter, Context.root(), "request", source, String.class); + TestObserver observer = result.test(); + + source.onError(exception); + observer.assertError(exception); + verify(instrumenter).end(Context.root(), "request", null, exception); + } + + @Test + void endsSpanWhenCancelled() { + when(span.storeInContext(any())).thenCallRealMethod(); + MaybeSubject source = MaybeSubject.create(); + Context context = Context.root().with(span); + + Maybe result = + (Maybe) underTest.end(instrumenter, context, "request", source, String.class); + TestObserver observer = result.test(); + + verifyNoInteractions(instrumenter); + + observer.dispose(); + + verify(instrumenter).end(context, "request", null, null); + verify(span, never()).setAttribute(CANCELED_ATTRIBUTE_KEY, true); + } + + @Test + void endsSpanWhenCancelledExperimentalAttributes() { + when(span.storeInContext(any())).thenCallRealMethod(); + MaybeSubject source = MaybeSubject.create(); + Context context = Context.root().with(span); + + Maybe result = + (Maybe) + underTestWithExperimentalAttributes.end( + instrumenter, context, "request", source, String.class); + TestObserver observer = result.test(); + + verifyNoInteractions(instrumenter); + verify(span, never()).setAttribute(CANCELED_ATTRIBUTE_KEY, true); + + observer.dispose(); + + verify(instrumenter).end(context, "request", null, null); + verify(span).setAttribute(CANCELED_ATTRIBUTE_KEY, true); + } + + @Test + void endsSpanOnceForMultipleSubscribers() { + MaybeSubject source = MaybeSubject.create(); + + Maybe result = + (Maybe) underTest.end(instrumenter, Context.root(), "request", source, String.class); + TestObserver observer1 = result.test(); + TestObserver observer2 = result.test(); + TestObserver observer3 = result.test(); + + verifyNoInteractions(instrumenter); + + source.onSuccess("response"); + + observer1.assertComplete(); + observer1.assertValue(value -> value.equals("response")); + observer2.assertComplete(); + observer2.assertValue(value -> value.equals("response")); + observer3.assertComplete(); + observer3.assertValue(value -> value.equals("response")); + verify(instrumenter).end(Context.root(), "request", "response", null); + } + } + + @Nested + class SingleTest { + @Test + void supported() { + assertThat(underTest.supports(Single.class)).isTrue(); + } + + @Test + void endsSpanOnAlreadyCompleted() { + Single result = + (Single) + underTest.end( + instrumenter, Context.root(), "request", Single.just("response"), String.class); + TestObserver observer = result.test(); + + observer.assertComplete(); + verify(instrumenter).end(Context.root(), "request", "response", null); + } + + @Test + void endsSpanOnAlreadyErrored() { + IllegalStateException exception = new IllegalStateException(); + + Single result = + (Single) + underTest.end( + instrumenter, Context.root(), "request", Single.error(exception), String.class); + TestObserver observer = result.test(); + + observer.assertError(exception); + verify(instrumenter).end(Context.root(), "request", null, exception); + } + + @Test + void endsSpanWhenCompleted() { + SingleSubject source = SingleSubject.create(); + + Single result = + (Single) underTest.end(instrumenter, Context.root(), "request", source, String.class); + TestObserver observer = result.test(); + + verifyNoInteractions(instrumenter); + + source.onSuccess("response"); + + observer.assertComplete(); + verify(instrumenter).end(Context.root(), "request", "response", null); + } + + @Test + void endsSpanWhenErrored() { + IllegalStateException exception = new IllegalStateException(); + SingleSubject source = SingleSubject.create(); + + Single result = + (Single) underTest.end(instrumenter, Context.root(), "request", source, String.class); + TestObserver observer = result.test(); + + verifyNoInteractions(instrumenter); + + source.onError(exception); + + observer.assertError(exception); + verify(instrumenter).end(Context.root(), "request", null, exception); + } + + @Test + void endsSpanWhenCancelled() { + when(span.storeInContext(any())).thenCallRealMethod(); + SingleSubject source = SingleSubject.create(); + Context context = Context.root().with(span); + + Single result = + (Single) underTest.end(instrumenter, context, "request", source, String.class); + TestObserver observer = result.test(); + + verifyNoInteractions(instrumenter); + + observer.dispose(); + verify(span, never()).setAttribute(CANCELED_ATTRIBUTE_KEY, true); + verify(instrumenter).end(context, "request", null, null); + } + + @Test + void endsSpanWhenCancelledExperimentalAttributes() { + when(span.storeInContext(any())).thenCallRealMethod(); + SingleSubject source = SingleSubject.create(); + Context context = Context.root().with(span); + + Single result = + (Single) + underTestWithExperimentalAttributes.end( + instrumenter, context, "request", source, String.class); + TestObserver observer = result.test(); + + verifyNoInteractions(instrumenter); + verify(span, never()).setAttribute(CANCELED_ATTRIBUTE_KEY, true); + + observer.dispose(); + + verify(instrumenter).end(context, "request", null, null); + verify(span).setAttribute(CANCELED_ATTRIBUTE_KEY, true); + } + + @Test + void endsSpanOnceForMultipleSubscribers() { + SingleSubject source = SingleSubject.create(); + + Single result = + (Single) underTest.end(instrumenter, Context.root(), "request", source, String.class); + TestObserver observer1 = result.test(); + TestObserver observer2 = result.test(); + TestObserver observer3 = result.test(); + + verifyNoInteractions(instrumenter); + + source.onSuccess("response"); + + observer1.assertValue(value -> value.equals("response")); + observer1.assertComplete(); + observer2.assertValue(value -> value.equals("response")); + observer2.assertComplete(); + observer3.assertValue(value -> value.equals("response")); + observer3.assertComplete(); + verify(instrumenter).end(Context.root(), "request", "response", null); + } + } + + @Nested + class ObservableTest { + @Test + void supported() { + assertThat(underTest.supports(Observable.class)).isTrue(); + } + + @Test + void endsSpanOnAlreadyCompleted() { + Observable result = + (Observable) + underTest.end( + instrumenter, + Context.root(), + "request", + Observable.just("response"), + String.class); + TestObserver observer = result.test(); + + verify(instrumenter).end(Context.root(), "request", null, null); + observer.assertComplete(); + } + + @Test + void endsSpanOnAlreadyErrored() { + IllegalStateException exception = new IllegalStateException(); + + Observable result = + (Observable) + underTest.end( + instrumenter, + Context.root(), + "request", + Observable.error(exception), + String.class); + TestObserver observer = result.test(); + + verify(instrumenter).end(Context.root(), "request", null, exception); + observer.assertError(exception); + } + + @Test + void endsSpanWhenCompleted() { + UnicastSubject source = UnicastSubject.create(); + + Observable result = + (Observable) + underTest.end(instrumenter, Context.root(), "request", source, String.class); + TestObserver observer = result.test(); + + verifyNoInteractions(instrumenter); + + source.onComplete(); + + verify(instrumenter).end(Context.root(), "request", null, null); + observer.assertComplete(); + } + + @Test + void endsSpanWhenErrored() { + IllegalStateException exception = new IllegalStateException(); + UnicastSubject source = UnicastSubject.create(); + + Observable result = + (Observable) + underTest.end(instrumenter, Context.root(), "request", source, String.class); + TestObserver observer = result.test(); + + verifyNoInteractions(instrumenter); + + source.onError(exception); + + verify(instrumenter).end(Context.root(), "request", null, exception); + observer.assertError(exception); + } + + @Test + void endsOnWhenCancelled() { + when(span.storeInContext(any())).thenCallRealMethod(); + UnicastSubject source = UnicastSubject.create(); + Context context = Context.root().with(span); + + Observable result = + (Observable) underTest.end(instrumenter, context, "request", source, String.class); + TestObserver observer = result.test(); + + verifyNoInteractions(instrumenter); + + observer.dispose(); + + verify(instrumenter).end(context, "request", null, null); + verify(span, never()).setAttribute(CANCELED_ATTRIBUTE_KEY, true); + } + + @Test + void endsSpanWhenCancelledExperimentalAttributes() { + when(span.storeInContext(any())).thenCallRealMethod(); + UnicastSubject source = UnicastSubject.create(); + Context context = Context.root().with(span); + + Observable result = + (Observable) + underTestWithExperimentalAttributes.end( + instrumenter, context, "request", source, String.class); + TestObserver observer = result.test(); + + verifyNoInteractions(instrumenter); + verify(span, never()).setAttribute(CANCELED_ATTRIBUTE_KEY, true); + + observer.dispose(); + + verify(instrumenter).end(context, "request", null, null); + verify(span).setAttribute(CANCELED_ATTRIBUTE_KEY, true); + } + + @Test + void endsSpanOnceForMultipleSubscribers() { + ReplaySubject source = ReplaySubject.create(); + + Observable result = + (Observable) + underTest.end(instrumenter, Context.root(), "request", source, String.class); + TestObserver observer1 = result.test(); + TestObserver observer2 = result.test(); + TestObserver observer3 = result.test(); + + verifyNoInteractions(instrumenter); + + source.onComplete(); + + observer1.assertComplete(); + observer2.assertComplete(); + observer3.assertComplete(); + verify(instrumenter).end(Context.root(), "request", null, null); + } + } + + @Nested + class FlowableTest { + @Test + void supported() { + assertThat(underTest.supports(Flowable.class)).isTrue(); + } + + @Test + void endsSpanOnAlreadyCompleted() { + Flowable result = + (Flowable) + underTest.end( + instrumenter, Context.root(), "request", Flowable.just("response"), String.class); + TestSubscriber observer = result.test(); + + verify(instrumenter).end(Context.root(), "request", null, null); + observer.assertComplete(); + } + + @Test + void endsOnAlreadyErrored() { + IllegalStateException exception = new IllegalStateException(); + + Flowable result = + (Flowable) + underTest.end( + instrumenter, Context.root(), "request", Flowable.error(exception), String.class); + TestSubscriber observer = result.test(); + + verify(instrumenter).end(Context.root(), "request", null, exception); + observer.assertError(exception); + } + + @Test + void endsSpanWhenCompleted() { + UnicastProcessor source = UnicastProcessor.create(); + + Flowable result = + (Flowable) + underTest.end(instrumenter, Context.root(), "request", source, String.class); + TestSubscriber observer = result.test(); + + verifyNoInteractions(instrumenter); + + source.onComplete(); + observer.assertComplete(); + verify(instrumenter).end(Context.root(), "request", null, null); + } + + @Test + void endsOnWhenErrored() { + IllegalStateException exception = new IllegalStateException(); + UnicastProcessor source = UnicastProcessor.create(); + + Flowable result = + (Flowable) + underTest.end(instrumenter, Context.root(), "request", source, String.class); + TestSubscriber observer = result.test(); + + verifyNoInteractions(instrumenter); + + source.onError(exception); + + observer.assertError(exception); + verify(instrumenter).end(Context.root(), "request", null, exception); + } + + @Test + void endsSpanWhenCancelled() { + when(span.storeInContext(any())).thenCallRealMethod(); + UnicastProcessor source = UnicastProcessor.create(); + Context context = Context.root().with(span); + + Flowable result = + (Flowable) underTest.end(instrumenter, context, "request", source, String.class); + TestSubscriber observer = result.test(); + + verifyNoInteractions(instrumenter); + + observer.cancel(); + + verify(span, never()).setAttribute(CANCELED_ATTRIBUTE_KEY, true); + verify(instrumenter).end(context, "request", null, null); + } + + @Test + void endsSpanWhenCancelledExperimentalAttributes() { + when(span.storeInContext(any())).thenCallRealMethod(); + UnicastProcessor source = UnicastProcessor.create(); + Context context = Context.root().with(span); + + Flowable result = + (Flowable) + underTestWithExperimentalAttributes.end( + instrumenter, context, "request", source, String.class); + TestSubscriber observer = result.test(); + + verifyNoInteractions(instrumenter); + verify(span, never()).setAttribute(CANCELED_ATTRIBUTE_KEY, true); + + observer.cancel(); + + verify(span).setAttribute(CANCELED_ATTRIBUTE_KEY, true); + verify(instrumenter).end(context, "request", null, null); + } + + @Test + void endsSpanOnceForMultipleSubscribers() { + ReplayProcessor source = ReplayProcessor.create(); + + Flowable result = + (Flowable) + underTest.end(instrumenter, Context.root(), "request", source, String.class); + TestSubscriber observer1 = result.test(); + TestSubscriber observer2 = result.test(); + TestSubscriber observer3 = result.test(); + + verifyNoInteractions(instrumenter); + + source.onComplete(); + observer1.assertComplete(); + observer2.assertComplete(); + observer3.assertComplete(); + verify(instrumenter).end(Context.root(), "request", null, null); + } + } + + @Nested + class ParallelFlowableTest { + @Test + void supported() { + assertThat(underTest.supports(ParallelFlowable.class)).isTrue(); + } + + @Test + void endsSpanOnAlreadyCompleted() { + ParallelFlowable result = + (ParallelFlowable) + underTest.end( + instrumenter, + Context.root(), + "request", + Flowable.just("response").parallel(), + String.class); + TestSubscriber observer = result.sequential().test(); + + observer.assertComplete(); + verify(instrumenter).end(Context.root(), "request", null, null); + } + + @Test + void endsSpanOnAlreadyErrored() { + IllegalStateException exception = new IllegalStateException(); + + ParallelFlowable result = + (ParallelFlowable) + underTest.end( + instrumenter, + Context.root(), + "request", + Flowable.error(exception).parallel(), + String.class); + TestSubscriber observer = result.sequential().test(); + + observer.assertError(exception); + verify(instrumenter).end(Context.root(), "request", null, exception); + } + + @Test + void endsSpanWhenCompleted() { + UnicastProcessor source = UnicastProcessor.create(); + + ParallelFlowable result = + (ParallelFlowable) + underTest.end( + instrumenter, Context.root(), "request", source.parallel(), String.class); + TestSubscriber observer = result.sequential().test(); + + verifyNoInteractions(instrumenter); + + source.onComplete(); + + observer.assertComplete(); + verify(instrumenter).end(Context.root(), "request", null, null); + } + + @Test + void endsSpanWhenErrored() { + IllegalStateException exception = new IllegalStateException(); + UnicastProcessor source = UnicastProcessor.create(); + + ParallelFlowable result = + (ParallelFlowable) + underTest.end( + instrumenter, Context.root(), "request", source.parallel(), String.class); + TestSubscriber observer = result.sequential().test(); + + verifyNoInteractions(instrumenter); + + source.onError(exception); + + observer.assertError(exception); + verify(instrumenter).end(Context.root(), "request", null, exception); + } + + @Test + void endsSpanWhenCancelled() { + when(span.storeInContext(any())).thenCallRealMethod(); + UnicastProcessor source = UnicastProcessor.create(); + Context context = Context.root().with(span); + + ParallelFlowable result = + (ParallelFlowable) + underTest.end(instrumenter, context, "request", source.parallel(), String.class); + TestSubscriber observer = result.sequential().test(); + + verifyNoInteractions(instrumenter); + + observer.cancel(); + + verify(span, never()).setAttribute(CANCELED_ATTRIBUTE_KEY, true); + verify(instrumenter).end(context, "request", null, null); + } + + @Test + void endsSpanWhenCancelledExperimentalAttributes() { + when(span.storeInContext(any())).thenCallRealMethod(); + UnicastProcessor source = UnicastProcessor.create(); + Context context = Context.root().with(span); + + ParallelFlowable result = + (ParallelFlowable) + underTestWithExperimentalAttributes.end( + instrumenter, context, "request", source.parallel(), String.class); + TestSubscriber observer = result.sequential().test(); + + verifyNoInteractions(instrumenter); + verify(span, never()).setAttribute(CANCELED_ATTRIBUTE_KEY, true); + + observer.cancel(); + verify(span).setAttribute(CANCELED_ATTRIBUTE_KEY, true); + verify(instrumenter).end(context, "request", null, null); + } + } + + @Nested + class PublisherTest { + @Test + void supported() { + assertThat(underTest.supports(Publisher.class)).isTrue(); + } + + @Test + void endsSpanWhenCompleted() { + CustomPublisher source = new CustomPublisher(); + + Flowable result = + (Flowable) + underTest.end(instrumenter, Context.root(), "request", source, String.class); + TestSubscriber observer = result.test(); + + verifyNoInteractions(instrumenter); + + source.onComplete(); + + observer.assertComplete(); + verify(instrumenter).end(Context.root(), "request", null, null); + } + + @Test + void endsSpanWhenErrored() { + IllegalStateException exception = new IllegalStateException(); + CustomPublisher source = new CustomPublisher(); + + Flowable result = + (Flowable) + underTest.end(instrumenter, Context.root(), "request", source, String.class); + TestSubscriber observer = result.test(); + + verifyNoInteractions(instrumenter); + + source.onError(exception); + + observer.assertError(exception); + verify(instrumenter).end(Context.root(), "request", null, exception); + } + + @Test + void endsSpanWhenCancelled() { + when(span.storeInContext(any())).thenCallRealMethod(); + CustomPublisher source = new CustomPublisher(); + Context context = Context.root().with(span); + + Flowable result = + (Flowable) underTest.end(instrumenter, context, "request", source, String.class); + TestSubscriber observer = result.test(); + + verifyNoInteractions(instrumenter); + + observer.cancel(); + + verify(span, never()).setAttribute(CANCELED_ATTRIBUTE_KEY, true); + verify(instrumenter).end(context, "request", null, null); + } + + @Test + void endsSpanWhenCancelledExperimentalAttributes() { + when(span.storeInContext(any())).thenCallRealMethod(); + CustomPublisher source = new CustomPublisher(); + Context context = Context.root().with(span); + + Flowable result = + (Flowable) + underTestWithExperimentalAttributes.end( + instrumenter, context, "request", source, String.class); + TestSubscriber observer = result.test(); + + verifyNoInteractions(instrumenter); + verify(span, never()).setAttribute(CANCELED_ATTRIBUTE_KEY, true); + + observer.cancel(); + + verify(span).setAttribute(CANCELED_ATTRIBUTE_KEY, true); + verify(instrumenter).end(context, "request", null, null); + } + + class CustomPublisher implements Publisher, Subscription { + Subscriber subscriber; + + @Override + public void subscribe(Subscriber subscriber) { + this.subscriber = subscriber; + subscriber.onSubscribe(this); + } + + public void onComplete() { + this.subscriber.onComplete(); + } + + public void onError(Throwable exception) { + this.subscriber.onError(exception); + } + + @Override + public void request(long l) {} + + @Override + public void cancel() {} + } + } +} diff --git a/instrumentation/rxjava/rxjava-3-common/testing/src/main/groovy/io/opentelemetry/instrumentation/rxjava/v3/common/AbstractRxJava3SubscriptionTest.groovy b/instrumentation/rxjava/rxjava-3-common/testing/src/main/groovy/io/opentelemetry/instrumentation/rxjava/v3/common/AbstractRxJava3SubscriptionTest.groovy deleted file mode 100644 index 3acb7761a851..000000000000 --- a/instrumentation/rxjava/rxjava-3-common/testing/src/main/groovy/io/opentelemetry/instrumentation/rxjava/v3/common/AbstractRxJava3SubscriptionTest.groovy +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.rxjava.v3.common - -import io.opentelemetry.api.GlobalOpenTelemetry -import io.opentelemetry.api.trace.SpanKind -import io.opentelemetry.instrumentation.test.InstrumentationSpecification -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.functions.Consumer - -import java.util.concurrent.CountDownLatch - -abstract class AbstractRxJava3SubscriptionTest extends InstrumentationSpecification { - - def "subscription test"() { - when: - CountDownLatch latch = new CountDownLatch(1) - runWithSpan("parent") { - Single connection = Single.create { - it.onSuccess(new Connection()) - } - connection.subscribe(new Consumer() { - @Override - void accept(Connection t) { - t.query() - latch.countDown() - } - }) - } - latch.await() - - then: - assertTraces(1) { - trace(0, 2) { - span(0) { - name "parent" - kind SpanKind.INTERNAL - hasNoParent() - } - span(1) { - name "Connection.query" - kind SpanKind.INTERNAL - childOf span(0) - } - } - } - } - - static class Connection { - static int query() { - def span = GlobalOpenTelemetry.getTracer("test").spanBuilder("Connection.query").startSpan() - span.end() - return new Random().nextInt() - } - } -} diff --git a/instrumentation/rxjava/rxjava-3-common/testing/src/main/groovy/io/opentelemetry/instrumentation/rxjava/v3/common/AbstractRxJava3Test.groovy b/instrumentation/rxjava/rxjava-3-common/testing/src/main/groovy/io/opentelemetry/instrumentation/rxjava/v3/common/AbstractRxJava3Test.groovy deleted file mode 100644 index 338a6b3587ec..000000000000 --- a/instrumentation/rxjava/rxjava-3-common/testing/src/main/groovy/io/opentelemetry/instrumentation/rxjava/v3/common/AbstractRxJava3Test.groovy +++ /dev/null @@ -1,491 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.rxjava.v3.common - -import com.google.common.collect.Lists -import io.opentelemetry.api.common.AttributeKey -import io.opentelemetry.api.trace.Span -import io.opentelemetry.api.trace.SpanKind -import io.opentelemetry.instrumentation.test.InstrumentationSpecification -import io.reactivex.rxjava3.core.BackpressureStrategy -import io.reactivex.rxjava3.core.Completable -import io.reactivex.rxjava3.core.Flowable -import io.reactivex.rxjava3.core.Maybe -import io.reactivex.rxjava3.core.Observable -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.internal.operators.flowable.FlowablePublish -import io.reactivex.rxjava3.internal.operators.observable.ObservablePublish -import io.reactivex.rxjava3.schedulers.Schedulers -import org.reactivestreams.Subscriber -import org.reactivestreams.Subscription -import spock.lang.Shared -import spock.lang.Unroll - -import static java.util.concurrent.TimeUnit.MILLISECONDS - -/** - *

Tests in this class may seem not exhaustive due to the fact that some classes are converted - * into others, ie. {@link Completable#toMaybe()}. Fortunately, RxJava3 uses helper classes like - * {@link io.reactivex.rxjava3.internal.operators.maybe.MaybeFromCompletable} and as a result we - * can test subscriptions and cancellations correctly. - */ -@Unroll -abstract class AbstractRxJava3Test extends InstrumentationSpecification { - - public static final String EXCEPTION_MESSAGE = "test exception" - - @Shared - def addOne = { i -> - addOneFunc(i) - } - - @Shared - def addTwo = { i -> - addTwoFunc(i) - } - - @Shared - def throwException = { - throw new IllegalStateException(EXCEPTION_MESSAGE) - } - - def addOneFunc(int i) { - runWithSpan("addOne") { - return i + 1 - } - } - - def addTwoFunc(int i) { - runWithSpan("addTwo") { - return i + 2 - } - } - - def "Publisher '#testName' test"() { - when: - def result = assemblePublisherUnderTrace(publisherSupplier) - - then: - result == expected - and: - assertTraces(1) { - sortSpansByStartTime() - trace(0, workSpans + 1) { - - span(0) { - name "publisher-parent" - kind SpanKind.INTERNAL - hasNoParent() - } - for (int i = 1; i < workSpans + 1; ++i) { - span(i) { - name "addOne" - kind SpanKind.INTERNAL - childOf span(0) - } - } - } - } - - where: - testName | expected | workSpans | publisherSupplier - "basic maybe" | 2 | 1 | { -> Maybe.just(1).map(addOne) } - "two operations maybe" | 4 | 2 | { -> Maybe.just(2).map(addOne).map(addOne) } - "delayed maybe" | 4 | 1 | { -> - Maybe.just(3).delay(100, MILLISECONDS).map(addOne) - } - "delayed twice maybe" | 6 | 2 | { -> - Maybe.just(4).delay(100, MILLISECONDS).map(addOne).delay(100, MILLISECONDS).map(addOne) - } - "basic flowable" | [6, 7] | 2 | { -> - Flowable.fromIterable([5, 6]).map(addOne) - } - "two operations flowable" | [8, 9] | 4 | { -> - Flowable.fromIterable([6, 7]).map(addOne).map(addOne) - } - "delayed flowable" | [8, 9] | 2 | { -> - Flowable.fromIterable([7, 8]).delay(100, MILLISECONDS).map(addOne) - } - "delayed twice flowable" | [10, 11] | 4 | { -> - Flowable.fromIterable([8, 9]).delay(100, MILLISECONDS).map(addOne).delay(100, MILLISECONDS).map(addOne) - } - "maybe from callable" | 12 | 2 | { -> - Maybe.fromCallable({ addOneFunc(10) }).map(addOne) - } - "basic single" | 1 | 1 | { -> Single.just(0).map(addOne) } - "basic observable" | [1] | 1 | { -> Observable.just(0).map(addOne) } - "connectable flowable" | [1] | 1 | { -> - FlowablePublish.just(0).delay(100, MILLISECONDS).map(addOne) - } - "connectable observable" | [1] | 1 | { -> - ObservablePublish.just(0).delay(100, MILLISECONDS).map(addOne) - } - } - - def "Publisher error '#testName' test"() { - when: - assemblePublisherUnderTrace(publisherSupplier) - - then: - def thrownException = thrown RuntimeException - thrownException.message == EXCEPTION_MESSAGE - and: - assertTraces(1) { - sortSpansByStartTime() - trace(0, 1) { - // It's important that we don't attach errors at the Reactor level so that we don't - // impact the spans on reactor integrations such as netty and lettuce, as reactor is - // more of a context propagation mechanism than something we would be tracking for - // errors this is ok. - span(0) { - name "publisher-parent" - kind SpanKind.INTERNAL - hasNoParent() - } - } - } - - where: - testName | publisherSupplier - "maybe" | { -> Maybe.error(new RuntimeException(EXCEPTION_MESSAGE)) } - "flowable" | { -> Flowable.error(new RuntimeException(EXCEPTION_MESSAGE)) } - "single" | { -> Single.error(new RuntimeException(EXCEPTION_MESSAGE)) } - "observable" | { -> Observable.error(new RuntimeException(EXCEPTION_MESSAGE)) } - "completable" | { -> Completable.error(new RuntimeException(EXCEPTION_MESSAGE)) } - } - - def "Publisher step '#testName' test"() { - when: - assemblePublisherUnderTrace(publisherSupplier) - - then: - def exception = thrown RuntimeException - exception.message == EXCEPTION_MESSAGE - and: - assertTraces(1) { - sortSpansByStartTime() - trace(0, workSpans + 1) { - // It's important that we don't attach errors at the Reactor level so that we don't - // impact the spans on reactor integrations such as netty and lettuce, as reactor is - // more of a context propagation mechanism than something we would be tracking for - // errors this is ok. - span(0) { - name "publisher-parent" - kind SpanKind.INTERNAL - hasNoParent() - } - - for (int i = 1; i < workSpans + 1; i++) { - span(i) { - name "addOne" - kind SpanKind.INTERNAL - childOf span(0) - } - } - } - } - - where: - testName | workSpans | publisherSupplier - "basic maybe failure" | 1 | { -> - Maybe.just(1).map(addOne).map({ throwException() }) - } - "basic flowable failure" | 1 | { -> - Flowable.fromIterable([5, 6]).map(addOne).map({ throwException() }) - } - } - - def "Publisher '#testName' cancel"() { - when: - cancelUnderTrace(publisherSupplier) - - then: - assertTraces(1) { - trace(0, 1) { - span(0) { - name "publisher-parent" - kind SpanKind.INTERNAL - hasNoParent() - } - } - } - - where: - testName | publisherSupplier - "basic maybe" | { -> Maybe.just(1) } - "basic flowable" | { -> Flowable.fromIterable([5, 6]) } - "basic single" | { -> Single.just(1) } - "basic completable" | { -> Completable.fromCallable({ -> 1 }) } - "basic observable" | { -> Observable.just(1) } - } - - def "Publisher chain spans have the correct parent for '#testName'"() { - when: - assemblePublisherUnderTrace(publisherSupplier) - - then: - assertTraces(1) { - trace(0, workSpans + 1) { - span(0) { - name "publisher-parent" - kind SpanKind.INTERNAL - hasNoParent() - } - - for (int i = 1; i < workSpans + 1; i++) { - span(i) { - name "addOne" - kind SpanKind.INTERNAL - childOf span(0) - } - } - } - } - - where: - testName | workSpans | publisherSupplier - "basic maybe" | 3 | { -> - Maybe.just(1).map(addOne).map(addOne).concatWith(Maybe.just(1).map(addOne)) - } - "basic flowable" | 5 | { -> - Flowable.fromIterable([5, 6]).map(addOne).map(addOne).concatWith(Maybe.just(1).map(addOne).toFlowable()) - } - } - - def "Publisher chain spans have the correct parents from subscription time"() { - when: - def maybe = Maybe.just(42) - .map(addOne) - .map(addTwo) - - runWithSpan("trace-parent") { - maybe.blockingGet() - } - - then: - assertTraces(1) { - trace(0, 3) { - sortSpansByStartTime() - span(0) { - name "trace-parent" - kind SpanKind.INTERNAL - hasNoParent() - } - span(1) { - name "addOne" - kind SpanKind.INTERNAL - childOf span(0) - } - span(2) { - name "addTwo" - kind SpanKind.INTERNAL - childOf span(0) - } - } - } - } - - def "Publisher chain spans have the correct parents from subscription time '#testName'"() { - when: - assemblePublisherUnderTrace { - // The "add one" operations in the publisher created here should be children of the publisher-parent - def publisher = publisherSupplier() - - runWithSpan("intermediate") { - if (publisher instanceof Maybe) { - return ((Maybe) publisher).map(addTwo) - } else if (publisher instanceof Flowable) { - return ((Flowable) publisher).map(addTwo) - } else if (publisher instanceof Single) { - return ((Single) publisher).map(addTwo) - } else if (publisher instanceof Observable) { - return ((Observable) publisher).map(addTwo) - } else if (publisher instanceof Completable) { - return ((Completable) publisher).toMaybe().map(addTwo) - } - throw new IllegalStateException("Unknown publisher type") - } - } - - then: - assertTraces(1) { - trace(0, 2 + 2 * workItems) { - sortSpansByStartTime() - span(0) { - name "publisher-parent" - kind SpanKind.INTERNAL - hasNoParent() - } - span(1) { - name "intermediate" - kind SpanKind.INTERNAL - childOf span(0) - } - - for (int i = 2; i < 2 + 2 * workItems; i = i + 2) { - span(i) { - name "addOne" - kind SpanKind.INTERNAL - childOf span(0) - } - span(i + 1) { - name "addTwo" - kind SpanKind.INTERNAL - childOf span(0) - } - } - } - } - - where: - testName | workItems | publisherSupplier - "basic maybe" | 1 | { -> Maybe.just(1).map(addOne) } - "basic flowable" | 2 | { -> Flowable.fromIterable([1, 2]).map(addOne) } - "basic single" | 1 | { -> Single.just(1).map(addOne) } - "basic observable" | 1 | { -> Observable.just(1).map(addOne) } - } - - def "Flowables produce the right number of results '#scheduler'"() { - when: - List values = runWithSpan("flowable root") { - Flowable.fromIterable([1, 2, 3, 4]) - .parallel() - .runOn(scheduler) - .flatMap({ num -> - Maybe.just(num).map(addOne).toFlowable() - }) - .sequential() - .toList() - .blockingGet() - } - - then: - values.size() == 4 - assertTraces(1) { - trace(0, 5) { - span(0) { - name "flowable root" - kind SpanKind.INTERNAL - hasNoParent() - } - for (int i = 1; i < values.size() + 1; i++) { - span(i) { - name "addOne" - kind SpanKind.INTERNAL - childOf span(0) - } - } - } - } - - where: - scheduler << [Schedulers.newThread(), Schedulers.computation(), Schedulers.single(), Schedulers.trampoline()] - } - - def "test many ongoing trace chains on '#scheduler'"() { - setup: - int iterations = 100 - Set remainingIterations = new HashSet<>((0L..(iterations - 1)).toList()) - - when: - RxJava3ConcurrencyTestHelper.launchAndWait(scheduler, iterations, 60000, testRunner()) - - then: - assertTraces(iterations) { - for (int i = 0; i < iterations; i++) { - trace(i, 3) { - long iteration = -1 - span(0) { - name("outer") - iteration = span.getAttributes().get(AttributeKey.longKey("iteration")).toLong() - assert remainingIterations.remove(iteration) - } - span(1) { - name("middle") - childOf(span(0)) - assert span.getAttributes().get(AttributeKey.longKey("iteration")) == iteration - } - span(2) { - name("inner") - childOf(span(1)) - assert span.getAttributes().get(AttributeKey.longKey("iteration")) == iteration - } - } - } - } - - assert remainingIterations.isEmpty() - - where: - scheduler << [Schedulers.newThread(), Schedulers.computation(), Schedulers.single(), Schedulers.trampoline()] - } - - def cancelUnderTrace(def publisherSupplier) { - runUnderTraceWithoutExceptionCatch("publisher-parent") { - def publisher = publisherSupplier() - if (publisher instanceof Maybe) { - publisher = publisher.toFlowable() - } else if (publisher instanceof Single) { - publisher = publisher.toFlowable() - } else if (publisher instanceof Completable) { - publisher = publisher.toFlowable() - } else if (publisher instanceof Observable) { - publisher = publisher.toFlowable(BackpressureStrategy.LATEST) - } - - publisher.subscribe(new Subscriber() { - void onSubscribe(Subscription subscription) { - subscription.cancel() - } - - void onNext(Integer t) { - } - - void onError(Throwable error) { - } - - void onComplete() { - } - }) - } - } - - @SuppressWarnings("unchecked") - def assemblePublisherUnderTrace(def publisherSupplier) { - // The "add two" operations below should be children of this span - runUnderTraceWithoutExceptionCatch("publisher-parent") { - def publisher = publisherSupplier() - - // Read all data from publisher - if (publisher instanceof Maybe) { - return ((Maybe) publisher).blockingGet() - } else if (publisher instanceof Flowable) { - return Lists.newArrayList(((Flowable) publisher).blockingIterable()) - } else if (publisher instanceof Single) { - return ((Single) publisher).blockingGet() - } else if (publisher instanceof Observable) { - return Lists.newArrayList(((Observable) publisher).blockingIterable()) - } else if (publisher instanceof Completable) { - return ((Completable) publisher).toMaybe().blockingGet() - } - - throw new IllegalStateException("Unknown publisher: " + publisher) - } - } - - def runUnderTraceWithoutExceptionCatch(String spanName, Closure c) { - Span span = openTelemetry.getTracer("test") - .spanBuilder(spanName) - .startSpan() - try { - return span.makeCurrent().withCloseable { - c.call() - } - } finally { - span.end() - } - } -} diff --git a/instrumentation/rxjava/rxjava-3-common/testing/src/main/groovy/io/opentelemetry/instrumentation/rxjava/v3/common/AbstractRxJava3WithSpanTest.groovy b/instrumentation/rxjava/rxjava-3-common/testing/src/main/groovy/io/opentelemetry/instrumentation/rxjava/v3/common/AbstractRxJava3WithSpanTest.groovy deleted file mode 100644 index 65b41ebbf812..000000000000 --- a/instrumentation/rxjava/rxjava-3-common/testing/src/main/groovy/io/opentelemetry/instrumentation/rxjava/v3/common/AbstractRxJava3WithSpanTest.groovy +++ /dev/null @@ -1,1117 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.rxjava.v3.common - - -import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes -import io.reactivex.rxjava3.core.Completable -import io.reactivex.rxjava3.core.Flowable -import io.reactivex.rxjava3.core.Maybe -import io.reactivex.rxjava3.core.Observable -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.observers.TestObserver -import io.reactivex.rxjava3.processors.UnicastProcessor -import io.reactivex.rxjava3.subjects.CompletableSubject -import io.reactivex.rxjava3.subjects.MaybeSubject -import io.reactivex.rxjava3.subjects.SingleSubject -import io.reactivex.rxjava3.subjects.UnicastSubject -import io.reactivex.rxjava3.subscribers.TestSubscriber -import org.reactivestreams.Publisher -import org.reactivestreams.Subscriber -import org.reactivestreams.Subscription - -import static io.opentelemetry.api.trace.SpanKind.INTERNAL -import static io.opentelemetry.api.trace.StatusCode.ERROR - -abstract class AbstractRxJava3WithSpanTest extends AgentInstrumentationSpecification { - - abstract AbstractTracedWithSpan newTraced() - - def "should capture span for already completed Completable"() { - setup: - def observer = new TestObserver() - def source = Completable.complete() - newTraced() - .completable(source) - .subscribe(observer) - observer.assertComplete() - - expect: - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.completable" - kind INTERNAL - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" { it.endsWith(".TracedWithSpan") } - "$SemanticAttributes.CODE_FUNCTION" "completable" - } - } - } - } - } - - def "should capture span for eventually completed Completable"() { - setup: - def source = CompletableSubject.create() - def observer = new TestObserver() - newTraced() - .completable(source) - .subscribe(observer) - - expect: - Thread.sleep(500) // sleep a bit just to make sure no span is captured - assertTraces(0) {} - - source.onComplete() - observer.assertComplete() - - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.completable" - kind INTERNAL - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" { it.endsWith(".TracedWithSpan") } - "$SemanticAttributes.CODE_FUNCTION" "completable" - } - } - } - } - } - - def "should capture span for already errored Completable"() { - setup: - def error = new IllegalArgumentException("Boom") - def observer = new TestObserver() - def source = Completable.error(error) - newTraced() - .completable(source) - .subscribe(observer) - observer.assertError(error) - - expect: - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.completable" - kind INTERNAL - hasNoParent() - status ERROR - errorEvent(IllegalArgumentException, "Boom") - attributes { - "$SemanticAttributes.CODE_NAMESPACE" { it.endsWith(".TracedWithSpan") } - "$SemanticAttributes.CODE_FUNCTION" "completable" - } - } - } - } - } - - def "should capture span for eventually errored Completable"() { - setup: - def error = new IllegalArgumentException("Boom") - def source = CompletableSubject.create() - def observer = new TestObserver() - newTraced() - .completable(source) - .subscribe(observer) - - expect: - Thread.sleep(500) // sleep a bit just to make sure no span is captured - assertTraces(0) {} - - source.onError(error) - observer.assertError(error) - - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.completable" - kind INTERNAL - hasNoParent() - status ERROR - errorEvent(IllegalArgumentException, "Boom") - attributes { - "$SemanticAttributes.CODE_NAMESPACE" { it.endsWith(".TracedWithSpan") } - "$SemanticAttributes.CODE_FUNCTION" "completable" - } - } - } - } - } - - def "should capture span for canceled Completable"() { - setup: - def source = CompletableSubject.create() - def observer = new TestObserver() - newTraced() - .completable(source) - .subscribe(observer) - - expect: - Thread.sleep(500) // sleep a bit just to make sure no span is captured - assertTraces(0) {} - - observer.dispose() - - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.completable" - kind INTERNAL - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" { it.endsWith(".TracedWithSpan") } - "$SemanticAttributes.CODE_FUNCTION" "completable" - "rxjava.canceled" true - } - } - } - } - } - - def "should capture span for already completed Maybe"() { - setup: - def observer = new TestObserver() - def source = Maybe.just("Value") - newTraced() - .maybe(source) - .subscribe(observer) - observer.assertValue("Value") - observer.assertComplete() - - expect: - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.maybe" - kind INTERNAL - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" { it.endsWith(".TracedWithSpan") } - "$SemanticAttributes.CODE_FUNCTION" "maybe" - } - } - } - } - } - - def "should capture span for already empty Maybe"() { - setup: - def observer = new TestObserver() - def source = Maybe. empty() - newTraced() - .maybe(source) - .subscribe(observer) - observer.assertComplete() - - expect: - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.maybe" - kind INTERNAL - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" { it.endsWith(".TracedWithSpan") } - "$SemanticAttributes.CODE_FUNCTION" "maybe" - } - } - } - } - } - - def "should capture span for eventually completed Maybe"() { - setup: - def source = MaybeSubject. create() - def observer = new TestObserver() - newTraced() - .maybe(source) - .subscribe(observer) - - expect: - Thread.sleep(500) // sleep a bit just to make sure no span is captured - assertTraces(0) {} - - source.onSuccess("Value") - observer.assertValue("Value") - observer.assertComplete() - - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.maybe" - kind INTERNAL - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" { it.endsWith(".TracedWithSpan") } - "$SemanticAttributes.CODE_FUNCTION" "maybe" - } - } - } - } - } - - def "should capture span for already errored Maybe"() { - setup: - def error = new IllegalArgumentException("Boom") - def observer = new TestObserver() - def source = Maybe. error(error) - newTraced() - .maybe(source) - .subscribe(observer) - observer.assertError(error) - - expect: - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.maybe" - kind INTERNAL - hasNoParent() - status ERROR - errorEvent(IllegalArgumentException, "Boom") - attributes { - "$SemanticAttributes.CODE_NAMESPACE" { it.endsWith(".TracedWithSpan") } - "$SemanticAttributes.CODE_FUNCTION" "maybe" - } - } - } - } - } - - def "should capture span for eventually errored Maybe"() { - setup: - def error = new IllegalArgumentException("Boom") - def source = MaybeSubject. create() - def observer = new TestObserver() - newTraced() - .maybe(source) - .subscribe(observer) - - expect: - Thread.sleep(500) // sleep a bit just to make sure no span is captured - assertTraces(0) {} - - source.onError(error) - observer.assertError(error) - - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.maybe" - kind INTERNAL - hasNoParent() - status ERROR - errorEvent(IllegalArgumentException, "Boom") - attributes { - "$SemanticAttributes.CODE_NAMESPACE" { it.endsWith(".TracedWithSpan") } - "$SemanticAttributes.CODE_FUNCTION" "maybe" - } - } - } - } - } - - def "should capture span for canceled Maybe"() { - setup: - def source = MaybeSubject. create() - def observer = new TestObserver() - newTraced() - .maybe(source) - .subscribe(observer) - - expect: - Thread.sleep(500) // sleep a bit just to make sure no span is captured - assertTraces(0) {} - - observer.dispose() - - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.maybe" - kind INTERNAL - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" { it.endsWith(".TracedWithSpan") } - "$SemanticAttributes.CODE_FUNCTION" "maybe" - "rxjava.canceled" true - } - } - } - } - } - - def "should capture span for already completed Single"() { - setup: - def observer = new TestObserver() - def source = Single.just("Value") - newTraced() - .single(source) - .subscribe(observer) - observer.assertValue("Value") - observer.assertComplete() - - expect: - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.single" - kind INTERNAL - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" { it.endsWith(".TracedWithSpan") } - "$SemanticAttributes.CODE_FUNCTION" "single" - } - } - } - } - } - - def "should capture span for eventually completed Single"() { - setup: - def source = SingleSubject. create() - def observer = new TestObserver() - newTraced() - .single(source) - .subscribe(observer) - - expect: - Thread.sleep(500) // sleep a bit just to make sure no span is captured - assertTraces(0) {} - - source.onSuccess("Value") - observer.assertValue("Value") - observer.assertComplete() - - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.single" - kind INTERNAL - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" { it.endsWith(".TracedWithSpan") } - "$SemanticAttributes.CODE_FUNCTION" "single" - } - } - } - } - } - - def "should capture span for already errored Single"() { - setup: - def error = new IllegalArgumentException("Boom") - def observer = new TestObserver() - def source = Single. error(error) - newTraced() - .single(source) - .subscribe(observer) - observer.assertError(error) - - expect: - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.single" - kind INTERNAL - hasNoParent() - status ERROR - errorEvent(IllegalArgumentException, "Boom") - attributes { - "$SemanticAttributes.CODE_NAMESPACE" { it.endsWith(".TracedWithSpan") } - "$SemanticAttributes.CODE_FUNCTION" "single" - } - } - } - } - } - - def "should capture span for eventually errored Single"() { - setup: - def error = new IllegalArgumentException("Boom") - def source = SingleSubject. create() - def observer = new TestObserver() - newTraced() - .single(source) - .subscribe(observer) - - expect: - Thread.sleep(500) // sleep a bit just to make sure no span is captured - assertTraces(0) {} - - source.onError(error) - observer.assertError(error) - - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.single" - kind INTERNAL - hasNoParent() - status ERROR - errorEvent(IllegalArgumentException, "Boom") - attributes { - "$SemanticAttributes.CODE_NAMESPACE" { it.endsWith(".TracedWithSpan") } - "$SemanticAttributes.CODE_FUNCTION" "single" - } - } - } - } - } - - def "should capture span for canceled Single"() { - setup: - def source = SingleSubject. create() - def observer = new TestObserver() - newTraced() - .single(source) - .subscribe(observer) - - expect: - Thread.sleep(500) // sleep a bit just to make sure no span is captured - assertTraces(0) {} - - observer.dispose() - - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.single" - kind INTERNAL - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" { it.endsWith(".TracedWithSpan") } - "$SemanticAttributes.CODE_FUNCTION" "single" - "rxjava.canceled" true - } - } - } - } - } - - def "should capture span for already completed Observable"() { - setup: - def observer = new TestObserver() - def source = Observable. just("Value") - newTraced() - .observable(source) - .subscribe(observer) - observer.assertValue("Value") - observer.assertComplete() - - expect: - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.observable" - kind INTERNAL - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" { it.endsWith(".TracedWithSpan") } - "$SemanticAttributes.CODE_FUNCTION" "observable" - } - } - } - } - } - - def "should capture span for eventually completed Observable"() { - setup: - def source = UnicastSubject. create() - def observer = new TestObserver() - newTraced() - .observable(source) - .subscribe(observer) - - expect: - Thread.sleep(500) // sleep a bit just to make sure no span is captured - assertTraces(0) {} - - source.onNext("Value") - observer.assertValue("Value") - - Thread.sleep(500) // sleep a bit just to make sure no span is captured - assertTraces(0) {} - - source.onComplete() - observer.assertComplete() - - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.observable" - kind INTERNAL - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" { it.endsWith(".TracedWithSpan") } - "$SemanticAttributes.CODE_FUNCTION" "observable" - } - } - } - } - } - - def "should capture span for already errored Observable"() { - setup: - def error = new IllegalArgumentException("Boom") - def observer = new TestObserver() - def source = Observable. error(error) - newTraced() - .observable(source) - .subscribe(observer) - observer.assertError(error) - - expect: - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.observable" - kind INTERNAL - hasNoParent() - status ERROR - errorEvent(IllegalArgumentException, "Boom") - attributes { - "$SemanticAttributes.CODE_NAMESPACE" { it.endsWith(".TracedWithSpan") } - "$SemanticAttributes.CODE_FUNCTION" "observable" - } - } - } - } - } - - def "should capture span for eventually errored Observable"() { - setup: - def error = new IllegalArgumentException("Boom") - def source = UnicastSubject. create() - def observer = new TestObserver() - newTraced() - .observable(source) - .subscribe(observer) - - expect: - Thread.sleep(500) // sleep a bit just to make sure no span is captured - assertTraces(0) {} - - source.onNext("Value") - observer.assertValue("Value") - - Thread.sleep(500) // sleep a bit just to make sure no span is captured - assertTraces(0) {} - - source.onError(error) - observer.assertError(error) - - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.observable" - kind INTERNAL - hasNoParent() - status ERROR - errorEvent(IllegalArgumentException, "Boom") - attributes { - "$SemanticAttributes.CODE_NAMESPACE" { it.endsWith(".TracedWithSpan") } - "$SemanticAttributes.CODE_FUNCTION" "observable" - } - } - } - } - } - - def "should capture span for canceled Observable"() { - setup: - def source = UnicastSubject. create() - def observer = new TestObserver() - newTraced() - .observable(source) - .subscribe(observer) - - expect: - Thread.sleep(500) // sleep a bit just to make sure no span is captured - assertTraces(0) {} - - source.onNext("Value") - observer.assertValue("Value") - - Thread.sleep(500) // sleep a bit just to make sure no span is captured - assertTraces(0) {} - - observer.dispose() - - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.observable" - kind INTERNAL - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" { it.endsWith(".TracedWithSpan") } - "$SemanticAttributes.CODE_FUNCTION" "observable" - "rxjava.canceled" true - } - } - } - } - } - - def "should capture span for already completed Flowable"() { - setup: - def observer = new TestSubscriber() - def source = Flowable. just("Value") - newTraced() - .flowable(source) - .subscribe(observer) - observer.assertValue("Value") - observer.assertComplete() - - expect: - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.flowable" - kind INTERNAL - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" { it.endsWith(".TracedWithSpan") } - "$SemanticAttributes.CODE_FUNCTION" "flowable" - } - } - } - } - } - - def "should capture span for eventually completed Flowable"() { - setup: - def source = UnicastProcessor. create() - def observer = new TestSubscriber() - newTraced() - .flowable(source) - .subscribe(observer) - - expect: - Thread.sleep(500) // sleep a bit just to make sure no span is captured - assertTraces(0) {} - - source.onNext("Value") - observer.assertValue("Value") - - Thread.sleep(500) // sleep a bit just to make sure no span is captured - assertTraces(0) {} - - source.onComplete() - observer.assertComplete() - - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.flowable" - kind INTERNAL - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" { it.endsWith(".TracedWithSpan") } - "$SemanticAttributes.CODE_FUNCTION" "flowable" - } - } - } - } - } - - def "should capture span for already errored Flowable"() { - setup: - def error = new IllegalArgumentException("Boom") - def observer = new TestSubscriber() - def source = Flowable. error(error) - newTraced() - .flowable(source) - .subscribe(observer) - observer.assertError(error) - - expect: - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.flowable" - kind INTERNAL - hasNoParent() - status ERROR - errorEvent(IllegalArgumentException, "Boom") - attributes { - "$SemanticAttributes.CODE_NAMESPACE" { it.endsWith(".TracedWithSpan") } - "$SemanticAttributes.CODE_FUNCTION" "flowable" - } - } - } - } - } - - def "should capture span for eventually errored Flowable"() { - setup: - def error = new IllegalArgumentException("Boom") - def source = UnicastProcessor. create() - def observer = new TestSubscriber() - newTraced() - .flowable(source) - .subscribe(observer) - - expect: - Thread.sleep(500) // sleep a bit just to make sure no span is captured - assertTraces(0) {} - - source.onNext("Value") - observer.assertValue("Value") - - Thread.sleep(500) // sleep a bit just to make sure no span is captured - assertTraces(0) {} - - source.onError(error) - observer.assertError(error) - - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.flowable" - kind INTERNAL - hasNoParent() - status ERROR - errorEvent(IllegalArgumentException, "Boom") - attributes { - "$SemanticAttributes.CODE_NAMESPACE" { it.endsWith(".TracedWithSpan") } - "$SemanticAttributes.CODE_FUNCTION" "flowable" - } - } - } - } - } - - def "should capture span for canceled Flowable"() { - setup: - def source = UnicastProcessor. create() - def observer = new TestSubscriber() - newTraced() - .flowable(source) - .subscribe(observer) - - expect: - Thread.sleep(500) // sleep a bit just to make sure no span is captured - assertTraces(0) {} - - source.onNext("Value") - observer.assertValue("Value") - - Thread.sleep(500) // sleep a bit just to make sure no span is captured - assertTraces(0) {} - - observer.cancel() - - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.flowable" - kind INTERNAL - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" { it.endsWith(".TracedWithSpan") } - "$SemanticAttributes.CODE_FUNCTION" "flowable" - "rxjava.canceled" true - } - } - } - } - } - - def "should capture span for already completed ParallelFlowable"() { - setup: - def observer = new TestSubscriber() - def source = Flowable. just("Value") - newTraced() - .parallelFlowable(source.parallel()) - .sequential() - .subscribe(observer) - observer.assertValue("Value") - observer.assertComplete() - - expect: - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.parallelFlowable" - kind INTERNAL - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" { it.endsWith(".TracedWithSpan") } - "$SemanticAttributes.CODE_FUNCTION" "parallelFlowable" - } - } - } - } - } - - def "should capture span for eventually completed ParallelFlowable"() { - setup: - def source = UnicastProcessor. create() - def observer = new TestSubscriber() - newTraced() - .parallelFlowable(source.parallel()) - .sequential() - .subscribe(observer) - - expect: - Thread.sleep(500) // sleep a bit just to make sure no span is captured - assertTraces(0) {} - - source.onNext("Value") - observer.assertValue("Value") - - Thread.sleep(500) // sleep a bit just to make sure no span is captured - assertTraces(0) {} - - source.onComplete() - observer.assertComplete() - - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.parallelFlowable" - kind INTERNAL - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" { it.endsWith(".TracedWithSpan") } - "$SemanticAttributes.CODE_FUNCTION" "parallelFlowable" - } - } - } - } - } - - def "should capture span for already errored ParallelFlowable"() { - setup: - def error = new IllegalArgumentException("Boom") - def observer = new TestSubscriber() - def source = Flowable. error(error) - newTraced() - .parallelFlowable(source.parallel()) - .sequential() - .subscribe(observer) - observer.assertError(error) - - expect: - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.parallelFlowable" - kind INTERNAL - hasNoParent() - status ERROR - errorEvent(IllegalArgumentException, "Boom") - attributes { - "$SemanticAttributes.CODE_NAMESPACE" { it.endsWith(".TracedWithSpan") } - "$SemanticAttributes.CODE_FUNCTION" "parallelFlowable" - } - } - } - } - } - - def "should capture span for eventually errored ParallelFlowable"() { - setup: - def error = new IllegalArgumentException("Boom") - def source = UnicastProcessor. create() - def observer = new TestSubscriber() - newTraced() - .parallelFlowable(source.parallel()) - .sequential() - .subscribe(observer) - - expect: - Thread.sleep(500) // sleep a bit just to make sure no span is captured - assertTraces(0) {} - - source.onNext("Value") - observer.assertValue("Value") - - Thread.sleep(500) // sleep a bit just to make sure no span is captured - assertTraces(0) {} - - source.onError(error) - observer.assertError(error) - - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.parallelFlowable" - kind INTERNAL - hasNoParent() - status ERROR - errorEvent(IllegalArgumentException, "Boom") - attributes { - "$SemanticAttributes.CODE_NAMESPACE" { it.endsWith(".TracedWithSpan") } - "$SemanticAttributes.CODE_FUNCTION" "parallelFlowable" - } - } - } - } - } - - def "should capture span for canceled ParallelFlowable"() { - setup: - def source = UnicastProcessor. create() - def observer = new TestSubscriber() - newTraced() - .parallelFlowable(source.parallel()) - .sequential() - .subscribe(observer) - - expect: - Thread.sleep(500) // sleep a bit just to make sure no span is captured - assertTraces(0) {} - - source.onNext("Value") - observer.assertValue("Value") - - Thread.sleep(500) // sleep a bit just to make sure no span is captured - assertTraces(0) {} - - observer.cancel() - - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.parallelFlowable" - kind INTERNAL - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" { it.endsWith(".TracedWithSpan") } - "$SemanticAttributes.CODE_FUNCTION" "parallelFlowable" - "rxjava.canceled" true - } - } - } - } - } - - def "should capture span for eventually completed Publisher"() { - setup: - def source = new CustomPublisher() - def observer = new TestSubscriber() - newTraced() - .publisher(source) - .subscribe(observer) - - expect: - Thread.sleep(500) // sleep a bit just to make sure no span is captured - assertTraces(0) {} - - source.onComplete() - observer.assertComplete() - - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.publisher" - kind INTERNAL - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" { it.endsWith(".TracedWithSpan") } - "$SemanticAttributes.CODE_FUNCTION" "publisher" - } - } - } - } - } - - def "should capture span for eventually errored Publisher"() { - setup: - def error = new IllegalArgumentException("Boom") - def source = new CustomPublisher() - def observer = new TestSubscriber() - newTraced() - .publisher(source) - .subscribe(observer) - - expect: - Thread.sleep(500) // sleep a bit just to make sure no span is captured - assertTraces(0) {} - - source.onError(error) - observer.assertError(error) - - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.publisher" - kind INTERNAL - hasNoParent() - status ERROR - errorEvent(IllegalArgumentException, "Boom") - attributes { - "$SemanticAttributes.CODE_NAMESPACE" { it.endsWith(".TracedWithSpan") } - "$SemanticAttributes.CODE_FUNCTION" "publisher" - } - } - } - } - } - - def "should capture span for canceled Publisher"() { - setup: - def source = new CustomPublisher() - def observer = new TestSubscriber() - newTraced() - .publisher(source) - .subscribe(observer) - - expect: - Thread.sleep(500) // sleep a bit just to make sure no span is captured - assertTraces(0) {} - - observer.cancel() - - assertTraces(1) { - trace(0, 1) { - span(0) { - name "TracedWithSpan.publisher" - kind INTERNAL - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" { it.endsWith(".TracedWithSpan") } - "$SemanticAttributes.CODE_FUNCTION" "publisher" - "rxjava.canceled" true - } - } - } - } - } - - static class CustomPublisher implements Publisher, Subscription { - Subscriber subscriber - - @Override - void subscribe(Subscriber subscriber) { - this.subscriber = subscriber - subscriber.onSubscribe(this) - } - - void onComplete() { - this.subscriber.onComplete() - } - - void onError(Throwable exception) { - this.subscriber.onError(exception) - } - - @Override - void request(long l) {} - - @Override - void cancel() {} - } -} diff --git a/instrumentation/rxjava/rxjava-3-common/testing/src/main/java/io/opentelemetry/instrumentation/rxjava/v3/common/AbstractRxJava3SubscriptionTest.java b/instrumentation/rxjava/rxjava-3-common/testing/src/main/java/io/opentelemetry/instrumentation/rxjava/v3/common/AbstractRxJava3SubscriptionTest.java new file mode 100644 index 000000000000..c1962937a9b2 --- /dev/null +++ b/instrumentation/rxjava/rxjava-3-common/testing/src/main/java/io/opentelemetry/instrumentation/rxjava/v3/common/AbstractRxJava3SubscriptionTest.java @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.rxjava.v3.common; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.disposables.Disposable; +import java.util.Random; +import java.util.concurrent.CountDownLatch; +import org.junit.jupiter.api.Test; + +public abstract class AbstractRxJava3SubscriptionTest { + + protected abstract InstrumentationExtension testing(); + + @Test + public void subscriptionTest() throws InterruptedException { + CountDownLatch countDownLatch = new CountDownLatch(1); + testing() + .runWithSpan( + "parent", + () -> { + Single connectionSingle = + Single.create(emitter -> emitter.onSuccess(new Connection())); + Disposable unused = + connectionSingle.subscribe( + connection -> { + connection.query(); + countDownLatch.countDown(); + }); + }); + countDownLatch.await(); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("Connection.query") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + static class Connection { + int query() { + Span span = GlobalOpenTelemetry.getTracer("test").spanBuilder("Connection.query").startSpan(); + span.end(); + return new Random().nextInt(); + } + } +} diff --git a/instrumentation/rxjava/rxjava-3-common/testing/src/main/java/io/opentelemetry/instrumentation/rxjava/v3/common/AbstractRxJava3Test.java b/instrumentation/rxjava/rxjava-3-common/testing/src/main/java/io/opentelemetry/instrumentation/rxjava/v3/common/AbstractRxJava3Test.java new file mode 100644 index 000000000000..d5471f952477 --- /dev/null +++ b/instrumentation/rxjava/rxjava-3-common/testing/src/main/java/io/opentelemetry/instrumentation/rxjava/v3/common/AbstractRxJava3Test.java @@ -0,0 +1,856 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.rxjava.v3.common; + +import static io.opentelemetry.sdk.testing.assertj.LogAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.attributeEntry; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.google.common.primitives.Ints; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.util.ThrowingRunnable; +import io.opentelemetry.instrumentation.testing.util.ThrowingSupplier; +import io.opentelemetry.sdk.testing.assertj.TraceAssert; +import io.reactivex.rxjava3.core.BackpressureStrategy; +import io.reactivex.rxjava3.core.Completable; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Maybe; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Scheduler; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.internal.operators.flowable.FlowablePublish; +import io.reactivex.rxjava3.internal.operators.observable.ObservablePublish; +import io.reactivex.rxjava3.schedulers.Schedulers; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public abstract class AbstractRxJava3Test { + private static final String EXCEPTION_MESSAGE = "test exception"; + private static final String PARENT = "publisher-parent"; + private static final String ADD_ONE = "addOne"; + private static final String ADD_TWO = "addTwo"; + + protected abstract InstrumentationExtension testing(); + + private static Stream schedulers() { + return Stream.of( + Arguments.of(Schedulers.newThread()), + Arguments.of(Schedulers.computation()), + Arguments.of(Schedulers.single()), + Arguments.of(Schedulers.trampoline())); + } + + private int addOne(int i) { + return testing().runWithSpan(ADD_ONE, () -> i + 1); + } + + private int addTwo(int i) { + return testing().runWithSpan(ADD_TWO, () -> i + 2); + } + + private T createParentSpan(ThrowingSupplier test) { + return testing().runWithSpan(PARENT, test); + } + + private void createParentSpan(ThrowingRunnable test) { + testing().runWithSpan(PARENT, test); + } + + private enum CancellingSubscriber implements Subscriber { + INSTANCE; + + @Override + public void onSubscribe(Subscription subscription) { + subscription.cancel(); + } + + @Override + public void onNext(Object o) {} + + @Override + public void onError(Throwable throwable) {} + + @Override + public void onComplete() {} + } + + @Test + public void basicMaybe() { + int result = createParentSpan(() -> Maybe.just(1).map(this::addOne).blockingGet()); + assertThat(result).isEqualTo(2); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName(PARENT).hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + @Test + public void twoOperationsMaybe() { + int result = + createParentSpan(() -> Maybe.just(2).map(this::addOne).map(this::addOne).blockingGet()); + assertThat(result).isEqualTo(4); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName(PARENT).hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + @Test + public void delayedMaybe() { + int result = + createParentSpan( + () -> Maybe.just(3).delay(100, TimeUnit.MILLISECONDS).map(this::addOne).blockingGet()); + assertThat(result).isEqualTo(4); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName(PARENT).hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + @Test + public void delayedTwiceMaybe() { + int result = + createParentSpan( + () -> + Maybe.just(4) + .delay(100, TimeUnit.MILLISECONDS) + .map(this::addOne) + .delay(100, TimeUnit.MILLISECONDS) + .map(this::addOne) + .blockingGet()); + assertThat(result).isEqualTo(6); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName(PARENT).hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + @Test + public void basicFlowable() { + Iterable result = + createParentSpan( + () -> + Flowable.fromIterable(Ints.asList(5, 6)).map(this::addOne).toList().blockingGet()); + assertThat(result).contains(6, 7); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName(PARENT).hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + @Test + public void twoOperationsFlowable() { + List result = + createParentSpan( + () -> + Flowable.fromIterable(Ints.asList(6, 7)) + .map(this::addOne) + .map(this::addOne) + .toList() + .blockingGet()); + assertThat(result).contains(8, 9); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName(PARENT).hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + @Test + public void delayedFlowable() { + List result = + createParentSpan( + () -> + Flowable.fromIterable(Ints.asList(7, 8)) + .delay(100, TimeUnit.MILLISECONDS) + .map(this::addOne) + .toList() + .blockingGet()); + assertThat(result).contains(8, 9); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName(PARENT).hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + @Test + public void delayedTwiceFlowable() { + List result = + createParentSpan( + () -> + Flowable.fromIterable(Ints.asList(8, 9)) + .delay(100, TimeUnit.MILLISECONDS) + .map(this::addOne) + .delay(100, TimeUnit.MILLISECONDS) + .map(this::addOne) + .toList() + .blockingGet()); + assertThat(result).contains(10, 11); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName(PARENT).hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + @Test + public void maybeFromCallable() { + Integer result = + createParentSpan( + () -> Maybe.fromCallable(() -> addOne(10)).map(this::addOne).blockingGet()); + assertThat(result).isEqualTo(12); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName(PARENT).hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + @Test + public void basicSingle() { + Integer result = createParentSpan(() -> Single.just(0).map(this::addOne).blockingGet()); + assertThat(result).isEqualTo(1); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName(PARENT).hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + @Test + public void basicObservable() { + List result = + createParentSpan(() -> Observable.just(0).map(this::addOne).toList().blockingGet()); + assertThat(result).contains(1); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName(PARENT).hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + @Test + public void connectableFlowable() { + List result = + createParentSpan( + () -> + FlowablePublish.just(0) + .delay(100, TimeUnit.MILLISECONDS) + .map(this::addOne) + .toList() + .blockingGet()); + assertThat(result).contains(1); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName(PARENT).hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + @Test + public void connectableObservable() { + List result = + createParentSpan( + () -> + ObservablePublish.just(0) + .delay(100, TimeUnit.MILLISECONDS) + .map(this::addOne) + .toList() + .blockingGet()); + assertThat(result).contains(1); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName(PARENT).hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + @Test + public void maybeError() { + IllegalStateException error = new IllegalStateException(EXCEPTION_MESSAGE); + assertThatThrownBy(() -> createParentSpan(() -> Maybe.error(error).blockingGet())) + .isEqualTo(error); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName(PARENT).hasKind(SpanKind.INTERNAL).hasNoParent())); + } + + @Test + public void flowableError() { + IllegalStateException error = new IllegalStateException(EXCEPTION_MESSAGE); + assertThatThrownBy(() -> createParentSpan(() -> Flowable.error(error)).toList().blockingGet()) + .isEqualTo(error); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName(PARENT).hasKind(SpanKind.INTERNAL).hasNoParent())); + } + + @Test + public void singleError() { + IllegalStateException error = new IllegalStateException(EXCEPTION_MESSAGE); + assertThatThrownBy(() -> createParentSpan(() -> Single.error(error)).blockingGet()) + .isEqualTo(error); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName(PARENT).hasKind(SpanKind.INTERNAL).hasNoParent())); + } + + @Test + public void observableError() { + IllegalStateException error = new IllegalStateException(EXCEPTION_MESSAGE); + assertThatThrownBy(() -> createParentSpan(() -> Observable.error(error).toList().blockingGet())) + .isEqualTo(error); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName(PARENT).hasKind(SpanKind.INTERNAL).hasNoParent())); + } + + @Test + public void completableError() { + IllegalStateException error = new IllegalStateException(EXCEPTION_MESSAGE); + assertThatThrownBy( + () -> createParentSpan(() -> Completable.error(error).toMaybe().blockingGet())) + .isEqualTo(error); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName(PARENT).hasKind(SpanKind.INTERNAL).hasNoParent())); + } + + @Test + public void basicMaybeFailure() { + IllegalStateException error = new IllegalStateException(EXCEPTION_MESSAGE); + assertThatThrownBy( + () -> + createParentSpan( + () -> + Maybe.just(1) + .map(this::addOne) + .map( + i -> { + throw error; + }) + .blockingGet())) + .isEqualTo(error); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName(PARENT).hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + @Test + public void basicFlowableFailure() { + IllegalStateException error = new IllegalStateException(EXCEPTION_MESSAGE); + assertThatThrownBy( + () -> + createParentSpan( + () -> + Flowable.fromIterable(Ints.asList(5, 6)) + .map(this::addOne) + .map( + i -> { + throw error; + }) + .toList() + .blockingGet())) + .isEqualTo(error); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName(PARENT).hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + @Test + public void basicMaybeCancel() { + createParentSpan( + () -> + Maybe.just(1).toFlowable().map(this::addOne).subscribe(CancellingSubscriber.INSTANCE)); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName(PARENT).hasKind(SpanKind.INTERNAL).hasNoParent())); + } + + @Test + public void basicFlowableCancel() { + createParentSpan( + () -> + Flowable.fromIterable(Ints.asList(5, 6)) + .map(this::addOne) + .subscribe(CancellingSubscriber.INSTANCE)); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName(PARENT).hasKind(SpanKind.INTERNAL).hasNoParent())); + } + + @Test + public void basicSingleCancel() { + createParentSpan( + () -> + Single.just(1).toFlowable().map(this::addOne).subscribe(CancellingSubscriber.INSTANCE)); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName(PARENT).hasKind(SpanKind.INTERNAL).hasNoParent())); + } + + @Test + public void basicCompletableCancel() { + createParentSpan( + () -> + Completable.fromCallable(() -> 1) + .toFlowable() + .subscribe(CancellingSubscriber.INSTANCE)); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName(PARENT).hasKind(SpanKind.INTERNAL).hasNoParent())); + } + + @Test + public void basicObservableCancel() { + createParentSpan( + () -> + Observable.just(1) + .toFlowable(BackpressureStrategy.LATEST) + .map(this::addOne) + .subscribe(CancellingSubscriber.INSTANCE)); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName(PARENT).hasKind(SpanKind.INTERNAL).hasNoParent())); + } + + @Test + public void basicMaybeChain() { + createParentSpan( + () -> + Maybe.just(1) + .map(this::addOne) + .map(this::addOne) + .concatWith(Maybe.just(1).map(this::addOne)) + .toList() + .blockingGet()); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName(PARENT).hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + @Test + public void basicFlowableChain() { + createParentSpan( + () -> + Flowable.fromIterable(Ints.asList(5, 6)) + .map(this::addOne) + .map(this::addOne) + .concatWith(Maybe.just(1).map(this::addOne)) + .toList() + .blockingGet()); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName(PARENT).hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + // Publisher chain spans have the correct parents from subscription time + @Test + public void maybeChainParentSpan() { + Maybe maybe = Maybe.just(42).map(this::addOne).map(this::addTwo); + testing().runWithSpan("trace-parent", () -> maybe.blockingGet()); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("trace-parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)), + span -> + span.hasName(ADD_TWO) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + @Test + public void maybeChainHasSubscriptionContext() { + Integer result = + createParentSpan( + () -> { + Maybe maybe = Maybe.just(1).map(this::addOne); + return testing() + .runWithSpan("intermediate", () -> maybe.map(this::addTwo)) + .blockingGet(); + }); + assertThat(result).isEqualTo(4); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName(PARENT).hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("intermediate") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)), + span -> + span.hasName(ADD_TWO) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + @Test + public void flowableChainHasSubscriptionContext() { + List result = + createParentSpan( + () -> { + Flowable flowable = + Flowable.fromIterable(Ints.asList(1, 2)).map(this::addOne); + return testing() + .runWithSpan("intermediate", () -> flowable.map(this::addTwo)) + .toList() + .blockingGet(); + }); + assertThat(result).contains(4, 5); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName(PARENT).hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("intermediate") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)), + span -> + span.hasName(ADD_TWO) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)), + span -> + span.hasName(ADD_TWO) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + @Test + public void singleChainHasSubscriptionContext() { + Integer result = + createParentSpan( + () -> { + Single single = Single.just(1).map(this::addOne); + return testing() + .runWithSpan("intermediate", () -> single.map(this::addTwo)) + .blockingGet(); + }); + assertThat(result).isEqualTo(4); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName(PARENT).hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("intermediate") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)), + span -> + span.hasName(ADD_TWO) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + @Test + public void observableChainHasSubscriptionContext() { + List result = + createParentSpan( + () -> { + Observable observable = Observable.just(1).map(this::addOne); + return testing() + .runWithSpan("intermediate", () -> observable.map(this::addTwo)) + .toList() + .blockingGet(); + }); + assertThat(result).contains(4); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName(PARENT).hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("intermediate") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)), + span -> + span.hasName(ADD_TWO) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + @ParameterizedTest + @MethodSource("schedulers") + public void flowableMultiResults(Scheduler scheduler) { + List result = + testing() + .runWithSpan( + "flowable root", + () -> { + return Flowable.fromIterable(Ints.asList(1, 2, 3, 4)) + .parallel() + .runOn(scheduler) + .flatMap(num -> Maybe.just(num).map(this::addOne).toFlowable()) + .sequential() + .toList() + .blockingGet(); + }); + assertThat(result.size()).isEqualTo(4); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("flowable root").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)), + span -> + span.hasName(ADD_ONE) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + @ParameterizedTest + @MethodSource("schedulers") + public void maybeMultipleTraceChains(Scheduler scheduler) { + int iterations = 100; + RxJava3ConcurrencyTestHelper.launchAndWait(scheduler, iterations, 60000, testing()); + @SuppressWarnings("unchecked") + Consumer[] assertions = (Consumer[]) new Consumer[iterations]; + for (int i = 0; i < iterations; i++) { + int iteration = i; + assertions[i] = + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("outer") + .hasNoParent() + .hasAttributes(attributeEntry("iteration", iteration)), + span -> + span.hasName("middle") + .hasParent(trace.getSpan(0)) + .hasAttributes(attributeEntry("iteration", iteration)), + span -> + span.hasName("inner") + .hasParent(trace.getSpan(1)) + .hasAttributes(attributeEntry("iteration", iteration))); + } + testing() + .waitAndAssertSortedTraces( + Comparator.comparing( + span -> span.get(0).getAttributes().get(AttributeKey.longKey("iteration"))), + assertions); + testing().clearData(); + } +} diff --git a/instrumentation/rxjava/rxjava-3-common/testing/src/main/java/io/opentelemetry/instrumentation/rxjava/v3/common/AbstractRxJava3WithSpanTest.java b/instrumentation/rxjava/rxjava-3-common/testing/src/main/java/io/opentelemetry/instrumentation/rxjava/v3/common/AbstractRxJava3WithSpanTest.java new file mode 100644 index 000000000000..1c0d50bd2dab --- /dev/null +++ b/instrumentation/rxjava/rxjava-3-common/testing/src/main/java/io/opentelemetry/instrumentation/rxjava/v3/common/AbstractRxJava3WithSpanTest.java @@ -0,0 +1,992 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.rxjava.v3.common; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.CODE_FUNCTION; +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.CODE_NAMESPACE; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.reactivex.rxjava3.core.Completable; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Maybe; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.processors.UnicastProcessor; +import io.reactivex.rxjava3.subjects.CompletableSubject; +import io.reactivex.rxjava3.subjects.MaybeSubject; +import io.reactivex.rxjava3.subjects.SingleSubject; +import io.reactivex.rxjava3.subjects.UnicastSubject; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +public abstract class AbstractRxJava3WithSpanTest { + private static final AttributeKey RXJAVA_CANCELED = + AttributeKey.booleanKey("rxjava.canceled"); + + protected abstract AbstractTracedWithSpan newTraced(); + + protected abstract InstrumentationExtension testing(); + + @Test + public void captureSpanForCompletedCompletable() { + TestObserver observer = new TestObserver<>(); + Completable source = Completable.complete(); + newTraced().completable(source).subscribe(observer); + observer.assertComplete(); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("TracedWithSpan.completable") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasAttributesSatisfyingExactly( + satisfies(CODE_NAMESPACE, val -> val.endsWith(".TracedWithSpan")), + equalTo(CODE_FUNCTION, "completable")))); + } + + @Test + public void captureSpanForEventuallyCompletedCompletable() throws InterruptedException { + CompletableSubject source = CompletableSubject.create(); + TestObserver observer = new TestObserver<>(); + newTraced().completable(source).subscribe(observer); + + // sleep a bit just to make sure no span is captured + Thread.sleep(500); + List> traces = testing().waitForTraces(0); + assertThat(traces).isEmpty(); + + source.onComplete(); + observer.assertComplete(); + + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("TracedWithSpan.completable") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasAttributesSatisfyingExactly( + satisfies(CODE_NAMESPACE, val -> val.endsWith(".TracedWithSpan")), + equalTo(CODE_FUNCTION, "completable")))); + } + + @Test + public void captureSpanForErrorCompletable() { + IllegalStateException error = new IllegalStateException("Boom"); + TestObserver observer = new TestObserver<>(); + Completable source = Completable.error(error); + newTraced().completable(source).subscribe(observer); + observer.assertError(error); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("TracedWithSpan.completable") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasStatus(StatusData.error()) + .hasException(error) + .hasAttributesSatisfyingExactly( + satisfies(CODE_NAMESPACE, val -> val.endsWith(".TracedWithSpan")), + equalTo(CODE_FUNCTION, "completable")))); + } + + @Test + public void captureSpanForEventuallyErrorCompletable() throws InterruptedException { + IllegalStateException error = new IllegalStateException("Boom"); + CompletableSubject source = CompletableSubject.create(); + TestObserver observer = new TestObserver<>(); + newTraced().completable(source).subscribe(observer); + + // sleep a bit just to make sure no span is captured + Thread.sleep(500); + List> traces = testing().waitForTraces(0); + assertThat(traces).isEmpty(); + + source.onError(error); + observer.assertError(error); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("TracedWithSpan.completable") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasStatus(StatusData.error()) + .hasException(error) + .hasAttributesSatisfyingExactly( + satisfies(CODE_NAMESPACE, val -> val.endsWith(".TracedWithSpan")), + equalTo(CODE_FUNCTION, "completable")))); + } + + @Test + public void captureSpanForCanceledCompletable() throws InterruptedException { + CompletableSubject source = CompletableSubject.create(); + TestObserver observer = new TestObserver<>(); + newTraced().completable(source).subscribe(observer); + + // sleep a bit just to make sure no span is captured + Thread.sleep(500); + List> traces = testing().waitForTraces(0); + assertThat(traces).isEmpty(); + + observer.dispose(); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("TracedWithSpan.completable") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasAttributesSatisfyingExactly( + satisfies(CODE_NAMESPACE, val -> val.endsWith(".TracedWithSpan")), + equalTo(CODE_FUNCTION, "completable"), + equalTo(RXJAVA_CANCELED, true)))); + } + + @Test + public void captureSpanForCompletedMaybe() { + Maybe source = Maybe.just("Value"); + TestObserver observer = new TestObserver<>(); + newTraced().maybe(source).subscribe(observer); + observer.assertValue("Value"); + observer.assertComplete(); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("TracedWithSpan.maybe") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasAttributesSatisfyingExactly( + satisfies(CODE_NAMESPACE, val -> val.endsWith(".TracedWithSpan")), + equalTo(CODE_FUNCTION, "maybe")))); + } + + @Test + public void captureSpanForEmptyMaybe() { + Maybe source = Maybe.empty(); + TestObserver observer = new TestObserver<>(); + newTraced().maybe(source).subscribe(observer); + observer.assertComplete(); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("TracedWithSpan.maybe") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasAttributesSatisfyingExactly( + satisfies(CODE_NAMESPACE, val -> val.endsWith(".TracedWithSpan")), + equalTo(CODE_FUNCTION, "maybe")))); + } + + @Test + public void captureSpanForEventuallyCompletedMaybe() throws InterruptedException { + MaybeSubject source = MaybeSubject.create(); + TestObserver observer = new TestObserver<>(); + newTraced().maybe(source).subscribe(observer); + + // sleep a bit just to make sure no span is captured + Thread.sleep(500); + List> traces = testing().waitForTraces(0); + assertThat(traces).isEmpty(); + + source.onSuccess("Value"); + observer.assertValue("Value"); + observer.assertComplete(); + + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("TracedWithSpan.maybe") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasAttributesSatisfyingExactly( + satisfies(CODE_NAMESPACE, val -> val.endsWith(".TracedWithSpan")), + equalTo(CODE_FUNCTION, "maybe")))); + } + + @Test + public void captureSpanForErrorMaybe() { + IllegalStateException error = new IllegalStateException("Boom"); + TestObserver observer = new TestObserver<>(); + Maybe source = Maybe.error(error); + newTraced().maybe(source).subscribe(observer); + observer.assertError(error); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("TracedWithSpan.maybe") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasStatus(StatusData.error()) + .hasException(error) + .hasAttributesSatisfyingExactly( + satisfies(CODE_NAMESPACE, val -> val.endsWith(".TracedWithSpan")), + equalTo(CODE_FUNCTION, "maybe")))); + } + + @Test + public void captureSpanForEventuallyErrorMaybe() throws InterruptedException { + IllegalStateException error = new IllegalStateException("Boom"); + MaybeSubject source = MaybeSubject.create(); + TestObserver observer = new TestObserver<>(); + newTraced().maybe(source).subscribe(observer); + + // sleep a bit just to make sure no span is captured + Thread.sleep(500); + List> traces = testing().waitForTraces(0); + assertThat(traces).isEmpty(); + + source.onError(error); + observer.assertError(error); + + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("TracedWithSpan.maybe") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasStatus(StatusData.error()) + .hasException(error) + .hasAttributesSatisfyingExactly( + satisfies(CODE_NAMESPACE, val -> val.endsWith(".TracedWithSpan")), + equalTo(CODE_FUNCTION, "maybe")))); + } + + @Test + public void captureSpanForCanceledMaybe() throws InterruptedException { + MaybeSubject source = MaybeSubject.create(); + TestObserver observer = new TestObserver<>(); + newTraced().maybe(source).subscribe(observer); + + // sleep a bit just to make sure no span is captured + Thread.sleep(500); + List> traces = testing().waitForTraces(0); + assertThat(traces).isEmpty(); + + observer.dispose(); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("TracedWithSpan.maybe") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasAttributesSatisfyingExactly( + satisfies(CODE_NAMESPACE, val -> val.endsWith(".TracedWithSpan")), + equalTo(CODE_FUNCTION, "maybe"), + equalTo(RXJAVA_CANCELED, true)))); + } + + @Test + public void captureSpanForCompletedSingle() { + Single source = Single.just("Value"); + TestObserver observer = new TestObserver<>(); + newTraced().single(source).subscribe(observer); + observer.assertValue("Value"); + observer.assertComplete(); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("TracedWithSpan.single") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasAttributesSatisfyingExactly( + satisfies(CODE_NAMESPACE, val -> val.endsWith(".TracedWithSpan")), + equalTo(CODE_FUNCTION, "single")))); + } + + @Test + public void captureSpanForEventuallyCompletedSingle() throws InterruptedException { + SingleSubject source = SingleSubject.create(); + TestObserver observer = new TestObserver<>(); + newTraced().single(source).subscribe(observer); + + // sleep a bit just to make sure no span is captured + Thread.sleep(500); + List> traces = testing().waitForTraces(0); + assertThat(traces).isEmpty(); + + source.onSuccess("Value"); + observer.assertValue("Value"); + observer.assertComplete(); + + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("TracedWithSpan.single") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasAttributesSatisfyingExactly( + satisfies(CODE_NAMESPACE, val -> val.endsWith(".TracedWithSpan")), + equalTo(CODE_FUNCTION, "single")))); + } + + @Test + public void captureSpanForErrorSingle() { + IllegalStateException error = new IllegalStateException("Boom"); + TestObserver observer = new TestObserver<>(); + Single source = Single.error(error); + newTraced().single(source).subscribe(observer); + observer.assertError(error); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("TracedWithSpan.single") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasStatus(StatusData.error()) + .hasException(error) + .hasAttributesSatisfyingExactly( + satisfies(CODE_NAMESPACE, val -> val.endsWith(".TracedWithSpan")), + equalTo(CODE_FUNCTION, "single")))); + } + + @Test + public void captureSpanForEventuallyErrorSingle() throws InterruptedException { + IllegalStateException error = new IllegalStateException("Boom"); + SingleSubject source = SingleSubject.create(); + TestObserver observer = new TestObserver<>(); + newTraced().single(source).subscribe(observer); + + // sleep a bit just to make sure no span is captured + Thread.sleep(500); + List> traces = testing().waitForTraces(0); + assertThat(traces).isEmpty(); + + source.onError(error); + observer.assertError(error); + + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("TracedWithSpan.single") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasStatus(StatusData.error()) + .hasException(error) + .hasAttributesSatisfyingExactly( + satisfies(CODE_NAMESPACE, val -> val.endsWith(".TracedWithSpan")), + equalTo(CODE_FUNCTION, "single")))); + } + + @Test + public void captureSpanForCanceledSingle() throws InterruptedException { + SingleSubject source = SingleSubject.create(); + TestObserver observer = new TestObserver<>(); + newTraced().single(source).subscribe(observer); + + // sleep a bit just to make sure no span is captured + Thread.sleep(500); + List> traces = testing().waitForTraces(0); + assertThat(traces).isEmpty(); + + observer.dispose(); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("TracedWithSpan.single") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasAttributesSatisfyingExactly( + satisfies(CODE_NAMESPACE, val -> val.endsWith(".TracedWithSpan")), + equalTo(CODE_FUNCTION, "single"), + equalTo(RXJAVA_CANCELED, true)))); + } + + @Test + public void captureSpanForCompletedObservable() { + TestObserver observer = new TestObserver<>(); + Observable source = Observable.just("Value"); + newTraced().observable(source).subscribe(observer); + observer.assertValue("Value"); + observer.assertComplete(); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("TracedWithSpan.observable") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasAttributesSatisfyingExactly( + satisfies(CODE_NAMESPACE, val -> val.endsWith(".TracedWithSpan")), + equalTo(CODE_FUNCTION, "observable")))); + } + + @Test + public void captureSpanForEventuallyCompletedObservable() throws InterruptedException { + TestObserver observer = new TestObserver<>(); + UnicastSubject source = UnicastSubject.create(); + newTraced().observable(source).subscribe(observer); + + // sleep a bit just to make sure no span is captured + Thread.sleep(500); + List> traces = testing().waitForTraces(0); + assertThat(traces).isEmpty(); + + source.onNext("Value"); + observer.assertValue("Value"); + + // sleep a bit just to make sure no span is captured + Thread.sleep(500); + traces = testing().waitForTraces(0); + assertThat(traces).isEmpty(); + + source.onComplete(); + observer.assertComplete(); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("TracedWithSpan.observable") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasAttributesSatisfyingExactly( + satisfies(CODE_NAMESPACE, val -> val.endsWith(".TracedWithSpan")), + equalTo(CODE_FUNCTION, "observable")))); + } + + @Test + public void captureSpanForErrorObservable() { + IllegalStateException error = new IllegalStateException("Boom"); + Observable source = Observable.error(error); + TestObserver observer = new TestObserver<>(); + newTraced().observable(source).subscribe(observer); + observer.assertError(error); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("TracedWithSpan.observable") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasStatus(StatusData.error()) + .hasException(error) + .hasAttributesSatisfyingExactly( + satisfies(CODE_NAMESPACE, val -> val.endsWith(".TracedWithSpan")), + equalTo(CODE_FUNCTION, "observable")))); + } + + @Test + public void captureSpanForEventuallyErrorObservable() throws InterruptedException { + IllegalStateException error = new IllegalStateException("Boom"); + UnicastSubject source = UnicastSubject.create(); + TestObserver observer = new TestObserver<>(); + newTraced().observable(source).subscribe(observer); + + // sleep a bit just to make sure no span is captured + Thread.sleep(500); + List> traces = testing().waitForTraces(0); + assertThat(traces).isEmpty(); + + source.onNext("Value"); + observer.assertValue("Value"); + + // sleep a bit just to make sure no span is captured + Thread.sleep(500); + traces = testing().waitForTraces(0); + assertThat(traces).isEmpty(); + + source.onError(error); + observer.assertError(error); + + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("TracedWithSpan.observable") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasStatus(StatusData.error()) + .hasException(error) + .hasAttributesSatisfyingExactly( + satisfies(CODE_NAMESPACE, val -> val.endsWith(".TracedWithSpan")), + equalTo(CODE_FUNCTION, "observable")))); + } + + @Test + public void captureSpanForCanceledObservable() throws InterruptedException { + UnicastSubject source = UnicastSubject.create(); + TestObserver observer = new TestObserver<>(); + newTraced().observable(source).subscribe(observer); + + // sleep a bit just to make sure no span is captured + Thread.sleep(500); + List> traces = testing().waitForTraces(0); + assertThat(traces).isEmpty(); + + source.onNext("Value"); + observer.assertValue("Value"); + + // sleep a bit just to make sure no span is captured + Thread.sleep(500); + traces = testing().waitForTraces(0); + assertThat(traces).isEmpty(); + + observer.dispose(); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("TracedWithSpan.observable") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasAttributesSatisfyingExactly( + satisfies(CODE_NAMESPACE, val -> val.endsWith(".TracedWithSpan")), + equalTo(CODE_FUNCTION, "observable"), + equalTo(RXJAVA_CANCELED, true)))); + } + + @Test + public void captureSpanForCompletedFlowable() { + TestSubscriber observe = new TestSubscriber<>(); + Flowable source = Flowable.just("Value"); + newTraced().flowable(source).subscribe(observe); + observe.assertValue("Value"); + observe.assertComplete(); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("TracedWithSpan.flowable") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasAttributesSatisfyingExactly( + satisfies(CODE_NAMESPACE, val -> val.endsWith(".TracedWithSpan")), + equalTo(CODE_FUNCTION, "flowable")))); + } + + @Test + public void captureForEventuallyCompletedFlowable() throws InterruptedException { + UnicastProcessor source = UnicastProcessor.create(); + TestSubscriber observer = new TestSubscriber<>(); + newTraced().flowable(source).subscribe(observer); + + // sleep a bit just to make sure no span is captured + Thread.sleep(500); + List> traces = testing().waitForTraces(0); + assertThat(traces).isEmpty(); + + source.onNext("Value"); + observer.assertValue("Value"); + + // sleep a bit just to make sure no span is captured + Thread.sleep(500); + traces = testing().waitForTraces(0); + assertThat(traces).isEmpty(); + + source.onComplete(); + observer.assertComplete(); + + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("TracedWithSpan.flowable") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasAttributesSatisfyingExactly( + satisfies(CODE_NAMESPACE, val -> val.endsWith(".TracedWithSpan")), + equalTo(CODE_FUNCTION, "flowable")))); + } + + @Test + public void captureSpanForErrorFlowable() { + IllegalStateException error = new IllegalStateException("Boom"); + TestSubscriber observer = new TestSubscriber<>(); + Flowable source = Flowable.error(error); + newTraced().flowable(source).subscribe(observer); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("TracedWithSpan.flowable") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasStatus(StatusData.error()) + .hasException(error) + .hasAttributesSatisfyingExactly( + satisfies(CODE_NAMESPACE, val -> val.endsWith(".TracedWithSpan")), + equalTo(CODE_FUNCTION, "flowable")))); + } + + @Test + public void captureSpanForEventuallyErrorFlowable() throws InterruptedException { + IllegalStateException error = new IllegalStateException("Boom"); + UnicastProcessor source = UnicastProcessor.create(); + TestSubscriber observer = new TestSubscriber<>(); + newTraced().flowable(source).subscribe(observer); + + // sleep a bit just to make sure no span is captured + Thread.sleep(500); + List> traces = testing().waitForTraces(0); + assertThat(traces).isEmpty(); + + source.onNext("Value"); + observer.assertValue("Value"); + + // sleep a bit just to make sure no span is captured + Thread.sleep(500); + traces = testing().waitForTraces(0); + assertThat(traces).isEmpty(); + + source.onError(error); + observer.assertError(error); + + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("TracedWithSpan.flowable") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasStatus(StatusData.error()) + .hasException(error) + .hasAttributesSatisfyingExactly( + satisfies(CODE_NAMESPACE, val -> val.endsWith(".TracedWithSpan")), + equalTo(CODE_FUNCTION, "flowable")))); + } + + @Test + public void captureSpanForCanceledFlowable() throws InterruptedException { + UnicastProcessor source = UnicastProcessor.create(); + TestSubscriber observer = new TestSubscriber<>(); + newTraced().flowable(source).subscribe(observer); + + // sleep a bit just to make sure no span is captured + Thread.sleep(500); + List> traces = testing().waitForTraces(0); + assertThat(traces).isEmpty(); + + source.onNext("Value"); + observer.assertValue("Value"); + + // sleep a bit just to make sure no span is captured + Thread.sleep(500); + traces = testing().waitForTraces(0); + assertThat(traces).isEmpty(); + + observer.cancel(); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("TracedWithSpan.flowable") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasAttributesSatisfyingExactly( + satisfies(CODE_NAMESPACE, val -> val.endsWith(".TracedWithSpan")), + equalTo(CODE_FUNCTION, "flowable"), + equalTo(RXJAVA_CANCELED, true)))); + } + + @Test + public void captureSpanForCompletedParallelFlowable() { + Flowable source = Flowable.just("Value"); + TestSubscriber observer = new TestSubscriber<>(); + newTraced().parallelFlowable(source.parallel()).sequential().subscribe(observer); + observer.assertValue("Value"); + observer.assertComplete(); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("TracedWithSpan.parallelFlowable") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasAttributesSatisfyingExactly( + satisfies(CODE_NAMESPACE, val -> val.endsWith(".TracedWithSpan")), + equalTo(CODE_FUNCTION, "parallelFlowable")))); + } + + @Test + public void captureSpanForEventuallyCompletedParallelFlowable() throws InterruptedException { + UnicastProcessor source = UnicastProcessor.create(); + TestSubscriber observer = new TestSubscriber<>(); + newTraced().parallelFlowable(source.parallel()).sequential().subscribe(observer); + + // sleep a bit just to make sure no span is captured + Thread.sleep(500); + List> traces = testing().waitForTraces(0); + assertThat(traces).isEmpty(); + + source.onNext("Value"); + observer.assertValue("Value"); + + // sleep a bit just to make sure no span is captured + Thread.sleep(500); + traces = testing().waitForTraces(0); + assertThat(traces).isEmpty(); + + source.onComplete(); + observer.assertComplete(); + + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("TracedWithSpan.parallelFlowable") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasAttributesSatisfyingExactly( + satisfies(CODE_NAMESPACE, val -> val.endsWith(".TracedWithSpan")), + equalTo(CODE_FUNCTION, "parallelFlowable")))); + } + + @Test + public void captureSpanForErrorParallelFlowable() { + IllegalStateException error = new IllegalStateException("Boom"); + TestSubscriber observer = new TestSubscriber<>(); + Flowable source = Flowable.error(error); + newTraced().parallelFlowable(source.parallel()).sequential().subscribe(observer); + observer.assertError(error); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("TracedWithSpan.parallelFlowable") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasStatus(StatusData.error()) + .hasException(error) + .hasAttributesSatisfyingExactly( + satisfies(CODE_NAMESPACE, val -> val.endsWith(".TracedWithSpan")), + equalTo(CODE_FUNCTION, "parallelFlowable")))); + } + + @Test + public void captureSpanForEventuallyErrorParallelFlowable() throws InterruptedException { + IllegalStateException error = new IllegalStateException("Boom"); + TestSubscriber observer = new TestSubscriber<>(); + UnicastProcessor source = UnicastProcessor.create(); + newTraced().parallelFlowable(source.parallel()).sequential().subscribe(observer); + + // sleep a bit just to make sure no span is captured + Thread.sleep(500); + List> traces = testing().waitForTraces(0); + assertThat(traces).isEmpty(); + + source.onNext("Value"); + observer.assertValue("Value"); + + // sleep a bit just to make sure no span is captured + Thread.sleep(500); + traces = testing().waitForTraces(0); + assertThat(traces).isEmpty(); + + source.onError(error); + observer.assertError(error); + + observer.assertError(error); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("TracedWithSpan.parallelFlowable") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasStatus(StatusData.error()) + .hasException(error) + .hasAttributesSatisfyingExactly( + satisfies(CODE_NAMESPACE, val -> val.endsWith(".TracedWithSpan")), + equalTo(CODE_FUNCTION, "parallelFlowable")))); + } + + @Test + public void captureSpanForCanceledParallelFlowable() throws InterruptedException { + TestSubscriber observer = new TestSubscriber<>(); + UnicastProcessor source = UnicastProcessor.create(); + newTraced().parallelFlowable(source.parallel()).sequential().subscribe(observer); + + // sleep a bit just to make sure no span is captured + Thread.sleep(500); + List> traces = testing().waitForTraces(0); + assertThat(traces).isEmpty(); + + source.onNext("Value"); + observer.assertValue("Value"); + + // sleep a bit just to make sure no span is captured + Thread.sleep(500); + traces = testing().waitForTraces(0); + assertThat(traces).isEmpty(); + + observer.cancel(); + + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("TracedWithSpan.parallelFlowable") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasAttributesSatisfyingExactly( + satisfies(CODE_NAMESPACE, val -> val.endsWith(".TracedWithSpan")), + equalTo(CODE_FUNCTION, "parallelFlowable"), + equalTo(RXJAVA_CANCELED, true)))); + } + + @Test + public void captureSpanForEventuallyCompletedPublisher() throws InterruptedException { + CustomPublisher source = new CustomPublisher(); + TestSubscriber observer = new TestSubscriber<>(); + newTraced().publisher(source).subscribe(observer); + + // sleep a bit just to make sure no span is captured + Thread.sleep(500); + List> traces = testing().waitForTraces(0); + assertThat(traces).isEmpty(); + + source.onComplete(); + observer.assertComplete(); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("TracedWithSpan.publisher") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasAttributesSatisfyingExactly( + satisfies(CODE_NAMESPACE, val -> val.endsWith(".TracedWithSpan")), + equalTo(CODE_FUNCTION, "publisher")))); + } + + @Test + public void captureSpanForEventuallyErrorPublisher() throws InterruptedException { + IllegalStateException error = new IllegalStateException("Boom"); + CustomPublisher source = new CustomPublisher(); + TestSubscriber observer = new TestSubscriber<>(); + newTraced().publisher(source).subscribe(observer); + + // sleep a bit just to make sure no span is captured + Thread.sleep(500); + List> traces = testing().waitForTraces(0); + assertThat(traces).isEmpty(); + + source.onError(error); + observer.assertError(error); + + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("TracedWithSpan.publisher") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasStatus(StatusData.error()) + .hasException(error) + .hasAttributesSatisfyingExactly( + satisfies(CODE_NAMESPACE, val -> val.endsWith(".TracedWithSpan")), + equalTo(CODE_FUNCTION, "publisher")))); + } + + @Test + public void captureSpanForCanceledPublisher() throws InterruptedException { + CustomPublisher source = new CustomPublisher(); + TestSubscriber observer = new TestSubscriber<>(); + newTraced().publisher(source).subscribe(observer); + + // sleep a bit just to make sure no span is captured + Thread.sleep(500); + List> traces = testing().waitForTraces(0); + assertThat(traces).isEmpty(); + + observer.cancel(); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("TracedWithSpan.publisher") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasAttributesSatisfyingExactly( + satisfies(CODE_NAMESPACE, val -> val.endsWith(".TracedWithSpan")), + equalTo(CODE_FUNCTION, "publisher"), + equalTo(RXJAVA_CANCELED, true)))); + } + + static class CustomPublisher implements Publisher, Subscription { + + Subscriber subscriber; + + @Override + public void subscribe(Subscriber subscriber) { + this.subscriber = subscriber; + subscriber.onSubscribe(this); + } + + void onComplete() { + this.subscriber.onComplete(); + } + + void onError(Throwable exception) { + this.subscriber.onError(exception); + } + + @Override + public void request(long l) {} + + @Override + public void cancel() {} + } +} diff --git a/instrumentation/rxjava/rxjava-3-common/testing/src/main/groovy/io/opentelemetry/instrumentation/rxjava/v3/common/RxJava3ConcurrencyTestHelper.java b/instrumentation/rxjava/rxjava-3-common/testing/src/main/java/io/opentelemetry/instrumentation/rxjava/v3/common/RxJava3ConcurrencyTestHelper.java similarity index 94% rename from instrumentation/rxjava/rxjava-3-common/testing/src/main/groovy/io/opentelemetry/instrumentation/rxjava/v3/common/RxJava3ConcurrencyTestHelper.java rename to instrumentation/rxjava/rxjava-3-common/testing/src/main/java/io/opentelemetry/instrumentation/rxjava/v3/common/RxJava3ConcurrencyTestHelper.java index 5625c98ae6f2..9d1ddb055d9c 100644 --- a/instrumentation/rxjava/rxjava-3-common/testing/src/main/groovy/io/opentelemetry/instrumentation/rxjava/v3/common/RxJava3ConcurrencyTestHelper.java +++ b/instrumentation/rxjava/rxjava-3-common/testing/src/main/java/io/opentelemetry/instrumentation/rxjava/v3/common/RxJava3ConcurrencyTestHelper.java @@ -6,7 +6,7 @@ package io.opentelemetry.instrumentation.rxjava.v3.common; import io.opentelemetry.api.trace.Span; -import io.opentelemetry.instrumentation.testing.InstrumentationTestRunner; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.reactivex.rxjava3.core.Scheduler; import io.reactivex.rxjava3.core.Single; import java.util.concurrent.CountDownLatch; @@ -23,8 +23,10 @@ * from different traces. */ public class RxJava3ConcurrencyTestHelper { + private RxJava3ConcurrencyTestHelper() {} + public static void launchAndWait( - Scheduler scheduler, int iterations, long timeoutMillis, InstrumentationTestRunner runner) { + Scheduler scheduler, int iterations, long timeoutMillis, InstrumentationExtension runner) { CountDownLatch latch = new CountDownLatch(iterations); for (int i = 0; i < iterations; i++) { @@ -40,7 +42,7 @@ public static void launchAndWait( } } - private static void launchOuter(Iteration iteration, InstrumentationTestRunner runner) { + private static void launchOuter(Iteration iteration, InstrumentationExtension runner) { runner.runWithSpan( "outer", () -> { @@ -58,7 +60,7 @@ private static void launchOuter(Iteration iteration, InstrumentationTestRunner r }); } - private static void launchInner(Iteration iteration, InstrumentationTestRunner runner) { + private static void launchInner(Iteration iteration, InstrumentationExtension runner) { runner.runWithSpan( "middle", () -> { diff --git a/instrumentation/rxjava/rxjava-3.0/javaagent/src/test/groovy/RxJava3ExtensionWithSpanTest.groovy b/instrumentation/rxjava/rxjava-3.0/javaagent/src/test/groovy/RxJava3ExtensionWithSpanTest.groovy deleted file mode 100644 index e910848831e3..000000000000 --- a/instrumentation/rxjava/rxjava-3.0/javaagent/src/test/groovy/RxJava3ExtensionWithSpanTest.groovy +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.rxjava.v3.common.AbstractRxJava3WithSpanTest -import io.opentelemetry.instrumentation.rxjava.v3.common.AbstractTracedWithSpan -import io.opentelemetry.instrumentation.rxjava.v3.common.extensionannotation.TracedWithSpan - -class RxJava3ExtensionWithSpanTest extends AbstractRxJava3WithSpanTest { - - @Override - AbstractTracedWithSpan newTraced() { - return new TracedWithSpan() - } -} diff --git a/instrumentation/rxjava/rxjava-3.0/javaagent/src/test/groovy/RxJava3InstrumentationWithSpanTest.groovy b/instrumentation/rxjava/rxjava-3.0/javaagent/src/test/groovy/RxJava3InstrumentationWithSpanTest.groovy deleted file mode 100644 index 68938741d3b9..000000000000 --- a/instrumentation/rxjava/rxjava-3.0/javaagent/src/test/groovy/RxJava3InstrumentationWithSpanTest.groovy +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.rxjava.v3.common.AbstractRxJava3WithSpanTest -import io.opentelemetry.instrumentation.rxjava.v3.common.AbstractTracedWithSpan -import io.opentelemetry.instrumentation.rxjava.v3.common.instrumentationannotation.TracedWithSpan - -class RxJava3InstrumentationWithSpanTest extends AbstractRxJava3WithSpanTest { - - @Override - AbstractTracedWithSpan newTraced() { - return new TracedWithSpan() - } -} diff --git a/instrumentation/rxjava/rxjava-3.0/javaagent/src/test/groovy/RxJava3SubscriptionTest.groovy b/instrumentation/rxjava/rxjava-3.0/javaagent/src/test/groovy/RxJava3SubscriptionTest.groovy deleted file mode 100644 index b46f33bcc050..000000000000 --- a/instrumentation/rxjava/rxjava-3.0/javaagent/src/test/groovy/RxJava3SubscriptionTest.groovy +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.rxjava.v3.common.AbstractRxJava3SubscriptionTest -import io.opentelemetry.instrumentation.test.AgentTestTrait - -class RxJava3SubscriptionTest extends AbstractRxJava3SubscriptionTest implements AgentTestTrait { -} diff --git a/instrumentation/rxjava/rxjava-3.0/javaagent/src/test/groovy/RxJava3Test.groovy b/instrumentation/rxjava/rxjava-3.0/javaagent/src/test/groovy/RxJava3Test.groovy deleted file mode 100644 index 706aae82a6f9..000000000000 --- a/instrumentation/rxjava/rxjava-3.0/javaagent/src/test/groovy/RxJava3Test.groovy +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.rxjava.v3.common.AbstractRxJava3Test -import io.opentelemetry.instrumentation.test.AgentTestTrait - -class RxJava3Test extends AbstractRxJava3Test implements AgentTestTrait { -} diff --git a/instrumentation/rxjava/rxjava-3.0/javaagent/src/test/java/RxJava3ExtensionWithSpanTest.java b/instrumentation/rxjava/rxjava-3.0/javaagent/src/test/java/RxJava3ExtensionWithSpanTest.java new file mode 100644 index 000000000000..47deb1fd7cef --- /dev/null +++ b/instrumentation/rxjava/rxjava-3.0/javaagent/src/test/java/RxJava3ExtensionWithSpanTest.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.rxjava.v3.common.AbstractRxJava3WithSpanTest; +import io.opentelemetry.instrumentation.rxjava.v3.common.AbstractTracedWithSpan; +import io.opentelemetry.instrumentation.rxjava.v3.common.extensionannotation.TracedWithSpan; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class RxJava3ExtensionWithSpanTest extends AbstractRxJava3WithSpanTest { + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Override + protected AbstractTracedWithSpan newTraced() { + return new TracedWithSpan(); + } + + @Override + protected InstrumentationExtension testing() { + return testing; + } +} diff --git a/instrumentation/rxjava/rxjava-3.0/javaagent/src/test/java/RxJava3InstrumentationWithSpanTest.java b/instrumentation/rxjava/rxjava-3.0/javaagent/src/test/java/RxJava3InstrumentationWithSpanTest.java new file mode 100644 index 000000000000..3e55565a3632 --- /dev/null +++ b/instrumentation/rxjava/rxjava-3.0/javaagent/src/test/java/RxJava3InstrumentationWithSpanTest.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.rxjava.v3.common.AbstractRxJava3WithSpanTest; +import io.opentelemetry.instrumentation.rxjava.v3.common.AbstractTracedWithSpan; +import io.opentelemetry.instrumentation.rxjava.v3.common.extensionannotation.TracedWithSpan; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class RxJava3InstrumentationWithSpanTest extends AbstractRxJava3WithSpanTest { + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Override + protected AbstractTracedWithSpan newTraced() { + return new TracedWithSpan(); + } + + @Override + protected InstrumentationExtension testing() { + return testing; + } +} diff --git a/instrumentation/rxjava/rxjava-3.0/javaagent/src/test/java/RxJava3SubscriptionTest.java b/instrumentation/rxjava/rxjava-3.0/javaagent/src/test/java/RxJava3SubscriptionTest.java new file mode 100644 index 000000000000..95ce51a2b612 --- /dev/null +++ b/instrumentation/rxjava/rxjava-3.0/javaagent/src/test/java/RxJava3SubscriptionTest.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.rxjava.v3.common.AbstractRxJava3SubscriptionTest; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class RxJava3SubscriptionTest extends AbstractRxJava3SubscriptionTest { + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Override + protected InstrumentationExtension testing() { + return testing; + } +} diff --git a/instrumentation/rxjava/rxjava-3.0/javaagent/src/test/java/RxJava3Test.java b/instrumentation/rxjava/rxjava-3.0/javaagent/src/test/java/RxJava3Test.java new file mode 100644 index 000000000000..9eb2fdfadb51 --- /dev/null +++ b/instrumentation/rxjava/rxjava-3.0/javaagent/src/test/java/RxJava3Test.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.rxjava.v3.common.AbstractRxJava3Test; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class RxJava3Test extends AbstractRxJava3Test { + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Override + protected InstrumentationExtension testing() { + return testing; + } +} diff --git a/instrumentation/rxjava/rxjava-3.0/library/src/test/groovy/RxJava3SubscriptionTest.groovy b/instrumentation/rxjava/rxjava-3.0/library/src/test/groovy/RxJava3SubscriptionTest.groovy deleted file mode 100644 index e855b04f4e16..000000000000 --- a/instrumentation/rxjava/rxjava-3.0/library/src/test/groovy/RxJava3SubscriptionTest.groovy +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.rxjava.v3.common.AbstractRxJava3SubscriptionTest -import io.opentelemetry.instrumentation.rxjava.v3_0.TracingAssembly -import io.opentelemetry.instrumentation.test.LibraryTestTrait -import spock.lang.Shared - -class RxJava3SubscriptionTest extends AbstractRxJava3SubscriptionTest implements LibraryTestTrait { - @Shared - TracingAssembly tracingAssembly = TracingAssembly.create() - - def setupSpec() { - tracingAssembly.enable() - } -} diff --git a/instrumentation/rxjava/rxjava-3.0/library/src/test/groovy/RxJava3Test.groovy b/instrumentation/rxjava/rxjava-3.0/library/src/test/groovy/RxJava3Test.groovy deleted file mode 100644 index 9bfdf0fd2195..000000000000 --- a/instrumentation/rxjava/rxjava-3.0/library/src/test/groovy/RxJava3Test.groovy +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.rxjava.v3.common.AbstractRxJava3Test -import io.opentelemetry.instrumentation.rxjava.v3_0.TracingAssembly -import io.opentelemetry.instrumentation.test.LibraryTestTrait -import spock.lang.Shared - -class RxJava3Test extends AbstractRxJava3Test implements LibraryTestTrait { - @Shared - TracingAssembly tracingAssembly = TracingAssembly.create() - - def setupSpec() { - tracingAssembly.enable() - } -} diff --git a/instrumentation/rxjava/rxjava-3.0/library/src/test/java/RxJava3SubscriptionTest.java b/instrumentation/rxjava/rxjava-3.0/library/src/test/java/RxJava3SubscriptionTest.java new file mode 100644 index 000000000000..7e2479a7aa60 --- /dev/null +++ b/instrumentation/rxjava/rxjava-3.0/library/src/test/java/RxJava3SubscriptionTest.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.rxjava.v3.common.AbstractRxJava3SubscriptionTest; +import io.opentelemetry.instrumentation.rxjava.v3_0.TracingAssembly; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class RxJava3SubscriptionTest extends AbstractRxJava3SubscriptionTest { + + @RegisterExtension + static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + @Override + protected InstrumentationExtension testing() { + return testing; + } + + static TracingAssembly tracingAssembly = TracingAssembly.create(); + + @BeforeAll + public static void setupSpec() { + tracingAssembly.enable(); + } +} diff --git a/instrumentation/rxjava/rxjava-3.0/library/src/test/java/RxJava3Test.java b/instrumentation/rxjava/rxjava-3.0/library/src/test/java/RxJava3Test.java new file mode 100644 index 000000000000..68cd95fb1f66 --- /dev/null +++ b/instrumentation/rxjava/rxjava-3.0/library/src/test/java/RxJava3Test.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.rxjava.v3.common.AbstractRxJava3Test; +import io.opentelemetry.instrumentation.rxjava.v3_0.TracingAssembly; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class RxJava3Test extends AbstractRxJava3Test { + @RegisterExtension + static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + static TracingAssembly tracingAssembly = TracingAssembly.create(); + + @Override + protected InstrumentationExtension testing() { + return testing; + } + + @BeforeAll + public void setupSpec() { + tracingAssembly.enable(); + } +} diff --git a/instrumentation/rxjava/rxjava-3.1.1/javaagent/src/test/groovy/RxJava3ExtensionWithSpanTest.groovy b/instrumentation/rxjava/rxjava-3.1.1/javaagent/src/test/groovy/RxJava3ExtensionWithSpanTest.groovy deleted file mode 100644 index e910848831e3..000000000000 --- a/instrumentation/rxjava/rxjava-3.1.1/javaagent/src/test/groovy/RxJava3ExtensionWithSpanTest.groovy +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.rxjava.v3.common.AbstractRxJava3WithSpanTest -import io.opentelemetry.instrumentation.rxjava.v3.common.AbstractTracedWithSpan -import io.opentelemetry.instrumentation.rxjava.v3.common.extensionannotation.TracedWithSpan - -class RxJava3ExtensionWithSpanTest extends AbstractRxJava3WithSpanTest { - - @Override - AbstractTracedWithSpan newTraced() { - return new TracedWithSpan() - } -} diff --git a/instrumentation/rxjava/rxjava-3.1.1/javaagent/src/test/groovy/RxJava3InstrumentationWithSpanTest.groovy b/instrumentation/rxjava/rxjava-3.1.1/javaagent/src/test/groovy/RxJava3InstrumentationWithSpanTest.groovy deleted file mode 100644 index 68938741d3b9..000000000000 --- a/instrumentation/rxjava/rxjava-3.1.1/javaagent/src/test/groovy/RxJava3InstrumentationWithSpanTest.groovy +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.rxjava.v3.common.AbstractRxJava3WithSpanTest -import io.opentelemetry.instrumentation.rxjava.v3.common.AbstractTracedWithSpan -import io.opentelemetry.instrumentation.rxjava.v3.common.instrumentationannotation.TracedWithSpan - -class RxJava3InstrumentationWithSpanTest extends AbstractRxJava3WithSpanTest { - - @Override - AbstractTracedWithSpan newTraced() { - return new TracedWithSpan() - } -} diff --git a/instrumentation/rxjava/rxjava-3.1.1/javaagent/src/test/groovy/RxJava3SubscriptionTest.groovy b/instrumentation/rxjava/rxjava-3.1.1/javaagent/src/test/groovy/RxJava3SubscriptionTest.groovy deleted file mode 100644 index b46f33bcc050..000000000000 --- a/instrumentation/rxjava/rxjava-3.1.1/javaagent/src/test/groovy/RxJava3SubscriptionTest.groovy +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.rxjava.v3.common.AbstractRxJava3SubscriptionTest -import io.opentelemetry.instrumentation.test.AgentTestTrait - -class RxJava3SubscriptionTest extends AbstractRxJava3SubscriptionTest implements AgentTestTrait { -} diff --git a/instrumentation/rxjava/rxjava-3.1.1/javaagent/src/test/groovy/RxJava3Test.groovy b/instrumentation/rxjava/rxjava-3.1.1/javaagent/src/test/groovy/RxJava3Test.groovy deleted file mode 100644 index 706aae82a6f9..000000000000 --- a/instrumentation/rxjava/rxjava-3.1.1/javaagent/src/test/groovy/RxJava3Test.groovy +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.rxjava.v3.common.AbstractRxJava3Test -import io.opentelemetry.instrumentation.test.AgentTestTrait - -class RxJava3Test extends AbstractRxJava3Test implements AgentTestTrait { -} diff --git a/instrumentation/rxjava/rxjava-3.1.1/javaagent/src/test/java/RxJava3ExtensionWithSpanTest.java b/instrumentation/rxjava/rxjava-3.1.1/javaagent/src/test/java/RxJava3ExtensionWithSpanTest.java new file mode 100644 index 000000000000..47deb1fd7cef --- /dev/null +++ b/instrumentation/rxjava/rxjava-3.1.1/javaagent/src/test/java/RxJava3ExtensionWithSpanTest.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.rxjava.v3.common.AbstractRxJava3WithSpanTest; +import io.opentelemetry.instrumentation.rxjava.v3.common.AbstractTracedWithSpan; +import io.opentelemetry.instrumentation.rxjava.v3.common.extensionannotation.TracedWithSpan; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class RxJava3ExtensionWithSpanTest extends AbstractRxJava3WithSpanTest { + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Override + protected AbstractTracedWithSpan newTraced() { + return new TracedWithSpan(); + } + + @Override + protected InstrumentationExtension testing() { + return testing; + } +} diff --git a/instrumentation/rxjava/rxjava-3.1.1/javaagent/src/test/java/RxJava3InstrumentationWithSpanTest.java b/instrumentation/rxjava/rxjava-3.1.1/javaagent/src/test/java/RxJava3InstrumentationWithSpanTest.java new file mode 100644 index 000000000000..3e55565a3632 --- /dev/null +++ b/instrumentation/rxjava/rxjava-3.1.1/javaagent/src/test/java/RxJava3InstrumentationWithSpanTest.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.rxjava.v3.common.AbstractRxJava3WithSpanTest; +import io.opentelemetry.instrumentation.rxjava.v3.common.AbstractTracedWithSpan; +import io.opentelemetry.instrumentation.rxjava.v3.common.extensionannotation.TracedWithSpan; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class RxJava3InstrumentationWithSpanTest extends AbstractRxJava3WithSpanTest { + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Override + protected AbstractTracedWithSpan newTraced() { + return new TracedWithSpan(); + } + + @Override + protected InstrumentationExtension testing() { + return testing; + } +} diff --git a/instrumentation/rxjava/rxjava-3.1.1/javaagent/src/test/java/RxJava3SubscriptionTest.java b/instrumentation/rxjava/rxjava-3.1.1/javaagent/src/test/java/RxJava3SubscriptionTest.java new file mode 100644 index 000000000000..95ce51a2b612 --- /dev/null +++ b/instrumentation/rxjava/rxjava-3.1.1/javaagent/src/test/java/RxJava3SubscriptionTest.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.rxjava.v3.common.AbstractRxJava3SubscriptionTest; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class RxJava3SubscriptionTest extends AbstractRxJava3SubscriptionTest { + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Override + protected InstrumentationExtension testing() { + return testing; + } +} diff --git a/instrumentation/rxjava/rxjava-3.1.1/javaagent/src/test/java/RxJava3Test.java b/instrumentation/rxjava/rxjava-3.1.1/javaagent/src/test/java/RxJava3Test.java new file mode 100644 index 000000000000..9eb2fdfadb51 --- /dev/null +++ b/instrumentation/rxjava/rxjava-3.1.1/javaagent/src/test/java/RxJava3Test.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.rxjava.v3.common.AbstractRxJava3Test; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class RxJava3Test extends AbstractRxJava3Test { + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Override + protected InstrumentationExtension testing() { + return testing; + } +} diff --git a/instrumentation/rxjava/rxjava-3.1.1/library/src/test/groovy/RxJava3SubscriptionTest.groovy b/instrumentation/rxjava/rxjava-3.1.1/library/src/test/groovy/RxJava3SubscriptionTest.groovy deleted file mode 100644 index aa0117118c15..000000000000 --- a/instrumentation/rxjava/rxjava-3.1.1/library/src/test/groovy/RxJava3SubscriptionTest.groovy +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.rxjava.v3.common.AbstractRxJava3SubscriptionTest -import io.opentelemetry.instrumentation.rxjava.v3_1_1.TracingAssembly -import io.opentelemetry.instrumentation.test.LibraryTestTrait -import spock.lang.Shared - -class RxJava3SubscriptionTest extends AbstractRxJava3SubscriptionTest implements LibraryTestTrait { - @Shared - TracingAssembly tracingAssembly = TracingAssembly.create() - - def setupSpec() { - tracingAssembly.enable() - } -} diff --git a/instrumentation/rxjava/rxjava-3.1.1/library/src/test/groovy/RxJava3Test.groovy b/instrumentation/rxjava/rxjava-3.1.1/library/src/test/groovy/RxJava3Test.groovy deleted file mode 100644 index a479bec24fd4..000000000000 --- a/instrumentation/rxjava/rxjava-3.1.1/library/src/test/groovy/RxJava3Test.groovy +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.rxjava.v3.common.AbstractRxJava3Test -import io.opentelemetry.instrumentation.rxjava.v3_1_1.TracingAssembly -import io.opentelemetry.instrumentation.test.LibraryTestTrait -import spock.lang.Shared - -class RxJava3Test extends AbstractRxJava3Test implements LibraryTestTrait { - @Shared - TracingAssembly tracingAssembly = TracingAssembly.create() - - def setupSpec() { - tracingAssembly.enable() - } -} diff --git a/instrumentation/rxjava/rxjava-3.1.1/library/src/test/java/RxJava3SubscriptionTest.java b/instrumentation/rxjava/rxjava-3.1.1/library/src/test/java/RxJava3SubscriptionTest.java new file mode 100644 index 000000000000..43f6e3988d4a --- /dev/null +++ b/instrumentation/rxjava/rxjava-3.1.1/library/src/test/java/RxJava3SubscriptionTest.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.rxjava.v3.common.AbstractRxJava3SubscriptionTest; +import io.opentelemetry.instrumentation.rxjava.v3_1_1.TracingAssembly; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class RxJava3SubscriptionTest extends AbstractRxJava3SubscriptionTest { + + @RegisterExtension + static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + @Override + protected InstrumentationExtension testing() { + return testing; + } + + static TracingAssembly tracingAssembly = TracingAssembly.create(); + + @BeforeAll + public static void setupSpec() { + tracingAssembly.enable(); + } +} diff --git a/instrumentation/rxjava/rxjava-3.1.1/library/src/test/java/RxJava3Test.java b/instrumentation/rxjava/rxjava-3.1.1/library/src/test/java/RxJava3Test.java new file mode 100644 index 000000000000..efb2f9f960a1 --- /dev/null +++ b/instrumentation/rxjava/rxjava-3.1.1/library/src/test/java/RxJava3Test.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.rxjava.v3.common.AbstractRxJava3Test; +import io.opentelemetry.instrumentation.rxjava.v3_1_1.TracingAssembly; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class RxJava3Test extends AbstractRxJava3Test { + @RegisterExtension + static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + static TracingAssembly tracingAssembly = TracingAssembly.create(); + + @Override + protected InstrumentationExtension testing() { + return testing; + } + + @BeforeAll + public void setupSpec() { + tracingAssembly.enable(); + } +}