spock icon indicating copy to clipboard operation
spock copied to clipboard

Accessing Spring from custom extension: Change global extension execution order

Open tburny opened this issue 7 years ago • 6 comments

Issue description

Currently global extensions are executed in the order they are discovered in the class path. That poses a problem when writing custom extensions which try to access fields in the Specification which will be initialized by Spring via @Autowired. When I have MyGlobalExtension in MyProject, which depends on spock-spring and last on spock-core, the Spring extension will always add its IMethodInterceptors after MyGlobalExtension.

As far as I understand, there are a few possible workarounds (solutions) to the problem:

  • Allow defining the order other extensions by listing them META-INF/services/org.spockframework.runtime.extension.IGlobalExtension, so the globalExtensionClasses in GlobalExtensionRegistry would probably become a LinkedHashSet<Class<?>>. This would make the order of extensions configurable explicitly. (BUG: Currently the SpringExtension will be loaded twice if you do add it to your own extension configuration file)
  • Reverse the order of Extensions before execution in ExtensionRunner:
  private void runGlobalExtensions() {
    final List<IGlobalExtension> globalExtensions = new ArrayList<IGlobalExtension>(extensionRegistry.getGlobalExtensions());
    Collections.reverse(globalExtensions);
    for (IGlobalExtension extension : globalExtensions) {
      extension.visitSpec(spec);
    }
  }

This ensures that all extensions in dependencies are loaded first, but would not fix the problem between extensions in different dependencies on the classpath

  • Always load the Spring extension first programatically

I will happily contribute a fix along with tests and a Gist if required :)

Java/JDK

java -version 1.7+

Groovy version

Note that versions older than 2.0 are no longer supported.

groovy -version 2.4

Build tool version

Gradle

gradle -version Gradle wrapper from spock-example project

Operating System

Windows 10

IDE

IntelliJ

Build-tool dependencies used

Gradle/Grails

compile 'org.spockframework:spock-core:1.1-groovy-2.4'
compile 'org.spockframework:spock-spring:1.1-groovy-2.4'

tburny avatar Jan 26 '18 19:01 tburny

releates #646

leonard84 avatar Jan 26 '18 21:01 leonard84

Thanks a lot for replying so quickly! Your are perfectly right in your comment in the pull request, sorry for wasting your time :-/

So as a result I took a step back, reconsidering what needs to be done.

In my opinion the problem could be split into three more or less independent components:

  • Declaring dependencies either via annotaiton or interface
  • Building and error-checking a dependency graph (keyword: Cycle detection)
  • Resolving and loading dependencies

I would like to suggest solving the first one and then moving on to the others 👍

Declaring dependencies of an extension

As an extension developer I would like to declare dependencies on other extensions so these are loaded first (happens-before relationship).

This has to be done before starting the extension and can be implemented

  • via an annotation, executed after all Extensions were discovered, but not instantiated

    @DependsOn([Foo, Bar, Baz])
    class MyExtension {}
    
    • Advantages: Static resolution: Dependencies can be resolved before the extension is instanciatiated
    • Disadvantages: Less flexible, cannot declare dependencies conditionally (e.g. depending on System environment)
  • via an interface, executed after the extensions are loaded, but not started yet

    interface IExtensionDependencies { Set<Class<?>> getDependencies() }
    
    class MyExtension extends AbstractGlobalExtension implements IExtensionDependencies {
    
      @Override
      Set<Class<?>> getDependencies(List<Class<?>> availableExtensions) { return [Foo, Bar].toSet() }
    }
    

    The availableExtensions parameter contains a list of all known extensions. This allows declaring a dependency if it is present (i.e. "load SpringExtension, if available").

    • Advantages: Most flexible solution, can declare dependencies flexibly within normal groovy code
    • Disadvantages: No static dependency analysis on the extension class (instance only).

As far as I understand both mechanisms could be used on global and annotation driven extensions. What do you think?

P.S.: I'm also available on Gitter.im (gitter.im/tburny) if you prefer discussing this on the spock channel/via DM :)

tburny avatar Feb 07 '18 09:02 tburny

I'd like to avoid having to do a full dependency tree calculation for each test, for global extensions this is so much of an issue, but annotation based ones can vary for each test. I was thinking of using the simple ordering style (simple int), but that too has limitations.

Only having dependencies might be a bit limiting, you can not express that you'd like to run before a certain extension.

Ordering just the extensions might also be not enough, since extensions usually work by adding interceptors, which do the actual work. Normally you'd want to have a certain order going in (setup...) and the reverse order going out (cleanup...). So you actually want to order interceptors and not the extensions themselves.

leonard84 avatar Feb 12 '18 17:02 leonard84

how to use this change "Fix spockframework#817" by using maven (1.3-groovy-2.x)

xukaizai avatar May 06 '19 13:05 xukaizai