mirror of
https://github.com/alibaba/testable-mock.git
synced 2025-01-24 03:10:14 +08:00
check method parameter count
This commit is contained in:
parent
7029579534
commit
39c788cd02
@ -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小时内回复并处理
|
||||
|
||||
-----
|
||||
|
@ -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<String>() {{ 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.
|
||||
|
||||
|
@ -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<String>() {{ append("data"); }}`)、匿名函数(譬如`list.stream().map(i -> i.get)`)等等,会在编译过程中生成额外的成员方法调用,导致Mock方法执行报错。
|
||||
|
||||
|
@ -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<String> privateOrFinalFields = new ArrayList<String>();
|
||||
/**
|
||||
* Record non-private fields
|
||||
*/
|
||||
public final List<String> nonPrivateNorFinalFields = new ArrayList<String>();
|
||||
/**
|
||||
* Record private methods and possible parameter counts (negative number means large or equals)
|
||||
*/
|
||||
public final Map<String, List<Integer>> privateMethods = new HashMap<String, List<Integer>>();
|
||||
/**
|
||||
* Record non-private methods and possible parameter counts (negative number means large or equals)
|
||||
*/
|
||||
public final Map<String, List<Integer>> nonPrivateMethods = new HashMap<String, List<Integer>>();
|
||||
|
||||
}
|
@ -13,8 +13,8 @@ public enum MemberType {
|
||||
STATIC_PRIVATE,
|
||||
|
||||
/**
|
||||
* None private member
|
||||
* Non-private member
|
||||
*/
|
||||
NONE_PRIVATE
|
||||
NON_PRIVATE
|
||||
|
||||
}
|
||||
|
@ -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<Name> sourceClassIns = new ListBuffer<Name>();
|
||||
/**
|
||||
* Record private and final fields
|
||||
* Member information of source class
|
||||
*/
|
||||
private final ListBuffer<String> privateOrFinalFields = new ListBuffer<String>();
|
||||
/**
|
||||
* Record private methods
|
||||
*/
|
||||
private final ListBuffer<String> privateMethods = new ListBuffer<String>();
|
||||
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<String, List<Integer>> map, String key, final int value) {
|
||||
if (map.containsKey(key)) {
|
||||
map.get(key).add(value);
|
||||
} else {
|
||||
map.put(key, new ArrayList<Integer>() {{ 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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<String> privateOrFinalFields;
|
||||
private final List<String> privateMethods;
|
||||
private final MemberRecord sourceMembers;
|
||||
|
||||
public PrivateAccessChecker(String className, List<String> privateOrFinalFields, List<String> 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<String, List<Integer>> 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user