Merge branch 'twilio' of github.com:darylrobbins/dd-trace-java into twilio

* 'twilio' of github.com:darylrobbins/dd-trace-java:
  Updates to handle async calls, which have broken all tests
  Missed Gradle file
  WIP Twilio SDK Instrumentation

# Conflicts:
#	dd-java-agent/instrumentation/twilio/src/main/java/datadog/trace/instrumentation/twilio/TwilioClientDecorator.java
#	dd-java-agent/instrumentation/twilio/src/main/java/datadog/trace/instrumentation/twilio/TwilioInstrumentation.java
#	dd-java-agent/instrumentation/twilio/src/test/groovy/test/TwilioClientTest.groovy

Merge branch 'twilio' of github.com:darylrobbins/dd-trace-java into twilio
Improved unit testing

* 'twilio' of github.com:darylrobbins/dd-trace-java:
  Updates to handle async calls, which have broken all tests
  Missed Gradle file
  WIP Twilio SDK Instrumentation

# Conflicts:
#	dd-java-agent/instrumentation/twilio/src/main/java/datadog/trace/instrumentation/twilio/TwilioClientDecorator.java
#	dd-java-agent/instrumentation/twilio/src/main/java/datadog/trace/instrumentation/twilio/TwilioInstrumentation.java
#	dd-java-agent/instrumentation/twilio/src/test/groovy/test/TwilioClientTest.groovy

Fix sleep times and choose Java7-friendly test dependencies

Corrected test assertion
This commit is contained in:
Daryl Robbins 2019-03-22 20:32:47 -04:00 committed by Tyler Benson
commit 315ae67fd2
5 changed files with 486 additions and 20 deletions

View File

