From 433b732734f4eaa0157f53951d16f1a5957853ff Mon Sep 17 00:00:00 2001 From: John Smart Date: Sun, 14 Feb 2016 00:08:45 +0000 Subject: [PATCH] Allow more elegant waits in the Screenplay module You can now write code like this: jane.should(eventually(seeThat(TheClickerValue.of(clicker), equalTo(10)))) This will not fail if the matcher cannot be evaluated the first time, but will retry up to a maximum of 'serenity.timouts' seconds (5 by default). --- .../net/serenitybdd/core/time/Stopwatch.java | 17 ++- .../core/model/failures/FailureAnalysis.java | 16 +++ .../FailureAnalysisConfiguration.java | 16 +++ .../model/stacktrace/RootCauseAnalyzer.java | 3 + .../screenplay/EventualConsequence.java | 79 +++++++++++ .../EventualConsequenceBuilder.java | 19 +++ .../SomethingBadHappenedException.groovy | 12 ++ .../WhenWaitingForDelayedResults.groovy | 129 ++++++++++++++++++ 8 files changed, 282 insertions(+), 9 deletions(-) create mode 100644 serenity-screenplay/src/main/java/net/serenitybdd/screenplay/EventualConsequence.java create mode 100644 serenity-screenplay/src/main/java/net/serenitybdd/screenplay/EventualConsequenceBuilder.java create mode 100644 serenity-screenplay/src/test/groovy/net/serenitybdd/screenplay/SomethingBadHappenedException.groovy create mode 100644 serenity-screenplay/src/test/groovy/net/serenitybdd/screenplay/WhenWaitingForDelayedResults.groovy diff --git a/serenity-core/src/main/java/net/serenitybdd/core/time/Stopwatch.java b/serenity-core/src/main/java/net/serenitybdd/core/time/Stopwatch.java index 82108b2bbb..3533ba3e94 100644 --- a/serenity-core/src/main/java/net/serenitybdd/core/time/Stopwatch.java +++ b/serenity-core/src/main/java/net/serenitybdd/core/time/Stopwatch.java @@ -3,30 +3,29 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/** - * Created by john on 14/03/15. - */ public class Stopwatch { private static final Logger LOGGER = LoggerFactory.getLogger(Stopwatch.class); - public static Stopwatch SYSTEM = new Stopwatch(); - long counter = 0; + long startTime = 0; public void start() { - counter = System.currentTimeMillis(); + startTime = System.currentTimeMillis(); } public long stop() { - long result = System.currentTimeMillis() - counter; - counter = 0; + long result = System.currentTimeMillis() - startTime; + startTime = 0; return result; } + public long lapTime() { + return System.currentTimeMillis() - startTime; + } + public long stop(String message) { long result = stop(); LOGGER.debug(message + " took {0} ms", +result); - System.out.println(message + " took " + result + " ms"); return result; } } diff --git a/serenity-core/src/main/java/net/thucydides/core/model/failures/FailureAnalysis.java b/serenity-core/src/main/java/net/thucydides/core/model/failures/FailureAnalysis.java index ff8342f6dc..85e4ae684b 100644 --- a/serenity-core/src/main/java/net/thucydides/core/model/failures/FailureAnalysis.java +++ b/serenity-core/src/main/java/net/thucydides/core/model/failures/FailureAnalysis.java @@ -67,6 +67,10 @@ public TestResult resultFor(Class testFailureCause) { DEFAULT_PENDING_TYPES.addAll(ImmutableList.of(PendingStepException.class, PendingException.class)); } + private final List> DEFAULT_ERROR = Lists.newArrayList(); + { + DEFAULT_ERROR.addAll(ImmutableList.of(Error.class)); + } public boolean reportAsFailure(Class testFailureCause) { if (testFailureCause == null) { return false; @@ -103,6 +107,18 @@ public boolean reportAsPending(Class testFailureCause) { return false; } + public boolean reportAsError(Class testFailureCause) { + if (testFailureCause == null) { + return false; + } + for(Class validErrorType: configured.errorTypes()) { + if (isA(validErrorType,testFailureCause)) { + return true; + } + } + return false; + } + private boolean isA(Class expectedClass, Class testFailureCause) { return expectedClass.isAssignableFrom(testFailureCause); } diff --git a/serenity-core/src/main/java/net/thucydides/core/model/failures/FailureAnalysisConfiguration.java b/serenity-core/src/main/java/net/thucydides/core/model/failures/FailureAnalysisConfiguration.java index 7fe250545d..d93d326d67 100644 --- a/serenity-core/src/main/java/net/thucydides/core/model/failures/FailureAnalysisConfiguration.java +++ b/serenity-core/src/main/java/net/thucydides/core/model/failures/FailureAnalysisConfiguration.java @@ -35,6 +35,11 @@ public class FailureAnalysisConfiguration { DEFAULT_PENDING_TYPES.addAll(ImmutableList.of(PendingStepException.class, PendingException.class)); } + private final List> DEFAULT_ERROR_TYPES = Lists.newArrayList(); + { + DEFAULT_ERROR_TYPES.addAll(ImmutableList.of(Error.class)); + } + public FailureAnalysisConfiguration(EnvironmentVariables environmentVariables) { this.environmentVariables = environmentVariables; } @@ -75,6 +80,17 @@ public List> pendingTypes() { return pendingTypes; } + public List> errorTypes() { + List> errorTypes = Lists.newArrayList(DEFAULT_ERROR_TYPES); + errorTypes.addAll(errorTypesDefinedIn(environmentVariables)); + + errorTypes.removeAll(pendingTypesDefinedIn(environmentVariables)); + errorTypes.removeAll(compromisedTypesDefinedIn(environmentVariables)); + errorTypes.removeAll(failureTypesDefinedIn(environmentVariables)); + + return errorTypes; + } + private List> errorTypesDefinedIn(EnvironmentVariables environmentVariables) { return typesDefinedIn(ThucydidesSystemProperty.SERENITY_ERROR_ON, environmentVariables); } diff --git a/serenity-core/src/main/java/net/thucydides/core/model/stacktrace/RootCauseAnalyzer.java b/serenity-core/src/main/java/net/thucydides/core/model/stacktrace/RootCauseAnalyzer.java index f6fbdaf7a9..1a321f8711 100644 --- a/serenity-core/src/main/java/net/thucydides/core/model/stacktrace/RootCauseAnalyzer.java +++ b/serenity-core/src/main/java/net/thucydides/core/model/stacktrace/RootCauseAnalyzer.java @@ -33,6 +33,9 @@ private Throwable originalExceptionFrom(Throwable thrownException) { if (!(thrownException instanceof WebdriverAssertionError) && ((thrownException instanceof SerenityManagedException) || (thrownException instanceof AssertionError))){ return thrownException; } + if (failureAnalysis.reportAsError(thrownException.getClass())) { + return thrownException; + } if (failureAnalysis.reportAsCompromised(thrownException.getClass())) { return thrownException; } diff --git a/serenity-screenplay/src/main/java/net/serenitybdd/screenplay/EventualConsequence.java b/serenity-screenplay/src/main/java/net/serenitybdd/screenplay/EventualConsequence.java new file mode 100644 index 0000000000..bc96f84aea --- /dev/null +++ b/serenity-screenplay/src/main/java/net/serenitybdd/screenplay/EventualConsequence.java @@ -0,0 +1,79 @@ +package net.serenitybdd.screenplay; + +import net.serenitybdd.core.time.Stopwatch; +import net.thucydides.core.guice.Injectors; +import net.thucydides.core.webdriver.Configuration; + +public class EventualConsequence implements Consequence { + public static final int A_SHORT_PERIOD_BETWEEN_TRIES = 100; + private final Consequence consequenceThatMightTakeSomeTime; + private final long timeout; + + private AssertionError caughtAssertionError = null; + private RuntimeException caughtRuntimeException = null; + + public EventualConsequence(Consequence consequenceThatMightTakeSomeTime, long timeout) { + this.consequenceThatMightTakeSomeTime = consequenceThatMightTakeSomeTime; + this.timeout = timeout; + } + + public EventualConsequence(Consequence consequenceThatMightTakeSomeTime) { + this(consequenceThatMightTakeSomeTime, + Injectors.getInjector().getInstance(Configuration.class).getElementTimeout() * 1000); + } + + public static EventualConsequence eventually(Consequence consequenceThatMightTakeSomeTime) { + return new EventualConsequence(consequenceThatMightTakeSomeTime); + } + + public EventualConsequenceBuilder waitingForNoLongerThan(long amount) { + return new EventualConsequenceBuilder(consequenceThatMightTakeSomeTime, amount); + } + + @Override + public void evaluateFor(Actor actor) { + Stopwatch stopwatch = new Stopwatch(); + + stopwatch.start(); + do { + try { + consequenceThatMightTakeSomeTime.evaluateFor(actor); + return; + } catch (AssertionError assertionError) { + caughtAssertionError = assertionError; + } catch (RuntimeException runtimeException) { + caughtRuntimeException = runtimeException; + } + pauseBeforeNextAttempt(); + } while (stopwatch.lapTime() < timeout); + + throwAnyCaughtErrors(); + } + + private void pauseBeforeNextAttempt() { + try { + Thread.sleep(A_SHORT_PERIOD_BETWEEN_TRIES); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + private void throwAnyCaughtErrors() { + if (caughtAssertionError != null) { + throw caughtAssertionError; + } + if (caughtRuntimeException != null) { + throw caughtRuntimeException; + } + } + + @Override + public Consequence orComplainWith(Class complaintType) { + return new EventualConsequence(consequenceThatMightTakeSomeTime.orComplainWith(complaintType)); + } + + @Override + public Consequence orComplainWith(Class complaintType, String complaintDetails) { + return new EventualConsequence(consequenceThatMightTakeSomeTime.orComplainWith(complaintType, complaintDetails)); + } +} diff --git a/serenity-screenplay/src/main/java/net/serenitybdd/screenplay/EventualConsequenceBuilder.java b/serenity-screenplay/src/main/java/net/serenitybdd/screenplay/EventualConsequenceBuilder.java new file mode 100644 index 0000000000..1d6e5b4bc3 --- /dev/null +++ b/serenity-screenplay/src/main/java/net/serenitybdd/screenplay/EventualConsequenceBuilder.java @@ -0,0 +1,19 @@ +package net.serenitybdd.screenplay; + +public class EventualConsequenceBuilder { + private final Consequence consequence; + private final long amount; + + public EventualConsequenceBuilder(Consequence consequence, long amount) { + this.consequence = consequence; + this.amount = amount; + } + + public EventualConsequence milliseconds() { + return new EventualConsequence(consequence, amount); + } + + public EventualConsequence seconds() { + return new EventualConsequence(consequence, amount * 1000); + } +} diff --git a/serenity-screenplay/src/test/groovy/net/serenitybdd/screenplay/SomethingBadHappenedException.groovy b/serenity-screenplay/src/test/groovy/net/serenitybdd/screenplay/SomethingBadHappenedException.groovy new file mode 100644 index 0000000000..2320f5e23e --- /dev/null +++ b/serenity-screenplay/src/test/groovy/net/serenitybdd/screenplay/SomethingBadHappenedException.groovy @@ -0,0 +1,12 @@ +package net.serenitybdd.screenplay + +public class SomethingBadHappenedException extends Error { + + public SomethingBadHappenedException(String msg) { + super(msg) + } + + public SomethingBadHappenedException(String msg, Throwable e) { + super(msg, e) + } +} diff --git a/serenity-screenplay/src/test/groovy/net/serenitybdd/screenplay/WhenWaitingForDelayedResults.groovy b/serenity-screenplay/src/test/groovy/net/serenitybdd/screenplay/WhenWaitingForDelayedResults.groovy new file mode 100644 index 0000000000..25d8d2015e --- /dev/null +++ b/serenity-screenplay/src/test/groovy/net/serenitybdd/screenplay/WhenWaitingForDelayedResults.groovy @@ -0,0 +1,129 @@ +package net.serenitybdd.screenplay +import net.serenitybdd.core.Serenity +import net.thucydides.core.model.TestResult +import static net.thucydides.core.model.TestResult.* +import net.thucydides.core.steps.StepEventBus +import spock.lang.Specification + +import static net.serenitybdd.screenplay.EventualConsequence.eventually +import static net.serenitybdd.screenplay.GivenWhenThen.seeThat +import static org.hamcrest.Matchers.equalTo + +class WhenWaitingForDelayedResults extends Specification { + + def setup() { + Serenity.initialize(this) + StepEventBus.eventBus.testStarted("some test") + } + + def "should get a result immediately by default"() { + given: + Actor jane = Actor.named("Jane") + Clicker clicker = new Clicker(); + when: + jane.should(seeThat(TheClickerValue.of(clicker), equalTo(1))) + then: + theTestResult() == SUCCESS + } + + def "should be able to wait for a result to become available if it is slow to arrive"() { + given: + Actor jane = Actor.named("Jane") + Clicker clicker = new Clicker(); + when: + jane.should(eventually(seeThat(TheClickerValue.of(clicker), equalTo(10)))) + then: + theTestResult() == SUCCESS + } + + def "should not wait forever if the result never arrives"() { + given: + Actor jane = Actor.named("Jane") + Clicker clicker = new Clicker(); + when: + jane.should(eventually(seeThat(TheClickerValue.of(clicker), equalTo(-1))).waitingForNoLongerThan(100).milliseconds()) + then: + theTestResult() == FAILURE + } + + def "should transmit an error if one happens"() { + given: + Actor jane = Actor.named("Jane") + Clicker clicker = new Clicker(); + when: + jane.should(eventually(seeThat(TheClickerValue.ofBroken(clicker), equalTo(1))).waitingForNoLongerThan(250).milliseconds()) + then: + theTestResult() == ERROR + } + + def "should report custom error if one happens"() { + given: + Actor jane = Actor.named("Jane") + Clicker clicker = new Clicker(); + when: + jane.should(eventually(seeThat(TheClickerValue.of(clicker), equalTo(-1)). + orComplainWith(SomethingBadHappenedException)). + waitingForNoLongerThan(100).milliseconds()) + then: + theTestResult() == ERROR + } + + def "should report custom error if one is declared outside of the eventually scope"() { + given: + Actor jane = Actor.named("Jane") + Clicker clicker = new Clicker(); + when: + jane.should(eventually(seeThat(TheClickerValue.of(clicker), equalTo(-1))). + waitingForNoLongerThan(100).milliseconds().orComplainWith(SomethingBadHappenedException)) + then: + theTestResult() == ERROR + } + + private TestResult theTestResult() { + StepEventBus.eventBus.baseStepListener.testOutcomes[0].result + } +} + +class Clicker { + int count = 0 + + def click() { + count++ + } +} + + +class TheClickerValue implements Question { + private final Clicker clicker; + + TheClickerValue(Clicker clicker) { + this.clicker = clicker + } + + public static TheClickerValue of(Clicker clicker) { + new TheClickerValue(clicker) + } + + public static TheClickerValue ofBroken(Clicker clicker) { + new BrokenClickerValue(clicker) + } + + @Override + Integer answeredBy(Actor actor) { + clicker.click(); + return clicker.count; + } +} + +class BrokenClickerValue extends TheClickerValue { + private final Clicker clicker; + + BrokenClickerValue(Clicker clicker) { + super(clicker); + } + + @Override + Integer answeredBy(Actor actor) { + throw new SomethingBadHappenedException("Oh crap"); + } +} \ No newline at end of file