package net.thucydides.core.steps; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.inject.Inject; import net.thucydides.core.guice.Injectors; import net.thucydides.core.model.*; import net.thucydides.core.screenshots.ScreenshotProcessor; import net.thucydides.core.webdriver.ThucydidesWebDriverSupport; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.*; /** * An event bus for Step-related notifications. * Use this to integrate Thucydides listeners with testing tools. * You create a listener (e.g. an instance of BaseStepListener, or your own), register it using * 'registerListener', and then implement the various methods (testStarted(), stepStarted()). Thucydides * will call these events on your listener as they occur. * You can register a new Thucydides listener by implementing the StepListener interface and * placing your class in the classpath. Thucydides will automatically detect the listener and add it to the * registered listeners. It will load custom listeners automatically when a test starts for the first time. */ public class StepEventBus { private static ThreadLocal<StepEventBus> stepEventBusThreadLocal = new ThreadLocal<StepEventBus>(); private static final String CORE_THUCYDIDES_PACKAGE = "net.thucydides.core"; private static final Logger LOGGER = LoggerFactory.getLogger(StepEventBus.class); /** * The event bus used to inform listening classes about when tests and test steps start and finish. * There is a separate event bus for each thread. */ public static synchronized StepEventBus getEventBus() { if (stepEventBusThreadLocal.get() == null) { stepEventBusThreadLocal.set(Injectors.getInjector().getInstance(StepEventBus.class)); } return stepEventBusThreadLocal.get(); } private List<StepListener> registeredListeners = new ArrayList<StepListener>(); /** * A reference to the base step listener, if registered. */ private BaseStepListener baseStepListener; private TestResultTally resultTally; private Stack<String> stepStack = new Stack<String>(); private Stack<Boolean> webdriverSuspensions = new Stack<Boolean>(); private Set<StepListener> customListeners; private boolean stepFailed; private boolean suspendedTest; private boolean assumptionViolated; private String assumptionViolatedMessage; private boolean uniqueSession; private Class<?> classUnderTest; private Story storyUnderTest; private final ScreenshotProcessor screenshotProcessor; @Inject public StepEventBus(ScreenshotProcessor screenshotProcessor) { this.screenshotProcessor = screenshotProcessor; } /** * Register a listener to receive notification at different points during a test's execution. * If you are writing your own listener, you shouldn't need to call this method - just set up your * listener implementation as a service (see http://download.oracle.com/javase/6/docs/api/java/util/ServiceLoader.html), * place the listener class on the classpath and it will be detected automatically. */ public StepEventBus registerListener(final StepListener listener) { if (!registeredListeners.contains(listener)) { registeredListeners.add(listener); if (BaseStepListener.class.isAssignableFrom(listener.getClass())) { baseStepListener = (BaseStepListener) listener; baseStepListener.setEventBus(this); } } return this; } private BaseStepListener getBaseStepListener() { Preconditions.checkNotNull(baseStepListener, "No BaseStepListener has been registered"); return baseStepListener; } public void testStarted(final String testName) { clear(); for (StepListener stepListener : getAllListeners()) { stepListener.testStarted(testName); } } public boolean isUniqueSession() { return uniqueSession; } public void setUniqueSession(boolean uniqueSession) { this.uniqueSession = uniqueSession; } public void testStarted(final String newTestName, final Story story) { startSuiteWithStoryForFirstTest(story); testStarted(newTestName); } public void testStarted(final String newTestName, final Class<?> testClass) { if (newTestName != null) { testStarted(newTestName); } } private void startSuiteWithStoryForFirstTest(final Story story) { if ((storyUnderTest == null) || (storyUnderTest != story)) { testSuiteStarted(story); } } protected List<StepListener> getAllListeners() { List<StepListener> allListeners = Lists.newArrayList(registeredListeners); allListeners.addAll(getCustomListeners()); return ImmutableList.copyOf(allListeners); } private Set<StepListener> getCustomListeners() { if (customListeners == null) { customListeners = Collections.synchronizedSet(new HashSet<StepListener>()); ServiceLoader<StepListener> stepListenerServiceLoader = ServiceLoader.load(StepListener.class); Iterator<StepListener> listenerImplementations = stepListenerServiceLoader.iterator(); // Iterator<?> listenerImplementations = Service.providers(StepListener.class); while (listenerImplementations.hasNext()) { try { StepListener listener = listenerImplementations.next(); if (!isACore(listener)) { LOGGER.info("Registering custom listener " + listener); customListeners.add(listener); } } catch (ServiceConfigurationError e) { LOGGER.error("Could not instantiate listener ", e); } } } return customListeners; } private boolean isACore(final StepListener listener) { return listener.getClass().getPackage().getName().startsWith(CORE_THUCYDIDES_PACKAGE); } public void testSuiteStarted(final Class<?> testClass) { LOGGER.debug("Test suite started for {}", testClass); clear(); updateClassUnderTest(testClass); for (StepListener stepListener : getAllListeners()) { stepListener.testSuiteStarted(testClass); } } private void updateClassUnderTest(final Class<?> testClass) { classUnderTest = testClass; } private void updateStoryUnderTest(final Story story) { storyUnderTest = story; } public void testSuiteStarted(final Story story) { LOGGER.debug("Test suite started for story {}", story); updateStoryUnderTest(story); for (StepListener stepListener : getAllListeners()) { stepListener.testSuiteStarted(story); } } public void clear() { stepStack.clear(); clearStepFailures(); currentTestIsNotSuspended(); noAssumptionsViolated(); resultTally = null; classUnderTest = null; webdriverSuspensions.clear(); } private void noAssumptionsViolated() { assumptionViolated = false; assumptionViolatedMessage = ""; } private void currentTestIsNotSuspended() { suspendedTest = false; } private TestResultTally getResultTally() { if (resultTally == null) { resultTally = TestResultTally.forTestClass(classUnderTest); } return resultTally; } public void testFinished() { screenshotProcessor.waitUntilDone(); TestOutcome outcome = getBaseStepListener().getCurrentTestOutcome(); for (StepListener stepListener : getAllListeners()) { stepListener.testFinished(outcome); } clear(); } public void testFinished(TestOutcome result) { screenshotProcessor.waitUntilDone(); for (StepListener stepListener : getAllListeners()) { stepListener.testFinished(result); } clear(); } public void testRetried() { for (StepListener stepListener : getAllListeners()) { stepListener.testRetried(); } clear(); } private void pushStep(String stepName) { stepStack.push(stepName); } private void popStep() { stepStack.pop(); } public void clearStepFailures() { stepFailed = false; } public boolean aStepInTheCurrentTestHasFailed() { return stepFailed; } public boolean isCurrentTestDataDriven() { return DataDrivenStep.inProgress(); } /** * Start the execution of a test step. */ public void stepStarted(final ExecutedStepDescription stepDescription) { pushStep(stepDescription.getName()); for (StepListener stepListener : getAllListeners()) { stepListener.stepStarted(stepDescription); } } /** * Record a step that is not scheduled to be executed (e.g. a skipped or ignored step). */ public void skippedStepStarted(final ExecutedStepDescription executedStepDescription) { pushStep(executedStepDescription.getName()); for (StepListener stepListener : getAllListeners()) { stepListener.skippedStepStarted(executedStepDescription); } } public void stepFinished() { stepDone(); getResultTally().logExecutedTest(); for (StepListener stepListener : getAllListeners()) { stepListener.stepFinished(); } } private void stepDone() { if (!stepStack.empty()) { popStep(); } } public void stepFailed(final StepFailure failure) { stepDone(); getResultTally().logFailure(failure); for (StepListener stepListener : getAllListeners()) { stepListener.stepFailed(failure); } stepFailed = true; } public void lastStepFailed(final StepFailure failure) { getResultTally().logFailure(failure); for (StepListener stepListener : getAllListeners()) { stepListener.stepFailed(failure); } stepFailed = true; } public void stepIgnored() { stepDone(); getResultTally().logIgnoredTest(); for (StepListener stepListener : getAllListeners()) { stepListener.stepIgnored(); } } public void stepPending() { stepPending(null); } public void stepPending(String message) { testPending(); stepDone(); getResultTally().logIgnoredTest(); for (StepListener stepListener : getAllListeners()) { if (message != null) { stepListener.stepPending(message); } else { stepListener.stepPending(); } } } public void assumptionViolated(String message) { testIgnored(); suspendTest(); stepDone(); getResultTally().logIgnoredTest(); for (StepListener stepListener : getAllListeners()) { stepListener.assumptionViolated(message); } assumptionViolated = true; assumptionViolatedMessage = message; } public void dropListener(final StepListener stepListener) { registeredListeners.remove(stepListener); } public void dropAllListeners() { registeredListeners.clear(); } public boolean webdriverCallsAreSuspended() { return aStepInTheCurrentTestHasFailed() || !webdriverSuspensions.isEmpty(); } public void reenableWebdriverCalls() { webdriverSuspensions.pop(); } public void temporarilySuspendWebdriverCalls() { webdriverSuspensions.push(true); } /** * The test failed, but not during the execution of a step. * * @param cause the underlying cause of the failure. */ public void testFailed(final Throwable cause) { TestOutcome outcome = getBaseStepListener().getCurrentTestOutcome(); for (StepListener stepListener : getAllListeners()) { try { stepListener.testFailed(outcome, cause); } catch (AbstractMethodError ame) { LOGGER.warn("Caught abstract method error - this seems to be mostly harmless."); } } } /** * Mark the current test method as pending. * The test will stil be executed to record the steps, but any webdriver calls will be skipped. */ public void testPending() { for (StepListener stepListener : getAllListeners()) { stepListener.testPending(); } suspendTest(); } public void suspendTest() { suspendedTest = true; } public boolean currentTestIsSuspended() { return suspendedTest; } public boolean assumptionViolated() { return assumptionViolated; } public void testIgnored() { for (StepListener stepListener : getAllListeners()) { stepListener.testIgnored(); } suspendTest(); } public void testSkipped() { for (StepListener stepListener : getAllListeners()) { stepListener.testSkipped(); } suspendTest(); } public boolean areStepsRunning() { return !stepStack.isEmpty(); } public void notifyScreenChange() { for (StepListener stepListener : getAllListeners()) { stepListener.notifyScreenChange(); } } public void testSuiteFinished() { for (StepListener stepListener : getAllListeners()) { stepListener.testSuiteFinished(); } if (!isUniqueSession()) { ThucydidesWebDriverSupport.closeAllDrivers(); } storyUnderTest = null; } public void testRunFinished() { System.out.println("TEST RUN FINISHED"); screenshotProcessor.waitUntilDone(); screenshotProcessor.terminate(); } public void updateCurrentStepTitle(String stepTitle) { getBaseStepListener().updateCurrentStepTitle(stepTitle); } public void addIssuesToCurrentStory(List<String> issues) { getBaseStepListener().addIssuesToCurrentStory(issues); } public void addIssuesToCurrentTest(List<String> issues) { getBaseStepListener().getCurrentTestOutcome().addIssues(issues); } public void addTagsToCurrentTest(List<TestTag> tags) { getBaseStepListener().getCurrentTestOutcome().addTags(tags); } public void addTagsToCurrentStory(List<TestTag> tags) { getBaseStepListener().addTagsToCurrentStory(tags); } public void addDescriptionToCurrentTest(String description) { getBaseStepListener().getCurrentTestOutcome().setDescription(description); } public void setBackgroundDescription(String description) { getBaseStepListener().getCurrentTestOutcome().setBackgroundDescription(description); } public void useExamplesFrom(DataTable table) { for (StepListener stepListener : getAllListeners()) { stepListener.useExamplesFrom(table); } } public void exampleStarted(Map<String, String> data) { for (StepListener stepListener : getAllListeners()) { stepListener.exampleStarted(data); } } public void exampleFinished() { for (StepListener stepListener : getAllListeners()) { stepListener.exampleFinished(); } } /** * Forces Thucydides to take a screenshot now. */ public void takeScreenshot() { getBaseStepListener().takeScreenshot(); } public boolean testSuiteHasStarted() { return getBaseStepListener().testSuiteRunning(); } public String getAssumptionViolatedMessage() { return assumptionViolatedMessage; } public Optional<TestStep> getCurrentStep() {return getBaseStepListener().cloneCurrentStep(); } /** * Set all steps in the current test outcome to a given result. * Used to set all steps to PENDING or SKIPPED, for example. * @param result */ public void setAllStepsTo(TestResult result) { baseStepListener.setAllStepsTo(result); } }