Skip to content

Conversation

@Vampire
Copy link
Member

@Vampire Vampire commented Sep 9, 2019

I have a test like

def '#testee.getClass().simpleName should have #aliases #expectedAliases'() {
    expect:
        testee.aliases == expectedAliases

    where:
        testee                || expectedAliases
        new BaseCommand() { } || [testee.getClass().typeName[(testee.getClass().package.name.length() + 1)..-1].uncapitalize()]

    and:
        aliases = expectedAliases.size() == 1 ? 'alias' : 'aliases'
}

which runs fine with Java 8, but with Java 11 I get

Illegal method name "#testee.getClass().simpleName should have #aliases #expectedAliases" in class net/kautler/command/api/CommandTest$1
java.lang.ClassFormatError: Illegal method name "#testee.getClass().simpleName should have #aliases #expectedAliases" in class net/kautler/command/api/CommandTest$1
	at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1016)
	at java.base/java.security.SecureClassLoader.defineClass(SecureClassLoader.java:174)
	at java.base/jdk.internal.loader.BuiltinClassLoader.defineClass(BuiltinClassLoader.java:802)
	at java.base/jdk.internal.loader.BuiltinClassLoader.findClassOnClassPathOrNull(BuiltinClassLoader.java:700)
	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClassOrNull(BuiltinClassLoader.java:623)
	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:581)
	at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521)
	at net.kautler.command.api.CommandTest.#testee.getClass().simpleName should have #aliases #expectedAliases(CommandTest.groovy:56)

Where CommandTest$1 is the new BaseCommand() { }.
If I instead do

@Unroll('#testee.getClass().simpleName should have #aliases #expectedAliases')
def 'foo'() {

it works fine.

I found out this has to do with the enclosing method.
If I run with Java 8 and do testee.getClass().getEnclosingMethodInfo(),
I get #testee.getClass().simpleName should have #aliases #expectedAliases as enclosing method,
but calling testee.getClass().getEnclosingMethod() throws an InternalError("Enclosing method not found"),
as in the transformed spec the enclosing method was changed due to AST rewriting,
but the inner class was not rewritten to have a valid method as enclosing method.

It seems in Java 11 the validation in java.lang.ClassLoader.defineClass1(...) was extended
to there already validate the enclosing method name for syntactical correctness.

I still get the InternalError when using the @Unroll annotation and name the method foo,
but at least then defineClass1(...) is happy.

I added a breakpoint in org.spockframework.compiler.SpecRewriter#transplantMethod
after newAst was created that does evaluate
oldAst.declaringClass.innerClasses.findAll { it.enclosingMethod == oldAst }*.enclosingMethod = newAst
and suddenly the test succeeds properly and also testee.enclosingMethod can be properly resolved.

So the changes in this PR do exactly this, they find inner classes that have the
old rewritten feature method as enclosing method and set the new rewritten feature
method as enclosing method instead.


This change is Reviewable

@codecov
Copy link

codecov bot commented Sep 9, 2019

Codecov Report

Merging #1027 into master will not change coverage.
The diff coverage is n/a.

Impacted file tree graph

@@            Coverage Diff            @@
##             master    #1027   +/-   ##
=========================================
  Coverage     74.01%   74.01%           
  Complexity     3434     3434           
=========================================
  Files           382      382           
  Lines         10614    10614           
  Branches       1297     1297           
=========================================
  Hits           7856     7856           
  Misses         2294     2294           
  Partials        464      464
Impacted Files Coverage Δ Complexity Δ
...pock-core/src/main/java/spock/mock/MockingApi.java 2.94% <ø> (ø) 2 <0> (ø) ⬇️
...n/java/spock/util/concurrent/BlockingVariable.java 93.75% <ø> (ø) 6 <0> (ø) ⬇️
.../java/spock/util/concurrent/PollingConditions.java 90.9% <ø> (ø) 14 <0> (ø) ⬇️
...in/java/spock/util/concurrent/AsyncConditions.java 93.54% <ø> (ø) 9 <0> (ø) ⬇️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 1d135ed...01634b2. Read the comment docs.

@Vampire
Copy link
Member Author

Vampire commented Sep 9, 2019

While trying to satisfy Codecov, I spent a tremendous amount of trial and error to find out what actually triggers the failure, as I had a bit of a problem to write a small but failing test.
The problem actually is the dot in the method name.
With def '.'() { it fails in ClassLoader#defineClass1 for Java 11, but not for Java 8.

So it "only" fails if either

  • Java 11 (or 9 or 10, didn't test with those)
  • any dot in the method name
  • an anonymous inner class in that method
    => fails in ClassLoader#defineClass1

or

  • no matter which Java version
  • an anonymous inner class in any feature method, no matter what name
  • something is trying to access Class#getEnclosingMethod() of the anonymous class
    => fails with InternalError

@Vampire Vampire force-pushed the fix-anonymous-classes branch from c84876b to f3e84e5 Compare September 9, 2019 22:05
@Vampire Vampire changed the title Fix anonymous inner classes (Java 11 incompatibility) Fix anonymous inner classes Sep 9, 2019
@Vampire
Copy link
Member Author

Vampire commented Sep 10, 2019

If anyone is in need for a work-around that does not involve changing the code under test but is a proper generic solution, here is a custom Groovy AST transformation that does what this PR is doing after Spock did its transformations:

@GroovyASTTransformation
public class AnonymousClassTransform implements ASTTransformation {
    @Override
    public void visit(ASTNode[] nodes, SourceUnit source) {
        ((ModuleNode) nodes[0])
                .getClasses()
                .stream()
                .filter(clazz -> clazz.getEnclosingMethod() != null)
                .forEach(clazz -> {
                    MethodNode enclosingMethod = clazz.getEnclosingMethod();
                    String enclosingMethodName = enclosingMethod.getName();
                    enclosingMethod
                            .getDeclaringClass()
                            .getMethods()
                            .stream()
                            .filter(method -> method
                                    .getAnnotations()
                                    .stream()
                                    .anyMatch(annotation -> {
                                        ClassNode annotationClass = annotation.getClassNode();
                                        Expression nameAttribute = annotation.getMember("name");
                                        return (annotationClass != null)
                                                && (nameAttribute != null)
                                                && "org.spockframework.runtime.model.FeatureMetadata".equals(annotationClass.getName())
                                                && enclosingMethodName.equals(nameAttribute.getText());
                                    }))
                            .findAny()
                            .ifPresent(clazz::setEnclosingMethod);
                });
    }
}

@Vampire Vampire force-pushed the fix-anonymous-classes branch from f3e84e5 to 1d135ed Compare November 5, 2019 10:50
@leonard84 leonard84 merged commit 3b2e01c into spockframework:master Jan 12, 2020
@Vampire Vampire deleted the fix-anonymous-classes branch January 12, 2020 22:32
@szpak szpak mentioned this pull request Jan 14, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants