mirror of
https://github.com/alibaba/testable-mock.git
synced 2025-02-05 01:01:02 +08:00
follow up to 0.4.11
This commit is contained in:
commit
02ba7ad83c
14
README.md
14
README.md
@ -10,19 +10,19 @@
|
||||
阅读[这里](https://mp.weixin.qq.com/s/KyU6Eu7mDkZU8FspfSqfMw)了解更多故事。
|
||||
|
||||
> 特别说明
|
||||
> 1. 如有遇到启动报空指针异常或"Operand stack overflow"错误,请升级`TestableMock`版本,这是BUG,已修复😛
|
||||
> 2. 如遇到"Attempt to access none-static member in mock method"错误,参见[常见问题](https://alibaba.github.io/testable-mock/#/zh-cn/doc/frequently-asked-questions)第8条
|
||||
> 3. 如果有遇到其他任何使用问题和建议,请直接在[Issue](https://github.com/alibaba/testable-mock/issues)中提出,也可通过[Pull Request](https://github.com/alibaba/testable-mock/pulls)提交您的代码,我们将在24小时内回复并处理
|
||||
> 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小时内回复并处理
|
||||
|
||||
-----
|
||||
|
||||
## 版本计划
|
||||
|
||||
`TestableMock`正在持续迭代演进,以下版本计划可能在开发过程中发生调整,请以最新内容为准
|
||||
`TestableMock`正在持续迭代演进,以下版本计划可能在开发过程中发生调整
|
||||
|
||||
- `0.4.x` 当前版本,进行中的工作内容参考[Issue](https://github.com/alibaba/testable-mock/issues)清单
|
||||
- `0.5` 实现以"Mock方法集"为单元的Mock方法复用机制,让测试类之间可以方便复用相同的Mock方法
|
||||
- `0.6` 实现第四项单元测试增强能力"快速入参构造器"。不论被测方法所需的参数结构多么错综复杂、甚至没有合适的构造方法、甚至需要私有内部类对象... 呼唤TestableMock,马上递给您~
|
||||
- `0.4` 当前版本,进行中的工作内容参考[Issue](https://github.com/alibaba/testable-mock/issues)清单
|
||||
- `0.5` 实现以"独立Mock类"为单元的Mock方法复用机制,让测试类之间可以方便复用相同的Mock方法
|
||||
- `0.6` 实现将Mock方法的默认生效范围缩小至当前被测类,避免Mock方法在测试类之间相互影响
|
||||
- `1.0` 功能稳定,一个崭新的开始
|
||||
|
||||
## 目录结构
|
||||
|
||||
|
@ -7,6 +7,15 @@ Write a mock method, add an `@MockMethod` annotation, everything is done.
|
||||
|
||||
Usage Document: https://alibaba.github.io/testable-mock/#/en-us/
|
||||
|
||||
## Loadmap
|
||||
|
||||
`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
|
||||
- `v1.0` all functions are stable, a brand-new start
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```bash
|
||||
|
@ -13,8 +13,8 @@ repositories {
|
||||
|
||||
dependencies {
|
||||
testImplementation('junit:junit:4.13.1')
|
||||
testImplementation('com.alibaba.testable:testable-all:0.4.9')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.4.9')
|
||||
testImplementation('com.alibaba.testable:testable-all:0.4.11')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.4.11')
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
|
@ -12,7 +12,7 @@
|
||||
<maven.compiler.source>1.8</maven.compiler.source>
|
||||
<maven.compiler.target>1.8</maven.compiler.target>
|
||||
<junit.version>4.13.1</junit.version>
|
||||
<testable.version>0.4.9</testable.version>
|
||||
<testable.version>0.4.11</testable.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@ -33,14 +33,14 @@
|
||||
<build>
|
||||
<plugins>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.0.0-M5</version>
|
||||
<configuration>
|
||||
<argLine>@{argLine} -javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar</argLine>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<configuration>
|
||||
<argLine>@{argLine} -javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar</argLine>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<!-- <plugin>-->
|
||||
<!-- <groupId>com.alibaba.testable</groupId>-->
|
||||
<!-- <artifactId>testable-maven-plugin</artifactId>-->
|
||||
|
@ -32,7 +32,7 @@ public class DemoPrivateAccess {
|
||||
* private static method with arguments
|
||||
*/
|
||||
private static String privateStaticFuncWithArgs(String str, int i) {
|
||||
return str + " + " + i;
|
||||
return (str == null ? "null" : str) + " + " + i;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -8,8 +8,8 @@ import org.junit.Test;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import static com.alibaba.testable.core.matcher.InvokeVerifier.verify;
|
||||
import static com.alibaba.testable.core.tool.TestableTool.SOURCE_METHOD;
|
||||
import static com.alibaba.testable.core.tool.TestableTool.MOCK_CONTEXT;
|
||||
import static com.alibaba.testable.core.tool.TestableTool.SOURCE_METHOD;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
/**
|
||||
|
@ -8,6 +8,7 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
/**
|
||||
* 演示私有成员访问功能
|
||||
@ -57,6 +58,20 @@ public class DemoPrivateAccessTest {
|
||||
public void should_able_to_update_final_field() {
|
||||
demoPrivateAccess.pi = 4.13;
|
||||
assertEquals(Double.valueOf(4.13), demoPrivateAccess.pi);
|
||||
|
||||
PrivateAccessor.set(demoPrivateAccess, "pi", 3.14);
|
||||
assertEquals(Double.valueOf(3.14), PrivateAccessor.get(demoPrivateAccess, "pi"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void should_able_to_use_null_parameter() {
|
||||
demoPrivateAccess.pi = null;
|
||||
assertNull(demoPrivateAccess.pi);
|
||||
assertEquals("null + 1", DemoPrivateAccess.privateStaticFuncWithArgs(null, 1));
|
||||
|
||||
PrivateAccessor.set(demoPrivateAccess, "pi", null);
|
||||
assertNull(PrivateAccessor.get(demoPrivateAccess, "pi"));
|
||||
assertEquals("null + 1", PrivateAccessor.invokeStatic(DemoPrivateAccess.class, "privateStaticFuncWithArgs", null, 1));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins {
|
||||
kotlin("jvm") version "1.4.10"
|
||||
kotlin("jvm") version "1.4.10"
|
||||
}
|
||||
|
||||
group = "com.alibaba.testable"
|
||||
@ -9,25 +9,25 @@ version = "1.0.0-SNAPSHOT"
|
||||
java.sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
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.4.9")
|
||||
testAnnotationProcessor("com.alibaba.testable:testable-processor:0.4.9")
|
||||
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.4.11")
|
||||
testAnnotationProcessor("com.alibaba.testable:testable-processor:0.4.11")
|
||||
}
|
||||
|
||||
tasks.withType<KotlinCompile> {
|
||||
kotlinOptions {
|
||||
freeCompilerArgs = listOf("-Xjsr305=strict")
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
kotlinOptions {
|
||||
freeCompilerArgs = listOf("-Xjsr305=strict")
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<Test> {
|
||||
jvmArgs("-javaagent:${classpath.find { it.name.contains("testable-agent") }!!.absolutePath}")
|
||||
useJUnitPlatform()
|
||||
jvmArgs("-javaagent:${classpath.find { it.name.contains("testable-agent") }!!.absolutePath}")
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
@ -1,119 +1,119 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>kotlin-demo</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<name>kotlin-demo</name>
|
||||
<description>Demo project for TestableMock</description>
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>kotlin-demo</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<name>kotlin-demo</name>
|
||||
<description>Demo project for TestableMock</description>
|
||||
|
||||
<properties>
|
||||
<java.version>1.8</java.version>
|
||||
<kotlin.version>1.3.72</kotlin.version>
|
||||
<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.4.9</testable.version>
|
||||
</properties>
|
||||
<properties>
|
||||
<java.version>1.8</java.version>
|
||||
<kotlin.version>1.3.72</kotlin.version>
|
||||
<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.4.11</testable.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-reflect</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-stdlib-jdk8</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
</dependency>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-reflect</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-stdlib-jdk8</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-all</artifactId>
|
||||
<version>${testable.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-all</artifactId>
|
||||
<version>${testable.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<sourceDirectory>src/main/kotlin</sourceDirectory>
|
||||
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.0.0-M5</version>
|
||||
<!-- <configuration>-->
|
||||
<!-- <argLine>@{argLine} -javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar</argLine>-->
|
||||
<!-- </configuration>-->
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-maven-plugin</artifactId>
|
||||
<version>${testable.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>prepare</id>
|
||||
<goals>
|
||||
<goal>prepare</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
<version>0.8.6</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>prepare-agent</id>
|
||||
<goals>
|
||||
<goal>prepare-agent</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>report</id>
|
||||
<phase>prepare-package</phase>
|
||||
<goals>
|
||||
<goal>report</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<dataFile>target/jacoco.exec</dataFile>
|
||||
<outputDirectory>target/jacoco-ut</outputDirectory>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-maven-plugin</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>compile</id>
|
||||
<phase>compile</phase>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>test-compile</id>
|
||||
<phase>test-compile</phase>
|
||||
<goals>
|
||||
<goal>test-compile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<build>
|
||||
<sourceDirectory>src/main/kotlin</sourceDirectory>
|
||||
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.0.0-M5</version>
|
||||
<!-- <configuration>-->
|
||||
<!-- <argLine>@{argLine} -javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar</argLine>-->
|
||||
<!-- </configuration>-->
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-maven-plugin</artifactId>
|
||||
<version>${testable.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>prepare</id>
|
||||
<goals>
|
||||
<goal>prepare</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
<version>0.8.6</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>prepare-agent</id>
|
||||
<goals>
|
||||
<goal>prepare-agent</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>report</id>
|
||||
<phase>prepare-package</phase>
|
||||
<goals>
|
||||
<goal>report</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<dataFile>target/jacoco.exec</dataFile>
|
||||
<outputDirectory>target/jacoco-ut</outputDirectory>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-maven-plugin</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>compile</id>
|
||||
<phase>compile</phase>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>test-compile</id>
|
||||
<phase>test-compile</phase>
|
||||
<goals>
|
||||
<goal>test-compile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
|
@ -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.
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
Quickly construct complex input parameters
|
||||
Parameter constructor
|
||||
---
|
||||
|
||||
Generate arbitrarily nested object instances, simplify their internal member assignment methods, and solve the problem of lengthy initialization code for the parameters of the tested method.
|
||||
No matter how intricate the parameter structure required by the method under test is, even there is no suitable construction method, or even there are private internal class objects... Call `TestableMock`, the parameter object will be handed to you immediately~
|
||||
|
||||
This feature is planned to be released in the `0.6` version.
|
||||
This feature is planned to be released in the `0.5.x` version.
|
||||
|
@ -1,5 +1,15 @@
|
||||
# Release Note
|
||||
|
||||
## 0.4.11
|
||||
- support accessing private members of class under test in different package path
|
||||
- validate the number of private method parameters accessed by `PrivateAccessor`
|
||||
- fix a bug which may cause errors when the mock method contains array parameters
|
||||
- fix an issue which cause some private members not be found in the IntelliJ build
|
||||
|
||||
## 0.4.10
|
||||
- fix an issue of using mock in lambda expression
|
||||
- fix the NullPointerException when invoke private method with parameter value `null`
|
||||
|
||||
## 0.4.9
|
||||
- fix an issue cause by improperly bytecode processing while using `targetClass` parameter
|
||||
- auto validate access target of `PrivateAccessor`, improve resistance to code refactoring
|
||||
|
@ -3,9 +3,10 @@ Use TestableMock
|
||||
|
||||
`TestableMock` is an assist tool for Java unit testing based on source code and bytecode enhancement, including the following functions:
|
||||
|
||||
- [Quickly mock arbitrary call](en-us/doc/use-mock.md): quickly replace any method invocation in the class under test with a mock method, solve the cumbersome use of traditional mock tools problem
|
||||
- [Access private members of the class under test](en-us/doc/private-accessor.md): enable unit tests directly invoke or access private members of the class under test, solve the problems of private member initialization and private method testing
|
||||
- [Quick mock arbitrary call](en-us/doc/use-mock.md): quickly replace any method invocation in the class under test with a mock method, solve the cumbersome use of traditional mock tools problem
|
||||
- [Auxiliary test void method](en-us/doc/test-void-method.md): use the mock validator to check the internal logic of method, solve the problem that unit testing is difficult to implement to the method with no return value
|
||||
- [Quickly construct complicated parameter object](en-us/doc/parameter-constructor.md):generate arbitrarily nested object instances, simplify their internal member assignment methods, solve the problem of long initialization codes for method parameters
|
||||
|
||||
## Use in Maven project
|
||||
|
||||
@ -15,7 +16,7 @@ It is recommended to add a `property` field that identifies the TestableMock ver
|
||||
|
||||
```xml
|
||||
<properties>
|
||||
<testable.version>0.4.9</testable.version>
|
||||
<testable.version>0.4.11</testable.version>
|
||||
</properties>
|
||||
```
|
||||
|
||||
@ -62,12 +63,12 @@ Add dependence of `TestableMock` in `build.gradle` file:
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
testImplementation('com.alibaba.testable:testable-all:0.4.9')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.4.9')
|
||||
testImplementation('com.alibaba.testable:testable-all:0.4.11')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.4.11')
|
||||
}
|
||||
```
|
||||
|
||||
Then add javaagent to `test` configuration:
|
||||
Then add `javaagent` to "test" configuration:
|
||||
|
||||
```groovy
|
||||
test {
|
||||
@ -76,3 +77,19 @@ test {
|
||||
```
|
||||
|
||||
See the [build.gradle](https://github.com/alibaba/testable-mock/blob/master/demo/java-demo/build.gradle) file of project `java-demo` and the [build.gradle.kts](https://github.com/alibaba/testable-mock/blob/master/demo/kotlin-demo/build.gradle.kts) file of project `kotlin-demo`.
|
||||
|
||||
> For Android project tested with `Robolectric` framework, please use the same method to add `TestableMock` dependency as above, and add `javaagent` configuration as follows:
|
||||
>
|
||||
> ```groovy
|
||||
> android {
|
||||
> testOptions {
|
||||
> unitTests {
|
||||
> all {
|
||||
> jvmArgs "-javaagent:${classpath.find { it.name.contains("testable-agent") }.absolutePath}"
|
||||
> }
|
||||
> }
|
||||
> }
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> See [issue-43](https://github.com/alibaba/testable-mock/issues/43) for a complete example.
|
||||
|
@ -38,3 +38,9 @@ The output log example is as follows:
|
||||
```
|
||||
|
||||
The log shows all the mocked invocation and corresponding code line numbers in the class under test.
|
||||
|
||||
- Self troubleshooting:
|
||||
|
||||
- If there is no output, please check whether the `pom.xml` or `build.gradle` configuration correctly introduces `TestableMock` dependencies
|
||||
- If only the first line of `Handling test class` is output, please check whether the test class is in the same package of the class under test, and the name is "<ClassUnderTest>+Test" (required for `0.4.x` version)
|
||||
- If `Handling source class` and `Handling method xxx` are output, but there is no mock replacement happen at the expected code line, please check whether the mock method definition matches the target method
|
||||
|
@ -5,10 +5,6 @@ Use TestableMock In IDE
|
||||
|
||||
IntelliJ IDE supports the `JSR-269` annotation processor and the `maven-surefire-plugin` arguments very well (both are techniques back the `TestableMock`). Usually you don't need any special configuration to make everything work, it's all out of the box.
|
||||
|
||||
> In IntelliJ 2020.3 and later versions, its built-in annotation processor had some parameter types changed, which no longer consistent with the standard `Maven` compilation process. In the version of `TestableMock` lower than `0.4.5`, you will encounter the problem that the `@EnablePrivateAccess` annotation does not take effect. You can turn on "Delegate IDE build/run actions to maven" option in "Build Tools > Maven > Runner" of the system configuration to solve the problem.
|
||||
>
|
||||
> ![delegate-ide-build-to-maven](https://testable-code.oss-cn-beijing.aliyuncs.com/delegate-ide-build-to-maven.png)
|
||||
|
||||
## Use Eclipse IDE
|
||||
|
||||
Since the built-in compilation feature of `Eclipse` is based on a self-made compiler, it is not compatible with the standard `javac` compilation process, which will cause the `@EnablePrivateAccess` annotation to be invalid when running test cases in the IDE. However, the function of accessing the private members of the class under test through the `PrivateAccessor` tool class will not be affected by differences in the compiler.
|
||||
|
@ -4,80 +4,86 @@ Fast Mocking
|
||||
Compared with the class-granularity mocking practices of existing mock tools, `TestableMock` allows developers to directly define a single method and use it for mocking. With the principle of convention over configuration, mock method replacement will automatically happen when the specified method in the test class match an invocation in the class under test.
|
||||
|
||||
> In summary, there are two simple rules:
|
||||
> - Mock non-constructive method, copy the original method definition to the test class, add a parameter of the same type as the caller, and add a `@MockMethod` annotation
|
||||
> - Mock non-constructive method, copy the original method definition to the test class, add a `@MockMethod` annotation
|
||||
> - Mock construction method, copy the original method definition to the test class, replace the return value with the constructed type, the method name is arbitrary, and add a `@MockContructor` annotation
|
||||
|
||||
> **Mock convention**:
|
||||
> - The name of the test class should be `<NameOfClassUnderTest> + Test` (and in the same package path), which is usually the by-default naming convention of Java project managed by `Maven` or `Gradle`. This constraint may be relaxed or removed in future versions of `TestableMock`.
|
||||
> - Do NOT access any non-`static` members in mock methods. Currently, methods that is decorated by `@MockMethod` or `@MockContructor` annotations will be automatically modified to `static` methods during runtime. In future versions, this constraint will be removed.
|
||||
|
||||
The detail mock method definition convention is as follows:
|
||||
|
||||
#### 1. Mock method calls of any class
|
||||
|
||||
Define an ordinary method annotated with `@MockMethod` in the test class with exactly the same signature (name, parameter, and return value type) as the method to be mocked, and then add an extra parameter as the first parameter of method, with the same type as the object that the method originally belongs to.
|
||||
Define an ordinary method annotated with `@MockMethod` in the test class with exactly the same signature (name, parameter, and return value type) as the method to be mocked, and then add the type of target object (which the method originally belongs to) as `targetMethod` parameter of `@MockMethod` annotation.
|
||||
|
||||
At this time, all invocations to that original method in the class under test will be automatically replaced with invocations to the above-mentioned mock method when the unit test is running.
|
||||
|
||||
**Note**: When several methods to be mocked have the same name, you can put the name of the method to be mocked in the `targetMethod` parameter of `@MockMethod` annotation, so that the mock method itself can be named at will.
|
||||
|
||||
For example, there is a call to `"anything".substring(1, 2)` in the class under test, and we want to change it to a fixed string when running the test, we only need to define the following method in the test class:
|
||||
|
||||
```java
|
||||
// The original method signature is `String substring(int, int)`
|
||||
// The object `"anything"` that invokes this method is of type `String`
|
||||
// Adds a `String` type parameter to the first position the mock method parameter list (parameter name is arbitrary)
|
||||
// This parameter can be used to get the value and context of the actual invoker at runtime
|
||||
@MockMethod
|
||||
private String substring(String self, int i, int j) {
|
||||
@MockMethod(targetClass = String.class)
|
||||
private String substring(int i, int j) {
|
||||
return "sub_string";
|
||||
}
|
||||
```
|
||||
|
||||
When several methods to be mocked have the same name, you can put the name of the method to be mocked in the `targetMethod` parameter of `@MockMethod` annotation, so that the mock method itself can be named at will.
|
||||
|
||||
The following example shows the usage of the `targetMethod` parameter, and its effect is the same as the above example:
|
||||
|
||||
```java
|
||||
// Use `targetMethod` to specify the name of the method that needs to be mocked
|
||||
// The method itself can now be named arbitrarily, but the method parameters still need to follow the same matching rules
|
||||
@MockMethod(targetMethod = "substring")
|
||||
private String use_any_mock_method_name(String self, int i, int j) {
|
||||
@MockMethod(targetClass = String.class, targetMethod = "substring")
|
||||
private String use_any_mock_method_name(int i, int j) {
|
||||
return "sub_string";
|
||||
}
|
||||
```
|
||||
|
||||
Sometimes, the mock method need to access the member variables in the original object that initiated the invocation, or invoke other methods of the original object. At this point, you can remove the `targetClass` parameter in the `@MockMethod` annotation, and then add a extra parameter whose type is the original object type of the method to the first index of the method parameter list.
|
||||
|
||||
The `TestableMock` convention is that when the `targetClass` parameter value of the `@MockMethod` annotation is empty, the first parameter of the mock method is the type of the target method, and the parameter name is arbitrary. In order to facilitate code reading, it is recommended to name this parameter as `self` or `src`. Example as follows:
|
||||
|
||||
```java
|
||||
// Adds a `String` type parameter to the first position the mock method parameter list (parameter name is arbitrary)
|
||||
// This parameter can be used to get the value and context of the actual invoker at runtime
|
||||
@MockMethod
|
||||
private String substring(String self, int i, int j) {
|
||||
// Call the original method is also allowed
|
||||
return self.substring(i, j);
|
||||
}
|
||||
```
|
||||
|
||||
For complete code examples, see the `should_able_to_mock_common_method()` test cases in the `java-demo` and `kotlin-demo` sample projects. (Because Kotlin has made magical changes to the String type, the method under test in the Kotlin example adds a layer of encapsulation to the `BlackBox` class)
|
||||
|
||||
#### 2. Mock the member method of the class under test itself
|
||||
|
||||
Sometimes, when testing certain methods, it is desirable to mock out some other member methods of the class under test itself.
|
||||
|
||||
The solution is the same as the previous case. The first parameter type of the mock method needs to be the same as that of the class under test.
|
||||
The solution is the same as the previous case. Just set `targetClass` parameter value to the type of class under test.
|
||||
|
||||
For example, there is a private method with the signature `String innerFunc(String)` in the class under test. If we want to replace it during testing, we only need to define the following method in the test class:
|
||||
|
||||
```java
|
||||
// The type to test is `DemoMock`
|
||||
// So when defining the mock method, add a parameter of type `DemoMock` to the first position of parameter list (the name is arbitrary)
|
||||
@MockMethod
|
||||
private String innerFunc(DemoMock self, String text) {
|
||||
@MockMethod(targetClass = DemoMock.class)
|
||||
private String innerFunc(String text) {
|
||||
return "mock_" + text;
|
||||
}
|
||||
```
|
||||
|
||||
Similarly, if the method in the above example needs to access the original tested object that initiated the call, it may not use the `targetClass` parameter, but when defining the mock method, add a parameter of type `DemoMock` to the first index of the method parameter list.
|
||||
|
||||
For complete code examples, see the `should_able_to_mock_member_method()` test case in the `java-demo` and `kotlin-demo` sample projects.
|
||||
|
||||
#### 3. Mock static methods of any class
|
||||
|
||||
Mock for static methods is the same as for any ordinary methods. But it should be noted that when the mock method of a static method is called, the actual value of the first parameter passed in is always `null`.
|
||||
Mock for static methods is the same as for any ordinary methods.
|
||||
|
||||
For example, if the static method `secretBox()` of the `BlackBox` type is invoked in the class under test, and the method signature is changed to `BlackBox secretBox()`, the mock method is as follows:
|
||||
For example, if the static method `secretBox()` of the `BlackBox` type is invoked in the class under test, and the method signature is `BlackBox secretBox()`, then the mock method is as follows:
|
||||
|
||||
```java
|
||||
// The target static method is defined in the `BlackBox` type
|
||||
// When defining the mock method, add a parameter of type `BlackBox` to the first position parameter list (the name is arbitrary)
|
||||
// This parameter is only used to identify the target type, the actual incoming value will always be `null`
|
||||
@MockMethod
|
||||
private BlackBox secretBox(BlackBox ignore) {
|
||||
@MockMethod(targetClass = BlackBox.class)
|
||||
private BlackBox secretBox() {
|
||||
return new BlackBox("not_secret_box");
|
||||
}
|
||||
```
|
||||
@ -151,3 +157,11 @@ For complete code examples, see the `should_able_to_get_source_method_name()` an
|
||||
In test cases, you can use the `TestableTool.verify()` method, and cooperate with `with()`, `withInOrder()`, `without()`, `withTimes()` and other methods to verify the mock call situation.
|
||||
|
||||
For details, please refer to the [Check Mock Call](en-us/doc/matcher.md) document.
|
||||
|
||||
#### Additional note
|
||||
|
||||
> **Mock convention in version 0.4.x**:
|
||||
> - The name of the test class must be `<NameOfClassUnderTest> + Test` (and in the same package path), which is usually the by-default naming convention of Java project managed by `Maven` or `Gradle`.
|
||||
> - Do NOT access any non-`static` members in mock methods. Currently, methods that is decorated by `@MockMethod` or `@MockContructor` annotations will be automatically modified to `static` methods during runtime. (When mock method contains some statement like _lambda function_, _anonymous class_ or _initiation block_, java compiler will generate additional method during compilation, these mock methods also have to be declared as `static` to avoid non-static dynamical method invoked.)
|
||||
>
|
||||
> These constraints will change in `0.5` versions of `TestableMock`.
|
||||
|
@ -1,8 +1,9 @@
|
||||
- Quick Start
|
||||
- [Use TestableMock](en-us/doc/setup.md)
|
||||
- [Private Accessor](en-us/doc/private-accessor.md)
|
||||
- [Fast Mocking](en-us/doc/use-mock.md)
|
||||
- [Private Accessor](en-us/doc/private-accessor.md)
|
||||
- [Test Void Method](en-us/doc/test-void-method.md)
|
||||
- [Parameter constructor](en-us/doc/parameter-constructor.md)
|
||||
|
||||
- Usage Guide
|
||||
- [Verify Mock Invocation](en-us/doc/invoke-matcher.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<String>() {{ append("data"); }}`)、匿名函数(譬如`list.stream().map(i -> i.get)`)等等,会在编译过程中生成额外的成员方法调用,导致Mock方法执行报错。
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
快速构造复杂入参
|
||||
---
|
||||
|
||||
生成任意多层嵌套的对象实例,并简化其内部成员赋值方式,解决被测方法参数初始化代码冗长的问题。
|
||||
不论被测方法所需的参数结构多么错综复杂、甚至没有合适的构造方法、甚至需要私有内部类对象... 呼唤TestableMock,马上递给您~
|
||||
|
||||
计划在`0.6`版本中推出。
|
||||
计划在`0.5.x`版本中推出。
|
||||
|
@ -1,5 +1,15 @@
|
||||
# Release Note
|
||||
|
||||
## 0.4.11
|
||||
- 支持测试类访问与自身包路径不同的被测类的私有成员
|
||||
- 增加`PrivateAccessor`访问的私有方法参数数目检查,提高抗代码重构能力
|
||||
- 修复被Mock方法包含数组参数可能导致出错的BUG (issue-48)
|
||||
- 修复一处会导致在IntelliJ中构建找不到私有成员的问题
|
||||
|
||||
## 0.4.10
|
||||
- 修复在Lambda函数中使用Mock出错的BUG(issue-44)
|
||||
- 修复调用私有方法时参数值不能为null的问题(issue-27)
|
||||
|
||||
## 0.4.9
|
||||
- 修复发起调用的对象不是局部或成员变量时Mock出错的BUG (issue-40)
|
||||
- 增加`PrivateAccessor`访问目标有效性检查,提高抗代码重构能力 (issue-21)
|
||||
|
@ -3,9 +3,10 @@
|
||||
|
||||
`TestableMock`是基于源码和字节码增强的Java单元测试辅助工具,包含以下功能:
|
||||
|
||||
- [访问被测类私有成员](zh-cn/doc/private-accessor.md):使单元测试能直接调用和访问被测类的私有成员,解决私有成员初始化和私有方法测试的问题
|
||||
- [快速Mock任意调用](zh-cn/doc/use-mock.md):使被测类的任意方法调用快速替换为Mock方法,实现"指哪换哪",解决传统Mock工具使用繁琐的问题
|
||||
- [访问被测类私有成员](zh-cn/doc/private-accessor.md):使单元测试能直接调用和访问被测类的私有成员,解决私有成员初始化和私有方法测试的问题
|
||||
- [辅助测试void方法](zh-cn/doc/test-void-method.md):利用Mock校验器对方法的内部逻辑进行检查,解决无返回值方法难以实施单元测试的问题
|
||||
- [快速构造参数对象](zh-cn/doc/parameter-constructor.md):生成任意多层嵌套的对象实例,并简化其内部成员赋值方式,解决被测方法参数初始化代码冗长的问题
|
||||
|
||||
## 在Maven项目中使用
|
||||
|
||||
@ -15,7 +16,7 @@
|
||||
|
||||
```xml
|
||||
<properties>
|
||||
<testable.version>0.4.9</testable.version>
|
||||
<testable.version>0.4.11</testable.version>
|
||||
</properties>
|
||||
```
|
||||
|
||||
@ -58,12 +59,12 @@
|
||||
|
||||
## 在Gradle项目中使用
|
||||
|
||||
在`build.gradle`文件中添加TestableMock依赖:
|
||||
在`build.gradle`文件中添加`TestableMock`依赖:
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
testImplementation('com.alibaba.testable:testable-all:0.4.9')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.4.9')
|
||||
testImplementation('com.alibaba.testable:testable-all:0.4.11')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.4.11')
|
||||
}
|
||||
```
|
||||
|
||||
@ -76,3 +77,19 @@ test {
|
||||
```
|
||||
|
||||
参见项目`java-demo`的[build.gradle](https://github.com/alibaba/testable-mock/blob/master/demo/java-demo/build.gradle)和`kotlin-demo`的[build.gradle.kts](https://github.com/alibaba/testable-mock/blob/master/demo/kotlin-demo/build.gradle.kts)文件。
|
||||
|
||||
> 若是基于`Robolectric`框架的Android项目,则添加`TestableMock`依赖方法同上,添加javaagent配置方法如下:
|
||||
>
|
||||
> ```groovy
|
||||
> android {
|
||||
> testOptions {
|
||||
> unitTests {
|
||||
> all {
|
||||
> jvmArgs "-javaagent:${classpath.find { it.name.contains("testable-agent") }.absolutePath}"
|
||||
> }
|
||||
> }
|
||||
> }
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> 完整示例参考[issue-43](https://github.com/alibaba/testable-mock/issues/43)
|
||||
|
@ -38,3 +38,9 @@ class DemoTest {
|
||||
```
|
||||
|
||||
该日志展示了被测类中所有发生了Mock替换的调用和相应代码行号。
|
||||
|
||||
简单排查方法:
|
||||
|
||||
- 若没有任何输出,请检查`pom.xml`或`build.gradle`配置是否正确引入了TestableMock依赖
|
||||
- 若只输出了第一行`Handling test class`,请检查被测类与测试类是否包路径相同,且名称为"被测类+Test"(`0.4.x`版本要求)
|
||||
- 若输出了`Handling source class`以及`Handling method xxx`,但预期的代码行位置没有发生Mock替换,请检查Mock方法定义是否未与目标方法匹配
|
||||
|
@ -5,10 +5,6 @@
|
||||
|
||||
IntelliJ IDE对`TestableMock`所用到的`JSR-269`注释处理器以及`maven-surefire-plugin`插件的附加参数均支持良好。通常无需特殊配置,可开箱即用。
|
||||
|
||||
> 在IntelliJ 2020.3及后续版本里,其内置的编译期注释处理器运行时参数类型与标准`Maven`编译过程不一致。在`TestableMock`低于`0.4.5`的版本中会遇到`@EnablePrivateAccess`注解未生效的问题,可通过在系统配置的"Build Tools > Maven > Runner"中开启"Delegate IDE build/run actions to maven"选项解决。
|
||||
>
|
||||
> ![delegate-ide-build-to-maven](https://testable-code.oss-cn-beijing.aliyuncs.com/delegate-ide-build-to-maven.png)
|
||||
|
||||
## 使用Eclipse IDE
|
||||
|
||||
由于`Eclipse`内置的自动编译功能基于三方编译器实现,与标准`javac`编译过程不兼容,会导致在IDE中运行测试用例时`@EnablePrivateAccess`注解无效。不过,通过`PrivateAccessor`工具类访问被测类私有成员的功能不会受编译器差异影响。
|
||||
|
@ -19,13 +19,15 @@
|
||||
</plugin>
|
||||
```
|
||||
|
||||
> 当使用`testable-maven-plugin`插件时,应该移除`maven-surefire-plugin`插件上的TestableMock相关配置
|
||||
**注意:**当使用`testable-maven-plugin`插件时,应该移除`maven-surefire-plugin`插件上的TestableMock相关配置。
|
||||
|
||||
`testable-maven-plugin`插件能够与Jacoco插件直接同时使用,无需额外适配,因此能使`pom.xml`文件编写起来更简单且美观。
|
||||
|
||||
但需要注意的是,当通过IDE运行单个测试用例时,Mock功能会失效。
|
||||
> 还有一种特殊情况,当`jacoco`插件是通过`maven`命令行参数引入的时候,若要使用`TestableMock`功能,则也必须通过命令行参数引入`testable-maven-plugin`插件。详见[issue-14](https://github.com/alibaba/testable-mock/issues/14)。
|
||||
|
||||
这是由于IDE运行单个测试用例时通常都只会运行`maven-surefire-plugin`插件,跳过了`testable-maven-plugin`插件执行,导致Mock功能所需的JavaAgent没有随测试注入。
|
||||
但需要注意的是,使用`testable-maven-plugin`插件后,通过IntelliJ IDE运行单个测试用例时,Mock功能会失效。
|
||||
|
||||
这是由于IntelliJ IDE运行单个测试用例时只会运行`maven-surefire-plugin`插件,跳过了`testable-maven-plugin`插件执行,导致Mock功能所需的JavaAgent未随测试注入。
|
||||
|
||||
该问题可以通过额外配置IDE的测试参数绕过。以IntelliJ为例,打开运行菜单的"编辑配置..."选型,如图中位置①
|
||||
|
||||
|
@ -7,12 +7,6 @@
|
||||
> - Mock非构造方法,拷贝原方法定义到测试类,加`@MockMethod`注解
|
||||
> - Mock构造方法,拷贝原方法定义到测试类,返回值换成构造的类型,方法名随意,加`@MockContructor`注解
|
||||
|
||||
> **Mock约定**:
|
||||
> - 测试类与被测类的包路径应相同,且名称为`被测类名+Test`。通常采用`Maven`或`Gradle`构建的Java项目均符合这种惯例。此约定在未来的`TestableMock`版本中会放宽(请关注[Issue-12](https://github.com/alibaba/testable-mock/issues/12))。
|
||||
|
||||
> **特别说明**:
|
||||
> - 当前Mock方法(即包含`@MockMethod`或`@MockContructor`注解的方法)会在运行期被自动修改为`static`方法,请勿在Mock方法的定义中访问任何非静态成员。当Mock方法内容较复杂(包含Lambda语句、构造块、匿名类等)时,编译器会在构建期生成额外的非静态临时方法,导致"Bad type in operand stack"错误。如果有遇到此类错误,请将Mock方法显式加上`static`修饰即可解决。这个问题会在`0.5`版本中彻底解决。
|
||||
|
||||
具体的Mock方法定义约定如下:
|
||||
|
||||
#### 1. 覆写任意类的方法调用
|
||||
@ -126,9 +120,9 @@ private BlackBox createBlackBox(String text) {
|
||||
```java
|
||||
@Test
|
||||
public void testDemo() {
|
||||
MOCK_CONTEXT.set("case", "data-ready");
|
||||
MOCK_CONTEXT.put("case", "data-ready");
|
||||
assertEquals(true, demo());
|
||||
MOCK_CONTEXT.set("case", "has-error");
|
||||
MOCK_CONTEXT.put("case", "has-error");
|
||||
assertEquals(false, demo());
|
||||
MOCK_CONTEXT.clear();
|
||||
}
|
||||
@ -163,3 +157,13 @@ private Data mockDemo() {
|
||||
在测试用例中可用通过`TestableTool.verify()`方法,配合`with()`、`withInOrder()`、`without()`、`withTimes()`等方法实现对Mock调用情况的验证。
|
||||
|
||||
详见[校验Mock调用](zh-cn/doc/matcher.md)文档。
|
||||
|
||||
#### 特别说明
|
||||
|
||||
> **0.4.x 版本的Mock约定**:
|
||||
> - 测试类与被测类的包路径应相同,且名称为`被测类名+Test`(通常采用`Maven`或`Gradle`构建的Java项目均符合这种惯例)
|
||||
> - Mock方法(即包含`@MockMethod`或`@MockContructor`注解的方法)会在运行期被自动修改为`static`方法,请勿在Mock方法的定义中访问任何非静态成员。
|
||||
>
|
||||
> 这两项约束会在`0.5`版本中去除
|
||||
>
|
||||
> 当Mock方法内容较复杂(包含Lambda语句、构造块、匿名类等)时,编译器会在构建期生成额外的非静态临时方法,导致"Bad type in operand stack"错误。如果有遇到此类错误,请将Mock方法显式加上`static`修饰即可解决。这个问题会在`0.5`版本中彻底解决。
|
||||
|
@ -1,8 +1,9 @@
|
||||
- 快速上手
|
||||
- [使用TestableMock](zh-cn/doc/setup.md)
|
||||
- [直接访问私有成员](zh-cn/doc/private-accessor.md)
|
||||
- [快速Mock任意方法](zh-cn/doc/use-mock.md)
|
||||
- [直接访问私有成员](zh-cn/doc/private-accessor.md)
|
||||
- [测试无返回值的方法](zh-cn/doc/test-void-method.md)
|
||||
- [快速构造复杂入参](zh-cn/doc/parameter-constructor.md)
|
||||
|
||||
- 使用指南
|
||||
- [校验Mock调用](zh-cn/doc/invoke-matcher.md)
|
||||
|
@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-parent</artifactId>
|
||||
<version>0.4.9</version>
|
||||
<version>0.4.11</version>
|
||||
<relativePath>../testable-parent</relativePath>
|
||||
</parent>
|
||||
<artifactId>testable-agent</artifactId>
|
||||
|
@ -14,6 +14,7 @@ public class PreMain {
|
||||
private static final String AND = "&";
|
||||
private static final String LOG_LEVEL = "logLevel";
|
||||
private static final String DUMP_PATH = "dumpPath";
|
||||
private static final String PKG_PREFIX = "pkgPrefix";
|
||||
private static final String EQUAL = "=";
|
||||
|
||||
public static void premain(String agentArgs, Instrumentation inst) {
|
||||
@ -34,6 +35,8 @@ public class PreMain {
|
||||
GlobalConfig.setLogLevel(v);
|
||||
} else if (k.equals(DUMP_PATH)) {
|
||||
GlobalConfig.setDumpPath(v);
|
||||
} else if (k.equals(PKG_PREFIX)) {
|
||||
GlobalConfig.setPkgPrefix(v);
|
||||
}
|
||||
} else {
|
||||
GlobalConfig.setLogLevel(a);
|
||||
|
@ -60,29 +60,21 @@ public class SourceClassHandler extends BaseClassHandler {
|
||||
Set<MethodInfo> newOperatorInjectMethods) {
|
||||
LogUtil.diagnose(" Handling method %s", mn.name);
|
||||
AbstractInsnNode[] instructions = mn.instructions.toArray();
|
||||
if (instructions.length == 0) {
|
||||
// native method (issue-52)
|
||||
return;
|
||||
}
|
||||
int i = 0;
|
||||
int maxStackDiff = 0;
|
||||
do {
|
||||
if (invokeOps.contains(instructions[i].getOpcode())) {
|
||||
MethodInsnNode node = (MethodInsnNode)instructions[i];
|
||||
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) {
|
||||
ModifiedInsnNodes modifiedInsnNodes = replaceMemberCallOps(cn, mn, mockMethod,
|
||||
instructions, node.owner, node.getOpcode(), rangeStart, i);
|
||||
instructions = modifiedInsnNodes.nodes;
|
||||
maxStackDiff = Math.max(maxStackDiff, modifiedInsnNodes.stackDiff);
|
||||
i = rangeStart;
|
||||
} else {
|
||||
LogUtil.warn("Potential missed mocking at %s:%s", mn.name, getLineNum(instructions, i));
|
||||
}
|
||||
} else if (ConstPool.CONSTRUCTOR.equals(node.name)) {
|
||||
// it's a new operation
|
||||
if (ConstPool.CONSTRUCTOR.equals(node.name)) {
|
||||
LogUtil.verbose(" Line %d, constructing \"%s\" as \"%s\"", getLineNum(instructions, i),
|
||||
node.owner, node.desc);
|
||||
String newOperatorInjectMethodName = getNewOperatorInjectMethodName(newOperatorInjectMethods, node);
|
||||
if (newOperatorInjectMethodName != null) {
|
||||
// and an inject method for it exist
|
||||
// it's a new operation and an inject method for it exist
|
||||
int rangeStart = getConstructorStart(instructions, node.owner, i);
|
||||
if (rangeStart >= 0) {
|
||||
ModifiedInsnNodes modifiedInsnNodes = replaceNewOps(cn, mn, newOperatorInjectMethodName,
|
||||
@ -92,6 +84,23 @@ public class SourceClassHandler extends BaseClassHandler {
|
||||
i = rangeStart;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LogUtil.verbose(" Line %d, invoking \"%s\" as \"%s\"", getLineNum(instructions, i),
|
||||
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) {
|
||||
ModifiedInsnNodes modifiedInsnNodes = replaceMemberCallOps(cn, mn, mockMethod,
|
||||
instructions, node.owner, node.getOpcode(), rangeStart, i);
|
||||
instructions = modifiedInsnNodes.nodes;
|
||||
maxStackDiff = Math.max(maxStackDiff, modifiedInsnNodes.stackDiff);
|
||||
i = rangeStart;
|
||||
} else {
|
||||
LogUtil.warn("Potential missed mocking at %s:%s", mn.name, getLineNum(instructions, i));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
i++;
|
||||
@ -175,10 +184,11 @@ public class SourceClassHandler extends BaseClassHandler {
|
||||
case Opcodes.INVOKESPECIAL:
|
||||
case Opcodes.INVOKEVIRTUAL:
|
||||
case Opcodes.INVOKEINTERFACE:
|
||||
return stackEffectOfInvocation(instruction) + 1;
|
||||
return stackEffectOfInvocation(((MethodInsnNode)instruction).desc) + 1;
|
||||
case Opcodes.INVOKESTATIC:
|
||||
return stackEffectOfInvocation(((MethodInsnNode)instruction).desc);
|
||||
case Opcodes.INVOKEDYNAMIC:
|
||||
return stackEffectOfInvocation(instruction);
|
||||
return stackEffectOfInvocation(((InvokeDynamicInsnNode)instruction).desc);
|
||||
case -1:
|
||||
// either LabelNode or LineNumberNode
|
||||
return 0;
|
||||
@ -187,14 +197,13 @@ public class SourceClassHandler extends BaseClassHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private int stackEffectOfInvocation(AbstractInsnNode instruction) {
|
||||
String desc = ((MethodInsnNode)instruction).desc;
|
||||
private int stackEffectOfInvocation(String desc) {
|
||||
return ClassUtil.getParameterTypes(desc).size() - (ClassUtil.getReturnType(desc).isEmpty() ? 0 : 1);
|
||||
}
|
||||
|
||||
private ModifiedInsnNodes replaceNewOps(ClassNode cn, MethodNode mn, String newOperatorInjectMethodName,
|
||||
AbstractInsnNode[] instructions, int start, int end) {
|
||||
LogUtil.diagnose(" Line %d, mock method %s used", getLineNum(instructions, start),
|
||||
LogUtil.diagnose(" Line %d, mock method \"%s\" used", getLineNum(instructions, start),
|
||||
newOperatorInjectMethodName);
|
||||
String classType = ((TypeInsnNode)instructions[start]).desc;
|
||||
String constructorDesc = ((MethodInsnNode)instructions[end]).desc;
|
||||
@ -224,7 +233,7 @@ public class SourceClassHandler extends BaseClassHandler {
|
||||
private ModifiedInsnNodes replaceMemberCallOps(ClassNode cn, MethodNode mn, MethodInfo mockMethod,
|
||||
AbstractInsnNode[] instructions, String ownerClass,
|
||||
int opcode, int start, int end) {
|
||||
LogUtil.diagnose(" Line %d, mock method %s used", getLineNum(instructions, start),
|
||||
LogUtil.diagnose(" Line %d, mock method \"%s\" used", getLineNum(instructions, start),
|
||||
mockMethod.getMockName());
|
||||
boolean shouldAppendTypeParameter = !mockMethod.getDesc().equals(mockMethod.getMockDesc());
|
||||
String testClassName = ClassUtil.getTestClassName(cn.name);
|
||||
|
@ -4,11 +4,11 @@ import com.alibaba.testable.agent.constant.ConstPool;
|
||||
import com.alibaba.testable.agent.tool.ImmutablePair;
|
||||
import com.alibaba.testable.agent.util.AnnotationUtil;
|
||||
import com.alibaba.testable.agent.util.ClassUtil;
|
||||
import com.alibaba.testable.core.model.NullType;
|
||||
import com.alibaba.testable.core.util.LogUtil;
|
||||
import org.objectweb.asm.Type;
|
||||
import org.objectweb.asm.tree.*;
|
||||
|
||||
import javax.lang.model.type.NullType;
|
||||
import java.util.List;
|
||||
|
||||
import static com.alibaba.testable.agent.util.ClassUtil.toDotSeparateFullClassName;
|
||||
|
@ -10,7 +10,6 @@ import com.alibaba.testable.agent.util.ClassUtil;
|
||||
import com.alibaba.testable.agent.util.GlobalConfig;
|
||||
import com.alibaba.testable.agent.util.StringUtil;
|
||||
import com.alibaba.testable.core.model.MockDiagnose;
|
||||
import com.alibaba.testable.core.model.NullType;
|
||||
import com.alibaba.testable.core.util.LogUtil;
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.Type;
|
||||
@ -18,6 +17,7 @@ import org.objectweb.asm.tree.AnnotationNode;
|
||||
import org.objectweb.asm.tree.ClassNode;
|
||||
import org.objectweb.asm.tree.MethodNode;
|
||||
|
||||
import javax.lang.model.type.NullType;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
@ -36,6 +36,7 @@ import static com.alibaba.testable.agent.util.ClassUtil.toDotSeparateFullClassNa
|
||||
public class TestableClassTransformer implements ClassFileTransformer {
|
||||
|
||||
private static final String FIELD_DIAGNOSE = "diagnose";
|
||||
private static final String COMMA = ",";
|
||||
|
||||
/**
|
||||
* Just avoid spend time to scan those surely non-user classes
|
||||
@ -57,21 +58,21 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
try {
|
||||
if (shouldTransformAsSourceClass(className)) {
|
||||
// it's a source class with testable enabled
|
||||
LogUtil.diagnose("Handling source class %s", className);
|
||||
List<MethodInfo> injectMethods = getTestableMockMethods(ClassUtil.getTestClassName(className));
|
||||
LogUtil.diagnose("Handling source class %s", className);
|
||||
bytes = new SourceClassHandler(injectMethods).getBytes(classFileBuffer);
|
||||
dumpByte(className, bytes);
|
||||
resetMockContext();
|
||||
} else if (shouldTransformAsTestClass(className)) {
|
||||
// it's a test class with testable enabled
|
||||
LogUtil.diagnose("Handling test class %s", className);
|
||||
bytes = new TestClassHandler().getBytes(classFileBuffer);
|
||||
dumpByte(className, bytes);
|
||||
resetMockContext();
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
LogUtil.warn("Failed to transform class " + className);
|
||||
LogUtil.diagnose(t.toString());
|
||||
} finally {
|
||||
LogUtil.resetLogLevel();
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
@ -105,14 +106,25 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
if (null == className) {
|
||||
return true;
|
||||
}
|
||||
for (String prefix : WHITELIST_PREFIXES) {
|
||||
if (className.startsWith(prefix)) {
|
||||
return false;
|
||||
String whitePrefix = GlobalConfig.getPkgPrefix();
|
||||
if (whitePrefix != null) {
|
||||
for (String prefix : whitePrefix.split(COMMA)) {
|
||||
if (className.startsWith(prefix)) {
|
||||
// Only consider package in provided list as non-system class
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (String prefix : BLACKLIST_PREFIXES) {
|
||||
if (className.startsWith(prefix)) {
|
||||
return true;
|
||||
return true;
|
||||
} else {
|
||||
for (String prefix : WHITELIST_PREFIXES) {
|
||||
if (className.startsWith(prefix)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (String prefix : BLACKLIST_PREFIXES) {
|
||||
if (className.startsWith(prefix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@ -140,9 +152,12 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
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,
|
||||
ClassUtil.extractParameters(mn.desc), ClassUtil.getReturnType(mn.desc));
|
||||
addMockConstructor(methodInfos, cn, mn);
|
||||
} else if (fullClassName.equals(ConstPool.MOCK_METHOD) ||
|
||||
fullClassName.equals(ConstPool.TESTABLE_MOCK)) {
|
||||
LogUtil.verbose(" Mock method \"%s\" as \"%s\"", mn.name, mn.desc);
|
||||
String targetMethod = AnnotationUtil.getAnnotationParameter(
|
||||
an, ConstPool.FIELD_TARGET_METHOD, mn.name, String.class);
|
||||
if (ConstPool.CONSTRUCTOR.equals(targetMethod)) {
|
||||
@ -218,14 +233,11 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
private void setupMockContext(AnnotationNode an) {
|
||||
MockDiagnose diagnose = AnnotationUtil.getAnnotationParameter(an, FIELD_DIAGNOSE, null, MockDiagnose.class);
|
||||
if (diagnose != null) {
|
||||
LogUtil.enableDiagnose(diagnose == MockDiagnose.ENABLE);
|
||||
LogUtil.setLevel(diagnose == MockDiagnose.ENABLE ? LogUtil.LogLevel.LEVEL_DIAGNOSE :
|
||||
(diagnose == MockDiagnose.VERBOSE ? LogUtil.LogLevel.LEVEL_VERBOSE : LogUtil.LogLevel.LEVEL_MUTE));
|
||||
}
|
||||
}
|
||||
|
||||
private void resetMockContext() {
|
||||
LogUtil.resetLogLevel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Split desc to "first parameter" and "desc of rest parameters"
|
||||
* @param desc method desc
|
||||
|
@ -115,17 +115,23 @@ public class ClassUtil {
|
||||
public static List<Byte> getParameterTypes(String desc) {
|
||||
List<Byte> parameterTypes = new ArrayList<Byte>();
|
||||
boolean travelingClass = false;
|
||||
boolean travelingArray = false;
|
||||
for (byte b : desc.getBytes()) {
|
||||
if (travelingClass) {
|
||||
if (b == CLASS_END) {
|
||||
travelingClass = false;
|
||||
travelingArray = false;
|
||||
}
|
||||
} else {
|
||||
if (isPrimaryType(b)) {
|
||||
parameterTypes.add(b);
|
||||
// should treat primary array as class (issue-48)
|
||||
parameterTypes.add(travelingArray ? TYPE_CLASS : b);
|
||||
travelingArray = false;
|
||||
} else if (b == TYPE_CLASS) {
|
||||
travelingClass = true;
|
||||
parameterTypes.add(b);
|
||||
} else if (b == TYPE_ARRAY) {
|
||||
travelingArray = true;
|
||||
} else if (b == PARAM_END) {
|
||||
break;
|
||||
}
|
||||
@ -134,6 +140,16 @@ public class ClassUtil {
|
||||
return parameterTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* extract parameter part of method desc
|
||||
* @param desc method description
|
||||
* @return parameter value
|
||||
*/
|
||||
public static String extractParameters(String desc) {
|
||||
int returnTypeEdge = desc.lastIndexOf(PARAM_END);
|
||||
return desc.substring(1, returnTypeEdge);
|
||||
}
|
||||
|
||||
/**
|
||||
* parse method desc, fetch return value types
|
||||
* @param desc method description
|
||||
|
@ -12,6 +12,7 @@ public class GlobalConfig {
|
||||
private static final String VERBOSE = "verbose";
|
||||
|
||||
private static String dumpPath = null;
|
||||
private static String pkgPrefix = null;
|
||||
|
||||
public static boolean setLogLevel(String level) {
|
||||
if (level.equals(MUTE)) {
|
||||
@ -35,4 +36,11 @@ public class GlobalConfig {
|
||||
return dumpPath;
|
||||
}
|
||||
|
||||
public static String getPkgPrefix() {
|
||||
return pkgPrefix;
|
||||
}
|
||||
|
||||
public static void setPkgPrefix(String pkgPrefix) {
|
||||
GlobalConfig.pkgPrefix = pkgPrefix;
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,12 @@ class ClassUtilTest {
|
||||
assertEquals(3, ClassUtil.getParameterTypes("(Ljava/lang/String;[I[Ljava/lang/String;)V").size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_extract_parameter() {
|
||||
assertEquals("", ClassUtil.extractParameters("()I"));
|
||||
assertEquals("Ljava/lang/String;", ClassUtil.extractParameters("(Ljava/lang/String;)I"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_get_return_type() {
|
||||
assertEquals("", ClassUtil.getReturnType("(Ljava/lang/String;)V"));
|
||||
|
@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-parent</artifactId>
|
||||
<version>0.4.9</version>
|
||||
<version>0.4.11</version>
|
||||
<relativePath>../testable-parent</relativePath>
|
||||
</parent>
|
||||
<artifactId>testable-all</artifactId>
|
||||
|
@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-parent</artifactId>
|
||||
<version>0.4.9</version>
|
||||
<version>0.4.11</version>
|
||||
<relativePath>../testable-parent</relativePath>
|
||||
</parent>
|
||||
<artifactId>testable-core</artifactId>
|
||||
|
@ -1,6 +1,6 @@
|
||||
package com.alibaba.testable.core.annotation;
|
||||
|
||||
import com.alibaba.testable.core.model.NullType;
|
||||
import javax.lang.model.type.NullType;
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
|
@ -14,6 +14,11 @@ public enum MockDiagnose {
|
||||
/**
|
||||
* Print diagnose logs
|
||||
*/
|
||||
ENABLE
|
||||
ENABLE,
|
||||
|
||||
/**
|
||||
* Print verbose log
|
||||
*/
|
||||
VERBOSE
|
||||
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
package com.alibaba.testable.core.model;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
public interface NullType {
|
||||
}
|
@ -25,6 +25,7 @@ public class InvokeRecordUtil {
|
||||
* Record mock method invoke event
|
||||
* @param args invocation parameters
|
||||
* @param isConstructor whether mocked method is constructor
|
||||
* @param isTargetClassInParameter whether use first parameter as target class
|
||||
*/
|
||||
public static void recordMockInvoke(Object[] args, boolean isConstructor, boolean isTargetClassInParameter) {
|
||||
StackTraceElement mockMethodTraceElement = Thread.currentThread().getStackTrace()[INDEX_OF_TEST_CLASS];
|
||||
|
@ -54,8 +54,8 @@ public class LogUtil {
|
||||
System.err.println(String.format("[ERROR] " + msg, args));
|
||||
}
|
||||
|
||||
public static void enableDiagnose(boolean enable) {
|
||||
currentLogLevel = enable ? LogLevel.LEVEL_DIAGNOSE : LogLevel.LEVEL_MUTE;
|
||||
public static void setLevel(LogLevel level) {
|
||||
currentLogLevel = level;
|
||||
}
|
||||
|
||||
public static void setDefaultLevel(LogLevel level) {
|
||||
|
@ -15,7 +15,8 @@ public class TypeUtil {
|
||||
public static Class<?>[] getClassesFromObjects(Object[] parameterObjects) {
|
||||
Class<?>[] cs = new Class[parameterObjects.length];
|
||||
for (int i = 0; i < cs.length; i++) {
|
||||
cs[i] = parameterObjects[i].getClass();
|
||||
Object pObj = parameterObjects[i];
|
||||
cs[i] = (pObj == null) ? null : pObj.getClass();
|
||||
}
|
||||
return cs;
|
||||
}
|
||||
@ -41,7 +42,7 @@ public class TypeUtil {
|
||||
/**
|
||||
* type equals
|
||||
* @param classesLeft class to be compared
|
||||
* @param classesRight class to compare
|
||||
* @param classesRight class to compare (item can be null)
|
||||
* @return whether all class equals
|
||||
*/
|
||||
private static boolean typeEquals(Class<?>[] classesLeft, Class<?>[] classesRight) {
|
||||
@ -49,6 +50,9 @@ public class TypeUtil {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < classesLeft.length; i++) {
|
||||
if (classesRight[i] == null) {
|
||||
return !classesLeft[i].isPrimitive();
|
||||
}
|
||||
if (!classesLeft[i].isAssignableFrom(classesRight[i]) && !fuzzyEqual(classesLeft[i], classesRight[i])) {
|
||||
return false;
|
||||
}
|
||||
|
@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-parent</artifactId>
|
||||
<version>0.4.9</version>
|
||||
<version>0.4.11</version>
|
||||
<relativePath>../testable-parent</relativePath>
|
||||
</parent>
|
||||
<artifactId>testable-maven-plugin</artifactId>
|
||||
|
@ -1,5 +1,6 @@
|
||||
package com.alibaba.testable;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import org.apache.maven.artifact.Artifact;
|
||||
import org.apache.maven.plugin.AbstractMojo;
|
||||
import org.apache.maven.plugin.MojoExecutionException;
|
||||
@ -45,6 +46,12 @@ public class TestableMojo extends AbstractMojo
|
||||
@Parameter
|
||||
private String dumpPath;
|
||||
|
||||
/**
|
||||
* Package prefixes of class need to be mocked (comma split)
|
||||
*/
|
||||
@Parameter
|
||||
private String pkgPrefix;
|
||||
|
||||
/**
|
||||
* Name of the Testable Agent artifact.
|
||||
*/
|
||||
@ -71,13 +78,17 @@ public class TestableMojo extends AbstractMojo
|
||||
getLog().error("failed to fetch project properties");
|
||||
return;
|
||||
}
|
||||
|
||||
String extraArgs = "";
|
||||
if (logLevel != null && !logLevel.isEmpty()) {
|
||||
if (!Strings.isNullOrEmpty(logLevel)) {
|
||||
extraArgs += "&logLevel=" + logLevel;
|
||||
}
|
||||
if (dumpPath != null && !dumpPath.isEmpty()) {
|
||||
if (!Strings.isNullOrEmpty(dumpPath)) {
|
||||
extraArgs += "&dumpPath=" + dumpPath;
|
||||
}
|
||||
if (!Strings.isNullOrEmpty(pkgPrefix)) {
|
||||
extraArgs += "&pkgPrefix=" + pkgPrefix;
|
||||
}
|
||||
final String oldArgs = projectProperties.getProperty(testArgsPropertyKey);
|
||||
String newArgs = (oldArgs == null) ? getAgentJarArgs().trim() : (oldArgs + getAgentJarArgs());
|
||||
if (!extraArgs.isEmpty()) {
|
||||
@ -91,6 +102,7 @@ public class TestableMojo extends AbstractMojo
|
||||
final Artifact testableAgentArtifact = pluginArtifactMap.get(AGENT_ARTIFACT_NAME);
|
||||
if (testableAgentArtifact == null) {
|
||||
getLog().error("failed to find testable agent jar");
|
||||
return "";
|
||||
}
|
||||
return " -javaagent:" + testableAgentArtifact.getFile().getAbsolutePath();
|
||||
}
|
||||
|
@ -5,7 +5,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-parent</artifactId>
|
||||
<version>0.4.9</version>
|
||||
<version>0.4.11</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.4.9</testable.version>
|
||||
<testable.version>0.4.11</testable.version>
|
||||
</properties>
|
||||
|
||||
<profiles>
|
||||
|
@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-parent</artifactId>
|
||||
<version>0.4.9</version>
|
||||
<version>0.4.11</version>
|
||||
<relativePath>../testable-parent</relativePath>
|
||||
</parent>
|
||||
<artifactId>testable-processor</artifactId>
|
||||
|
@ -7,11 +7,13 @@ import com.alibaba.testable.processor.translator.EnablePrivateAccessTranslator;
|
||||
import com.alibaba.testable.processor.util.JavacUtil;
|
||||
import com.alibaba.testable.processor.util.TestableLogger;
|
||||
import com.sun.tools.javac.api.JavacTrees;
|
||||
import com.sun.tools.javac.code.Attribute;
|
||||
import com.sun.tools.javac.code.Symbol;
|
||||
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
|
||||
import com.sun.tools.javac.tree.JCTree;
|
||||
import com.sun.tools.javac.tree.TreeMaker;
|
||||
import com.sun.tools.javac.util.Names;
|
||||
import com.sun.tools.javac.util.Pair;
|
||||
|
||||
import javax.annotation.processing.AbstractProcessor;
|
||||
import javax.annotation.processing.ProcessingEnvironment;
|
||||
@ -19,7 +21,6 @@ import javax.annotation.processing.RoundEnvironment;
|
||||
import javax.annotation.processing.SupportedAnnotationTypes;
|
||||
import javax.lang.model.SourceVersion;
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.element.Name;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import java.util.Set;
|
||||
|
||||
@ -29,6 +30,8 @@ import java.util.Set;
|
||||
@SupportedAnnotationTypes("com.alibaba.testable.processor.annotation.EnablePrivateAccess")
|
||||
public class EnablePrivateAccessProcessor extends AbstractProcessor {
|
||||
|
||||
private static final String SRC_CLASS = "srcClass";
|
||||
|
||||
private TestableContext cx;
|
||||
|
||||
@Override
|
||||
@ -53,8 +56,10 @@ public class EnablePrivateAccessProcessor extends AbstractProcessor {
|
||||
}
|
||||
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(EnablePrivateAccess.class);
|
||||
for (Element element : elements) {
|
||||
if (element.getKind().isClass() && isTestClass(element.getSimpleName())) {
|
||||
processClassElement((Symbol.ClassSymbol)element);
|
||||
if (element.getKind().isClass()) {
|
||||
Symbol.ClassSymbol testClass = (Symbol.ClassSymbol)element;
|
||||
String sourceClassName = getSourceClassName(testClass);
|
||||
processClassElement(testClass, sourceClassName);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@ -66,6 +71,19 @@ public class EnablePrivateAccessProcessor extends AbstractProcessor {
|
||||
return SourceVersion.values()[SourceVersion.values().length - 1];
|
||||
}
|
||||
|
||||
private String getSourceClassName(Symbol.ClassSymbol testClass) {
|
||||
for (Attribute.Compound annotation : testClass.getMetadata().getDeclarationAttributes()) {
|
||||
if (ConstPool.ENABLE_PRIVATE_ACCESS.equals(annotation.type.tsym.toString())) {
|
||||
for (Pair<Symbol.MethodSymbol, Attribute> p : annotation.values) {
|
||||
if (SRC_CLASS.equals(p.fst.name.toString())) {
|
||||
return p.snd.getValue().toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private JavacProcessingEnvironment getJavacProcessingEnvironment(ProcessingEnvironment processingEnv) {
|
||||
try {
|
||||
return JavacUtil.getJavacProcessingEnvironment(processingEnv);
|
||||
@ -74,14 +92,10 @@ public class EnablePrivateAccessProcessor extends AbstractProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isTestClass(Name name) {
|
||||
return name.toString().endsWith(ConstPool.TEST_POSTFIX);
|
||||
}
|
||||
|
||||
private void processClassElement(Symbol.ClassSymbol clazz) {
|
||||
private void processClassElement(Symbol.ClassSymbol testClass, String sourceClassName) {
|
||||
if (cx.trees != null) {
|
||||
JCTree tree = cx.trees.getTree(clazz);
|
||||
tree.accept(new EnablePrivateAccessTranslator(clazz, cx));
|
||||
JCTree tree = cx.trees.getTree(testClass);
|
||||
tree.accept(new EnablePrivateAccessTranslator(cx, testClass, sourceClassName));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
package com.alibaba.testable.processor.annotation;
|
||||
|
||||
import javax.lang.model.type.NullType;
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
@ -11,4 +12,11 @@ import java.lang.annotation.*;
|
||||
@Target(ElementType.TYPE)
|
||||
@Documented
|
||||
public @interface EnablePrivateAccess {
|
||||
|
||||
/**
|
||||
* explicit specify the source class to be tested
|
||||
* @return
|
||||
*/
|
||||
Class<?> srcClass() default NullType.class;
|
||||
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ package com.alibaba.testable.processor.constant;
|
||||
*/
|
||||
public final class ConstPool {
|
||||
|
||||
public static final String ENABLE_PRIVATE_ACCESS = "com.alibaba.testable.processor.annotation.EnablePrivateAccess";
|
||||
public static final String TESTABLE_PRIVATE_ACCESSOR = "com.alibaba.testable.core.accessor.PrivateAccessor";
|
||||
public static final String TEST_POSTFIX = "Test";
|
||||
|
||||
|
@ -6,7 +6,16 @@ package com.alibaba.testable.processor.exception;
|
||||
public class MemberNotExistException extends RuntimeException {
|
||||
|
||||
public MemberNotExistException(String type, String className, String target) {
|
||||
super(type + " \"" + target + "\" not exist in class \"" + className + "\"");
|
||||
super(String.format("%s \"%s\" not exist in class \"%s\"", type, target, className));
|
||||
}
|
||||
|
||||
public MemberNotExistException(String type, String className, String target, int count) {
|
||||
super(String.format("%s \"%s\" with %d %s not exist in class \"%s\"",
|
||||
type, target, count, parameters(count), className));
|
||||
}
|
||||
|
||||
private static String parameters(int count) {
|
||||
return count > 1 ? "parameters" : "parameter";
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
}
|
||||
|
@ -1,7 +1,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 +17,11 @@ 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;
|
||||
|
||||
import static com.alibaba.testable.processor.constant.ConstPool.TEST_POSTFIX;
|
||||
|
||||
/**
|
||||
* Travel AST
|
||||
@ -39,25 +44,25 @@ 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;
|
||||
|
||||
public EnablePrivateAccessTranslator(Symbol.ClassSymbol clazz, TestableContext cx) {
|
||||
String pkgName = ((Symbol.PackageSymbol)clazz.owner).fullname.toString();
|
||||
String testClassName = clazz.getSimpleName().toString();
|
||||
String sourceClass = testClassName.substring(0, testClassName.length() - ConstPool.TEST_POSTFIX.length());
|
||||
public EnablePrivateAccessTranslator(TestableContext cx, Symbol.ClassSymbol clazz, String srcClassName) {
|
||||
String sourceClassFullName;
|
||||
if (srcClassName == null) {
|
||||
String testClassFullName = clazz.fullname.toString();
|
||||
sourceClassFullName = testClassFullName.substring(0, testClassFullName.length() - TEST_POSTFIX.length());
|
||||
} else {
|
||||
sourceClassFullName = srcClassName;
|
||||
}
|
||||
String sourceClassShortName = sourceClassFullName.substring(sourceClassFullName.lastIndexOf('.') + 1);
|
||||
this.privateAccessStatementGenerator = new PrivateAccessStatementGenerator(cx);
|
||||
this.sourceClassName = cx.names.fromString(sourceClass);
|
||||
this.sourceClassName = cx.names.fromString(sourceClassShortName);
|
||||
try {
|
||||
String sourceClassFullName = pkgName + "." + sourceClass;
|
||||
Class<?> cls = getSourceClass(clazz, sourceClassFullName);
|
||||
if (cls == null) {
|
||||
cx.logger.error("Failed to load source class: " + sourceClassFullName);
|
||||
@ -67,8 +72,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, sourceClassShortName, memberRecord);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -147,13 +151,13 @@ public class EnablePrivateAccessTranslator extends BaseTranslator {
|
||||
// check is invoking a private method of source class
|
||||
if (expr instanceof JCMethodInvocation) {
|
||||
JCMethodInvocation invocation = (JCMethodInvocation)expr;
|
||||
privateAccessChecker.validate(invocation);
|
||||
MemberType memberType = checkInvokeType(invocation);
|
||||
if (memberType.equals(MemberType.PRIVATE_OR_FINAL)) {
|
||||
expr = privateAccessStatementGenerator.fetchInvokeStatement(invocation);
|
||||
} else if (memberType.equals(MemberType.STATIC_PRIVATE)) {
|
||||
expr = privateAccessStatementGenerator.fetchStaticInvokeStatement(invocation);
|
||||
}
|
||||
privateAccessChecker.validate((JCMethodInvocation)expr);
|
||||
}
|
||||
// check the casted expression
|
||||
if (expr instanceof JCTypeCast) {
|
||||
@ -171,18 +175,18 @@ public class EnablePrivateAccessTranslator extends BaseTranslator {
|
||||
cls = Class.forName(sourceClassFullName);
|
||||
} catch (ClassNotFoundException e) {
|
||||
if (System.getProperty(IDEA_PATHS_SELECTOR) != null) {
|
||||
// fit for intellij 2020.3+
|
||||
// fit for intellij build
|
||||
String sourceFileWrapperString = clazz.sourcefile.toString();
|
||||
String sourceFilePath = sourceFileWrapperString.substring(
|
||||
sourceFileWrapperString.lastIndexOf("[") + 1, sourceFileWrapperString.indexOf("]"));
|
||||
int indexOfSrc = sourceFilePath.lastIndexOf(File.separator + "src" + File.separator);
|
||||
String basePath = sourceFilePath.substring(0, indexOfSrc);
|
||||
String targetFolderPath = PathUtil.fitPathString(basePath + MAVEN_CLASS_FOLDER);
|
||||
try {
|
||||
String targetFolderPath = PathUtil.fitPathString(basePath + MAVEN_CLASS_FOLDER);
|
||||
cls = loadClass(targetFolderPath, sourceClassFullName);
|
||||
} catch (ClassNotFoundException e2) {
|
||||
targetFolderPath = PathUtil.fitPathString(basePath + GRADLE_CLASS_FOLDER);
|
||||
cls = loadClass(targetFolderPath, sourceClassFullName);
|
||||
String buildFolderPath = PathUtil.fitPathString(basePath + GRADLE_CLASS_FOLDER);
|
||||
cls = loadClass(buildFolderPath, sourceClassFullName);
|
||||
}
|
||||
} else {
|
||||
// fit for gradle build
|
||||
@ -202,38 +206,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 +270,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
|
||||
@ -14,6 +17,7 @@ import java.util.List;
|
||||
public class PrivateAccessChecker {
|
||||
|
||||
private static final String CLASS_NAME_PRIVATE_ACCESSOR = "PrivateAccessor";
|
||||
private static final String CLASS_NAME_PRIVATE_ACCESSOR_FULL = "com.alibaba.testable.core.accessor.PrivateAccessor";
|
||||
private static final List<String> FIELD_ACCESS_METHOD = Arrays.asList(new String[]
|
||||
{ "get", "set", "getStatic", "setStatic" }.clone());
|
||||
private static final List<String> FIELD_INVOKE_METHOD = Arrays.asList(new String[]
|
||||
@ -21,32 +25,72 @@ 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) {
|
||||
if (invocation.meth instanceof JCTree.JCFieldAccess && invocation.args.length() >= 2) {
|
||||
JCTree.JCFieldAccess fieldAccess = (JCTree.JCFieldAccess)invocation.meth;
|
||||
if (fieldAccess.selected instanceof JCTree.JCIdent && invocation.args.get(1) instanceof JCTree.JCLiteral &&
|
||||
((JCTree.JCIdent)fieldAccess.selected).name.toString().equals(CLASS_NAME_PRIVATE_ACCESSOR)) {
|
||||
if (invocation.args.get(1) instanceof JCTree.JCLiteral && isPrivateAccessor(fieldAccess)) {
|
||||
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.nonPrivateMethods, (String)target, parameterCount)) {
|
||||
cx.logger.warn("Method " + className + "::" + target + " is not private.");
|
||||
} else {
|
||||
throw new MemberNotExistException(TYPE_METHOD, className, (String)target, parameterCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isPrivateAccessor(JCTree.JCFieldAccess fieldAccess) {
|
||||
return isPrivateAccessorWithShortRef(fieldAccess) || isPrivateAccessorWithFullRef(fieldAccess);
|
||||
}
|
||||
|
||||
private boolean isPrivateAccessorWithShortRef(JCTree.JCFieldAccess fieldAccess) {
|
||||
return fieldAccess.selected instanceof JCTree.JCIdent &&
|
||||
((JCTree.JCIdent)fieldAccess.selected).name.toString().equals(CLASS_NAME_PRIVATE_ACCESSOR);
|
||||
}
|
||||
|
||||
private boolean isPrivateAccessorWithFullRef(JCTree.JCFieldAccess fieldAccess) {
|
||||
return fieldAccess.selected instanceof JCTree.JCFieldAccess &&
|
||||
fieldAccess.selected.toString().equals(CLASS_NAME_PRIVATE_ACCESSOR_FULL);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ public class PathUtil {
|
||||
private static final String PROPERTY_OS_NAME = "os.name";
|
||||
private static final String PATH_SPLIT_UNIX = "/";
|
||||
private static final String PATH_SPLIT_WIN = "\\\\";
|
||||
private static final String PROTOCOL_FILE = "file:/";
|
||||
private static final String PROTOCOL_FILE = "file:";
|
||||
|
||||
/**
|
||||
* Fit path according to operation system type
|
||||
|
@ -17,10 +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) {
|
||||
// Message level WARNING won't show, use MANDATORY_WARNING instead
|
||||
messager.printMessage(Diagnostic.Kind.MANDATORY_WARNING, msg);
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user