Repository: activemq Updated Branches: refs/heads/activemq-5.13.x fda982dcc -> 53c9d7ecb
https://issues.apache.org/jira/browse/AMQ-6305 Add test and supporting enhancements to the JUnit module to allow for easy repetition of tests that don't always fail reliably (cherry picked from commit 6ae169e2755257b1e8e5068473bdb3156160790d) Project: http://git-wip-us.apache.org/repos/asf/activemq/repo Commit: http://git-wip-us.apache.org/repos/asf/activemq/commit/508c12d9 Tree: http://git-wip-us.apache.org/repos/asf/activemq/tree/508c12d9 Diff: http://git-wip-us.apache.org/repos/asf/activemq/diff/508c12d9 Branch: refs/heads/activemq-5.13.x Commit: 508c12d9487ad653f05806be7a492e4bd870522b Parents: fda982d Author: Timothy Bish <[email protected]> Authored: Thu May 26 18:02:59 2016 -0400 Committer: Christopher L. Shannon (cshannon) <[email protected]> Committed: Tue Jul 5 19:54:54 2016 +0000 ---------------------------------------------------------------------- activemq-amqp/pom.xml | 5 + .../transport/amqp/protocol/AmqpSender.java | 24 ++- .../transport/amqp/JMSClientContext.java | 2 +- .../transport/amqp/JMSQueueBrowserTest.java | 85 +++++++++++ .../src/test/resources/log4j.properties | 10 +- .../activemq/junit/ActiveMQTestRunner.java | 116 +++++++++++++++ .../java/org/apache/activemq/junit/Repeat.java | 34 +++++ .../org/apache/activemq/junit/RepeatRule.java | 38 +++++ .../apache/activemq/junit/RepeatStatement.java | 147 +++++++++++++++++++ 9 files changed, 455 insertions(+), 6 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/activemq/blob/508c12d9/activemq-amqp/pom.xml ---------------------------------------------------------------------- diff --git a/activemq-amqp/pom.xml b/activemq-amqp/pom.xml index 0e41e58..1ee7277 100644 --- a/activemq-amqp/pom.xml +++ b/activemq-amqp/pom.xml @@ -104,6 +104,11 @@ <scope>test</scope> </dependency> <dependency> + <groupId>org.apache.activemq.tooling</groupId> + <artifactId>activemq-junit</artifactId> + <scope>test</scope> + </dependency> + <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <scope>test</scope> http://git-wip-us.apache.org/repos/asf/activemq/blob/508c12d9/activemq-amqp/src/main/java/org/apache/activemq/transport/amqp/protocol/AmqpSender.java ---------------------------------------------------------------------- diff --git a/activemq-amqp/src/main/java/org/apache/activemq/transport/amqp/protocol/AmqpSender.java b/activemq-amqp/src/main/java/org/apache/activemq/transport/amqp/protocol/AmqpSender.java index e0e7276..8cf7033 100644 --- a/activemq-amqp/src/main/java/org/apache/activemq/transport/amqp/protocol/AmqpSender.java +++ b/activemq-amqp/src/main/java/org/apache/activemq/transport/amqp/protocol/AmqpSender.java @@ -154,8 +154,11 @@ public class AmqpSender extends AmqpAbstractLink<Sender> { public void flow() throws Exception { int updatedCredit = getEndpoint().getCredit(); - LOG.trace("Flow: drain={} credit={}, remoteCredit={}", - getEndpoint().getDrain(), getEndpoint().getCredit(), getEndpoint().getRemoteCredit()); + if (LOG.isTraceEnabled()) { + LOG.trace("Flow: currentCredit={}, draining={}, drain={} credit={}, remoteCredit={}, queued={}", + currentCredit, draining, getEndpoint().getDrain(), + getEndpoint().getCredit(), getEndpoint().getRemoteCredit(), getEndpoint().getQueued()); + } if (getEndpoint().getDrain() && (updatedCredit != currentCredit || !draining)) { currentCredit = updatedCredit >= 0 ? updatedCredit : 0; @@ -166,6 +169,9 @@ public class AmqpSender extends AmqpAbstractLink<Sender> { control.setConsumerId(getConsumerId()); control.setDestination(getDestination()); control.setPrefetch(0); + + LOG.trace("Flow: Pull case -> consumer control with prefetch (0)"); + sendToActiveMQ(control, null); // Now request dispatch of the drain amount, we request immediate @@ -177,6 +183,9 @@ public class AmqpSender extends AmqpAbstractLink<Sender> { pullRequest.setTimeout(-1); pullRequest.setAlwaysSignalDone(true); pullRequest.setQuantity(currentCredit); + + LOG.trace("Pull case -> consumer pull request quantity = {}", currentCredit); + sendToActiveMQ(pullRequest, null); } else if (updatedCredit != currentCredit) { currentCredit = updatedCredit >= 0 ? updatedCredit : 0; @@ -184,7 +193,12 @@ public class AmqpSender extends AmqpAbstractLink<Sender> { control.setConsumerId(getConsumerId()); control.setDestination(getDestination()); control.setPrefetch(currentCredit); + + LOG.trace("Flow: update -> consumer control with prefetch (0)"); + sendToActiveMQ(control, null); + } else { + LOG.trace("Flow: no credit change -> no broker updates needed"); } } @@ -403,6 +417,12 @@ public class AmqpSender extends AmqpAbstractLink<Sender> { draining = false; currentCredit = 0; } else { + if (LOG.isTraceEnabled()) { + LOG.trace("Sender:[{}] msgId={} currentCredit={}, draining={}, drain={} credit={}, remoteCredit={}, queued={}", + getEndpoint().getName(), jms.getJMSMessageID(), currentCredit, draining, getEndpoint().getDrain(), + getEndpoint().getCredit(), getEndpoint().getRemoteCredit(), getEndpoint().getQueued()); + } + jms.setRedeliveryCounter(md.getRedeliveryCounter()); jms.setReadOnlyBody(true); final EncodedMessage amqp = outboundTransformer.transform(jms); http://git-wip-us.apache.org/repos/asf/activemq/blob/508c12d9/activemq-amqp/src/test/java/org/apache/activemq/transport/amqp/JMSClientContext.java ---------------------------------------------------------------------- diff --git a/activemq-amqp/src/test/java/org/apache/activemq/transport/amqp/JMSClientContext.java b/activemq-amqp/src/test/java/org/apache/activemq/transport/amqp/JMSClientContext.java index 08ebbb6..574e9f0 100644 --- a/activemq-amqp/src/test/java/org/apache/activemq/transport/amqp/JMSClientContext.java +++ b/activemq-amqp/src/test/java/org/apache/activemq/transport/amqp/JMSClientContext.java @@ -182,7 +182,7 @@ public class JMSClientContext { factory.setUsername(username); factory.setPassword(password); - factory.setAlwaysSyncSend(syncPublish); + factory.setForceSyncSend(syncPublish); factory.setTopicPrefix("topic://"); factory.setQueuePrefix("queue://"); http://git-wip-us.apache.org/repos/asf/activemq/blob/508c12d9/activemq-amqp/src/test/java/org/apache/activemq/transport/amqp/JMSQueueBrowserTest.java ---------------------------------------------------------------------- diff --git a/activemq-amqp/src/test/java/org/apache/activemq/transport/amqp/JMSQueueBrowserTest.java b/activemq-amqp/src/test/java/org/apache/activemq/transport/amqp/JMSQueueBrowserTest.java new file mode 100644 index 0000000..dfcc108 --- /dev/null +++ b/activemq-amqp/src/test/java/org/apache/activemq/transport/amqp/JMSQueueBrowserTest.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.activemq.transport.amqp; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; + +import java.util.Enumeration; + +import javax.jms.Message; +import javax.jms.Queue; +import javax.jms.QueueBrowser; +import javax.jms.Session; + +import org.apache.activemq.broker.jmx.QueueViewMBean; +import org.apache.activemq.junit.ActiveMQTestRunner; +import org.apache.activemq.junit.Repeat; +import org.apache.qpid.jms.JmsConnectionFactory; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Tests for various QueueBrowser scenarios with an AMQP JMS client. + */ +@RunWith(ActiveMQTestRunner.class) +public class JMSQueueBrowserTest extends JMSClientTestSupport { + + protected static final Logger LOG = LoggerFactory.getLogger(JMSClientTest.class); + + @Test(timeout = 60000) + @Repeat(repetitions = 1) + public void testBrowseAllInQueueZeroPrefetch() throws Exception { + + final int MSG_COUNT = 5; + + JmsConnectionFactory cf = new JmsConnectionFactory(getBrokerURI() + "?jms.prefetchPolicy.all=0"); + connection = cf.createConnection(); + connection.start(); + + Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); + assertNotNull(session); + Queue queue = session.createQueue(getDestinationName()); + sendMessages(name.getMethodName(), MSG_COUNT, false); + + QueueViewMBean proxy = getProxyToQueue(getDestinationName()); + assertEquals(MSG_COUNT, proxy.getQueueSize()); + + QueueBrowser browser = session.createBrowser(queue); + assertNotNull(browser); + Enumeration<?> enumeration = browser.getEnumeration(); + int count = 0; + while (count < MSG_COUNT && enumeration.hasMoreElements()) { + Message msg = (Message) enumeration.nextElement(); + assertNotNull(msg); + LOG.debug("Recv: {}", msg); + count++; + } + + LOG.debug("Received all expected message, checking that hasMoreElements returns false"); + assertFalse(enumeration.hasMoreElements()); + assertEquals(5, count); + } + + @Override + protected boolean isUseOpenWireConnector() { + return true; + } +} http://git-wip-us.apache.org/repos/asf/activemq/blob/508c12d9/activemq-amqp/src/test/resources/log4j.properties ---------------------------------------------------------------------- diff --git a/activemq-amqp/src/test/resources/log4j.properties b/activemq-amqp/src/test/resources/log4j.properties index 4111e16..be0efab 100755 --- a/activemq-amqp/src/test/resources/log4j.properties +++ b/activemq-amqp/src/test/resources/log4j.properties @@ -20,21 +20,25 @@ # log4j.rootLogger=WARN, console, file log4j.logger.org.apache.activemq=INFO -log4j.logger.org.apache.activemq.broker.scheduler=TRACE log4j.logger.org.apache.activemq.transport.amqp=DEBUG log4j.logger.org.apache.activemq.transport.amqp.FRAMES=INFO log4j.logger.org.fusesource=INFO + +# Configure various level of detail for Qpid JMS logs. log4j.logger.org.apache.qpid.jms=INFO +log4j.logger.org.apache.qpid.jms.provider=INFO +log4j.logger.org.apache.qpid.jms.provider.amqp=INFO +log4j.logger.org.apache.qpid.jms.provider.amqp.FRAMES=INFO # Console will only display warnnings log4j.appender.console=org.apache.log4j.ConsoleAppender log4j.appender.console.layout=org.apache.log4j.PatternLayout -log4j.appender.console.layout.ConversionPattern=%d | %-5p | %t | %m%n +log4j.appender.console.layout.ConversionPattern=%d [%-15.15t] - %-5p %-25.30c{1} - %m%n log4j.appender.console.threshold=TRACE # File appender will contain all info messages log4j.appender.file=org.apache.log4j.FileAppender log4j.appender.file.layout=org.apache.log4j.PatternLayout -log4j.appender.file.layout.ConversionPattern=%d | %-5p | %m | %c | %t%n +log4j.appender.file.layout.ConversionPattern=%d [%-15.15t] - %-5p %-30.30c{1} - %m%n log4j.appender.file.file=target/test.log log4j.appender.file.append=true http://git-wip-us.apache.org/repos/asf/activemq/blob/508c12d9/activemq-tooling/activemq-junit/src/main/java/org/apache/activemq/junit/ActiveMQTestRunner.java ---------------------------------------------------------------------- diff --git a/activemq-tooling/activemq-junit/src/main/java/org/apache/activemq/junit/ActiveMQTestRunner.java b/activemq-tooling/activemq-junit/src/main/java/org/apache/activemq/junit/ActiveMQTestRunner.java new file mode 100644 index 0000000..ab793ff --- /dev/null +++ b/activemq-tooling/activemq-junit/src/main/java/org/apache/activemq/junit/ActiveMQTestRunner.java @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.activemq.junit; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; +import org.junit.internal.runners.statements.FailOnTimeout; +import org.junit.runners.BlockJUnit4ClassRunner; +import org.junit.runners.model.FrameworkMethod; +import org.junit.runners.model.InitializationError; +import org.junit.runners.model.Statement; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A Custom JUnit test runner for customizing JUnit tests run in ActiveMQ. + */ +public class ActiveMQTestRunner extends BlockJUnit4ClassRunner { + + private static final Logger LOG = LoggerFactory.getLogger(ActiveMQTestRunner.class); + + public ActiveMQTestRunner(Class<?> klass) throws InitializationError { + super(klass); + } + + @Override + protected Statement methodBlock(final FrameworkMethod method) { + Statement statement = super.methodBlock(method); + + // Check for repeats needed + statement = withPotentialRepeat(method, statement); + + return statement; + } + + /** + * Perform the same logic as + * {@link BlockJUnit4ClassRunner#withPotentialTimeout(FrameworkMethod, Object, Statement)} + * but with additional support for changing the coded timeout with an extended value. + * + * @return either a {@link FailOnTimeout}, or the supplied {@link Statement} as appropriate. + */ + @SuppressWarnings("deprecation") + @Override + protected Statement withPotentialTimeout(FrameworkMethod frameworkMethod, Object testInstance, Statement next) { + long testTimeout = getOriginalTimeout(frameworkMethod); + + if (testTimeout > 0) { + String multiplierString = System.getProperty("org.apache.activemq.junit.testTimeoutMultiplier"); + double multiplier = 0.0; + + try { + multiplier = Double.parseDouble(multiplierString); + } catch (NullPointerException npe) { + } catch (NumberFormatException nfe) { + LOG.warn("Ignoring testTimeoutMultiplier not set to a valid value: " + multiplierString); + } + + if (multiplier > 0.0) { + LOG.info("Test timeout multiple {} applied to test timeout {}ms: new timeout = {}", + multiplier, testTimeout, (long) (testTimeout * multiplier)); + testTimeout = (long) (testTimeout * multiplier); + } + + next = FailOnTimeout.builder(). + withTimeout(testTimeout, TimeUnit.MILLISECONDS).build(next); + } else { + next = super.withPotentialTimeout(frameworkMethod, testInstance, next); + } + + return next; + } + + /** + * Check for the presence of a {@link Repeat} annotation and return a {@link RepeatStatement} + * to handle executing the test repeated or the original value if not repeating. + * + * @return either a {@link RepeatStatement}, or the supplied {@link Statement} as appropriate. + */ + protected Statement withPotentialRepeat(FrameworkMethod frameworkMethod, Statement next) { + + Repeat repeatAnnotation = frameworkMethod.getAnnotation(Repeat.class); + + if (repeatAnnotation != null) { + next = RepeatStatement.builder().build(repeatAnnotation, next); + } + + return next; + } + + /** + * Retrieve the original JUnit {@code timeout} from the {@link Test @Test} + * annotation on the incoming {@linkplain FrameworkMethod test method}. + * + * @return the timeout, or {@code 0} if none was specified + */ + protected long getOriginalTimeout(FrameworkMethod frameworkMethod) { + Test test = frameworkMethod.getAnnotation(Test.class); + return (test != null && test.timeout() > 0 ? test.timeout() : 0); + } +} http://git-wip-us.apache.org/repos/asf/activemq/blob/508c12d9/activemq-tooling/activemq-junit/src/main/java/org/apache/activemq/junit/Repeat.java ---------------------------------------------------------------------- diff --git a/activemq-tooling/activemq-junit/src/main/java/org/apache/activemq/junit/Repeat.java b/activemq-tooling/activemq-junit/src/main/java/org/apache/activemq/junit/Repeat.java new file mode 100644 index 0000000..e702071 --- /dev/null +++ b/activemq-tooling/activemq-junit/src/main/java/org/apache/activemq/junit/Repeat.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.activemq.junit; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A Custom Test annotation used to repeat a troublesome test multiple + * times when attempting to reproduce an intermittent failure. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ java.lang.annotation.ElementType.METHOD }) +public @interface Repeat { + + int repetitions() default 1; + + boolean untilFailure() default false; +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/activemq/blob/508c12d9/activemq-tooling/activemq-junit/src/main/java/org/apache/activemq/junit/RepeatRule.java ---------------------------------------------------------------------- diff --git a/activemq-tooling/activemq-junit/src/main/java/org/apache/activemq/junit/RepeatRule.java b/activemq-tooling/activemq-junit/src/main/java/org/apache/activemq/junit/RepeatRule.java new file mode 100644 index 0000000..6c897f7 --- /dev/null +++ b/activemq-tooling/activemq-junit/src/main/java/org/apache/activemq/junit/RepeatRule.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.activemq.junit; + +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +/** + * Test rule used to allow a test to have the Repeat annotation applied. + */ +public class RepeatRule implements TestRule { + + @Override + public Statement apply(Statement statement, Description description) { + Repeat repeat = description.getAnnotation(Repeat.class); + + if (repeat != null) { + statement = RepeatStatement.builder().build(repeat, statement); + } + + return statement; + } +} http://git-wip-us.apache.org/repos/asf/activemq/blob/508c12d9/activemq-tooling/activemq-junit/src/main/java/org/apache/activemq/junit/RepeatStatement.java ---------------------------------------------------------------------- diff --git a/activemq-tooling/activemq-junit/src/main/java/org/apache/activemq/junit/RepeatStatement.java b/activemq-tooling/activemq-junit/src/main/java/org/apache/activemq/junit/RepeatStatement.java new file mode 100644 index 0000000..e7a414a --- /dev/null +++ b/activemq-tooling/activemq-junit/src/main/java/org/apache/activemq/junit/RepeatStatement.java @@ -0,0 +1,147 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.activemq.junit; + +import org.junit.runners.model.Statement; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class RepeatStatement extends Statement { + + private static final Logger LOG = LoggerFactory.getLogger(RepeatStatement.class); + + private final int repetitions; + private final boolean untilFailure; + private final Statement statement; + + public static Builder builder() { + return new Builder(); + } + + public RepeatStatement(int times, boolean untilFailure, Statement statement) { + this.repetitions = times; + this.untilFailure = untilFailure; + this.statement = statement; + } + + protected RepeatStatement(Builder builder, Statement next) { + this.repetitions = builder.getRepetitions(); + this.untilFailure = builder.isUntilFailure(); + this.statement = next; + } + + @Override + public void evaluate() throws Throwable { + for (int i = 0; i < repetitions && !untilFailure; i++) { + if (untilFailure) { + LOG.info("Running test iteration: {}.", i + 1); + } else { + LOG.info("Running test iteration: {} of configured repetitions: {}", i + 1, repetitions); + } + statement.evaluate(); + } + } + + /** + * Builder for {@link Repeat}. + */ + public static class Builder { + private int repetitions = 1; + private boolean untilFailure = false; + + protected Builder() {} + + /** + * Specifies the number of times to run the test. + * + * @param repetitions + * The number of times to run the test. + * + * @return {@code this} for method chaining. + */ + public Builder withRepetitions(int repetitions) { + if (repetitions <= 0) { + throw new IllegalArgumentException("repetitions must be greater than zero"); + } + + this.repetitions = repetitions; + return this; + } + + /** + * Specifies the number of times to run the test. + * + * @param untilFailure + * true if the test should run until a failure occurs. + * + * @return {@code this} for method chaining. + */ + public Builder withRunUntilFailure(boolean untilFailure) { + this.untilFailure = untilFailure; + return this; + } + + protected int getRepetitions() { + return repetitions; + } + + protected boolean isUntilFailure() { + return untilFailure; + } + + /** + * Builds a {@link RepeatStatement} instance using the values in this builder. + * + * @param next + * The statement instance to wrap with the newly create repeat statement. + * + * @return a new {@link RepeatStatement} that wraps the given {@link Statement}. + */ + public RepeatStatement build(Statement next) { + if (next == null) { + throw new NullPointerException("statement cannot be null"); + } + + return new RepeatStatement(this, next); + } + + /** + * Builds a {@link RepeatStatement} instance using the values in this builder. + * + * @param annotation + * The {@link Repeat} annotation that triggered this statement being created. + * @param next + * The statement instance to wrap with the newly create repeat statement. + * + * @return a new {@link RepeatStatement} that wraps the given {@link Statement}. + */ + public RepeatStatement build(Repeat annotation, Statement next) { + if (next == null) { + throw new NullPointerException("statement cannot be null"); + } + + if (annotation == null) { + throw new NullPointerException("annotation cannot be null"); + } + + withRepetitions(annotation.repetitions()); + withRunUntilFailure(annotation.untilFailure()); + + return new RepeatStatement(this, next); + } + } +} \ No newline at end of file
