Compare commits

...

14 Commits
0.5 ... v0.5.1

Author SHA1 Message Date
金戟
e46ec3d98a release v0.5.1 2021-03-03 09:46:21 +08:00
金戟
1849cf85c5 use kotlin style method descriptor 2021-03-02 23:42:04 +08:00
金戟
f8450d7047 use pre-check to reduce unnecessary verbose parameter calculation 2021-03-02 23:33:17 +08:00
金戟
196dffc73d print diagnose verbose using java style descriptor 2021-03-02 23:18:27 +08:00
金戟
1ff6ec5961 support junit 5 parameterized test 2021-03-01 23:47:50 +08:00
金戟
e50633b884 fit for ternary operation in object constructor parameter 2021-02-27 00:59:22 +08:00
金戟
2976ca0211 print diagnose descriptor in java-like format 2021-02-26 14:02:58 +08:00
金戟
24c6a9cc5c validate mock method before use 2021-02-25 13:48:33 +08:00
金戟
9e7ceb2dc1 fix diagnose setup when using MockWith annotation 2021-02-24 21:29:48 +08:00
金戟
8dfd0b4516 handle bytecode jump opcode 2021-02-24 21:07:56 +08:00
金戟
190cf4cc3c should handle frame byte code when mock substitutions 2021-02-24 14:31:50 +08:00
金戟
d49f526376 add placeholder for english docs 2021-02-23 13:12:23 +08:00
金戟
f01ff10fba narrow down Exception scope when possible 2021-02-22 15:56:50 +08:00
金戟
f64b07e9da add doc for mock design 2021-02-21 14:32:32 +08:00
50 changed files with 523 additions and 142 deletions

View File

@@ -11,9 +11,8 @@ Usage Document: https://alibaba.github.io/testable-mock/#/en-us/
`TestableMock` is still under heavy development, the following version plans may be adjusted during the iteration
- `v0.4` it's the current version, refer to the [issue](https://github.com/alibaba/testable-mock/issues) list for the work in progress
- `v0.5` implementation mock class inherit mechanism, so that the same mock method can be reused between test classes conveniently
- `v0.6` narrows the default effective scope of mock methods to the class under test, to avoid interaction of mocking cross classes
- `v0.5` it's the current version, refer to the [issue](https://github.com/alibaba/testable-mock/issues) list for the work in progress
- `v0.6` implement the functionality of "parameter constructor"
- `v1.0` all functions are stable, a brand-new start
## Directory Structure

View File

