diff --git a/README.md b/README.md index 5e9f0fe..7563700 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ 阅读[这里](https://mp.weixin.qq.com/s/KyU6Eu7mDkZU8FspfSqfMw)了解更多故事。 > 特别说明 -> 1. 如遇到"Attempt to access none-static member in mock method"错误,参见[常见问题](https://alibaba.github.io/testable-mock/#/zh-cn/doc/frequently-asked-questions)第8条 +> 1. 如遇到"Attempt to access non-static member in mock method"错误,参见[常见问题](https://alibaba.github.io/testable-mock/#/zh-cn/doc/frequently-asked-questions)第8条 > 2. 如果有遇到其他任何使用问题和建议,请直接在[Issue](https://github.com/alibaba/testable-mock/issues)中提出,也可通过[Pull Request](https://github.com/alibaba/testable-mock/pulls)提交您的代码,我们将在24小时内回复并处理 ----- diff --git a/docs/en-us/doc/frequently-asked-questions.md b/docs/en-us/doc/frequently-asked-questions.md index 98a0e47..6500f7e 100644 --- a/docs/en-us/doc/frequently-asked-questions.md +++ b/docs/en-us/doc/frequently-asked-questions.md @@ -47,7 +47,7 @@ It can be used in combination with [Roboelectric](https://github.com/robolectric The `Dalvik` and `ART` virtual machines of the Android system use a bytecode system different from the standard JVM, which will affect the normal functionality of `TestableMock`. The `Roboelectric` framework can run Android unit tests on a standard JVM virtual machine, which is much faster than running unit tests through the Android virtual machine. Recently, most Android App unit tests are written with the `Roboelectric` framework. -#### 8. Meet "Attempt to access none-static member in mock method" error during mocking? +#### 8. Meet "Attempt to access non-static member in mock method" error during mocking? The current design of `TestableMock` does not allow access to the non-`static` members of the test class in the mock method (because the mock method itself will be dynamically modified to the `static` type during runtime). However, some Java statements include building blocks (like `new ArrayList() {{ append("data"); }}`), lambda expression (like `list.stream().map(i -> i. get)`) and so on, will generate additional member method invocations during compilation, causing mock method execution report above error. diff --git a/docs/zh-cn/doc/frequently-asked-questions.md b/docs/zh-cn/doc/frequently-asked-questions.md index f607b84..e5d6dc5 100644 --- a/docs/zh-cn/doc/frequently-asked-questions.md +++ b/docs/zh-cn/doc/frequently-asked-questions.md @@ -47,7 +47,7 @@ Kotlin语言中的`String`类型实际上是`kotlin.String`,而非`java.lang.S Android系统的`Dalvik`和`ART`虚拟机采用了与标准JVM不同的字节码体系,会影响`TestableMock`的正常工作。`Roboelectric`框架能在普通JVM虚拟机上运行Android单元测试,其速度比通过Android虚拟机运行单元测试快非常多,绝大多数Android App的单元测试都在使用`Roboelectric`框架。 -#### 8. 使用Mock时候遇到"Attempt to access none-static member in mock method"错误? +#### 8. 使用Mock时候遇到"Attempt to access non-static member in mock method"错误? 当前`TestableMock`的设计不允许在Mock方法中访问测试类的非`static`成员(因为Mock方法自身会在运行期被动态修改为`static`类型)。然而有些Java语句,包括构造块(譬如`new ArrayList() {{ append("data"); }}`)、匿名函数(譬如`list.stream().map(i -> i.get)`)等等,会在编译过程中生成额外的成员方法调用,导致Mock方法执行报错。 diff --git a/testable-processor/src/main/java/com/alibaba/testable/processor/model/MemberRecord.java b/testable-processor/src/main/java/com/alibaba/testable/processor/model/MemberRecord.java new file mode 100644 index 0000000..b5a9280 --- /dev/null +++ b/testable-processor/src/main/java/com/alibaba/testable/processor/model/MemberRecord.java @@ -0,0 +1,30 @@ +package com.alibaba.testable.processor.model; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author flin + */ +public class MemberRecord { + + /** + * Record private and final fields + */ + public final List privateOrFinalFields = new ArrayList(); + /** + * Record non-private fields + */ + public final List nonPrivateNorFinalFields = new ArrayList(); + /** + * Record private methods and possible parameter counts (negative number means large or equals) + */ + public final Map> privateMethods = new HashMap>(); + /** + * Record non-private methods and possible parameter counts (negative number means large or equals) + */ + public final Map> nonPrivateMethods = new HashMap>(); + +} diff --git a/testable-processor/src/main/java/com/alibaba/testable/processor/model/MemberType.java b/testable-processor/src/main/java/com/alibaba/testable/processor/model/MemberType.java index 6cab0bb..bc88853 100644 --- a/testable-processor/src/main/java/com/alibaba/testable/processor/model/MemberType.java +++ b/testable-processor/src/main/java/com/alibaba/testable/processor/model/MemberType.java @@ -13,8 +13,8 @@ public enum MemberType { STATIC_PRIVATE, /** - * None private member + * Non-private member */ - NONE_PRIVATE + NON_PRIVATE } diff --git a/testable-processor/src/main/java/com/alibaba/testable/processor/translator/EnablePrivateAccessTranslator.java b/testable-processor/src/main/java/com/alibaba/testable/processor/translator/EnablePrivateAccessTranslator.java index a39d891..bd3b375 100644 --- a/testable-processor/src/main/java/com/alibaba/testable/processor/translator/EnablePrivateAccessTranslator.java +++ b/testable-processor/src/main/java/com/alibaba/testable/processor/translator/EnablePrivateAccessTranslator.java @@ -2,6 +2,7 @@ package com.alibaba.testable.processor.translator; import com.alibaba.testable.processor.constant.ConstPool; import com.alibaba.testable.processor.generator.PrivateAccessStatementGenerator; +import com.alibaba.testable.processor.model.MemberRecord; import com.alibaba.testable.processor.model.MemberType; import com.alibaba.testable.processor.model.TestableContext; import com.alibaba.testable.processor.util.PathUtil; @@ -17,6 +18,9 @@ import java.lang.reflect.Modifier; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; /** * Travel AST @@ -39,13 +43,9 @@ public class EnablePrivateAccessTranslator extends BaseTranslator { */ private final ListBuffer sourceClassIns = new ListBuffer(); /** - * Record private and final fields + * Member information of source class */ - private final ListBuffer privateOrFinalFields = new ListBuffer(); - /** - * Record private methods - */ - private final ListBuffer privateMethods = new ListBuffer(); + private final MemberRecord memberRecord = new MemberRecord(); private final PrivateAccessStatementGenerator privateAccessStatementGenerator; private final PrivateAccessChecker privateAccessChecker; @@ -67,8 +67,7 @@ public class EnablePrivateAccessTranslator extends BaseTranslator { } catch (Exception e) { e.printStackTrace(); } - this.privateAccessChecker = new PrivateAccessChecker(sourceClassName.toString(), - privateOrFinalFields.toList(), privateMethods.toList()); + this.privateAccessChecker = new PrivateAccessChecker(cx, sourceClassName.toString(), memberRecord); } /** @@ -202,38 +201,62 @@ public class EnablePrivateAccessTranslator extends BaseTranslator { Field[] fields = cls.getDeclaredFields(); for (Field f : fields) { if (Modifier.isFinal(f.getModifiers()) || Modifier.isPrivate(f.getModifiers())) { - privateOrFinalFields.add(f.getName()); + memberRecord.privateOrFinalFields.add(f.getName()); + } else { + memberRecord.nonPrivateNorFinalFields.add(f.getName()); } } Method[] methods = cls.getDeclaredMethods(); - for (Method m : methods) { + for (final Method m : methods) { if (Modifier.isPrivate(m.getModifiers())) { - privateMethods.add(m.getName()); + checkAndAdd(memberRecord.privateMethods, m.getName(), getParameterLength(m)); + } else { + checkAndAdd(memberRecord.nonPrivateMethods, m.getName(), getParameterLength(m)); } } } + private void checkAndAdd(Map> map, String key, final int value) { + if (map.containsKey(key)) { + map.get(key).add(value); + } else { + map.put(key, new ArrayList() {{ add(value); }}); + } + } + + private int getParameterLength(Method m) { + int length = m.getParameterTypes().length; + if (length == 0) { + return 0; + } + if (m.getParameterTypes()[length - 1].getName().startsWith("[")) { + return -(length - 1); + } else { + return length; + } + } + private MemberType checkGetterType(JCFieldAccess access) { - if (access.selected instanceof JCIdent && privateOrFinalFields.contains(access.name.toString())) { + if (access.selected instanceof JCIdent && memberRecord.privateOrFinalFields.contains(access.name.toString())) { return checkSourceClassOrIns(((JCIdent)access.selected).name); } - return MemberType.NONE_PRIVATE; + return MemberType.NON_PRIVATE; } private MemberType checkSetterType(JCAssign assign) { if (assign.lhs instanceof JCFieldAccess && ((JCFieldAccess)(assign).lhs).selected instanceof JCIdent && - privateOrFinalFields.contains(((JCFieldAccess)(assign).lhs).name.toString())) { + memberRecord.privateOrFinalFields.contains(((JCFieldAccess)(assign).lhs).name.toString())) { return checkSourceClassOrIns(((JCIdent)((JCFieldAccess)(assign).lhs).selected).name); } - return MemberType.NONE_PRIVATE; + return MemberType.NON_PRIVATE; } private MemberType checkInvokeType(JCMethodInvocation expr) { if (expr.meth instanceof JCFieldAccess && ((JCFieldAccess)(expr).meth).selected instanceof JCIdent && - privateMethods.contains(((JCFieldAccess)(expr).meth).name.toString())) { + memberRecord.privateMethods.containsKey(((JCFieldAccess)(expr).meth).name.toString())) { return checkSourceClassOrIns(((JCIdent)((JCFieldAccess)(expr).meth).selected).name); } - return MemberType.NONE_PRIVATE; + return MemberType.NON_PRIVATE; } private MemberType checkSourceClassOrIns(Name name) { @@ -242,7 +265,7 @@ public class EnablePrivateAccessTranslator extends BaseTranslator { } else if (sourceClassIns.contains(name)) { return MemberType.PRIVATE_OR_FINAL; } - return MemberType.NONE_PRIVATE; + return MemberType.NON_PRIVATE; } } diff --git a/testable-processor/src/main/java/com/alibaba/testable/processor/translator/PrivateAccessChecker.java b/testable-processor/src/main/java/com/alibaba/testable/processor/translator/PrivateAccessChecker.java index f72f03d..ca63058 100644 --- a/testable-processor/src/main/java/com/alibaba/testable/processor/translator/PrivateAccessChecker.java +++ b/testable-processor/src/main/java/com/alibaba/testable/processor/translator/PrivateAccessChecker.java @@ -1,10 +1,13 @@ package com.alibaba.testable.processor.translator; import com.alibaba.testable.processor.exception.MemberNotExistException; +import com.alibaba.testable.processor.model.MemberRecord; +import com.alibaba.testable.processor.model.TestableContext; import com.sun.tools.javac.tree.JCTree; import java.util.Arrays; import java.util.List; +import java.util.Map; /** * Validate parameter of PrivateAccessor methods to prevent broken by refactor @@ -21,14 +24,14 @@ public class PrivateAccessChecker { private static final String TYPE_FIELD = "Field"; private static final String TYPE_METHOD = "Method"; + private final TestableContext cx; private final String className; - private final List privateOrFinalFields; - private final List privateMethods; + private final MemberRecord sourceMembers; - public PrivateAccessChecker(String className, List privateOrFinalFields, List privateMethods) { + public PrivateAccessChecker(TestableContext cx, String className, MemberRecord memberRecord) { + this.cx = cx; this.className = className; - this.privateOrFinalFields = privateOrFinalFields; - this.privateMethods = privateMethods; + this.sourceMembers = memberRecord; } public void validate(JCTree.JCMethodInvocation invocation) { @@ -39,14 +42,41 @@ public class PrivateAccessChecker { Object target = ((JCTree.JCLiteral)invocation.args.get(1)).getValue(); if (target instanceof String) { String methodName = fieldAccess.name.toString(); - if (FIELD_ACCESS_METHOD.contains(methodName) && !privateOrFinalFields.contains(target)) { - throw new MemberNotExistException(TYPE_FIELD, className, (String)target); - } else if (FIELD_INVOKE_METHOD.contains(methodName) && !privateMethods.contains(target)) { - throw new MemberNotExistException(TYPE_METHOD, className, (String)target); + if (FIELD_ACCESS_METHOD.contains(methodName)) { + if (sourceMembers.nonPrivateNorFinalFields.contains(target)) { + cx.logger.warn("Field " + className + "::" + target + " is neither private nor final."); + } else if (!sourceMembers.privateOrFinalFields.contains(target)) { + throw new MemberNotExistException(TYPE_FIELD, className, (String)target); + } + } else if (FIELD_INVOKE_METHOD.contains(methodName)) { + int parameterCount = invocation.args.length() - 2; + // Because of override, check private method list first + if (sourceMembers.privateMethods.containsKey(target) && + checkParameterCount(sourceMembers.privateMethods, (String)target, parameterCount)) { + // Let it go + } else if (sourceMembers.nonPrivateMethods.containsKey(target) && + checkParameterCount(sourceMembers.privateMethods, (String)target, parameterCount)) { + cx.logger.warn("Method " + className + "::" + target + " is not private."); + } else { + throw new MemberNotExistException(TYPE_METHOD, className, (String)target); + } } } } } } + private boolean checkParameterCount(Map> methods, String target, int parameterCount) { + for (Integer expectCount : methods.get(target)) { + if (countMatch(parameterCount, expectCount)) { + return true; + } + } + return false; + } + + private boolean countMatch(int parameterCount, Integer expectCount) { + return expectCount == parameterCount || (expectCount < 0 && parameterCount >= -expectCount); + } + } diff --git a/testable-processor/src/main/java/com/alibaba/testable/processor/util/TestableLogger.java b/testable-processor/src/main/java/com/alibaba/testable/processor/util/TestableLogger.java index b326715..568dedb 100644 --- a/testable-processor/src/main/java/com/alibaba/testable/processor/util/TestableLogger.java +++ b/testable-processor/src/main/java/com/alibaba/testable/processor/util/TestableLogger.java @@ -17,11 +17,12 @@ public class TestableLogger { } public void info(String msg) { + // Message level lower than warning is not shown by default, use stdout instead System.out.println("[INFO] " + msg); } public void warn(String msg) { - messager.printMessage(Diagnostic.Kind.MANDATORY_WARNING, msg); + messager.printMessage(Diagnostic.Kind.WARNING, msg); } public void error(String msg) {