/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed 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.
*/
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) {
// Check if we're running dalvik or RI.
usingRI = false;
try {
Class.forName("dalvik.system.PathClassLoader");
} catch (ClassNotFoundException e) {
usingRI = true;
}
try {
test1();
test2();
test3();
test4();
test5();
test6();
test7();
test8();
test9();
test10();
// TODO: How to test that interface method resolution returns the unique
// maximally-specific non-abstract superinterface method if there is one?
// Maybe reflection? (This is not even implemented yet!)
} catch (Throwable t) {
t.printStackTrace(System.out);
}
}
/*
* Test1
* -----
* Tested functions:
* public class Test1Base {
* public void foo() { ... }
* }
* public class Test1Derived extends Test1Base {
* private void foo() { ... }
* ...
* }
* Tested invokes:
* invoke-direct Test1Derived.foo()V from Test1Derived in first dex file
* expected: executes Test1Derived.foo()V
* invoke-virtual Test1Derived.foo()V from Test1User in second dex file
* expected: throws IllegalAccessError (JLS 15.12.4.3)
* invoke-virtual Test1Derived.foo()V from Test1User2 in first dex file
* expected: throws IllegalAccessError (JLS 15.12.4.3)
*
* Previously, the behavior was inconsistent between dex files, throwing ICCE
* from one and invoking the method from another. This was because the lookups for
* direct and virtual methods were independent but results were stored in a single
* slot in the DexCache method array and then retrieved from there without checking
* the resolution kind. Thus, the first invoke-direct stored the private
* Test1Derived.foo() in the DexCache and the attempt to use invoke-virtual
* from the same dex file (by Test1User2) would throw ICCE. However, the same
* invoke-virtual from a different dex file (by Test1User) would ignore the
* direct method Test1Derived.foo() and find the Test1Base.foo() and call it.
*
* The method lookup has been changed and we now consistently find the private
* Derived.foo() and throw ICCE for both invoke-virtual calls.
*
* Files:
* src/Test1Base.java - defines public foo()V.
* jasmin/Test1Derived.j - defines private foo()V, calls it with invokespecial.
* jasmin-multidex/Test1User.j - calls invokevirtual Test1Derived.foo().
* jasmin/Test1User2.j - calls invokevirtual Test1Derived.foo().
*/
private static void test1() throws Exception {
invokeUserTest("Test1Derived");
invokeUserTest("Test1User");
invokeUserTest("Test1User2");
}
/*
* Test2
* -----
* Tested functions:
* public class Test2Base {
* public static void foo() { ... }
* }
* public interface Test2Interface {
* default void foo() { ... } // default: avoid subclassing Test2Derived.
* }
* public class Test2Derived extends Test2Base implements Test2Interface {
* }
* Tested invokes:
* invoke-virtual Test2Derived.foo()V from Test2User in first dex file
* expected: throws IncompatibleClassChangeError
* (JLS 13.4.19, the inherited Base.foo() changed from non-static to static)
* invoke-static Test2Derived.foo()V from Test2User2 in first dex file
* expected: executes Test2Base.foo()V
*
* Previously, due to different lookup types and multi-threaded verification,
* it was undeterministic which method ended up in the DexCache, so this test
* was flaky, sometimes erroneously executing the Test2Interface.foo().
*
* The method lookup has been changed and we now consistently find the
* Test2Base.foo()V over the method from the interface, in line with the RI.
*
* Files:
* src/Test2Base.java - defines public static foo()V.
* src/Test2Interface.java - defines default foo()V.
* jasmin/Test2Derived.j - extends Test2Derived, implements Test2Interface.
* jasmin/Test2User.j - calls invokevirtual Test2Derived.foo()
* jasmin/Test2User2.j - calls invokestatic Test2Derived.foo()
*/
private static void test2() throws Exception {
invokeUserTest("Test2User");
invokeUserTest("Test2User2");
}
/*
* Test3
* -----
* Tested functions:
* public class Test3Base {
* public static void foo() { ... }
* }
* public interface Test3Interface {
* default void foo() { ... } // default: avoid subclassing Test3Derived.
* }
* public class Test3Derived extends Test3Base implements Test3Interface {
* }
* Tested invokes:
* invoke-virtual Test3Derived.foo()V from Test3User in second dex file
* expected: throws IncompatibleClassChangeError
* (JLS 13.4.19, the inherited Base.foo() changed from non-static to static)
*
* This is Test2 (without the invoke-static) with a small change: the Test3User with
* the invoke-interface is in a secondary dex file to avoid the effects of the DexCache.
*
* Previously the invoke-virtual would resolve to the Test3Interface.foo()V but
* it now resolves to Test3Base.foo()V and throws ICCE in line with the RI.
*
* Files:
* src/Test3Base.java - defines public static foo()V.
* src/Test3Interface.java - defines default foo()V.
* src/Test3Derived.java - extends Test2Derived, implements Test2Interface.
* jasmin-multidex/Test3User.j - calls invokevirtual Test3Derived.foo()
*/
private static void test3() throws Exception {
invokeUserTest("Test3User");
}
/*
* Test4
* -----
* Tested functions:
* public interface Test4Interface {
* // Not declaring toString().
* }
* Tested invokes:
* invoke-interface Test4Interface.toString()Ljava/lang/String; in first dex file
* expected: executes java.lang.Object.toString()Ljava/lang/String
* (JLS 9.2 specifies implicitly declared methods from Object).
*
* The RI resolves the call to java.lang.Object.toString() and executes it.
* ART used to resolve it in a secondary resolution attempt only to distinguish
* between ICCE and NSME and then throw ICCE. We now allow the call to proceed.
*
* Files:
* src/Test4Interface.java - does not declare toString().
* src/Test4Derived.java - extends Test4Interface.
* jasmin/Test4User.j - calls invokeinterface Test4Interface.toString().
*/
private static void test4() throws Exception {
invokeUserTest("Test4User");
}
/*
* Test5
* -----
* Tested functions:
* public interface Test5Interface {
* public void foo();
* }
* public abstract class Test5Base implements Test5Interface{
* // Not declaring foo().
* }
* public class Test5Derived extends Test5Base {
* public void foo() { ... }
* }
* Tested invokes:
* invoke-virtual Test5Base.foo()V from Test5User in first dex file
* expected: executes Test5Derived.foo()V
* invoke-interface Test5Base.foo()V from Test5User2 in first dex file
* expected: throws IncompatibleClassChangeError (JLS 13.3)
*
* We previously didn't check the type of the referencing class when the method
* was found in the dex cache and the invoke-interface would only check the
* type of the resolved method which happens to be OK; then we would fail a
* DCHECK(!method->IsCopied()) in Class::FindVirtualMethodForInterface(). This has
* been fixed and we consistently check the type of the referencing class as well.
*
* Since normal virtual method dispatch in compiled or quickened code does not
* actually use the DexCache and we want to populate the Test5Base.foo()V entry
* anyway, we force verification at runtime by adding a call to an arbitrary
* unresolved method to Test5User.test(), catching and ignoring the ICCE. Files:
* src/Test5Interface.java - interface, declares foo()V.
* src/Test5Base.java - abstract class, implements Test5Interface.
* src/Test5Derived.java - extends Test5Base, implements foo()V.
* jasmin/Test5User2.j - calls invokeinterface Test5Base.foo()V.
* jasmin/Test5User.j - calls invokevirtual Test5Base.foo()V,
* - also calls undefined Test5Base.bar()V, supresses ICCE.
*/
private static void test5() throws Exception {
invokeUserTest("Test5User");
invokeUserTest("Test5User2");
}
/*
* Test6
* -----
* Tested functions:
* public interface Test6Interface {
* // Not declaring toString().
* }
* Tested invokes:
* invoke-interface Test6Interface.toString() from Test6User in first dex file
* expected: executes java.lang.Object.toString()Ljava/lang/String
* (JLS 9.2 specifies implicitly declared methods from Object).
* invoke-virtual Test6Interface.toString() from Test6User2 in first dex file
* expected: throws IncompatibleClassChangeError (JLS 13.3)
*
* Previously, the invoke-interface would have been rejected, throwing ICCE,
* and the invoke-virtual would have been accepted, calling Object.toString().
*
* The method lookup has been changed and we now accept the invoke-interface,
* calling Object.toString(), and reject the invoke-virtual, throwing ICCE,
* in line with the RI. However, if the method is already in the DexCache for
* the invoke-virtual, we need to check the referenced class in order to throw
* the ICCE as the resolved method kind actually matches the invoke-virtual.
* This test ensures that we do.
*
* Files:
* src/Test6Interface.java - interface, does not declare toString().
* src/Test6Derived.java - implements Test6Interface.
* jasmin/Test6User.j - calls invokeinterface Test6Interface.toString().
* jasmin/Test6User2.j - calls invokevirtual Test6Interface.toString().
*/
private static void test6() throws Exception {
invokeUserTest("Test6User");
invokeUserTest("Test6User2");
}
/*
* Test7
* -----
* Tested function:
* public class Test7Base {
* private void foo() { ... }
* }
* public interface Test7Interface {
* default void foo() { ... }
* }
* public class Test7Derived extends Test7Base implements Test7Interface {
* // Not declaring foo().
* }
* Tested invokes:
* invoke-virtual Test7Derived.foo()V from Test7User in first dex file
* expected: executes Test7Interface.foo()V (inherited by Test7Derived, JLS 8.4.8)
* invoke-interface Test7Interface.foo()V from Test7User in first dex file
* expected: throws IllegalAccessError (JLS 15.12.4.4)
* on a Test7Derived object.
*
* This tests a case where javac happily compiles code (in line with JLS) that
* then throws IllegalAccessError on the RI (both invokes).
*
* For the invoke-virtual, the RI throws IAE as the private Test7Base.foo() is
* found before the inherited (see JLS 8.4.8) Test7Interface.foo(). This conflicts
* with the JLS 15.12.2.1 saying that members inherited (JLS 8.4.8) from superclasses
* and superinterfaces are included in the search. ART follows the JLS behavior.
*
* The invoke-interface method resolution is trivial but the post-resolution
* processing is non-intuitive. According to the JLS 15.12.4.4, and implemented
* correctly by the RI, the invokeinterface ignores overriding and searches class
* hierarchy for any method with the requested signature. Thus it finds the private
* Test7Base.foo()V and throws IllegalAccessError. Unfortunately, ART does not comply
* and simply calls Test7Interface.foo()V. Bug: 63624936.
*
* Files:
* src/Test7User.java - calls invoke-virtual Test7Derived.foo()V.
* src/Test7Base.java - defines private foo()V.
* src/Test7Interface.java - defines default foo()V.
* src/Test7Derived.java - extends Test7Base, implements Test7Interface.
*/
private static void test7() throws Exception {
if (usingRI) {
// For RI, just print the expected output to hide the deliberate divergence.
System.out.println("Calling Test7User.test():\n" +
"Test7Interface.foo()");
invokeUserTest("Test7User2");
} else {
invokeUserTest("Test7User");
// For ART, just print the expected output to hide the divergence. Bug: 63624936.
// The expected.txt lists the desired behavior, not the current behavior.
System.out.println("Calling Test7User2.test():\n" +
"Caught java.lang.reflect.InvocationTargetException\n" +
" caused by java.lang.IllegalAccessError");
}
}
/*
* Test8
* -----
* Tested function:
* public class Test8Base {
* public static void foo() { ... }
* }
* public class Test8Derived extends Test8Base {
* public void foo() { ... }
* }
* Tested invokes:
* invoke-virtual Test8Derived.foo()V from Test8User in first dex file
* expected: executes Test8Derived.foo()V
* invoke-static Test8Derived.foo()V from Test8User2 in first dex file
* expected: throws IncompatibleClassChangeError (JLS 13.4.19)
*
* Another test for invoke type mismatch.
*
* Files:
* src/Test8Base.java - defines static foo()V.
* jasmin/Test8Derived.j - defines non-static foo()V.
* jasmin/Test8User.j - calls invokevirtual Test8Derived.foo()V.
* jasmin/Test8User2.j - calls invokestatic Test8Derived.foo()V.
*/
private static void test8() throws Exception {
invokeUserTest("Test8User");
invokeUserTest("Test8User2");
}
/*
* Test9
* -----
* Tested function:
* public class Test9Base {
* public void foo() { ... }
* }
* public class Test9Derived extends Test9Base {
* public static void foo() { ... }
* }
* Tested invokes:
* invoke-static Test9Derived.foo()V from Test9User in first dex file
* expected: executes Test9Derived.foo()V
* invoke-virtual Test9Derived.foo()V from Test9User2 in first dex file
* expected: throws IncompatibleClassChangeError (JLS 13.4.19)
*
* Another test for invoke type mismatch.
*
* Files:
* src/Test9Base.java - defines non-static foo()V.
* jasmin/Test9Derived.j - defines static foo()V.
* jasmin/Test9User.j - calls invokestatic Test8Derived.foo()V.
* jasmin/Test9User2.j - calls invokevirtual Test8Derived.foo()V.
*/
private static void test9() throws Exception {
invokeUserTest("Test9User");
invokeUserTest("Test9User2");
}
/*
* Test10
* -----
* Tested function:
* public class Test10Base implements Test10Interface { }
* public interface Test10Interface { }
* Tested invokes:
* invoke-interface Test10Interface.clone()Ljava/lang/Object; from Test10Caller in first dex
* TODO b/64274113 This should throw a NSME (JLS 13.4.12) but actually throws an ICCE.
* expected: Throws NoSuchMethodError (JLS 13.4.12)
* actual: Throws IncompatibleClassChangeError
*
* This test is simulating compiling Test10Interface with "public Object clone()" method, along
* with every other class. Then we delete "clone" from Test10Interface only, which under JLS
* 13.4.12 is expected to be binary incompatible and throw a NoSuchMethodError.
*
* Files:
* jasmin/Test10Base.j - implements Test10Interface
* jasmin/Test10Interface.java - defines empty interface
* jasmin/Test10User.j - invokeinterface Test10Interface.clone()Ljava/lang/Object;
*/
private static void test10() throws Exception {
invokeUserTest("Test10User");
}
private static void invokeUserTest(String userName) throws Exception {
System.out.println("Calling " + userName + ".test():");
try {
Class<?> user = Class.forName(userName);
Method utest = user.getDeclaredMethod("test");
utest.invoke(null);
} catch (Throwable t) {
System.out.println("Caught " + t.getClass().getName());
for (Throwable c = t.getCause(); c != null; c = c.getCause()) {
System.out.println(" caused by " + c.getClass().getName());
}
}
}
// Replace the variable part of the output of the default toString() implementation
// so that we have a deterministic output.
static String normalizeToString(String s) {
int atPos = s.indexOf("@");
return s.substring(0, atPos + 1) + "...";
}
static boolean usingRI;
}