mirror of
https://github.com/alibaba/testable-mock.git
synced 2026-08-23 03:33:30 +08:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d57dedb6fc | ||
|
|
d860684e91 | ||
|
|
7bbdf82a41 | ||
|
|
126c7257d8 | ||
|
|
adfba4bac0 | ||
|
|
2c18ea12ed | ||
|
|
d4bce13918 | ||
|
|
b23b8b98b3 | ||
|
|
f228ed5002 | ||
|
|
2303ac188e | ||
|
|
290d0db400 | ||
|
|
77b955a5ce | ||
|
|
ab8a0b32e5 | ||
|
|
39c788cd02 | ||
|
|
7029579534 | ||
|
|
ba951ce744 | ||
|
|
2bbe2eba93 | ||
|
|
4c96f4cc0e | ||
|
|
3a2e3032df | ||
|
|
530f3295bd |
11
README.md
11
README.md
@@ -10,18 +10,19 @@
|
||||
阅读[这里](https://mp.weixin.qq.com/s/KyU6Eu7mDkZU8FspfSqfMw)了解更多故事。
|
||||
|
||||
> 特别说明
|
||||
> 1. 如遇到"Attempt to access none-static member in mock method"错误,参见[常见问题](https://alibaba.github.io/testable-mock/#/zh-cn/doc/frequently-asked-questions)第8条
|
||||
> 1. 如遇到"Attempt to access non-static member in mock method"错误,参见[常见问题](https://alibaba.github.io/testable-mock/#/zh-cn/doc/frequently-asked-questions)第8条
|
||||
> 2. 如果有遇到其他任何使用问题和建议,请直接在[Issue](https://github.com/alibaba/testable-mock/issues)中提出,也可通过[Pull Request](https://github.com/alibaba/testable-mock/pulls)提交您的代码,我们将在24小时内回复并处理
|
||||
|
||||
-----
|
||||
|
||||
## 版本计划
|
||||
|
||||
`TestableMock`正在持续迭代演进,以下版本计划可能在开发过程中发生调整,请以最新内容为准
|
||||
`TestableMock`正在持续迭代演进,以下版本计划可能在开发过程中发生调整
|
||||
|
||||
- `0.4.x` 当前版本,进行中的工作内容参考[Issue](https://github.com/alibaba/testable-mock/issues)清单
|
||||
- `0.5` 实现以"Mock方法集"为单元的Mock方法复用机制,让测试类之间可以方便复用相同的Mock方法
|
||||
- `0.5.?` 实现第四项单元测试增强能力"[快速入参构造器]()"
|
||||
- `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('org.junit.jupiter:junit-jupiter:5.6.2')
|
||||
testImplementation('com.alibaba.testable:testable-all:0.4.10')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.4.10')
|
||||
testImplementation('com.alibaba.testable:testable-all:0.4.12')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.4.12')
|
||||
}
|
||||
|
||||
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>5.6.2</junit.version>
|
||||
<testable.version>0.4.10</testable.version>
|
||||
<testable.version>0.4.12</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>-->
|
||||
|
||||
@@ -8,8 +8,8 @@ import org.junit.jupiter.api.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.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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.10")
|
||||
testAnnotationProcessor("com.alibaba.testable:testable-processor:0.4.10")
|
||||
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.12")
|
||||
testAnnotationProcessor("com.alibaba.testable:testable-processor:0.4.12")
|
||||
}
|
||||
|
||||
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.10</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.12</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,5 +1,17 @@
|
||||
# Release Note
|
||||
|
||||
## 0.4.12
|
||||
- support verbose diagnose log for better self-troubleshooting
|
||||
- support disable private access target existence check
|
||||
- support specify mock scanning packages
|
||||
- fix an ArrayIndexOutOfBoundsException issue when transforming native method
|
||||
|
||||
## 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`
|
||||
|
||||
@@ -16,7 +16,7 @@ It is recommended to add a `property` field that identifies the TestableMock ver
|
||||
|
||||
```xml
|
||||
<properties>
|
||||
<testable.version>0.4.10</testable.version>
|
||||
<testable.version>0.4.12</testable.version>
|
||||
</properties>
|
||||
```
|
||||
|
||||
@@ -63,8 +63,8 @@ Add dependence of `TestableMock` in `build.gradle` file:
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
testImplementation('com.alibaba.testable:testable-all:0.4.10')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.4.10')
|
||||
testImplementation('com.alibaba.testable:testable-all:0.4.12')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.4.12')
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -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.
|
||||
>
|
||||
> 
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -162,6 +162,6 @@ For details, please refer to the [Check Mock Call](en-us/doc/matcher.md) documen
|
||||
|
||||
> **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.
|
||||
> - 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 be removed in `0.5` versions of `TestableMock`.
|
||||
> These constraints will change in `0.5` versions of `TestableMock`.
|
||||
|
||||
@@ -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,5 +1,17 @@
|
||||
# Release Note
|
||||
|
||||
## 0.4.12
|
||||
- 支持`VERBOSE`级别的Mocking过程日志,增强错误自助排查能力
|
||||
- 支持使用`verifyTargetOnCompile`参数禁用编译期私有目标校验功能
|
||||
- 支持通过agent参数指定Mock目标的扫描包范围
|
||||
- 修复一处`ArrayIndexOutOfBoundsException`异常 (issue-52)
|
||||
|
||||
## 0.4.11
|
||||
- 支持测试类访问与自身包路径不同的被测类的私有成员
|
||||
- 增加`PrivateAccessor`访问的私有方法参数数目检查,提高抗代码重构能力
|
||||
- 修复被Mock方法包含数组参数可能导致出错的BUG (issue-48)
|
||||
- 修复一处会导致在IntelliJ中构建找不到私有成员的问题
|
||||
|
||||
## 0.4.10
|
||||
- 修复在Lambda函数中使用Mock出错的BUG(issue-44)
|
||||
- 修复调用私有方法时参数值不能为null的问题(issue-27)
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
```xml
|
||||
<properties>
|
||||
<testable.version>0.4.10</testable.version>
|
||||
<testable.version>0.4.12</testable.version>
|
||||
</properties>
|
||||
```
|
||||
|
||||
@@ -63,8 +63,8 @@
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
testImplementation('com.alibaba.testable:testable-all:0.4.10')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.4.10')
|
||||
testImplementation('com.alibaba.testable:testable-all:0.4.12')
|
||||
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.4.12')
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -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"选项解决。
|
||||
>
|
||||
> 
|
||||
|
||||
## 使用Eclipse IDE
|
||||
|
||||
由于`Eclipse`内置的自动编译功能基于三方编译器实现,与标准`javac`编译过程不兼容,会导致在IDE中运行测试用例时`@EnablePrivateAccess`注解无效。不过,通过`PrivateAccessor`工具类访问被测类私有成员的功能不会受编译器差异影响。
|
||||
|
||||
@@ -120,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();
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-parent</artifactId>
|
||||
<version>0.4.10</version>
|
||||
<version>0.4.12</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++;
|
||||
@@ -194,7 +203,7 @@ public class SourceClassHandler extends BaseClassHandler {
|
||||
|
||||
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.10</version>
|
||||
<version>0.4.12</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.10</version>
|
||||
<version>0.4.12</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) {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-parent</artifactId>
|
||||
<version>0.4.10</version>
|
||||
<version>0.4.12</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.10</version>
|
||||
<version>0.4.12</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.10</testable.version>
|
||||
<testable.version>0.4.12</testable.version>
|
||||
</properties>
|
||||
|
||||
<profiles>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>com.alibaba.testable</groupId>
|
||||
<artifactId>testable-parent</artifactId>
|
||||
<version>0.4.10</version>
|
||||
<version>0.4.12</version>
|
||||
<relativePath>../testable-parent</relativePath>
|
||||
</parent>
|
||||
<artifactId>testable-processor</artifactId>
|
||||
|
||||
@@ -2,16 +2,19 @@ package com.alibaba.testable.processor;
|
||||
|
||||
import com.alibaba.testable.processor.annotation.EnablePrivateAccess;
|
||||
import com.alibaba.testable.processor.constant.ConstPool;
|
||||
import com.alibaba.testable.processor.model.Parameters;
|
||||
import com.alibaba.testable.processor.model.TestableContext;
|
||||
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 +22,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 +31,9 @@ import java.util.Set;
|
||||
@SupportedAnnotationTypes("com.alibaba.testable.processor.annotation.EnablePrivateAccess")
|
||||
public class EnablePrivateAccessProcessor extends AbstractProcessor {
|
||||
|
||||
private static final String SRC_CLASS = "srcClass";
|
||||
private static final String VERIFY_ON_COMPILE = "verifyTargetOnCompile";
|
||||
|
||||
private TestableContext cx;
|
||||
|
||||
@Override
|
||||
@@ -53,8 +58,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;
|
||||
Parameters parameters = getAnnotationParameters(testClass);
|
||||
processClassElement(testClass, parameters);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -66,6 +73,22 @@ public class EnablePrivateAccessProcessor extends AbstractProcessor {
|
||||
return SourceVersion.values()[SourceVersion.values().length - 1];
|
||||
}
|
||||
|
||||
private Parameters getAnnotationParameters(Symbol.ClassSymbol testClass) {
|
||||
Parameters parameters = new Parameters();
|
||||
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())) {
|
||||
parameters.sourceClassName = p.snd.getValue().toString();
|
||||
} else if (VERIFY_ON_COMPILE.equals(p.fst.name.toString())) {
|
||||
parameters.verifyTargetExistence = (Boolean)p.snd.getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return parameters;
|
||||
}
|
||||
|
||||
private JavacProcessingEnvironment getJavacProcessingEnvironment(ProcessingEnvironment processingEnv) {
|
||||
try {
|
||||
return JavacUtil.getJavacProcessingEnvironment(processingEnv);
|
||||
@@ -74,14 +97,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, Parameters parameters) {
|
||||
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, parameters));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.alibaba.testable.processor.annotation;
|
||||
|
||||
import javax.lang.model.type.NullType;
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
@@ -11,4 +12,17 @@ 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;
|
||||
|
||||
/**
|
||||
* whether enable compile-time existence verification for the private members accessed
|
||||
* @return
|
||||
*/
|
||||
boolean verifyTargetOnCompile() default true;
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.alibaba.testable.processor.model;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
public class Parameters {
|
||||
|
||||
public String sourceClassName;
|
||||
|
||||
public Boolean verifyTargetExistence;
|
||||
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
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.Parameters;
|
||||
import com.alibaba.testable.processor.model.TestableContext;
|
||||
import com.alibaba.testable.processor.util.PathUtil;
|
||||
import com.sun.tools.javac.code.Symbol;
|
||||
@@ -17,6 +18,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 +45,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, Parameters p) {
|
||||
String sourceClassFullName;
|
||||
if (p.sourceClassName == null) {
|
||||
String testClassFullName = clazz.fullname.toString();
|
||||
sourceClassFullName = testClassFullName.substring(0, testClassFullName.length() - TEST_POSTFIX.length());
|
||||
} else {
|
||||
sourceClassFullName = p.sourceClassName;
|
||||
}
|
||||
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 +73,8 @@ public class EnablePrivateAccessTranslator extends BaseTranslator {
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
this.privateAccessChecker = new PrivateAccessChecker(sourceClassName.toString(),
|
||||
privateOrFinalFields.toList(), privateMethods.toList());
|
||||
this.privateAccessChecker = (p.verifyTargetExistence == null || p.verifyTargetExistence) ?
|
||||
new PrivateAccessChecker(cx, sourceClassShortName, memberRecord) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -147,13 +153,15 @@ 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);
|
||||
}
|
||||
if (privateAccessChecker != null) {
|
||||
privateAccessChecker.validate((JCMethodInvocation)expr);
|
||||
}
|
||||
}
|
||||
// check the casted expression
|
||||
if (expr instanceof JCTypeCast) {
|
||||
@@ -171,18 +179,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 +210,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 +274,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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user