@@ -13,8 +13,8 @@ repositories {
dependencies {
testImplementation('org.junit.jupiter:junit-jupiter:5.6.2')
testImplementation('com.alibaba.testable:testable-all:0.5.0')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.5.0')
testImplementation('com.alibaba.testable:testable-all:0.5.1')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.5.1')
}
tasks.withType(JavaCompile) {

View File

@@ -12,7 +12,7 @@
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<junit.version>5.6.2</junit.version>
<testable.version>0.5.0</testable.version>
<testable.version>0.5.1</testable.version>
</properties>
<dependencies>

View File

@@ -2,7 +2,6 @@ package com.alibaba.testable.demo.basic;
import com.alibaba.testable.core.annotation.MockConstructor;
import com.alibaba.testable.core.annotation.MockMethod;
import com.alibaba.testable.demo.basic.DemoMock;
import com.alibaba.testable.demo.basic.model.BlackBox;
import org.junit.jupiter.api.Test;

View File

@@ -16,8 +16,8 @@ dependencies {
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
testImplementation("org.junit.jupiter:junit-jupiter:5.6.2")
testImplementation("com.alibaba.testable:testable-all:0.5.0")
testAnnotationProcessor("com.alibaba.testable:testable-processor:0.5.0")
testImplementation("com.alibaba.testable:testable-all:0.5.1")
testAnnotationProcessor("com.alibaba.testable:testable-processor:0.5.1")
}
tasks.withType<KotlinCompile> {

View File

@@ -14,7 +14,7 @@
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<junit.version>5.6.2</junit.version>
<testable.version>0.5.0</testable.version>
<testable.version>0.5.1</testable.version>
</properties>
<dependencies>

View File

@@ -0,0 +1,4 @@
Available Annotations
---
TO BE TRANSLATED

View File

@@ -0,0 +1,4 @@
How Mock Works
---
TO BE TRANSLATED

View File

@@ -0,0 +1,4 @@
Available Global Arguments
---
TO BE TRANSLATED

View File

@@ -0,0 +1,4 @@
Reusing Mock Methods
---
TO BE TRANSLATED

View File

@@ -1,5 +1,11 @@
# Release Note
## 0.5.1
- Use kotlin-style method descriptor in `VERBOSE` diagnose logs
- Support parameterized test of JUnit 5
- Fix an exception caused by method parameter with ternary operator
- Fix a bug cause log message lost when `@MockWith` annotation used
## 0.4.12
- support verbose diagnose log for better self-troubleshooting
- support disable private access target existence check

View File

@@ -0,0 +1,4 @@
Effective Scope Of Mocking
---
TO BE TRANSLATED

View File

@@ -16,7 +16,7 @@ It is recommended to add a `property` field that identifies the TestableMock ver
```xml
<properties>
<testable.version>0.5.0</testable.version>
<testable.version>0.5.1</testable.version>
</properties>
```
@@ -63,8 +63,8 @@ Add dependence of `TestableMock` in `build.gradle` file:
```groovy
dependencies {
testImplementation('com.alibaba.testable:testable-all:0.5.0')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.5.0')
testImplementation('com.alibaba.testable:testable-all:0.5.1')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.5.1')
}
```

View File

@@ -15,9 +15,11 @@ class DemoTest {
The output log example is as follows:
```text
[DIAGNOSE] Handling test class com/alibaba/testable/demo/DemoMockTest
[DIAGNOSE] Handling test class com/alibaba/testable/demo/basic/DemoMockTest
[DIAGNOSE] Found 6 test cases
[DIAGNOSE] Handling mock class com/alibaba/testable/demo/basic/DemoMockTest$Mock
[DIAGNOSE] Found 8 mock methods
[DIAGNOSE] Handling source class com/alibaba/testable/demo/DemoMock
[DIAGNOSE] Handling source class com/alibaba/testable/demo/basic/DemoMock
[DIAGNOSE] Handling method <init>
[DIAGNOSE] Handling method newFunc
[DIAGNOSE] Line 19, mock method "createBlackBox" used
@@ -48,39 +50,33 @@ class DemoTest {
}
```
Executing the unit test again will print out the runtime-signatures of all mock methods, and the runtime-signatures of all invocations scanned in the class under test:
Executing the unit test again will print out the signatures of all mock methods, and the signatures of all invocations scanned in the class under test:
```text
[DIAGNOSE] Handling test class com/alibaba/testable/demo/DemoMockTest
[VERBOSE] Mock constructor "createBlackBox" as "(Ljava/lang/String;)V" for "com/alibaba/testable/demo/model/BlackBox"
[VERBOSE] Mock method "innerFunc" as "(Ljava/lang/String;)Ljava/lang/String;"
[VERBOSE] Mock method "staticFunc" as "()Ljava/lang/String;"
[VERBOSE] Mock method "trim" as "()Ljava/lang/String;"
[VERBOSE] Mock method "sub" as "(II)Ljava/lang/String;"
[VERBOSE] Mock method "startsWith" as "(Ljava/lang/String;)Z"
[VERBOSE] Mock method "secretBox" as "()Lcom/alibaba/testable/demo/model/BlackBox;"
[DIAGNOSE] Handling test class com/alibaba/testable/demo/basic/DemoMockTest
[VERBOSE] Test case "should_able_to_mock_new_object"
... ...
[VERBOSE] Test case "should_able_to_set_mock_context"
[DIAGNOSE] Found 6 test cases
[DIAGNOSE] Handling mock class com/alibaba/testable/demo/basic/DemoMockTest$Mock
[VERBOSE] Mock constructor "createBlackBox" as "com.alibaba.testable.demo.basic.model.BlackBox(java.lang.String)"
[VERBOSE] Mock method "innerFunc" as "com.alibaba.testable.demo.basic.DemoMock::innerFunc(java.lang.String) : java.lang.String"
... ...
[VERBOSE] Mock method "callFromDifferentMethod" as "()Ljava/lang/String;"
[DIAGNOSE] Found 8 mock methods
[DIAGNOSE] Handling source class com/alibaba/testable/demo/DemoMock
[DIAGNOSE] Handling source class com/alibaba/testable/demo/basic/DemoMock
[DIAGNOSE] Handling method <init>
[VERBOSE] Line 13, constructing "java/lang/Object" as "()V"
[VERBOSE] Line 13, constructing "java.lang.Object()"
[DIAGNOSE] Handling method newFunc
[VERBOSE] Line 19, constructing "com/alibaba/testable/demo/model/BlackBox" as "(Ljava/lang/String;)V"
[VERBOSE] Line 19, constructing "com.alibaba.testable.demo.basic.model.BlackBox(java.lang.String)"
[DIAGNOSE] Line 19, mock method "createBlackBox" used
[VERBOSE] Line 19, invoking "createBlackBox" as "(Ljava/lang/String;)Lcom/alibaba/testable/demo/model/BlackBox;"
[VERBOSE] Line 20, invoking "get" as "()Ljava/lang/String;"
[VERBOSE] Line 19, invoking "com.alibaba.testable.demo.basic.DemoMockTest$Mock::createBlackBox(java.lang.String) : com.alibaba.testable.demo.basic.model.BlackBox"
[VERBOSE] Line 20, invoking "com.alibaba.testable.demo.basic.model.BlackBox::get() : java.lang.String"
[DIAGNOSE] Handling method outerFunc
[VERBOSE] Line 27, constructing "java/lang/StringBuilder" as "()V"
[VERBOSE] Line 27, invoking "append" as "(Ljava/lang/String;)Ljava/lang/StringBuilder;"
[VERBOSE] Line 27, invoking "innerFunc" as "(Ljava/lang/String;)Ljava/lang/String;"
[VERBOSE] Line 27, constructing "java.lang.StringBuilder()"
[VERBOSE] Line 27, invoking "java.lang.StringBuilder::append(java.lang.String) : java.lang.StringBuilder"
[VERBOSE] Line 27, invoking "com.alibaba.testable.demo.basic.DemoMock::innerFunc(java.lang.String) : java.lang.String"
[DIAGNOSE] Line 27, mock method "innerFunc" used
[VERBOSE] Line 27, invoking "innerFunc" as "(Ljava/lang/String;)Ljava/lang/String;"
[VERBOSE] Line 27, invoking "append" as "(Ljava/lang/String;)Ljava/lang/StringBuilder;"
[VERBOSE] Line 27, invoking "staticFunc" as "()Ljava/lang/String;"
[DIAGNOSE] Line 27, mock method "staticFunc" used
[VERBOSE] Line 27, invoking "append" as "(Ljava/lang/String;)Ljava/lang/StringBuilder;"
[VERBOSE] Line 27, invoking "append" as "(Ljava/lang/String;)Ljava/lang/StringBuilder;"
[VERBOSE] Line 27, invoking "toString" as "()Ljava/lang/String;"
... ...
```

View File

@@ -1,4 +1,4 @@
Upgrade to version 0.5
Upgrade To Version 0.5
---
After nearly a month of design and development, the `0.5` version of TestableMock has finally come out. Compared with the `0.4` version, the new version solves the three historical problems left over before:

View File

@@ -0,0 +1,4 @@
Use MockWith Annotation
---
TO BE TRANSLATED

View File

@@ -7,13 +7,19 @@
- Usage Guide
- [Verify Mock Invocation](en-us/doc/invoke-matcher.md)
- [Effective Scope Of Mocking](zh-cn/doc/scope-of-mock.md)
- [Reusing Mock Methods](zh-cn/doc/mock-method-reusing.md)
- [Use MockWith Annotation](zh-cn/doc/use-mock-with.md)
- [Frequently Asked Questions](en-us/doc/frequently-asked-questions.md)
- [Use TestableMock In IDE](en-us/doc/use-in-ide.md)
- [Self-Help Troubleshooting](en-us/doc/troubleshooting.md)
- [Testable Maven Plugin](en-us/doc/use-maven-plugin.md)
- Technical Reference
- [Upgrade To 0.5 Version](en-us/doc/upgrade-to-v05.md)
- [Upgrade To Version 0.5](en-us/doc/upgrade-to-v05.md)
- [Available Global Arguments](zh-cn/doc/javaagent-args.md)
- [Available Annotations](zh-cn/doc/annotations.md)
- [How Mock Works](zh-cn/doc/design-and-mechanism.md)
- [Mock Tools Comparison](en-us/doc/comparation.md)
- [Release Note](en-us/doc/release-note.md)
- [About Us](en-us/doc/about-us.md)

View File

@@ -0,0 +1,29 @@
TestableMock的设计和原理
---
这篇文档主要介绍`TestableMock`中Mock功能的设计思想和实现原理。
与常见的Mock工具在每个测试用例里写Mock定义不同`TestableMock`让每个业务类直接提供自己的Mock方法集合描述自身在测试时需要被Mock的调用以及相应替代逻辑即每个业务类有自己的独立Test类和独立Mock类。采用约定优于配置降低Mock学习理解成本、减少冗余信息。
这种设计基于两项基本假设:
1. 同一个测试类里一个测试用例里需要Mock掉的方法在其他测试用例里通常也都需要Mock。因为这些被Mock的方法往往访问了不便于测试的外部依赖。
2. 需要Mock的调用都来自被测类的代码。此假设是符合单元测试初衷的即单元测试只应该关注当前单元的内部行为单元外的逻辑应该被替换为Mock
据此通过约定来简化符合该假设的单元测试场景,通过配置来支持其余复杂的使用场景。
`TestableMock`的原理可以用一句话概括:<u>利用JavaAgent动态修改字节码把被测的业务类中与所有与Mock方法定义匹配的调用在单元测试运行时替换成对Mock方法的调用</u>。
最终达到的效果则是不论代码用什么服务框架、什么对象容器不论要Mock的目标对象是注入的、new出来的、全局的还是局部的不论要Mock的目标方法是私有的、外部的、静态的、继承来的或者重载过的全部无差别通吃让单元测试回归简单。
> 划重点Mock的目标是**被测类**中的**方法调用**。测试用例里的代码不会被Mock方法的定义本身没有变化只是发起调用的代码被替换了。
具体来说,在单元测试启动时,`TestableMock`会对加载到内存中的类进行预处理同时分别建立“被测类”、“测试类”、“Mock容器类”之间的关联关系可以是一对一也可以是多对一。这个关联一方面是为了在测试用例执行时能够正确匹配Mock调用并进行替换另一方面则是为了能控制Mock方法的生效范围。
对于被测类将匹配到的调用换成对Mock容器方法的调用。
对于测试类在每个测试用例开头插入Mock上下文初始化代码。
对于Mock容器类增加`testableIns()`方法变成单例类在每个Mock方法开头插入记录调用的代码。
以上是整个Mock的核心逻辑更多实现细节请参考源码。若有任何问题、建议、改进提议都欢迎通过Github Issue和Pull Request参与讨论、贡献😃

View File

@@ -1,9 +1,13 @@
常见使用问题
---
#### 1. 如何Mock被测类中通过`@Autowired`初始化的字段?
#### 1. 如何初始化被测类中通过`@Autowired`或`@Resource`注入的私有字段?
直接创建被测类对象,然后利用`TestableMock`访问私有成员的能力直接给这些字段赋值即可
若该对象的方法在测试时需要被Mock则无需初始化
若测试运行时需用到该对象的真实调用,则可以在测试类的构造方法内直接创建对象,然后利用`TestableMock`访问私有成员的能力给这些字段赋值。
对于JUnit框架还可以使用`@RunWith(SpringRunner.class)`注解将Spring上下文启动起来然后在测试类里用`@Resource`把需要依赖对象注入进来,再利用`PrivateAccessor`赋值给被测类的私有字段。
#### 2. `TestableMock`是否能够与其他Mock工具一起使用

View File

@@ -1,5 +1,11 @@
# Release Note
## 0.5.1
-`VERBOSE`级别诊断日志中使用更易于阅读的方法签名格式
- 增加对JUnit5参数化测试的支持 (issue-98)
- 修复在调用参数中包含三元表达式时的Mock异常issue-92
- 修复使用`@MockWith`日志输出丢失的BUG (issue-99)
## 0.5.0
- 分离测试类与Mock类实现Mock类和Mock方法的复用机制
- 支持测试类与被测类在不同包路径的情况下实施Mock

View File

@@ -16,7 +16,7 @@
```xml
<properties>
<testable.version>0.5.0</testable.version>
<testable.version>0.5.1</testable.version>
</properties>
```
@@ -63,8 +63,8 @@
```groovy
dependencies {
testImplementation('com.alibaba.testable:testable-all:0.5.0')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.5.0')
testImplementation('com.alibaba.testable:testable-all:0.5.1')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.5.1')
}
```

View File

@@ -17,11 +17,11 @@ class DemoTest {
输出日志示例如下:
```text
[DIAGNOSE] Handling test class com/alibaba/testable/demo/DemoMockTest
[DIAGNOSE] Handling test class com/alibaba/testable/demo/basic/DemoMockTest
[DIAGNOSE] Found 6 test cases
[DIAGNOSE] Handling mock class com/alibaba/testable/demo/DemoMockTest$Mock
[DIAGNOSE] Handling mock class com/alibaba/testable/demo/basic/DemoMockTest$Mock
[DIAGNOSE] Found 8 mock methods
[DIAGNOSE] Handling source class com/alibaba/testable/demo/DemoMock
[DIAGNOSE] Handling source class com/alibaba/testable/demo/basic/DemoMock
[DIAGNOSE] Handling method <init>
[DIAGNOSE] Handling method newFunc
[DIAGNOSE] Line 19, mock method "createBlackBox" used
@@ -55,43 +55,41 @@ class DemoTest {
}
```
再次执行单元测试此时将会打印出所有Mock方法的运行期签名,以及被测类中扫描到所有调用的运行期签名:
再次执行单元测试此时将会打印出所有Mock方法的签名定义,以及被测类中扫描到所有调用的实际方法签名:
```text
[DIAGNOSE] Handling test class com/alibaba/testable/demo/DemoMockTest
[DIAGNOSE] Handling test class com/alibaba/testable/demo/basic/DemoMockTest
[VERBOSE] Test case "should_able_to_mock_new_object"
... ...
[VERBOSE] Test case "should_able_to_set_mock_context"
[DIAGNOSE] Found 6 test cases
[DIAGNOSE] Handling mock class com/alibaba/testable/demo/DemoMockTest$Mock
[VERBOSE] Mock constructor "createBlackBox" as "(Ljava/lang/String;)V" for "com/alibaba/testable/demo/model/BlackBox"
[VERBOSE] Mock method "innerFunc" as "(Ljava/lang/String;)Ljava/lang/String;"
[DIAGNOSE] Handling mock class com/alibaba/testable/demo/basic/DemoMockTest$Mock
[VERBOSE] Mock constructor "createBlackBox" as "com.alibaba.testable.demo.basic.model.BlackBox(java.lang.String)"
[VERBOSE] Mock method "innerFunc" as "com.alibaba.testable.demo.basic.DemoMock::innerFunc(java.lang.String) : java.lang.String"
... ...
[VERBOSE] Mock method "callFromDifferentMethod" as "()Ljava/lang/String;"
[DIAGNOSE] Found 8 mock methods
[DIAGNOSE] Handling source class com/alibaba/testable/demo/DemoMock
[DIAGNOSE] Handling source class com/alibaba/testable/demo/basic/DemoMock
[DIAGNOSE] Handling method <init>
[VERBOSE] Line 13, constructing "java/lang/Object" as "()V"
[VERBOSE] Line 13, constructing "java.lang.Object()"
[DIAGNOSE] Handling method newFunc
[VERBOSE] Line 19, constructing "com/alibaba/testable/demo/model/BlackBox" as "(Ljava/lang/String;)V"
[VERBOSE] Line 19, constructing "com.alibaba.testable.demo.basic.model.BlackBox(java.lang.String)"
[DIAGNOSE] Line 19, mock method "createBlackBox" used
[VERBOSE] Line 19, invoking "createBlackBox" as "(Ljava/lang/String;)Lcom/alibaba/testable/demo/model/BlackBox;"
[VERBOSE] Line 20, invoking "get" as "()Ljava/lang/String;"
[VERBOSE] Line 19, invoking "com.alibaba.testable.demo.basic.DemoMockTest$Mock::createBlackBox(java.lang.String) : com.alibaba.testable.demo.basic.model.BlackBox"
[VERBOSE] Line 20, invoking "com.alibaba.testable.demo.basic.model.BlackBox::get() : java.lang.String"
[DIAGNOSE] Handling method outerFunc
[VERBOSE] Line 27, constructing "java/lang/StringBuilder" as "()V"
[VERBOSE] Line 27, invoking "append" as "(Ljava/lang/String;)Ljava/lang/StringBuilder;"
[VERBOSE] Line 27, invoking "innerFunc" as "(Ljava/lang/String;)Ljava/lang/String;"
[VERBOSE] Line 27, constructing "java.lang.StringBuilder()"
[VERBOSE] Line 27, invoking "java.lang.StringBuilder::append(java.lang.String) : java.lang.StringBuilder"
[VERBOSE] Line 27, invoking "com.alibaba.testable.demo.basic.DemoMock::innerFunc(java.lang.String) : java.lang.String"
[DIAGNOSE] Line 27, mock method "innerFunc" used
... ...
```
输出日志结构参考如下:
- `Mock constructor "<Mock方法名>" as "<方法签名>" for "<类型>"` 在测试类中扫描到的**Mock构造方法**及其运行期签名
- `Mock method "<Mock方法名>" as "<方法签名>"` 在测试类中扫描到的**普通Mock方法**及其运行期签名
- `Line XX, constructing "<类型>" as "<方法签名>"` 在被测类中扫描掉的**构造方法调用**及其运行期签名
- `Line XX, invoking "<方法名>" as "<方法签名>"` 在被测类中扫描到的**成员方法调用**及其运行期签名
"运行期签名"是目标方法参数和返回值类型的在字节码中的表示形式其结构相比方法的原始Java签名更紧凑精炼通过对比相应代码行调用时的方法签名与Mock方法的实际签名通常能够快速定位出Mock未匹配的原因。
- `Mock constructor "<Mock方法名>" as "<方法签名>" for "<类型>"` 在测试类中扫描到的**Mock构造方法**及其签名
- `Mock method "<Mock方法名>" as "<方法签名>"` 在测试类中扫描到的**普通Mock方法**及其签名
- `Line XX, constructing "<类型>" as "<方法签名>"` 在被测类中扫描掉的**构造方法调用**及其签名
- `Line XX, invoking "<方法名>" as "<方法签名>"` 在被测类中扫描到的**成员方法调用**及其签名
> 在`0.4.x`版本使用测试类添加`@MockWith`注解的`diagnose`参数来启用诊断信息的方法在`0.5`版本中依然可用,但将在未来版本中移除,请优先使用`@MockDiagnose`注解替代。

View File

@@ -88,7 +88,7 @@ public class ServiceTest {
完整代码示例见`java-demo``kotlin-demo`示例项目中`OneToMultiSvcTest`测试类的用例。
> 由于当前版本里,测试类无法通过`@MockWith`与多个Mock容器关联生效范围为`MockScope.ASSOCIATED`的Mock方法会遇到Mock无效的情况已知BUG将在未来版本中修复。
> 由于当前版本里,测试类无法通过`@MockWith`与多个Mock容器关联目前这种用法仅支持生效范围为`MockScope.GLOBAL`的Mock方法这是已知BUG将在未来版本中修复。
### 4. 使用不包含Mock方法的Mock容器类

View File

@@ -19,6 +19,7 @@
- [升级到0.5版本](zh-cn/doc/upgrade-to-v05.md)
- [全局运行参数](zh-cn/doc/javaagent-args.md)
- [注解参数清单](zh-cn/doc/annotations.md)
- [Mock的设计和原理](zh-cn/doc/design-and-mechanism.md)
- [主流Mock工具对比](zh-cn/doc/comparation.md)
- [版本更新](zh-cn/doc/release-note.md)
- [关于我们](zh-cn/doc/about-us.md)

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-parent</artifactId>
<version>0.5.0</version>
<version>0.5.1</version>
<relativePath>../testable-parent</relativePath>
</parent>
<artifactId>testable-agent</artifactId>

View File

@@ -14,6 +14,7 @@ public class ByteCodeConst {
public static final byte TYPE_VOID = 'V';
public static final byte PARAM_END = ')';
public static final byte CLASS_END = ';';
public static final byte PKG_SEGMENT = '/';
public static final byte TYPE_ARRAY = '[';
}

View File

@@ -14,6 +14,8 @@ import org.objectweb.asm.tree.*;
import java.util.List;
import static com.alibaba.testable.agent.constant.ByteCodeConst.TYPE_ARRAY;
import static com.alibaba.testable.agent.constant.ByteCodeConst.TYPE_CLASS;
import static com.alibaba.testable.agent.util.ClassUtil.toDotSeparateFullClassName;
import static com.alibaba.testable.core.constant.ConstPool.CONSTRUCTOR;
@@ -44,10 +46,13 @@ public class MockClassHandler extends BaseClassWithContextHandler {
mn.access &= ~ACC_PRIVATE;
mn.access &= ~ACC_PROTECTED;
mn.access |= ACC_PUBLIC;
// below transform order is important
// firstly, unfold target class from annotation to parameter
unfoldTargetClass(mn);
// secondly, add invoke recorder at the beginning of mock method
injectInvokeRecorder(mn);
// thirdly, add association checker before invoke recorder
injectAssociationChecker(mn);
// finally, handle testable util variables
handleTestableUtil(mn);
}
}
@@ -170,7 +175,7 @@ public class MockClassHandler extends BaseClassWithContextHandler {
if (VOID_RES.equals(returnType)) {
il.add(new InsnNode(POP));
il.add(new InsnNode(RETURN));
} else if (returnType.startsWith("[") || returnType.startsWith("L")) {
} else if (returnType.charAt(0) == TYPE_ARRAY ||returnType.charAt(0) == TYPE_CLASS) {
il.add(new TypeInsnNode(CHECKCAST, returnType));
il.add(new InsnNode(ARETURN));
} else {
@@ -187,13 +192,12 @@ public class MockClassHandler extends BaseClassWithContextHandler {
Type className;
String methodName = mn.name;
for (AnnotationNode an : mn.visibleAnnotations) {
if (ClassUtil.toByteCodeClassName(ConstPool.MOCK_METHOD).equals(an.desc)) {
String name = AnnotationUtil.getAnnotationParameter(an, ConstPool.FIELD_TARGET_METHOD,
null, String.class);
if (isMockMethodAnnotation(an)) {
String name = AnnotationUtil.getAnnotationParameter(an, ConstPool.FIELD_TARGET_METHOD, null, String.class);
if (name != null) {
methodName = name;
}
} else if (ClassUtil.toByteCodeClassName(ConstPool.MOCK_CONSTRUCTOR).equals(an.desc)) {
} else if (isMockConstructorAnnotation(an)) {
methodName = CONSTRUCTOR;
}
}
@@ -207,8 +211,7 @@ public class MockClassHandler extends BaseClassWithContextHandler {
private boolean isGlobalScope(MethodNode mn) {
for (AnnotationNode an : mn.visibleAnnotations) {
if (ClassUtil.toByteCodeClassName(ConstPool.MOCK_METHOD).equals(an.desc) ||
ClassUtil.toByteCodeClassName(ConstPool.MOCK_CONSTRUCTOR).equals(an.desc)) {
if (isMockMethodAnnotation(an) || isMockConstructorAnnotation(an)) {
MockScope scope = AnnotationUtil.getAnnotationParameter(an, ConstPool.FIELD_SCOPE,
GlobalConfig.getDefaultMockScope(), MockScope.class);
if (scope.equals(MockScope.GLOBAL)) {
@@ -224,14 +227,23 @@ public class MockClassHandler extends BaseClassWithContextHandler {
return false;
}
for (AnnotationNode an : mn.visibleAnnotations) {
if (ClassUtil.toByteCodeClassName(ConstPool.MOCK_METHOD).equals(an.desc) ||
ClassUtil.toByteCodeClassName(ConstPool.MOCK_CONSTRUCTOR).equals(an.desc)) {
if (isMockMethodAnnotation(an) && AnnotationUtil.isValidMockMethod(mn, an)) {
return true;
} else if (isMockConstructorAnnotation(an)) {
return true;
}
}
return false;
}
private boolean isMockConstructorAnnotation(AnnotationNode an) {
return ClassUtil.toByteCodeClassName(ConstPool.MOCK_CONSTRUCTOR).equals(an.desc);
}
private boolean isMockMethodAnnotation(AnnotationNode an) {
return ClassUtil.toByteCodeClassName(ConstPool.MOCK_METHOD).equals(an.desc);
}
private void injectInvokeRecorder(MethodNode mn) {
InsnList il = new InsnList();
il.add(duplicateParameters(mn));

View File

@@ -1,10 +1,12 @@
package com.alibaba.testable.agent.handler;
import com.alibaba.testable.agent.model.MethodInfo;
import com.alibaba.testable.agent.model.TravelStatus;
import com.alibaba.testable.agent.util.BytecodeUtil;
import com.alibaba.testable.agent.util.ClassUtil;
import com.alibaba.testable.agent.util.MethodUtil;
import com.alibaba.testable.core.util.LogUtil;
import org.objectweb.asm.Label;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.*;
@@ -65,25 +67,35 @@ public class SourceClassHandler extends BaseClassHandler {
if (invokeOps.contains(instructions[i].getOpcode())) {
MethodInsnNode node = (MethodInsnNode)instructions[i];
if (CONSTRUCTOR.equals(node.name)) {
LogUtil.verbose(" Line %d, constructing \"%s\" as \"%s\"", getLineNum(instructions, i),
node.owner, node.desc);
if (LogUtil.isVerboseEnabled()) {
LogUtil.verbose(" Line %d, constructing \"%s\"", getLineNum(instructions, i),
MethodUtil.toJavaMethodDesc(node.owner, node.desc));
}
MethodInfo newOperatorInjectMethod = getNewOperatorInjectMethod(newOperatorInjectMethods, node);
if (newOperatorInjectMethod != null) {
// it's a new operation and an inject method for it exist
int rangeStart = getConstructorStart(instructions, node.owner, i);
if (rangeStart >= 0) {
if (rangeStart < i) {
handleFrameStackChange(mn, newOperatorInjectMethod, rangeStart, i);
}
instructions = replaceNewOps(mn, newOperatorInjectMethod, instructions, rangeStart, i);
i = rangeStart;
}
}
} else {
LogUtil.verbose(" Line %d, invoking \"%s\" as \"%s\"", getLineNum(instructions, i),
node.name, node.desc);
if (LogUtil.isVerboseEnabled()) {
LogUtil.verbose(" Line %d, invoking \"%s\"", getLineNum(instructions, i),
MethodUtil.toJavaMethodDesc(node.owner, node.name, node.desc));
}
MethodInfo mockMethod = getMemberInjectMethodName(memberInjectMethods, node);
if (mockMethod != null) {
// it's a member or static method and an inject method for it exist
int rangeStart = getMemberMethodStart(instructions, i);
if (rangeStart >= 0) {
if (rangeStart < i) {
handleFrameStackChange(mn, mockMethod, rangeStart, i);
}
instructions = replaceMemberCallOps(mn, mockMethod,
instructions, node.owner, node.getOpcode(), rangeStart, i);
i = rangeStart;
@@ -144,10 +156,36 @@ public class SourceClassHandler extends BaseClassHandler {
if (stackLevel < 0) {
return rangeEnd;
}
Label labelToJump = null;
TravelStatus status = TravelStatus.Normal;
for (int i = rangeEnd - 1; i >= 0; i--) {
stackLevel += getStackLevelChange(instructions[i]);
if (stackLevel < 0) {
return i;
switch (status) {
case Normal:
if (instructions[i] instanceof FrameNode) {
status = TravelStatus.LookingForLabel;
} else {
stackLevel += getStackLevelChange(instructions[i]);
if (stackLevel < 0) {
return i;
}
}
break;
case LookingForLabel:
if (instructions[i] instanceof LabelNode) {
labelToJump = ((LabelNode)instructions[i]).getLabel();
status = TravelStatus.LookingForJump;
}
break;
case LookingForJump:
if (instructions[i] instanceof JumpInsnNode &&
((JumpInsnNode)instructions[i]).label.getLabel().equals(labelToJump)) {
stackLevel += getStackLevelChange(instructions[i]);
labelToJump = null;
status = TravelStatus.Normal;
}
break;
default:
break;
}
}
return -1;
@@ -249,6 +287,26 @@ public class SourceClassHandler extends BaseClassHandler {
return mn.instructions.toArray();
}
private void handleFrameStackChange(MethodNode mn, MethodInfo mockMethod, int start, int end) {
AbstractInsnNode curInsn = mn.instructions.get(start);
AbstractInsnNode endInsn = mn.instructions.get(end);
do {
if (curInsn instanceof FrameNode) {
FrameNode fn = (FrameNode)curInsn;
if (fn.type == F_FULL) {
fn.stack.add(0, mockMethod.getMockClass());
// remove label reference in stack of frame node
for (int i = fn.stack.size() - 1; i >= 0; i--) {
if (fn.stack.get(i) instanceof LabelNode) {
fn.stack.remove(i);
}
}
}
}
curInsn = curInsn.getNext();
} while (!curInsn.equals(endInsn));
}
private boolean isCompanionMethod(String ownerClass, int opcode) {
return Opcodes.INVOKEVIRTUAL == opcode && ClassUtil.isCompanionClassName(ownerClass);
}

View File

@@ -1,10 +1,11 @@
package com.alibaba.testable.agent.handler.test;
import com.alibaba.testable.agent.model.TestCaseMethodType;
import com.alibaba.testable.agent.util.CollectionUtil;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.MethodNode;
import java.util.List;
import java.util.Set;
abstract public class Framework {
@@ -22,7 +23,7 @@ abstract public class Framework {
hasTestAfterMethod = true;
return true;
} else {
return methodAnnotations.contains(getTestAnnotation());
return CollectionUtil.containsAny(methodAnnotations, getTestAnnotations());
}
}
@@ -31,7 +32,7 @@ abstract public class Framework {
return TestCaseMethodType.OTHERS;
}
for (AnnotationNode an : mn.visibleAnnotations) {
if (an.desc.equals(getTestAnnotation())) {
if (getTestAnnotations().contains(an.desc)) {
return TestCaseMethodType.TEST;
} else if (an.desc.equals(getTestAfterAnnotation())) {
return TestCaseMethodType.AFTER_TEST;
@@ -40,7 +41,7 @@ abstract public class Framework {
return TestCaseMethodType.OTHERS;
}
public abstract String getTestAnnotation();
public abstract List<String> getTestAnnotations();
public abstract String getTestAfterAnnotation();

View File

@@ -1,13 +1,16 @@
package com.alibaba.testable.agent.handler.test;
import java.util.Collections;
import java.util.List;
public class JUnit4Framework extends Framework {
private static final String ANNOTATION_TEST = "Lorg/junit/Test;";
private static final String ANNOTATION_AFTER_TEST = "Lorg/junit/After;";
@Override
public String getTestAnnotation() {
return ANNOTATION_TEST;
public List<String> getTestAnnotations() {
return Collections.singletonList(ANNOTATION_TEST);
}
@Override

View File

@@ -1,13 +1,17 @@
package com.alibaba.testable.agent.handler.test;
import java.util.Arrays;
import java.util.List;
public class JUnit5Framework extends Framework {
private static final String ANNOTATION_TEST = "Lorg/junit/jupiter/api/Test;";
private static final String ANNOTATION_PARAMETERIZED_TEST = "Lorg/junit/jupiter/params/ParameterizedTest;";
private static final String ANNOTATION_AFTER_TEST = "Lorg/junit/jupiter/api/AfterEach;";
@Override
public String getTestAnnotation() {
return ANNOTATION_TEST;
public List<String> getTestAnnotations() {
return Arrays.asList(ANNOTATION_TEST, ANNOTATION_PARAMETERIZED_TEST);
}
@Override

View File

@@ -1,13 +1,16 @@
package com.alibaba.testable.agent.handler.test;
import java.util.Collections;
import java.util.List;
public class TestNgFramework extends Framework {
private static final String ANNOTATION_TEST = "Lorg/testng/annotations/Test;";
private static final String ANNOTATION_AFTER_TEST = "Lorg/testng/annotations/AfterMethod;";
@Override
public String getTestAnnotation() {
return ANNOTATION_TEST;
public List<String> getTestAnnotations() {
return Collections.singletonList(ANNOTATION_TEST);
}
@Override

View File

@@ -1,6 +1,7 @@
package com.alibaba.testable.agent.handler.test;
import com.alibaba.testable.agent.model.TestCaseMethodType;
import com.alibaba.testable.agent.util.CollectionUtil;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.MethodNode;
@@ -12,7 +13,7 @@ public class TestNgOnClassFramework extends TestNgFramework {
@Override
public boolean fit(Set<String> classAnnotations, Set<String> methodAnnotations) {
if (classAnnotations.contains(getTestAnnotation())) {
if (CollectionUtil.containsAny(classAnnotations, getTestAnnotations())) {
if (methodAnnotations.contains(getTestAfterAnnotation())) {
hasTestAfterMethod = true;
}

View File

@@ -17,6 +17,10 @@ public class MethodInfo {
* parameter and return value of the source method
*/
private final String desc;
/**
* name of the class where this mock method defined (in slash-separate format)
*/
private final String mockClass;
/**
* name of the mock method
*/
@@ -30,10 +34,11 @@ public class MethodInfo {
*/
private final boolean isStatic;
public MethodInfo(String clazz, String name, String desc, String mockName, String mockDesc, boolean isStatic) {
public MethodInfo(String clazz, String name, String desc, String mockClass, String mockName, String mockDesc, boolean isStatic) {
this.clazz = clazz;
this.name = name;
this.desc = desc;
this.mockClass = mockClass;
this.mockName = mockName;
this.mockDesc = mockDesc;
this.isStatic = isStatic;
@@ -51,6 +56,10 @@ public class MethodInfo {
return desc;
}
public String getMockClass() {
return mockClass;
}
public String getMockName() {
return mockName;
}
@@ -74,6 +83,7 @@ public class MethodInfo {
if (!clazz.equals(that.clazz)) { return false; }
if (!name.equals(that.name)) { return false; }
if (!desc.equals(that.desc)) { return false; }
if (!mockClass.equals(that.mockClass)) { return false; }
if (!mockName.equals(that.mockName)) { return false; }
return mockDesc.equals(that.mockDesc);
}
@@ -83,6 +93,7 @@ public class MethodInfo {
int result = clazz.hashCode();
result = 31 * result + name.hashCode();
result = 31 * result + desc.hashCode();
result = 31 * result + mockClass.hashCode();
result = 31 * result + mockName.hashCode();
result = 31 * result + mockDesc.hashCode();
result = 31 * result + (isStatic ? 1 : 0);

View File

@@ -0,0 +1,23 @@
package com.alibaba.testable.agent.model;
/**
* @author flin
*/
public enum TravelStatus {
/**
* traveling common bytecode
*/
Normal,
/**
* looking for label opcode
*/
LookingForLabel,
/**
* looking for jump opcode
*/
LookingForJump
}

View File

@@ -86,17 +86,22 @@ public class MockClassParser {
for (AnnotationNode an : mn.visibleAnnotations) {
String fullClassName = toDotSeparateFullClassName(an.desc);
if (fullClassName.equals(ConstPool.MOCK_CONSTRUCTOR)) {
LogUtil.verbose(" Mock constructor \"%s\" as \"(%s)V\" for \"%s\"", mn.name,
MethodUtil.extractParameters(mn.desc), MethodUtil.getReturnType(mn.desc));
if (LogUtil.isVerboseEnabled()) {
LogUtil.verbose(" Mock constructor \"%s\" as \"%s\"", mn.name, MethodUtil.toJavaMethodDesc(
ClassUtil.toDotSeparateFullClassName(MethodUtil.getReturnType(mn.desc)), mn.desc));
}
addMockConstructor(methodInfos, cn, mn);
} else if (fullClassName.equals(ConstPool.MOCK_METHOD)) {
LogUtil.verbose(" Mock method \"%s\" as \"%s\"", mn.name, getTargetMethodDesc(mn, an));
} else if (fullClassName.equals(ConstPool.MOCK_METHOD) && AnnotationUtil.isValidMockMethod(mn, an)) {
if (LogUtil.isVerboseEnabled()) {
LogUtil.verbose(" Mock method \"%s\" as \"%s\"", mn.name, MethodUtil.toJavaMethodDesc(
getTargetMethodOwner(mn, an), getTargetMethodName(mn, an), getTargetMethodDesc(mn, an)));
}
String targetMethod = AnnotationUtil.getAnnotationParameter(
an, ConstPool.FIELD_TARGET_METHOD, mn.name, String.class);
if (CONSTRUCTOR.equals(targetMethod)) {
addMockConstructor(methodInfos, cn, mn);
} else {
MethodInfo mi = getMethodInfo(mn, an, targetMethod);
MethodInfo mi = getMethodInfo(cn, mn, an, targetMethod);
if (mi != null) {
methodInfos.add(mi);
}
@@ -106,13 +111,25 @@ public class MockClassParser {
}
}
private String getTargetMethodOwner(MethodNode mn, AnnotationNode mockMethodAnnotation) {
Type type = AnnotationUtil.getAnnotationParameter(mockMethodAnnotation, ConstPool.FIELD_TARGET_CLASS,
null, Type.class);
return type == null ? MethodUtil.getFirstParameter(mn.desc) : type.getClassName();
}
private String getTargetMethodName(MethodNode mn, AnnotationNode mockMethodAnnotation) {
String name = AnnotationUtil.getAnnotationParameter(mockMethodAnnotation, ConstPool.FIELD_TARGET_METHOD,
null, String.class);
return name == null ? mn.name : name;
}
private String getTargetMethodDesc(MethodNode mn, AnnotationNode mockMethodAnnotation) {
Type type = AnnotationUtil.getAnnotationParameter(mockMethodAnnotation, ConstPool.FIELD_TARGET_CLASS,
null, Type.class);
return type == null ? MethodUtil.removeFirstParameter(mn.desc) : mn.desc;
}
private MethodInfo getMethodInfo(MethodNode mn, AnnotationNode an, String targetMethod) {
private MethodInfo getMethodInfo(ClassNode cn, MethodNode mn, AnnotationNode an, String targetMethod) {
Type targetType = AnnotationUtil.getAnnotationParameter(an, ConstPool.FIELD_TARGET_CLASS, null, Type.class);
boolean isStatic = isStatic(mn);
if (targetType == null) {
@@ -121,18 +138,19 @@ public class MockClassParser {
if (methodDescPair == null) {
return null;
}
return new MethodInfo(methodDescPair.left, targetMethod, methodDescPair.right, mn.name, mn.desc, isStatic);
return new MethodInfo(methodDescPair.left, targetMethod, methodDescPair.right, cn.name, mn.name, mn.desc,
isStatic);
} else {
// "targetClass" found, use it as target class type
String slashSeparatedName = ClassUtil.toSlashSeparatedName(targetType.getClassName());
return new MethodInfo(slashSeparatedName, targetMethod, mn.desc, mn.name,
return new MethodInfo(slashSeparatedName, targetMethod, mn.desc, cn.name, mn.name,
MethodUtil.addParameterAtBegin(mn.desc, ClassUtil.toByteCodeClassName(slashSeparatedName)), isStatic);
}
}
private void addMockConstructor(List<MethodInfo> methodInfos, ClassNode cn, MethodNode mn) {
String sourceClassName = ClassUtil.getSourceClassName(cn.name);
methodInfos.add(new MethodInfo(sourceClassName, CONSTRUCTOR, mn.desc, mn.name, mn.desc, isStatic(mn)));
methodInfos.add(new MethodInfo(sourceClassName, CONSTRUCTOR, mn.desc, cn.name, mn.name, mn.desc, isStatic(mn)));
}
/**

View File

@@ -232,6 +232,7 @@ public class TestableClassTransformer implements ClassFileTransformer {
if (isExpectedType(cn.name, type, expectedType)) {
Type clazz = AnnotationUtil.getAnnotationParameter(an, FIELD_VALUE,
Type.getType(NullType.class), Type.class);
DiagnoseUtil.setupByClass(ClassUtil.getClassNode(clazz.getClassName()));
return clazz.getClassName();
}
}

View File

@@ -1,6 +1,11 @@
package com.alibaba.testable.agent.util;
import com.alibaba.testable.agent.constant.ConstPool;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.MethodNode;
import static com.alibaba.testable.agent.constant.ByteCodeConst.TYPE_CLASS;
/**
* @author flin
@@ -59,4 +64,16 @@ public class AnnotationUtil {
}
return false;
}
/**
* Check is MockMethod annotation is used on a valid mock method
* @param mn mock method
* @param an MockMethod annotation
* @return valid or not
*/
public static boolean isValidMockMethod(MethodNode mn, AnnotationNode an) {
Type targetClass = AnnotationUtil.getAnnotationParameter(an, ConstPool.FIELD_TARGET_CLASS, null, Type.class);
String firstParameter = MethodUtil.getFirstParameter(mn.desc);
return targetClass != null || firstParameter.charAt(0) == TYPE_CLASS;
}
}

View File

@@ -15,21 +15,21 @@ public class DiagnoseUtil {
private static final String FIELD_DIAGNOSE = "diagnose";
public static void setupByClass(ClassNode cn) {
if (cn.visibleAnnotations == null) {
if (cn == null || cn.visibleAnnotations == null) {
return;
}
for (AnnotationNode an : cn.visibleAnnotations) {
setupByAnnotation(an);
if (toDotSeparateFullClassName(an.desc).equals(ConstPool.MOCK_DIAGNOSE)) {
setupDiagnose(an, FIELD_VALUE);
}
}
}
public static void setupByAnnotation(AnnotationNode an) {
// to be remove in v0.6
if (toDotSeparateFullClassName(an.desc).equals(MOCK_WITH)) {
setupDiagnose(an, FIELD_DIAGNOSE);
}
if (toDotSeparateFullClassName(an.desc).equals(ConstPool.MOCK_DIAGNOSE)) {
setupDiagnose(an, FIELD_VALUE);
}
}
private static void setupDiagnose(AnnotationNode an, String fieldDiagnose) {

View File

@@ -73,13 +73,13 @@ public class MethodUtil {
}
/**
* parse method desc, fetch first parameter type
* parse method desc, fetch first parameter type (assume first parameter is an object type)
* @param desc method description
* @return types of first parameter
*/
public static String getFirstParameter(String desc) {
int typeEdge = desc.indexOf(CLASS_END);
return desc.substring(1, typeEdge + 1);
return typeEdge > 0 ? desc.substring(1, typeEdge + 1) : "";
}
/**
@@ -105,4 +105,105 @@ public class MethodUtil {
return b == TYPE_BYTE || b == TYPE_CHAR || b == TYPE_DOUBLE || b == TYPE_FLOAT
|| b == TYPE_INT || b == TYPE_LONG || b == TYPE_SHORT || b == TYPE_BOOL;
}
/**
* format to java style constructor descriptor
* @param owner class of method belongs to
* @param desc method constructor in bytecode format
* @return java style constructor descriptor
*/
public static String toJavaMethodDesc(String owner, String desc) {
String ownerInDotFormat = ClassUtil.toDotSeparatedName(owner);
String parameters = toJavaParameterDesc(extractParameters(desc));
return String.format("%s(%s)", ownerInDotFormat, parameters);
}
/**
* format to java style method descriptor
* @param owner class of method belongs to
* @param name method name
* @param desc method descriptor in bytecode format
* @return java style method descriptor
*/
public static String toJavaMethodDesc(String owner, String name, String desc) {
String ownerInDotFormat = ClassUtil.toDotSeparatedName(owner);
String returnType = toJavaParameterDesc(getReturnType(desc));
String parameters = toJavaParameterDesc(extractParameters(desc));
return String.format("%s::%s(%s) : %s", ownerInDotFormat, name, parameters, returnType);
}
/**
* convert bytecode style parameter descriptor to java style descriptor
* @param desc bytecode style descriptor
* @return java style descriptor
*/
private static String toJavaParameterDesc(String desc) {
if (desc.isEmpty()) {
return "";
}
StringBuilder sb = new StringBuilder();
boolean isArray = false;
boolean isTravellingClass = false;
for (byte b : desc.getBytes()) {
if (isTravellingClass) {
switch (b) {
case CLASS_END:
sb.append(isArray ? "[]" : "");
isArray = false;
isTravellingClass = false;
break;
case PKG_SEGMENT:
sb.append('.');
break;
default:
sb.append((char)b);
}
} else {
switch (b) {
case TYPE_ARRAY:
isArray = true;
break;
case TYPE_BYTE:
sb.append(",byte").append(isArray ? "[]" : "");
isArray = false;
break;
case TYPE_CHAR:
sb.append(",char").append(isArray ? "[]" : "");
isArray = false;
break;
case TYPE_DOUBLE:
sb.append(",double").append(isArray ? "[]" : "");
isArray = false;
break;
case TYPE_FLOAT:
sb.append(",float").append(isArray ? "[]" : "");
isArray = false;
break;
case TYPE_INT:
sb.append(",int").append(isArray ? "[]" : "");
isArray = false;
break;
case TYPE_LONG:
sb.append(",long").append(isArray ? "[]" : "");
isArray = false;
break;
case TYPE_SHORT:
sb.append(",short").append(isArray ? "[]" : "");
isArray = false;
break;
case TYPE_BOOL:
sb.append(",boolean").append(isArray ? "[]" : "");
isArray = false;
break;
case TYPE_CLASS:
sb.append(",");
isTravellingClass = true;
break;
default:
break;
}
}
}
return sb.substring(1);
}
}

View File

@@ -1,5 +1,6 @@
package com.alibaba.testable.agent.util;
import com.alibaba.testable.core.accessor.PrivateAccessor;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -33,6 +34,18 @@ class MethodUtilTest {
@Test
void should_able_to_get_first_parameter() {
assertEquals("Ljava/lang/String;", MethodUtil.getFirstParameter("(Ljava/lang/String;Ljava/lang/Object;I)V"));
assertEquals("Ljava/lang/String;", MethodUtil.getFirstParameter("(Ljava/lang/String;)V"));
assertEquals("", MethodUtil.getFirstParameter("()V"));
}
@Test
void should_able_to_convert_bytecode_parameters() {
assertEquals("", PrivateAccessor.invokeStatic(MethodUtil.class, "toJavaParameterDesc", ""));
assertEquals("char", PrivateAccessor.invokeStatic(MethodUtil.class, "toJavaParameterDesc", "C"));
assertEquals("int,long", PrivateAccessor.invokeStatic(MethodUtil.class, "toJavaParameterDesc", "IJ"));
assertEquals("int[],long[]", PrivateAccessor.invokeStatic(MethodUtil.class, "toJavaParameterDesc", "[I[J"));
assertEquals("int,java.lang.String", PrivateAccessor.invokeStatic(MethodUtil.class, "toJavaParameterDesc", "ILjava/lang/String;"));
assertEquals("java.lang.String,int,long[]", PrivateAccessor.invokeStatic(MethodUtil.class, "toJavaParameterDesc", "Ljava/lang/String;I[J"));
}
}

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-parent</artifactId>
<version>0.5.0</version>
<version>0.5.1</version>
<relativePath>../testable-parent</relativePath>
</parent>
<artifactId>testable-all</artifactId>

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-parent</artifactId>
<version>0.5.0</version>
<version>0.5.1</version>
<relativePath>../testable-parent</relativePath>
</parent>
<artifactId>testable-core</artifactId>

View File

@@ -5,6 +5,7 @@ import com.alibaba.testable.core.util.TypeUtil;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
@@ -24,8 +25,10 @@ public class PrivateAccessor {
Field declaredField = ref.getClass().getDeclaredField(field);
declaredField.setAccessible(true);
return (T)declaredField.get(ref);
} catch (Exception e) {
throw new MemberAccessException("Failed to get private field \"" + field + "\"", e);
} catch (IllegalAccessException e) {
throw new MemberAccessException("Failed to access private field \"" + field + "\"", e);
} catch (NoSuchFieldException e) {
throw new MemberAccessException("Private field \"" + field + "\" not exist", e);
}
}
@@ -40,8 +43,10 @@ public class PrivateAccessor {
Field declaredField = ref.getClass().getDeclaredField(field);
declaredField.setAccessible(true);
declaredField.set(ref, value);
} catch (Exception e) {
throw new MemberAccessException("Failed to set private field \"" + field + "\"", e);
} catch (IllegalAccessException e) {
throw new MemberAccessException("Failed to access private field \"" + field + "\"", e);
} catch (NoSuchFieldException e) {
throw new MemberAccessException("Private field \"" + field + "\" not exist", e);
}
}
@@ -60,10 +65,15 @@ public class PrivateAccessor {
declaredMethod.setAccessible(true);
return (T)declaredMethod.invoke(ref, args);
}
} catch (Exception e) {
throw new MemberAccessException("Failed to invoke private method \"" + method + "\"", e);
} catch (IllegalAccessException e) {
throw new MemberAccessException("Failed to access private method \"" + method + "\"", e);
} catch (InvocationTargetException e) {
if (e.getTargetException() instanceof RuntimeException) {
throw (RuntimeException)e.getTargetException();
}
throw new MemberAccessException("Invoke private method \"" + method + "\" failed with exception", e);
}
throw new MemberAccessException("Private method \"" + method + "\" not found");
throw new MemberAccessException("Private method \"" + method + "\" not exist");
}
/**
@@ -76,8 +86,10 @@ public class PrivateAccessor {
Field declaredField = clazz.getDeclaredField(field);
declaredField.setAccessible(true);
return (T)declaredField.get(null);
} catch (Exception e) {
throw new MemberAccessException("Failed to get private static field \"" + field + "\"", e);
} catch (IllegalAccessException e) {
throw new MemberAccessException("Failed to access private static field \"" + field + "\"", e);
} catch (NoSuchFieldException e) {
throw new MemberAccessException("Private static field \"" + field + "\" not exist", e);
}
}
@@ -92,8 +104,10 @@ public class PrivateAccessor {
Field declaredField = clazz.getDeclaredField(field);
declaredField.setAccessible(true);
declaredField.set(null, value);
} catch (Exception e) {
throw new MemberAccessException("Failed to set private static field \"" + field + "\"", e);
} catch (IllegalAccessException e) {
throw new MemberAccessException("Failed to access private static field \"" + field + "\"", e);
} catch (NoSuchFieldException e) {
throw new MemberAccessException("Private static field \"" + field + "\" not exist", e);
}
}
@@ -120,10 +134,17 @@ public class PrivateAccessor {
declaredMethod.setAccessible(true);
return (T)declaredMethod.invoke(companionInstance, args);
}
} catch (Exception e) {
throw new MemberAccessException("Failed to invoke private static method \"" + method + "\"", e);
} catch (IllegalAccessException e) {
throw new MemberAccessException("Failed to access private static method \"" + method + "\"", e);
} catch (NoSuchFieldException e) {
throw new MemberAccessException("Private static method \"" + method + "\" not exist");
} catch (InvocationTargetException e) {
if (e.getTargetException() instanceof RuntimeException) {
throw (RuntimeException)e.getTargetException();
}
throw new MemberAccessException("Invoke private static method \"" + method + "\" failed with exception", e);
}
throw new MemberAccessException("Private static method \"" + method + "\" not found");
throw new MemberAccessException("Neither Private static method nor companion method \"" + method + "\" exist");
}
/**
@@ -139,9 +160,16 @@ public class PrivateAccessor {
constructor.setAccessible(true);
return (T)constructor.newInstance(args);
}
} catch (Exception e) {
throw new MemberAccessException("Failed to invoke private constructor of \"" + clazz.getSimpleName() + "\"", e);
} catch (IllegalAccessException e) {
throw new MemberAccessException("Failed to access private constructor of \"" + clazz.getSimpleName() + "\"", e);
} catch (InvocationTargetException e) {
if (e.getTargetException() instanceof RuntimeException) {
throw (RuntimeException)e.getTargetException();
}
throw new MemberAccessException("Invoke private constructor of \"" + clazz.getSimpleName() + "\" failed with exception", e);
} catch (InstantiationException e) {
throw new MemberAccessException("Failed to instantiate object of \"" + clazz.getSimpleName() + "\"", e);
}
throw new MemberAccessException("Private static constructor of \"" + clazz.getSimpleName() + "\" not found");
throw new MemberAccessException("Private constructor of \"" + clazz.getSimpleName() + "\" not exist");
}
}

View File

@@ -33,7 +33,7 @@ public class LogUtil {
private static LogLevel currentLogLevel = LogLevel.LEVEL_WARN;
public static void verbose(String msg, Object... args) {
if (currentLogLevel.level >= LogLevel.LEVEL_VERBOSE.level) {
if (isVerboseEnabled()) {
System.out.println(String.format("[VERBOSE] " + msg, args));
}
}
@@ -54,6 +54,13 @@ public class LogUtil {
System.err.println(String.format("[ERROR] " + msg, args));
}
/**
* a pre-check method for reduce verbose parameter calculation
*/
public static boolean isVerboseEnabled() {
return currentLogLevel.level >= LogLevel.LEVEL_VERBOSE.level;
}
public static void setLevel(LogLevel level) {
currentLogLevel = level;
}

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-parent</artifactId>
<version>0.5.0</version>
<version>0.5.1</version>
<relativePath>../testable-parent</relativePath>
</parent>
<artifactId>testable-maven-plugin</artifactId>

View File

@@ -5,7 +5,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-parent</artifactId>
<version>0.5.0</version>
<version>0.5.1</version>
<packaging>pom</packaging>
<name>testable-parent</name>
<description>Unit test enhancement toolkit</description>
@@ -42,7 +42,7 @@
<plugin.gpg.version>1.6</plugin.gpg.version>
<plugin.staging.version>1.6.8</plugin.staging.version>
<plugin.maven.version>3.6.0</plugin.maven.version>
<testable.version>0.5.0</testable.version>
<testable.version>0.5.1</testable.version>
</properties>
<profiles>

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-parent</artifactId>
<version>0.5.0</version>
<version>0.5.1</version>
<relativePath>../testable-parent</relativePath>
</parent>
<artifactId>testable-processor</artifactId>

View File

@@ -66,12 +66,13 @@ public class EnablePrivateAccessTranslator extends BaseTranslator {
try {
Class<?> cls = getSourceClass(clazz, sourceClassFullName);
if (cls == null) {
cx.logger.error("Failed to load source class: " + sourceClassFullName);
cx.logger.fatal("Failed to load source class \"" + sourceClassFullName + "\"");
} else {
findAllPrivateMembers(cls);
}
} catch (Exception e) {
e.printStackTrace();
// for any reason, interrupt the compile process
cx.logger.fatal("Failed to load source class \"" + sourceClassFullName + "\": " + e);
}
this.privateAccessChecker = (p.verifyTargetExistence == null || p.verifyTargetExistence) ?
new PrivateAccessChecker(cx, sourceClassShortName, memberRecord) : null;

View File

@@ -16,17 +16,27 @@ public class TestableLogger {
this.messager = messager;
}
/**
* Print hint message
*/
public void info(String msg) {
// Message level lower than warning is not shown by default, use stdout instead
System.out.println("[INFO] " + msg);
}
/**
* Print warning message
*/
public void warn(String msg) {
// Message level WARNING won't show, use MANDATORY_WARNING instead
messager.printMessage(Diagnostic.Kind.MANDATORY_WARNING, msg);
}
public void error(String msg) {
/**
* Print fatal message
* Note: this will stop current compile process
*/
public void fatal(String msg) {
messager.printMessage(Diagnostic.Kind.ERROR, msg);
}
}