/* * 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 com.github.database.rider.cucumber; import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.*; import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.Logger; import javax.enterprise.context.ApplicationScoped; import javax.enterprise.context.RequestScoped; import javax.enterprise.context.SessionScoped; import javax.inject.Singleton; import org.apache.deltaspike.cdise.api.CdiContainer; import org.apache.deltaspike.cdise.api.CdiContainerLoader; import org.apache.deltaspike.cdise.api.ContextControl; import org.apache.deltaspike.core.api.projectstage.ProjectStage; import org.apache.deltaspike.core.api.provider.BeanProvider; import org.apache.deltaspike.core.util.ExceptionUtils; import org.apache.deltaspike.core.util.ProjectStageProducer; import org.apache.deltaspike.core.util.ServiceUtils; import org.apache.deltaspike.testcontrol.api.TestControl; import org.apache.deltaspike.testcontrol.api.junit.CdiTestSuiteRunner; import org.apache.deltaspike.testcontrol.api.literal.TestControlLiteral; import org.apache.deltaspike.testcontrol.spi.ExternalContainer; import org.apache.deltaspike.testcontrol.spi.TestAware; import org.apache.deltaspike.testcontrol.spi.TestControlValidator; import org.junit.runner.notification.RunNotifier; import org.junit.runners.model.InitializationError; import cucumber.api.junit.Cucumber; import cucumber.runtime.junit.FeatureRunner; /** * Created by pestano on 05/10/15. */ public class CdiCucumberTestRunner extends Cucumber { private static final Logger LOGGER = Logger.getLogger(CdiCucumberTestRunner.class.getName()); private boolean containerStarted = false; private ContainerAwareTestContext testContext; public CdiCucumberTestRunner(Class<?> testClass) throws InitializationError, IOException { super(testClass); TestControl testControl = testClass.getAnnotation(TestControl.class); this.testContext = new ContainerAwareTestContext(testControl, null); //benefits from the fallback-handling in ContainerAwareTestContext Class<? extends Handler> logHandlerClass = this.testContext.getLogHandlerClass(); if (!Handler.class.equals(logHandlerClass)) { try { LOGGER.addHandler(logHandlerClass.newInstance()); } catch (Exception e) { throw ExceptionUtils.throwAsRuntimeException(e); } } } @Override public void run(RunNotifier runNotifier) { CdiContainer container = CdiContainerLoader.getCdiContainer(); if (!containerStarted) { container.boot(CdiTestSuiteRunner.getTestContainerConfig()); containerStarted = true; } super.run(runNotifier); } @Override protected void runChild(FeatureRunner featureRunner, RunNotifier notifier) { TestControl testControl = getTestClass().getJavaClass().getAnnotation(TestControl.class); ContainerAwareTestContext currentTestContext = new ContainerAwareTestContext(testControl, this.testContext); currentTestContext.applyBeforeFeatureConfig(getTestClass().getJavaClass()); try { super.runChild(featureRunner, notifier); } finally { currentTestContext.applyAfterFeatureConfig(); } } private static class ContainerAwareTestContext { private ContainerAwareTestContext parent; private final ProjectStage projectStage; private final TestControl testControl; private ProjectStage previousProjectStage; private boolean containerStarted = false; //only true for the layer it was started in private Stack<Class<? extends Annotation>> startedScopes = new Stack<Class<? extends Annotation>>(); private List<ExternalContainer> externalContainers; ContainerAwareTestContext(TestControl testControl, ContainerAwareTestContext parent) { this.parent = parent; Class<? extends ProjectStage> foundProjectStageClass; if (testControl == null) { this.testControl = new TestControlLiteral(); if (parent != null) { foundProjectStageClass = parent.testControl.projectStage(); } else { foundProjectStageClass = this.testControl.projectStage(); } } else { this.testControl = testControl; foundProjectStageClass = this.testControl.projectStage(); } this.projectStage = ProjectStage.valueOf(foundProjectStageClass.getSimpleName()); ProjectStageProducer.setProjectStage(this.projectStage); } boolean isContainerStarted() { return this.containerStarted || (this.parent != null && this.parent.isContainerStarted()); } Class<? extends Handler> getLogHandlerClass() { return this.testControl.logHandler(); } void applyBeforeFeatureConfig(Class testClass) { CdiContainer container = CdiContainerLoader.getCdiContainer(); if (!isContainerStarted()) { container.boot(CdiTestSuiteRunner.getTestContainerConfig()); containerStarted = true; bootExternalContainers(testClass); } List<Class<? extends Annotation>> restrictedScopes = new ArrayList<Class<? extends Annotation>>(); //controlled by the container and not supported by weld: restrictedScopes.add(ApplicationScoped.class); restrictedScopes.add(Singleton.class); if (this.parent == null && this.testControl.getClass().equals(TestControlLiteral.class)) { //skip scope-handling if @TestControl isn't used explicitly on the test-class -> TODO re-visit it restrictedScopes.add(RequestScoped.class); restrictedScopes.add(SessionScoped.class); } this.previousProjectStage = ProjectStageProducer.getInstance().getProjectStage(); ProjectStageProducer.setProjectStage(this.projectStage); startScopes(container, testClass, null, restrictedScopes.toArray(new Class[restrictedScopes.size()])); } private void bootExternalContainers(Class testClass) { if (!this.testControl.startExternalContainers()) { return; } if (this.externalContainers == null) { List<ExternalContainer> configuredExternalContainers = ServiceUtils.loadServiceImplementations(ExternalContainer.class); Collections.sort(configuredExternalContainers, new Comparator<ExternalContainer>() { @Override public int compare(ExternalContainer ec1, ExternalContainer ec2) { return ec1.getOrdinal() > ec2.getOrdinal() ? 1 : -1; } }); this.externalContainers = new ArrayList<ExternalContainer>(configuredExternalContainers.size()); ExternalContainer externalContainerBean; for (ExternalContainer externalContainer : configuredExternalContainers) { //needed to use cdi-observers in the container optionally externalContainerBean = BeanProvider.getContextualReference(externalContainer.getClass(), true); if (externalContainerBean != null) { this.externalContainers.add(externalContainerBean); } else { this.externalContainers.add(externalContainer); } } for (ExternalContainer externalContainer : this.externalContainers) { try { if (externalContainer instanceof TestAware) { ((TestAware) externalContainer).setTestClass(testClass); } externalContainer.boot(); } catch (RuntimeException e) { Logger.getLogger(CdiCucumberTestRunner.class.getName()).log(Level.WARNING, "booting " + externalContainer.getClass().getName() + " failed", e); } } } } void applyAfterFeatureConfig() { ProjectStageProducer.setProjectStage(previousProjectStage); previousProjectStage = null; CdiContainer container = CdiContainerLoader.getCdiContainer(); stopStartedScopes(container); if (this.containerStarted) { if (isStopContainerAllowed()) { shutdownExternalContainers(); container.shutdown(); //stop the container on the same level which started it containerStarted = false; } } } private boolean isStopContainerAllowed() { return true; } private void shutdownExternalContainers() { if (this.externalContainers == null) { return; } for (ExternalContainer externalContainer : this.externalContainers) { try { externalContainer.shutdown(); } catch (RuntimeException e) { Logger.getLogger(CdiCucumberTestRunner.class.getName()).log(Level.WARNING, "shutting down " + externalContainer.getClass().getName() + " failed", e); } } } private void startScopes(CdiContainer container, Class testClass, Method testMethod, Class<? extends Annotation>... restrictedScopes) { ContextControl contextControl = container.getContextControl(); List<Class<? extends Annotation>> scopeClasses = new ArrayList<Class<? extends Annotation>>(); Collections.addAll(scopeClasses, this.testControl.startScopes()); if (scopeClasses.isEmpty()) { addScopesForDefaultBehavior(scopeClasses); } else { List<TestControlValidator> testControlValidatorList = ServiceUtils.loadServiceImplementations(TestControlValidator.class); for (TestControlValidator testControlValidator : testControlValidatorList) { if (testControlValidator instanceof TestAware) { if (testMethod != null) { ((TestAware) testControlValidator).setTestMethod(testMethod); } ((TestAware) testControlValidator).setTestClass(testClass); } try { testControlValidator.validate(this.testControl); } finally { if (testControlValidator instanceof TestAware) { ((TestAware) testControlValidator).setTestClass(null); ((TestAware) testControlValidator).setTestMethod(null); } } } } for (Class<? extends Annotation> scopeAnnotation : scopeClasses) { if (this.parent != null && this.parent.isScopeStarted(scopeAnnotation)) { continue; } if (isRestrictedScope(scopeAnnotation, restrictedScopes)) { continue; } try { //force a clean context - TODO discuss onScopeStopped call contextControl.stopContext(scopeAnnotation); contextControl.startContext(scopeAnnotation); this.startedScopes.add(scopeAnnotation); onScopeStarted(scopeAnnotation); } catch (RuntimeException e) { Logger logger = Logger.getLogger(CdiCucumberTestRunner.class.getName()); logger.setLevel(Level.SEVERE); logger.log(Level.SEVERE, "failed to start scope @" + scopeAnnotation.getName(), e); } } } private void addScopesForDefaultBehavior(List<Class<? extends Annotation>> scopeClasses) { if (this.parent != null && !this.parent.isScopeStarted(RequestScoped.class)) { if (!scopeClasses.contains(RequestScoped.class)) { scopeClasses.add(RequestScoped.class); } } if (this.parent != null && !this.parent.isScopeStarted(SessionScoped.class)) { if (!scopeClasses.contains(SessionScoped.class)) { scopeClasses.add(SessionScoped.class); } } } private boolean isRestrictedScope(Class<? extends Annotation> scopeAnnotation, Class<? extends Annotation>[] restrictedScopes) { for (Class<? extends Annotation> restrictedScope : restrictedScopes) { if (scopeAnnotation.equals(restrictedScope)) { return true; } } return false; } private boolean isScopeStarted(Class<? extends Annotation> scopeAnnotation) { return this.startedScopes.contains(scopeAnnotation); } private void stopStartedScopes(CdiContainer container) { while (!this.startedScopes.empty()) { Class<? extends Annotation> scopeAnnotation = this.startedScopes.pop(); //TODO check if context was started by parent try { container.getContextControl().stopContext(scopeAnnotation); onScopeStopped(scopeAnnotation); } catch (RuntimeException e) { Logger logger = Logger.getLogger(CdiCucumberTestRunner.class.getName()); logger.setLevel(Level.SEVERE); logger.log(Level.SEVERE, "failed to stop scope @" + scopeAnnotation.getName(), e); } } } private void onScopeStarted(Class<? extends Annotation> scopeClass) { List<ExternalContainer> externalContainerList = collectExternalContainers(this); for (ExternalContainer externalContainer : externalContainerList) { externalContainer.startScope(scopeClass); } } private void onScopeStopped(Class<? extends Annotation> scopeClass) { List<ExternalContainer> externalContainerList = collectExternalContainers(this); for (ExternalContainer externalContainer : externalContainerList) { externalContainer.stopScope(scopeClass); } } private static List<ExternalContainer> collectExternalContainers(ContainerAwareTestContext testContext) { List<ExternalContainer> result = new ArrayList<ExternalContainer>(); if (testContext.externalContainers != null) { result.addAll(testContext.externalContainers); } if (testContext.parent != null) { result.addAll(collectExternalContainers(testContext.parent)); } return result; } } }