@ -37,7 +37,7 @@ public class TwilioClientDecorator extends ClientDecorator {
return COMPONENT_NAME;
}
/** Decorate trace based on service execution metadata */
/** Decorate trace based on service execution metadata. */
public Span onServiceExecution(
final Span span, final Object serviceExecutor, final String methodName) {
@ -76,13 +76,17 @@ public class TwilioClientDecorator extends ClientDecorator {
span.setTag("twilio.type", result.getClass().getCanonicalName());
span.setTag("twilio.account", message.getAccountSid());
span.setTag("twilio.sid", message.getSid());
span.setTag("twilio.status", message.getStatus().toString());
if (message.getStatus() != null) {
span.setTag("twilio.status", message.getStatus().toString());
}
} else if (result instanceof Call) {
final Call call = (Call) result;
span.setTag("twilio.account", call.getAccountSid());
span.setTag("twilio.sid", call.getSid());
span.setTag("twilio.parentSid", call.getParentCallSid());
span.setTag("twilio.status", call.getStatus().toString());
if (call.getStatus() != null) {
span.setTag("twilio.status", call.getStatus().toString());
}
} else {
// Use reflection to gather insight from other types; note that Twilio requests take close to
// 1 second, so the added hit from reflection here is relatively minimal in the grand scheme

View File

@ -64,9 +64,10 @@ public class TwilioInstrumentation extends Instrumenter.Default {
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
/*
We are listing out the main service calls on the Creator, Deleter, Fetcher, Reader, and Updater abstract
classes. The isDeclaredBy() matcher did not work in the unit tests and we found that there were certain
methods declared on the base class (particularly Reader), which we weren't interested in annotating.
We are listing out the main service calls on the Creator, Deleter, Fetcher, Reader, and
Updater abstract classes. The isDeclaredBy() matcher did not work in the unit tests and
we found that there were certain methods declared on the base class (particularly Reader),
which we weren't interested in annotating.
*/
return singletonMap(
@ -85,7 +86,7 @@ public class TwilioInstrumentation extends Instrumenter.Default {
/** Advice for instrumenting Twilio service classes. */
public static class TwilioClientAdvice {
/** Method entry instrumentation */
/** Method entry instrumentation. */
@Advice.OnMethodEnter(suppress = Throwable.class)
public static Scope methodEnter(
@Advice.This final Object that, @Advice.Origin("#m") final String methodName) {
@ -121,7 +122,7 @@ public class TwilioInstrumentation extends Instrumenter.Default {
return scope;
}
/** Method exit instrumentation */
/** Method exit instrumentation. */
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void methodExit(
@Advice.Enter final Scope scope,
@ -137,6 +138,7 @@ public class TwilioInstrumentation extends Instrumenter.Default {
final Span span = scope.span();
DECORATE.onError(span, throwable);
DECORATE.beforeFinish(span);
// If we're calling an async operation, we still need to finish the span when it's
// complete and report the results; set an appropriate callback
@ -165,7 +167,7 @@ public class TwilioInstrumentation extends Instrumenter.Default {
*/
public static class SpanFinishingCallback implements FutureCallback {
/** Span that we should finish and annotate when the future is complete */
/** Span that we should finish and annotate when the future is complete. */
private final Span span;
public SpanFinishingCallback(final Span span) {

View File

@ -4,14 +4,22 @@ import com.fasterxml.jackson.databind.ObjectMapper
import com.google.common.util.concurrent.ListenableFuture
import com.twilio.Twilio
import com.twilio.exception.ApiException
import com.twilio.http.NetworkHttpClient
import com.twilio.http.Response
import com.twilio.http.TwilioRestClient
import com.twilio.rest.api.v2010.account.Call
import com.twilio.rest.api.v2010.account.Message
import com.twilio.type.PhoneNumber
import datadog.trace.agent.test.AgentTestRunner
import datadog.trace.api.DDSpanTypes
import io.opentracing.tag.Tags
import io.opentracing.util.GlobalTracer
import org.apache.http.HttpEntity
import org.apache.http.HttpStatus
import org.apache.http.StatusLine
import org.apache.http.client.methods.CloseableHttpResponse
import org.apache.http.impl.client.CloseableHttpClient
import org.apache.http.impl.client.HttpClientBuilder
import java.util.concurrent.ExecutionException
import java.util.concurrent.TimeUnit
@ -46,6 +54,41 @@ class TwilioClientTest extends AgentTestRunner {
}
"""
final static String CALL_RESPONSE_BODY = """
{
"account_sid": "ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"annotation": null,
"answered_by": null,
"api_version": "2010-04-01",
"caller_name": null,
"date_created": "Tue, 31 Aug 2010 20:36:28 +0000",
"date_updated": "Tue, 31 Aug 2010 20:36:44 +0000",
"direction": "inbound",
"duration": "15",
"end_time": "Tue, 31 Aug 2010 20:36:44 +0000",
"forwarded_from": "+141586753093",
"from": "+15017122661",
"from_formatted": "(501) 712-2661",
"group_sid": null,
"parent_call_sid": null,
"phone_number_sid": "PNXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"price": -0.03000,
"price_unit": "USD",
"sid": "CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"start_time": "Tue, 31 Aug 2010 20:36:29 +0000",
"status": "completed",
"subresource_uris": {
"notifications": "/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Calls/CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Notifications.json",
"recordings": "/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Calls/CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Recordings.json",
"feedback": "/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Calls/CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Feedback.json",
"feedback_summaries": "/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Calls/FeedbackSummary.json"
},
"to": "+15558675310",
"to_formatted": "(555) 867-5310",
"uri": "/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Calls/CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.json"
}
"""
final static String ERROR_RESPONSE_BODY = """
{
"code": 123,
@ -61,7 +104,7 @@ class TwilioClientTest extends AgentTestRunner {
Twilio.init(ACCOUNT_SID, AUTH_TOKEN)
}
def "synchronous call"() {
def "synchronous message"() {
setup:
twilioRestClient.getObjectMapper() >> new ObjectMapper()
@ -75,8 +118,6 @@ class TwilioClientTest extends AgentTestRunner {
"Hello world!" // SMS body
).create(twilioRestClient)
Thread.sleep(1000);
def scope = GlobalTracer.get().scopeManager().active()
if (scope) {
scope.close()
@ -115,6 +156,384 @@ class TwilioClientTest extends AgentTestRunner {
}
}
def "synchronous call"() {
setup:
twilioRestClient.getObjectMapper() >> new ObjectMapper()
1 * twilioRestClient.request(_) >> new Response(new ByteArrayInputStream(CALL_RESPONSE_BODY.getBytes()), 200)
GlobalTracer.get().buildSpan("test").startActive(true)
Call call = Call.creator(
new PhoneNumber("+15558881234"), // To number
new PhoneNumber("+15559994321"), // From number
// Read TwiML at this URL when a call connects (hold music)
new URI("http://twimlets.com/holdmusic?Bucket=com.twilio.music.ambient")
).create(twilioRestClient)
def scope = GlobalTracer.get().scopeManager().active()
if (scope) {
scope.close()
}
expect:
call.status == Call.Status.COMPLETED
assertTraces(1) {
trace(0, 2) {
span(0) {
serviceName "unnamed-java-app"
operationName "test"
resourceName "test"
errored false
parent()
}
span(1) {
serviceName "twilio-sdk"
operationName "twilio.sdk"
resourceName "api.v2010.account.CallCreator.create"
spanType DDSpanTypes.HTTP_CLIENT
errored false
tags {
"$Tags.COMPONENT.key" "twilio-sdk"
"$Tags.SPAN_KIND.key" Tags.SPAN_KIND_CLIENT
"twilio.type" "com.twilio.rest.api.v2010.account.Call"
"twilio.account" "ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
"twilio.sid" "CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
"twilio.status" "completed"
defaultTags()
}
}
}
}
}
def "http client"() {
setup:
HttpClientBuilder clientBuilder = Mock()
CloseableHttpClient httpClient = Mock()
CloseableHttpResponse httpResponse = Mock()
HttpEntity httpEntity = Mock()
StatusLine statusLine = Mock()
clientBuilder.build() >> httpClient
httpClient.execute(_) >> httpResponse
httpResponse.getEntity() >> httpEntity
httpResponse.getStatusLine() >> statusLine
httpEntity.getContent() >> { new ByteArrayInputStream(MESSAGE_RESPONSE_BODY.getBytes()) }
httpEntity.isRepeatable() >> true
httpEntity.getContentLength() >> MESSAGE_RESPONSE_BODY.length()
statusLine.getStatusCode() >> HttpStatus.SC_OK
NetworkHttpClient networkHttpClient = new NetworkHttpClient(clientBuilder)
TwilioRestClient realTwilioRestClient =
new TwilioRestClient.Builder("username", "password")
.accountSid(ACCOUNT_SID)
.httpClient(networkHttpClient)
.build()
GlobalTracer.get().buildSpan("test").startActive(true)
Message message = Message.creator(
new PhoneNumber("+1 555 720 5913"), // To number
new PhoneNumber("+1 555 555 5215"), // From number
"Hello world!" // SMS body
).create(realTwilioRestClient)
def scope = GlobalTracer.get().scopeManager().active()
if (scope) {
scope.close()
}
expect:
message.body == "Hello, World!"
assertTraces(1) {
trace(0, 3) {
span(0) {
serviceName "unnamed-java-app"
operationName "test"
resourceName "test"
errored false
parent()
}
span(1) {
serviceName "twilio-sdk"
operationName "twilio.sdk"
resourceName "api.v2010.account.MessageCreator.create"
spanType DDSpanTypes.HTTP_CLIENT
errored false
tags {
"$Tags.COMPONENT.key" "twilio-sdk"
"$Tags.SPAN_KIND.key" Tags.SPAN_KIND_CLIENT
"twilio.type" "com.twilio.rest.api.v2010.account.Message"
"twilio.account" "AC14984e09e497506cf0d5eb59b1f6ace7"
"twilio.sid" "MMXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
"twilio.status" "sent"
defaultTags()
}
}
span(2) {
serviceName "twilio-sdk"
operationName "http.request"
resourceName "POST /?/Accounts/abc/Messages.json"
spanType DDSpanTypes.HTTP_CLIENT
errored false
}
}
}
}
def "http client retry"() {
setup:
HttpClientBuilder clientBuilder = Mock()
CloseableHttpClient httpClient = Mock()
CloseableHttpResponse httpResponse1 = Mock()
CloseableHttpResponse httpResponse2 = Mock()
HttpEntity httpEntity1 = Mock()
HttpEntity httpEntity2 = Mock()
StatusLine statusLine1 = Mock()
StatusLine statusLine2 = Mock()
clientBuilder.build() >> httpClient
httpClient.execute(_) >>> [httpResponse1, httpResponse2]
// First response is an HTTP/500 error, which should drive a retry
httpResponse1.getEntity() >> httpEntity1
httpResponse1.getStatusLine() >> statusLine1
httpEntity1.getContent() >> { new ByteArrayInputStream(ERROR_RESPONSE_BODY.getBytes()) }
httpEntity1.isRepeatable() >> true
httpEntity1.getContentLength() >> ERROR_RESPONSE_BODY.length()
statusLine1.getStatusCode() >> HttpStatus.SC_INTERNAL_SERVER_ERROR
// Second response is HTTP/200 success
httpResponse2.getEntity() >> httpEntity2
httpResponse2.getStatusLine() >> statusLine2
httpEntity2.getContent() >> {
new ByteArrayInputStream(MESSAGE_RESPONSE_BODY.getBytes())
}
httpEntity2.isRepeatable() >> true
httpEntity2.getContentLength() >> MESSAGE_RESPONSE_BODY.length()
statusLine2.getStatusCode() >> HttpStatus.SC_OK
NetworkHttpClient networkHttpClient = new NetworkHttpClient(clientBuilder)
TwilioRestClient realTwilioRestClient =
new TwilioRestClient.Builder("username", "password")
.accountSid(ACCOUNT_SID)
.httpClient(networkHttpClient)
.build()
GlobalTracer.get().buildSpan("test").startActive(true)
Message message = Message.creator(
new PhoneNumber("+1 555 720 5913"), // To number
new PhoneNumber("+1 555 555 5215"), // From number
"Hello world!" // SMS body
).create(realTwilioRestClient)
def scope = GlobalTracer.get().scopeManager().active()
if (scope) {
scope.close()
}
expect:
message.body == "Hello, World!"
assertTraces(1) {
trace(0, 4) {
span(0) {
serviceName "unnamed-java-app"
operationName "test"
resourceName "test"
errored false
parent()
}
span(1) {
serviceName "twilio-sdk"
operationName "twilio.sdk"
resourceName "api.v2010.account.MessageCreator.create"
spanType DDSpanTypes.HTTP_CLIENT
errored false
tags {
"$Tags.COMPONENT.key" "twilio-sdk"
"$Tags.SPAN_KIND.key" Tags.SPAN_KIND_CLIENT
"twilio.type" "com.twilio.rest.api.v2010.account.Message"
"twilio.account" "AC14984e09e497506cf0d5eb59b1f6ace7"
"twilio.sid" "MMXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
"twilio.status" "sent"
defaultTags()
}
}
span(2) {
serviceName "twilio-sdk"
operationName "http.request"
resourceName "POST /?/Accounts/abc/Messages.json"
spanType DDSpanTypes.HTTP_CLIENT
errored false
}
span(3) {
serviceName "twilio-sdk"
operationName "http.request"
resourceName "POST /?/Accounts/abc/Messages.json"
spanType DDSpanTypes.HTTP_CLIENT
errored true
}
}
}
}
def "http client retry async"() {
setup:
HttpClientBuilder clientBuilder = Mock()
CloseableHttpClient httpClient = Mock()
CloseableHttpResponse httpResponse1 = Mock()
CloseableHttpResponse httpResponse2 = Mock()
HttpEntity httpEntity1 = Mock()
HttpEntity httpEntity2 = Mock()
StatusLine statusLine1 = Mock()
StatusLine statusLine2 = Mock()
clientBuilder.build() >> httpClient
httpClient.execute(_) >>> [httpResponse1, httpResponse2]
// First response is an HTTP/500 error, which should drive a retry
httpResponse1.getEntity() >> httpEntity1
httpResponse1.getStatusLine() >> statusLine1
httpEntity1.getContent() >> { new ByteArrayInputStream(ERROR_RESPONSE_BODY.getBytes()) }
httpEntity1.isRepeatable() >> true
httpEntity1.getContentLength() >> ERROR_RESPONSE_BODY.length()
statusLine1.getStatusCode() >> HttpStatus.SC_INTERNAL_SERVER_ERROR
// Second response is HTTP/200 success
httpResponse2.getEntity() >> httpEntity2
httpResponse2.getStatusLine() >> statusLine2
httpEntity2.getContent() >> {
new ByteArrayInputStream(MESSAGE_RESPONSE_BODY.getBytes())
}
httpEntity2.isRepeatable() >> true
httpEntity2.getContentLength() >> MESSAGE_RESPONSE_BODY.length()
statusLine2.getStatusCode() >> HttpStatus.SC_OK
NetworkHttpClient networkHttpClient = new NetworkHttpClient(clientBuilder)
TwilioRestClient realTwilioRestClient =
new TwilioRestClient.Builder("username", "password")
.accountSid(ACCOUNT_SID)
.httpClient(networkHttpClient)
.build()
GlobalTracer.get().buildSpan("test").startActive(true)
ListenableFuture<Message> future = Message.creator(
new PhoneNumber("+1 555 720 5913"), // To number
new PhoneNumber("+1 555 555 5215"), // From number
"Hello world!" // SMS body
).createAsync(realTwilioRestClient)
Message message
try {
message = future.get(10, TimeUnit.SECONDS)
} finally {
// Give the future callback a chance to run
Thread.sleep(1000)
def scope = GlobalTracer.get().scopeManager().active()
if (scope) {
scope.span().finish()
scope.close()
}
}
expect:
message.body == "Hello, World!"
assertTraces(1) {
trace(0, 5) {
span(0) {
serviceName "unnamed-java-app"
operationName "test"
resourceName "test"
errored false
parent()
}
span(1) {
serviceName "twilio-sdk"
operationName "twilio.sdk"
resourceName "api.v2010.account.MessageCreator.createAsync"
spanType DDSpanTypes.HTTP_CLIENT
errored false
tags {
"$Tags.COMPONENT.key" "twilio-sdk"
"$Tags.SPAN_KIND.key" Tags.SPAN_KIND_CLIENT
"twilio.type" "com.twilio.rest.api.v2010.account.Message"
"twilio.account" "AC14984e09e497506cf0d5eb59b1f6ace7"
"twilio.sid" "MMXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
"twilio.status" "sent"
defaultTags()
}
}
span(2) {
serviceName "twilio-sdk"
operationName "twilio.sdk"
resourceName "api.v2010.account.MessageCreator.create"
spanType DDSpanTypes.HTTP_CLIENT
errored false
tags {
"$Tags.COMPONENT.key" "twilio-sdk"
"$Tags.SPAN_KIND.key" Tags.SPAN_KIND_CLIENT
"twilio.type" "com.twilio.rest.api.v2010.account.Message"
"twilio.account" "AC14984e09e497506cf0d5eb59b1f6ace7"
"twilio.sid" "MMXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
"twilio.status" "sent"
defaultTags()
}
}
span(3) {
serviceName "twilio-sdk"
operationName "http.request"
resourceName "POST /?/Accounts/abc/Messages.json"
spanType DDSpanTypes.HTTP_CLIENT
errored true
}
span(4) {
serviceName "twilio-sdk"
operationName "http.request"
resourceName "POST /?/Accounts/abc/Messages.json"
spanType DDSpanTypes.HTTP_CLIENT
errored false
}
}
}
cleanup:
Twilio.getExecutorService().shutdown()
Twilio.setExecutorService(null)
Twilio.setRestClient(null)
}
def "Sync Failure"() {
setup:
@ -198,14 +617,17 @@ class TwilioClientTest extends AgentTestRunner {
}
}
}
}
def "asynchronous call"() {
def "asynchronous call"(a) {
setup:
twilioRestClient.getObjectMapper() >> new ObjectMapper()
1 * twilioRestClient.request(_) >> new Response(new ByteArrayInputStream(MESSAGE_RESPONSE_BODY.getBytes()), 200)
when:
GlobalTracer.get().buildSpan("test").startActive(true)
ListenableFuture<Message> future = Message.creator(
@ -214,14 +636,20 @@ class TwilioClientTest extends AgentTestRunner {
"Hello world!" // SMS body
).createAsync(twilioRestClient)
Message message = future.get(10, TimeUnit.SECONDS)
def scope = GlobalTracer.get().scopeManager().active()
if (scope) {
scope.close()
Message message
try {
message = future.get(10, TimeUnit.SECONDS)
} finally {
// Give the future callback a chance to run
Thread.sleep(1000)
def scope = GlobalTracer.get().scopeManager().active()
if (scope) {
scope.span().finish()
scope.close()
}
}
expect:
then:
message != null
message.body == "Hello, World!"
@ -269,6 +697,16 @@ class TwilioClientTest extends AgentTestRunner {
}
}
}
cleanup:
Twilio.getExecutorService().shutdown()
Twilio.setExecutorService(null)
Twilio.setRestClient(null)
where:
a | _
1 | _
2 | _
}
def "asynchronous error"() {
@ -292,6 +730,7 @@ class TwilioClientTest extends AgentTestRunner {
message = future.get(10, TimeUnit.SECONDS)
} finally {
Thread.sleep(1000)
def scope = GlobalTracer.get().scopeManager().active()
if (scope) {
scope.close()
@ -328,6 +767,11 @@ class TwilioClientTest extends AgentTestRunner {
}
}
}
cleanup:
Twilio.getExecutorService().shutdown()
Twilio.setExecutorService(null)
Twilio.setRestClient(null)
}
}

View File

@ -20,6 +20,9 @@ dependencies {
testCompile group: 'com.twilio.sdk', name: 'twilio', version: '7.36.2'
testCompile project(':dd-java-agent:testing')
testCompile project(':dd-java-agent:instrumentation:apache-httpclient-4')
testCompile project(':dd-java-agent:instrumentation:java-concurrent')
testCompile group: 'org.objenesis', name: 'objenesis', version: '3.0.1'
testCompile group: 'org.objenesis', name: 'objenesis', version: '2.6' // Last version to support Java7
testCompile group: 'nl.jqno.equalsverifier', name: 'equalsverifier', version: '2.5.2' // Last version to support Java7
}

View File

@ -19,6 +19,7 @@ import groovy.transform.stc.SimpleType;
import io.opentracing.Tracer;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.util.Iterator;
import java.util.List;
import java.util.ServiceLoader;
import java.util.Set;
@ -84,6 +85,8 @@ public abstract class AgentTestRunner extends Specification {
TEST_WRITER =
new ListWriter() {
private static final long serialVersionUID = 705972961882897201L;
@Override
public boolean add(final List<DDSpan> trace) {
final boolean result = super.add(trace);
@ -194,6 +197,14 @@ public abstract class AgentTestRunner extends Specification {
options = "datadog.trace.agent.test.asserts.ListWriterAssert")
@DelegatesTo(value = ListWriterAssert.class, strategy = Closure.DELEGATE_FIRST)
final Closure spec) {
final Iterator<List<DDSpan>> iterator = TEST_WRITER.iterator();
while (iterator.hasNext()) {
final List<DDSpan> next = iterator.next();
final Iterator<DDSpan> iterator1 = next.iterator();
while (iterator1.hasNext()) {
System.out.println(iterator1.next());
}
}
ListWriterAssert.assertTraces(TEST_WRITER, size, spec);
}
@ -265,6 +276,8 @@ public abstract class AgentTestRunner extends Specification {
/** Used to signal that a transformation was intentionally aborted and is not an error. */
public static class AbortTransformationException extends RuntimeException {
private static final long serialVersionUID = -1849465286193994582L;
public AbortTransformationException() {
super();
}