-
-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix the glue class autowiring, transaction and cucumber-glue scope issues of the spring module #711
Changes from 14 commits
6f6aba0
54d04f7
8da3bb1
6a5814e
13640b2
61c9d58
73039ae
e53331e
17aef31
ebf0da7
8ac8f43
f895b9e
5cad5cf
de2eeaf
4879f9e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,3 @@ | ||
@txn | ||
Feature: See Messages | ||
|
||
Scenario: See another user's messages | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,34 +1,37 @@ | ||
package cucumber.runtime.java.spring; | ||
|
||
import java.io.PrintWriter; | ||
import java.io.StringWriter; | ||
import java.util.Collection; | ||
import java.util.HashMap; | ||
import java.util.HashSet; | ||
import java.util.Map; | ||
|
||
import cucumber.runtime.CucumberException; | ||
import cucumber.runtime.java.ObjectFactory; | ||
import org.springframework.beans.BeansException; | ||
import org.springframework.beans.factory.config.BeanDefinition; | ||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; | ||
import org.springframework.beans.factory.support.BeanDefinitionBuilder; | ||
import org.springframework.beans.factory.support.BeanDefinitionRegistry; | ||
import org.springframework.context.ConfigurableApplicationContext; | ||
import org.springframework.context.support.GenericXmlApplicationContext; | ||
import org.springframework.test.context.ContextConfiguration; | ||
import org.springframework.test.context.ContextHierarchy; | ||
import org.springframework.test.context.TestContextManager; | ||
|
||
import cucumber.runtime.CucumberException; | ||
import cucumber.runtime.java.ObjectFactory; | ||
import java.lang.annotation.Annotation; | ||
import java.util.Collection; | ||
import java.util.HashSet; | ||
|
||
/** | ||
* Spring based implementation of ObjectFactory. | ||
* <p/> | ||
* <p> | ||
* <ul> | ||
* <li>It uses TestContextManager to create and prepare test instances. Configuration via: @ContextConfiguration | ||
* <li>It uses TestContextManager to create and prepare test instances. | ||
* Configuration via: @ContextConfiguration of @ContextHierarcy | ||
* At least on step definition class needs to have a @ContextConfiguration or | ||
* @ContextHierarchy annotation. If more that one step definition class has such | ||
* an annotation, the annotations must be equal on the different step definition | ||
* classes.</li> | ||
* <li>The step definitions class with @ContextConfiguration or @ContextHierarchy | ||
* annotation, may also have a @WebAppConfiguration or @DirtiesContext annotation. | ||
* </li> | ||
* <li>It also uses a context which contains the step definitions and is reloaded for each | ||
* scenario.</li> | ||
* <li>The step definitions added to the TestContextManagers context and | ||
* is reloaded for each scenario.</li> | ||
* </ul> | ||
* </p> | ||
* <p/> | ||
|
@@ -39,137 +42,146 @@ | |
*/ | ||
public class SpringFactory implements ObjectFactory { | ||
|
||
private static ConfigurableApplicationContext applicationContext; | ||
private static ConfigurableListableBeanFactory beanFactory; | ||
private ConfigurableListableBeanFactory beanFactory; | ||
private CucumberTestContextManager testContextManager; | ||
|
||
private final Collection<Class<?>> stepClasses = new HashSet<Class<?>>(); | ||
private final Map<Class<?>, TestContextManager> contextManagersByClass = new HashMap<Class<?>, TestContextManager>(); | ||
private Class<?> stepClassWithSpringContext = null; | ||
|
||
public SpringFactory() { | ||
} | ||
|
||
static { | ||
applicationContext = new GenericXmlApplicationContext("cucumber/runtime/java/spring/cucumber-glue.xml"); | ||
applicationContext.registerShutdownHook(); | ||
beanFactory = applicationContext.getBeanFactory(); | ||
} | ||
|
||
@Override | ||
public void addClass(final Class<?> stepClass) { | ||
if (!stepClasses.contains(stepClass)) { | ||
if (dependsOnSpringContext(stepClass)) { | ||
if (stepClassWithSpringContext == null) { | ||
stepClassWithSpringContext = stepClass; | ||
} else { | ||
checkAnnotationsEqual(stepClassWithSpringContext, stepClass); | ||
} | ||
} | ||
stepClasses.add(stepClass); | ||
|
||
BeanDefinitionRegistry registry = (BeanDefinitionRegistry) applicationContext.getAutowireCapableBeanFactory(); | ||
BeanDefinition beanDefinition = BeanDefinitionBuilder | ||
.genericBeanDefinition(stepClass) | ||
.setScope(GlueCodeScope.NAME) | ||
.getBeanDefinition(); | ||
registry.registerBeanDefinition(stepClass.getName(), beanDefinition); | ||
} | ||
} | ||
|
||
@Override | ||
public void start() { | ||
GlueCodeContext.INSTANCE.start(); | ||
} | ||
|
||
@Override | ||
public void stop() { | ||
notifyContextManagersAboutTestClassFinished(); | ||
|
||
GlueCodeContext.INSTANCE.stop(); | ||
beanFactory.destroySingletons(); | ||
private void checkAnnotationsEqual(Class<?> stepClassWithSpringContext, Class<?> stepClass) { | ||
Annotation[] annotations1 = stepClassWithSpringContext.getAnnotations(); | ||
Annotation[] annotations2 = stepClass.getAnnotations(); | ||
if (annotations1.length != annotations2.length) { | ||
throw new CucumberException("Annotations differs on glue classes found: " + | ||
stepClassWithSpringContext.getName() + ", " + | ||
stepClass.getName()); | ||
} | ||
for (Annotation annotation : annotations1) { | ||
if (!isAnnotationInArray(annotation, annotations2)) { | ||
throw new CucumberException("Annotations differs on glue classes found: " + | ||
stepClassWithSpringContext.getName() + ", " + | ||
stepClass.getName()); | ||
} | ||
} | ||
} | ||
|
||
private void notifyContextManagersAboutTestClassFinished() { | ||
Map<Class<?>, Exception> exceptionsThrown = new HashMap<Class<?>, Exception>(); | ||
|
||
for (Map.Entry<Class<?>, TestContextManager> classTestContextManagerEntry : contextManagersByClass | ||
.entrySet()) { | ||
try { | ||
classTestContextManagerEntry.getValue().afterTestClass(); | ||
} catch (Exception e) { | ||
exceptionsThrown.put(classTestContextManagerEntry.getKey(), e); | ||
private boolean isAnnotationInArray(Annotation annotation, Annotation[] annotations) { | ||
for (Annotation annotationFromArray: annotations) { | ||
if (annotation.equals(annotationFromArray)) { | ||
return true; | ||
} | ||
} | ||
|
||
contextManagersByClass.clear(); | ||
|
||
rethrowExceptionsIfAny(exceptionsThrown); | ||
return false; | ||
} | ||
|
||
private void rethrowExceptionsIfAny(Map<Class<?>, Exception> exceptionsThrown) { | ||
if (exceptionsThrown.isEmpty()) { | ||
return; | ||
@Override | ||
public void start() { | ||
if (stepClassWithSpringContext == null) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What are we protecting here against? It is logical to expect that there will be some annotated glue classes when we use SpringFactory, but imagine there are some scenarios that do not depend on Spring contexts and we have SpringFactory in our class path - would not it be better for such scenarios to execute peacefully even if their glue classes are not annotated, and when they are called individually? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When not requiring every glue class that uses spring to have an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. created a branch to illustrate the problem: mgurov@effb7d4 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is fixed in the latest commit. Now, if no |
||
throw new CucumberException("No glue class with @ContextConfiguration or " + | ||
"@ContextHierarcy annotation found in: " + stepClasses.toString()); | ||
} | ||
testContextManager = new CucumberTestContextManager(stepClassWithSpringContext); | ||
notifyContextManagerAboutTestClassStarted(); | ||
if (isFirstScenario() || isNewContextCreated()) { | ||
beanFactory = testContextManager.getBeanFactory(); | ||
for (Class<?> stepClass : stepClasses) { | ||
registerStepClassBeanDefinition(stepClass); | ||
} | ||
} | ||
GlueCodeContext.INSTANCE.start(); | ||
} | ||
|
||
if (exceptionsThrown.size() == 1) { | ||
//ony one exception, throw an exception with the correct cause | ||
Exception e = exceptionsThrown.values().iterator().next(); | ||
private void notifyContextManagerAboutTestClassStarted() { | ||
try { | ||
testContextManager.beforeTestClass(); | ||
} catch (Exception e) { | ||
throw new CucumberException(e.getMessage(), e); | ||
} | ||
} | ||
|
||
//multiple exceptions but we can only have one cause, put relevant info in the exception message | ||
//to not lose the interesting data | ||
throw new CucumberException(getMultipleExceptionMessage(exceptionsThrown)); | ||
private boolean isFirstScenario() { | ||
return beanFactory == null; | ||
} | ||
|
||
private String getMultipleExceptionMessage(Map<Class<?>, Exception> exceptionsThrow) { | ||
StringBuilder exceptionsThrown = new StringBuilder(1000); | ||
exceptionsThrown.append("Multiple exceptions occurred during processing of the TestExecutionListeners\n\n"); | ||
private boolean isNewContextCreated() { | ||
return !beanFactory.equals(testContextManager.getBeanFactory()); | ||
} | ||
|
||
for (Map.Entry<Class<?>, Exception> classExceptionEntry : exceptionsThrow.entrySet()) { | ||
exceptionsThrown.append("Exception during processing of TestExecutionListeners of "); | ||
exceptionsThrown.append(classExceptionEntry.getKey()); | ||
exceptionsThrown.append('\n'); | ||
exceptionsThrown.append(classExceptionEntry.getValue().toString()); | ||
exceptionsThrown.append('\n'); | ||
private void registerStepClassBeanDefinition(Class<?> stepClass) { | ||
BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory; | ||
BeanDefinition beanDefinition = BeanDefinitionBuilder | ||
.genericBeanDefinition(stepClass) | ||
.setScope(GlueCodeScope.NAME) | ||
.getBeanDefinition(); | ||
registry.registerBeanDefinition(stepClass.getName(), beanDefinition); | ||
} | ||
|
||
StringWriter stackTraceStringWriter = new StringWriter(); | ||
PrintWriter stackTracePrintWriter = new PrintWriter(stackTraceStringWriter); | ||
classExceptionEntry.getValue().printStackTrace(stackTracePrintWriter); | ||
exceptionsThrown.append(stackTraceStringWriter.toString()); | ||
exceptionsThrown.append('\n'); | ||
@Override | ||
public void stop() { | ||
notifyContextManagerAboutTestClassFinished(); | ||
GlueCodeContext.INSTANCE.stop(); | ||
} | ||
|
||
private void notifyContextManagerAboutTestClassFinished() { | ||
try { | ||
testContextManager.afterTestClass(); | ||
} catch (Exception e) { | ||
throw new CucumberException(e.getMessage(), e); | ||
} | ||
|
||
return exceptionsThrown.toString(); | ||
} | ||
|
||
@Override | ||
public <T> T getInstance(final Class<T> type) { | ||
if (!beanFactory.containsSingleton(type.getName())) { | ||
beanFactory.registerSingleton(type.getName(), getTestInstance(type)); | ||
try { | ||
return beanFactory.getBean(type); | ||
} catch (BeansException e) { | ||
throw new CucumberException(e.getMessage(), e); | ||
} | ||
|
||
return applicationContext.getBean(type); | ||
} | ||
|
||
private <T> T getTestInstance(final Class<T> type) { | ||
try { | ||
T instance = createTest(type); | ||
private boolean dependsOnSpringContext(Class<?> type) { | ||
return type.isAnnotationPresent(ContextConfiguration.class) | ||
|| type.isAnnotationPresent(ContextHierarchy.class); | ||
} | ||
} | ||
|
||
if (dependsOnSpringContext(type)) { | ||
TestContextManager contextManager = new TestContextManager(type); | ||
contextManager.prepareTestInstance(instance); | ||
contextManager.beforeTestClass(); | ||
class CucumberTestContextManager extends TestContextManager { | ||
|
||
contextManagersByClass.put(type, contextManager); | ||
} | ||
public CucumberTestContextManager(Class<?> testClass) { | ||
super(testClass); | ||
registerGlueCodeScope(getContext()); | ||
} | ||
|
||
return instance; | ||
} catch (Exception e) { | ||
throw new CucumberException(e.getMessage(), e); | ||
} | ||
public ConfigurableListableBeanFactory getBeanFactory() { | ||
return getContext().getBeanFactory(); | ||
} | ||
|
||
@SuppressWarnings("unchecked") | ||
protected <T> T createTest(Class<T> type) throws Exception { | ||
return (T) type.getConstructors()[0].newInstance(); | ||
private ConfigurableApplicationContext getContext() { | ||
return (ConfigurableApplicationContext)getTestContext().getApplicationContext(); | ||
} | ||
|
||
private boolean dependsOnSpringContext(Class<?> type) { | ||
return type.isAnnotationPresent(ContextConfiguration.class) | ||
|| type.isAnnotationPresent(ContextHierarchy.class); | ||
private void registerGlueCodeScope(ConfigurableApplicationContext context) { | ||
do { | ||
context.getBeanFactory().registerScope(GlueCodeScope.NAME, new GlueCodeScope()); | ||
context = (ConfigurableApplicationContext)context.getParent(); | ||
} while (context != null); | ||
} | ||
} |
This file was deleted.
This file was deleted.
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we want to fail if glue classes with different Spring annotations are found?
In real life we often see Spring-y tests that define slightly different contexts and are more or less independent from each other. AFAIU when similar independent step definitions will be loaded by the same runtime (e.g. one JUnit runner class), we'll throw an error in this PR. I'm afraid real users might find that counter-intuitive and inconvenient.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only one Spring test is loaded during each run, but more than one Cucumber step definition is loaded for each scenario. I agree with @brasmusson we should allow only one Spring context. @mgurov while I see advantages in injecting a specific Spring context in JUnit tests, I'd like to see an example of where this is needed in Cucumber. Supporting multiple context is controversial enough to deserve a separate entry in the issue tracker and a deeper discussion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also in Cucumber-JvM v1.1.3 only one spring context was allowed, then it was hardcoded that the file
cucumber.xml
on the classpath defined the spring context.From an implementation point of view, probable the biggest problem with allowing multiple context is to find out in which the SpringTransactionHooks need to be in to find the bean implementing the PlatformTransactionManager it needs to do its job.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@paoloambrosio in general I agree that less contexts is better but IMHO it is not the mission of cucumber-jvm to force one or another spring practice.
I'm a bit concerned about the proliferation of the
RunCukesTest.java
s that we see even in this change, but if you guys do not see an issue with that then fine with me.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mgurov I just do not see how it should work in detail. In which of the several context should the SpringTransactionHooks class be put (so it finds the PlatformTransactionManager it needs)? Or should it be forbidden to have more than one context if you use transactions?
And what about glue classes without context annotations, in which context should they be put? Or should we require context annotations on all glue classes (that use spring)? I have got the impression that the requirement that every glue class (that use spring) must have an context annotation is one of the problems the v1.1.6.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@brasmusson yes, that can be tricky with the
TestContextManager
. One of the options could be to (partially) reimplement it for our needs and build the context (or hierarchy of contexts) ourself. But maybe the current solution would be good enough as long as heavy spring module users do not complain.