Compare commits

..

20 Commits

Author SHA1 Message Date
金戟
d57dedb6fc release v0.4.12 2021-02-07 10:41:07 +08:00
金戟
d860684e91 add switch to disable private access target existence check 2021-02-06 21:56:36 +08:00
金戟
7bbdf82a41 support specify package prefix of class to be mocked 2021-02-06 11:31:51 +08:00
金戟
126c7257d8 fix out of index exception when mocking native method 2021-02-06 10:44:51 +08:00
金戟
adfba4bac0 support verbose log for self diagnosing 2021-02-05 22:30:04 +08:00
金戟
2c18ea12ed use space instead of tab 2021-02-04 17:59:26 +08:00
金戟
d4bce13918 remove note about 0.4.5 version 2021-02-03 18:03:07 +08:00
金戟
b23b8b98b3 add self troubleshooting information 2021-02-02 14:24:12 +08:00
金戟
f228ed5002 release v0.4.11 2021-02-02 07:49:20 +08:00
金戟
2303ac188e add roadmap 2021-02-01 23:49:16 +08:00
金戟
290d0db400 support specify source class for private accessor 2021-02-01 08:08:33 +08:00
金戟
77b955a5ce use system null type 2021-01-31 21:46:46 +08:00
金戟
ab8a0b32e5 fix compile time check for private member access 2021-01-31 20:18:53 +08:00
金戟
39c788cd02 check method parameter count 2021-01-31 15:10:34 +08:00
金戟
7029579534 fix intellij gradle build issue with incorrect file protocol syntax 2021-01-31 12:39:58 +08:00
金戟
ba951ce744 should always load array variable via aload 2021-01-30 23:53:22 +08:00
Fan Lin
2bbe2eba93 be more precise 2021-01-29 11:23:58 +08:00
Fan Lin
4c96f4cc0e Update use-mock.md
use static declaration for complicate mock method
2021-01-29 11:17:00 +08:00
Fan Lin
3a2e3032df Merge pull request #53 from npvip/patch-1
Update use-mock.md
2021-01-28 19:24:23 +08:00
npvip
530f3295bd Update use-mock.md
MOCK_CONTEXT.put不是set
2021-01-28 17:53:50 +08:00
49 changed files with 537 additions and 271 deletions

View File

@@ -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` 功能稳定,一个崭新的开始
## 目录结构

View File

@@ -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

View File

@@ -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) {

View File

@@ -12,7 +12,7 @@
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<junit.version>5.6.2</junit.version>
<testable.version>0.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>-->

View File

@@ -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;
/**

View File

@@ -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()
}

View File

@@ -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>

View File

@@ -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.

View File

@@ -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`

View File

@@ -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')
}
```

View File

@@ -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

View File

@@ -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.

View File

@@ -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`.

View File

@@ -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方法执行报错。

View File

@@ -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出错的BUGissue-44
- 修复调用私有方法时参数值不能为null的问题issue-27

View File

@@ -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')
}
```

View File

@@ -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方法定义是否未与目标方法匹配

View File

@@ -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`工具类访问被测类私有成员的功能不会受编译器差异影响。

View File

@@ -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();
}

View File

@@ -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>

View File

@@ -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);

View File

@@ -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);

View File

@@ -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;

View File

@@ -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

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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"));

View File

@@ -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>

View File

@@ -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>

View File

@@ -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.*;
/**

View File

@@ -14,6 +14,11 @@ public enum MockDiagnose {
/**
* Print diagnose logs
*/
ENABLE
ENABLE,
/**
* Print verbose log
*/
VERBOSE
}

View File

@@ -1,7 +0,0 @@
package com.alibaba.testable.core.model;
/**
* @author flin
*/
public interface NullType {
}

View File

@@ -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];

View File

@@ -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) {

View File

@@ -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>

View File

@@ -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();
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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));
}
}

View File

@@ -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;
}

View File

@@ -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";

View File

@@ -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";
}
}

View File

@@ -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>>();
}

View File

@@ -13,8 +13,8 @@ public enum MemberType {
STATIC_PRIVATE,
/**
* None private member
* Non-private member
*/
NONE_PRIVATE
NON_PRIVATE
}

View File

@@ -0,0 +1,12 @@
package com.alibaba.testable.processor.model;
/**
* @author flin
*/
public class Parameters {
public String sourceClassName;
public Boolean verifyTargetExistence;
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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);
}