Compare commits

..

50 Commits
0.7 ... v0.7.8

Author SHA1 Message Date
金戟
46e4499035 release v0.7.8 2022-11-13 20:13:40 +08:00
金戟
f508f7aba9 refactor: always use the collection method in core module 2022-11-13 18:15:42 +08:00
金戟
6e029f61d8 feat: add util method for quick create set 2022-11-01 16:35:47 +08:00
金戟
b654a3cfaa feat: support generate method with template types 2022-10-09 20:31:13 +08:00
金戟
038ae8dba0 feat: support construct simple interface and abstract class 2022-10-08 20:20:18 +08:00
金戟
11a5565cee feat: use same package path of parent to avoid access error 2022-10-08 11:30:43 +08:00
金戟
ba77a0138c fix: let the kotlin testcases pass with 'mvn test' locally 2022-09-29 17:22:35 +08:00
金戟
9788dd9ecc feat: add common used collection creation function to library 2022-08-09 14:25:52 +08:00
金戟
55a6e141fc doc: update faq to fit current mock inovke annotation parameter 2022-06-11 22:48:26 +08:00
金戟
81a2817b6c fix: fit for jvm 1.6 2022-06-04 23:23:59 +08:00
金戟
ef6099a63d feat: generate empty sub class for interface 2022-06-04 23:11:12 +08:00
金戟
3c11676408 feat: add original InMemoryJavaCompiler 2022-06-03 13:29:25 +08:00
金戟
b2a87bffbe refactor: use constant dollar symbol 2022-05-29 14:08:35 +08:00
金戟
1a2083416e doc: describe the MockContext fields 2022-05-29 11:35:23 +08:00
金戟
c1e6d00953 doc: one more thing to mention 2022-05-28 17:39:35 +08:00
金戟
39b742b290 release v0.7.7 2022-05-28 13:34:27 +08:00
金戟
0a5666c0be fix: only generate named lambda method for mocked ones 2022-05-28 13:26:37 +08:00
金戟
006e742c28 fix: upgrade asm to support java 17 2022-05-19 19:00:59 +08:00
金戟
237b9ebb15 doc: fix link to invoke matcher page 2022-05-19 18:53:13 +08:00
金戟
7aedcf63b5 release v0.7.6 2022-04-22 17:14:33 +08:00
金戟
0f64d52718 feat: should skip mockito generated class 2022-04-22 17:09:56 +08:00
金戟
4ed1323a62 feat: package mapping map shuold be ordered 2022-04-22 16:38:26 +08:00
Fan Lin
ae1ba8b4ff Merge pull request #274 from zcbbpo/ISSUES-272
Fixed java9+ String makeConcatWithConstants not work for method refer…
2022-04-01 17:01:30 +08:00
zcbbpo
00f25d3622 Fixed java9+ String makeConcatWithConstants not work for method reference 2022-04-01 11:14:49 +08:00
金戟
2e58ae8360 release v0.7.5 2022-03-08 12:38:35 +08:00
金戟
ff54c57608 fix: should handle array type method properly 2022-03-08 12:35:18 +08:00
金戟
351b3399e6 feat: elastic plugin class should contain only one constructor 2022-02-20 22:22:00 +08:00
金戟
df9e2395cc doc: describe no unique public constructor error 2022-02-20 22:09:45 +08:00
金戟
b9064d3d47 doc: intellij plugin usage english edition 2022-02-05 22:52:48 +08:00
金戟
c967456569 doc: add intellij plugin usage 2022-02-05 21:28:28 +08:00
金戟
692b4435aa release v0.7.4 2022-02-02 21:02:28 +08:00
金戟
6379f16266 fix #261, java.net.URL is loaded before OmniClassHandler 2022-02-02 21:02:28 +08:00
Fan Lin
63a6a9fdca Merge pull request #259 from oldratlee/upgrade-ttl
upgrade TTL to 2.12.4, fix ClassCircularityError
2022-01-12 10:05:23 +08:00
Jerry Lee
7e24503611 upgrade TTL to 2.12.4, fix ClassCircularityError
more discussion see
https://github.com/alibaba/transmittable-thread-local/issues/278
2022-01-12 00:43:02 +08:00
金戟
3120eba3af update release note 2022-01-10 20:55:39 +08:00
金戟
02f15177fc release v0.7.3 2022-01-09 22:45:14 +08:00
Fan Lin
a25f14c0ec Merge pull request #257 from zcbbpo/FIXED-INVOKEINTERFACE_1
Fixed invokeinterface issuse
2022-01-07 14:55:25 +08:00
jimcao
635cbf62fb completed 2022-01-06 15:13:32 +08:00
jimcao
17b31b39dd refine code 2022-01-06 13:13:09 +08:00
jimcao
0d43d93a29 temp update 2022-01-06 11:52:18 +08:00
jimcao
66db3cff7d temp 2022-01-05 09:49:49 +08:00
jimcao
3343a26de8 Add interface static test case 2022-01-05 09:12:38 +08:00
jimcao
de2e520d95 Fixed interface default method 2022-01-04 18:02:49 +08:00
金戟
59bcbfdf37 release v0.7.2 2021-12-17 14:39:20 +08:00
Fan Lin
cdad0d2446 Merge pull request #251 from zcbbpo/Fxied-static-instance-reference-issue
fixed static instance reference issue
2021-12-17 13:40:45 +08:00
jimcao
ba5102ffd9 fixed static instance reference issue 2021-12-16 17:26:38 +08:00
金戟
db4e02a201 release v0.7.1 2021-12-11 22:59:49 +08:00
金戟
fda7ee23c7 fix #248, should not look up package mapping if found mock class 2021-12-11 14:40:27 +08:00
金戟
140e54983b release v0.7.0 2021-11-07 11:07:39 +08:00
金戟
548d7ba21f add upgrade guide for 0.7.0 2021-11-07 11:06:37 +08:00
73 changed files with 1936 additions and 330 deletions

View File

@@ -9,7 +9,7 @@
阅读[这里](https://mp.weixin.qq.com/s/KyU6Eu7mDkZU8FspfSqfMw)了解更多故事。
<font size="5">**0.6版本已发布**</font>,从`0.5.x`升级到`0.6.x`版本请参考[版本升级指南](https://alibaba.github.io/testable-mock/#/zh-cn/doc/upgrade-guide)
<font size="5">**0.7版本已发布**</font>,从`0.6.x`升级到`0.7.x`版本请参考[版本升级指南](https://alibaba.github.io/testable-mock/#/zh-cn/doc/upgrade-guide)
-----
@@ -31,7 +31,9 @@
|-- tool ➜ 项目开发过程中的工具脚本
|-- demo
| |-- java-demo ➜ Java语言的示例代码
| `-- kotlin-demo ➜ Kotlin语言的示例代码
| |-- kotlin-demo ➜ Kotlin语言的示例代码
| |-- android-demo ➜ Android项目的示例代码
| `-- spock-demo ➜ Spock测试框架的示例代码
`-- docs ➜ 项目使用文档
```

View File

@@ -9,11 +9,7 @@ Usage Document: https://alibaba.github.io/testable-mock/#/en-us/
## Roadmap
`TestableMock` is still under heavy development, the following version plans may be adjusted during the iteration
- `v0.6` it's the current version, refer to the [issue](https://github.com/alibaba/testable-mock/issues) list for the work in progress
- `v0.7` better anti-refactoring support of mocking, allow quick mock all method in specified class
- `v1.0` all functions are stable, a brand-new start
`TestableMock` is still under heavy development, if you get sucked somehow, just go ahead raise an [issue](https://github.com/alibaba/testable-mock/issues) for it.
## Directory Structure
@@ -27,7 +23,9 @@ Usage Document: https://alibaba.github.io/testable-mock/#/en-us/
|-- tool ➜ Scripts for project maintain
|-- demo
| |-- java-demo ➜ Java code example
| `-- kotlin-demo ➜ Kotlin code example
| |-- kotlin-demo ➜ Kotlin code example
| |-- android-demo ➜ Android app code example
| `-- spock-demo ➜ Spock framwork code example
`-- docs ➜ Source code of usage document
```

View File

@@ -49,7 +49,7 @@ dependencies {
testImplementation 'androidx.test:runner:1.4.0-alpha05'
testImplementation 'junit:junit:4.+'
testImplementation 'org.robolectric:robolectric:4.5.1'
testImplementation 'com.alibaba.testable:testable-all:0.6.10'
testImplementation 'com.alibaba.testable:testable-all:0.7.8'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}

View File

@@ -13,8 +13,8 @@ repositories {
dependencies {
testImplementation('org.junit.jupiter:junit-jupiter:5.6.2')
testImplementation('com.alibaba.testable:testable-all:0.6.10')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.6.10')
testImplementation('com.alibaba.testable:testable-all:0.7.8')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.7.8')
}
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.6.10</testable.version>
<testable.version>0.7.8</testable.version>
</properties>
<dependencies>
@@ -35,7 +35,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
<version>3.0.0-M6</version>
<configuration>
<argLine>@{argLine} -javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar</argLine>
</configuration>

View File

@@ -15,6 +15,14 @@ public class Child {
private InnerChild subChild;
/**
* 这是一个私有内部类
* An private inner class
*/
private class InnerChild {
private String secret;
}
/* ---------------------------------------
以下Getters/Setters方法仅为便于功能演示而添加
并非OmniConstructor或OmniAccessor功能所需
@@ -36,12 +44,4 @@ public class Child {
this.subChild = subChild;
}
/**
* 这是一个私有内部类
* An private inner class
*/
private class InnerChild {
private String secret;
}
}

View File

@@ -0,0 +1,42 @@
package com.alibaba.demo.lambda;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
/**
* @author jim
*/
public interface BaseDemo {
default void blackHole(Object... ignore) {}
default void consumesRun(Runnable r) {
r.run();
}
default void consumesFunction0(Runnable f) {
f.run();
}
default <T> void consumesFunction1(Consumer<T> f) {
f.accept(null);
}
default <T, R> void consumesFunction2(Function<T, R> f) {
f.apply(null);
}
default <T1, T2, R> void consumesFunction3(BiFunction<T1, T2, R> f) {
f.apply(null, null);
}
default <T> void consumesSupplier(Supplier<T> supplier) {
supplier.get();
}
default <T, R> void consumesTwoFunction2(Function<T, R> f1, Function<T, R> f2) {
f1.apply(null);
f2.apply(null);
}
}

View File

@@ -0,0 +1,121 @@
package com.alibaba.demo.lambda;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* @author jim
*/
@SuppressWarnings("SimplifyStreamApiCallChains")
public class CollectionListCodeDemo implements BaseDemo {
public void list() {
List<User> userList = getUsers();
double d = userList.stream()
.map(v -> v)
.map(v -> Function.<User>identity().apply(v))
.map(User::getId)
.mapToDouble(Double::valueOf)
.reduce(0, Double::sum);
//.reduce(0, (v1, v2) -> v1 + v2);
blackHole(d);
blackHole(userList.stream()
.map(User::getId)
.mapToLong(Long::valueOf)
.reduce(0, Long::sum));
blackHole(userList.stream()
.map(User::getId)
.mapToInt(Long::intValue)
.reduce(0, Integer::sum));
Map<Long, User> map = userList.stream()
.filter(User::isActive)
.collect(Collectors.toMap(User::getId, Function.identity(), (v1, v2) -> v1));
blackHole(map);
List<User> l1 = userList.stream()
.collect(Collectors.toList())
.stream()
.collect(Collectors.toSet())
.stream()
.collect(Collectors.groupingBy(User::getName))
.entrySet()
.stream()
.filter(v -> v.getValue().stream().anyMatch(User::isActive))
.map(Map.Entry::getValue)
.flatMap(Collection::parallelStream)
.collect(Collectors.groupingBy(User::getId))
.values()
.stream()
.flatMap(List::stream)
.collect(Collectors.toList());
blackHole(l1);
userList.stream()
.map(User::isActive)
.reduce(Boolean::logicalAnd)
.ifPresent(this::blackHole);
}
private List<User> getUsers() {
List<User> userList = new ArrayList<>();
for (int i=0; i<10; i++) {
userList.add(getUser(i));
}
return userList;
}
private User getUser(int index) {
User u1 = new User();
u1.id = (long)index;
u1.name = "u" + index;
u1.age = index;
u1.active = true;
return u1;
}
private static class User {
private Long id;
private String name;
private int age;
private boolean active;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
}
}

View File

@@ -0,0 +1,86 @@
package com.alibaba.demo.lambda;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
/**
* @author jim
*/
public class InvokeInterfaceDemo implements BaseDemo {
public void collectionInterfaceDefaultOrStatic() {
blackHole(collectionStreamTest());
blackHole(arraysStreamTest());
}
public void interfaceDefault() {
ILambda l = new LambdaFoo();
consumesRun(l::run);
consumesFunction1(l::function1);
}
public void interfaceStatic() {
consumesRun(ILambda::staticRun);
consumesFunction1(ILambda::staticFunction1);
}
public Object collectionStreamTest() {
List<List<String>> testList = new ArrayList<>();
List<String> fooList = new ArrayList<>();
fooList.add("123");
fooList.add("456");
testList.add(fooList);
return testList.stream()
//.flatMap(v -> v.stream())
.flatMap(Collection::stream)
.map(Double::valueOf)
.map(BigDecimal::new)
.reduce(BigDecimal::add);
}
public Object arraysStreamTest() {
List<String[]> zz = new ArrayList<>();
zz.add(new String[]{"1", "2", "3"});
return zz.stream()
.flatMap(Arrays::stream)
.map(Double::valueOf)
.map(BigDecimal::new)
.reduce(BigDecimal::add);
}
public void objectStaticMethodReference() {
List<Boolean> f = new ArrayList<>();
f.add(false);
f.add(false);
f.add(false);
blackHole(f.stream()
.reduce(Boolean::logicalAnd)
.get()
);
}
public interface ILambda {
default void run() {
}
default void function1(String s) {
}
static void staticRun() {
}
static void staticFunction1(String s) {
}
}
public static class LambdaFoo implements ILambda {
}
}

View File

@@ -9,7 +9,7 @@ import java.util.function.Supplier;
* @author jimca
*/
@SuppressWarnings({"WrapperTypeMayBePrimitive", "ResultOfMethodCallIgnored", "MismatchedReadAndWriteOfArray", "unused"})
public class ExternalLambdaDemo {
public class InvokeVirtualDemo {
public void string1() {
String s = "";
@@ -197,8 +197,8 @@ public class ExternalLambdaDemo {
}
public void function3() {
ExternalLambdaDemo externalLambdaDemo = new ExternalLambdaDemo();
consumesFunction3(externalLambdaDemo::f3);
InvokeVirtualDemo invokeVirtualDemo = new InvokeVirtualDemo();
consumesFunction3(invokeVirtualDemo::f3);
}
public Boolean f3(String s1, Long l) {

View File

@@ -0,0 +1,33 @@
package com.alibaba.demo.lambda;
/**
* @author jim
*/
public class StaticInstanceReference implements BaseDemo {
private static final StaticClassA STATIC_CLASS_A = new StaticClassA();
public void staticMethodReference() {
consumesRun(STATIC_CLASS_A::doIt);
consumesFunction1(STATIC_CLASS_A::function1);
consumesFunction2(STATIC_CLASS_A::function2);
consumesFunction3(STATIC_CLASS_A::function3);
}
public static class StaticClassA {
public void doIt() {
}
public void function1(String s) {
}
public Integer function2(String s) {
return 1;
}
public Integer function3(String s, Double d) {
return 1;
}
}
}

View File

@@ -18,6 +18,12 @@ class DemoInheritTest {
private DemoInherit demoInherit = new DemoInherit();
public static class Mock {
/**
* 当需要引用原对象时,不加`targetClass`参数将原类型作为Mock方法的首个参数建议将参数命名为`self`
* When method body need to refer the invoked object, skip the use `targetClass` parameter,
* and add a parameter `self` with target class type before other paramter of the target method
*/
@MockInvoke(targetMethod = "put")
private void put_into_box(Box self, String something) {
self.put("put_" + something + "_into_box");
@@ -28,28 +34,32 @@ class DemoInheritTest {
self.put("put_" + something + "_into_blackbox");
}
@MockInvoke(targetMethod = "get")
private String get_from_box(Box self) {
/**
* 当不需要引用原对象时,将目标类型作为`@MockInvoke`注解的`targetClass`参数值即可
* When the invoked object is not used in the mock method, set `targetClass` value to the type who owns the mocked method
*/
@MockInvoke(targetClass = Box.class, targetMethod = "get")
private String get_from_box() {
return "get_from_box";
}
@MockInvoke(targetMethod = "get")
private String get_from_blackbox(BlackBox self) {
@MockInvoke(targetClass = BlackBox.class, targetMethod = "get")
private String get_from_blackbox() {
return "get_from_blackbox";
}
@MockInvoke(targetMethod = "getColor")
private String get_color_from_color(Color self) {
@MockInvoke(targetClass = Color.class, targetMethod = "getColor")
private String get_color_from_color() {
return "color_from_color";
}
@MockInvoke(targetMethod = "getColor")
private String get_color_from_blackbox(BlackBox self) {
@MockInvoke(targetClass = BlackBox.class, targetMethod = "getColor")
private String get_color_from_blackbox() {
return "color_from_blackbox";
}
@MockInvoke(targetMethod = "getColorIndex")
private String get_colorIdx_from_color(Color self) {
@MockInvoke(targetClass = Color.class, targetMethod = "getColorIndex")
private String get_colorIdx_from_color() {
return "colorIdx_from_color";
}
}

View File

@@ -0,0 +1,25 @@
package com.alibaba.demo.lambda;
import com.alibaba.testable.core.annotation.MockInvoke;
import org.junit.jupiter.api.Test;
/**
* @author jim
*/
public class CollectionListCodeDemoTest {
private final CollectionListCodeDemo instance = new CollectionListCodeDemo();
public static class Mock {
@MockInvoke(targetClass = String.class, targetMethod = "contains")
public boolean mockContains(CharSequence s) {
return false;
}
}
@Test
public void listTest() {
instance.list();
}
}

View File

@@ -0,0 +1,69 @@
package com.alibaba.demo.lambda;
import com.alibaba.testable.core.annotation.MockInvoke;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Stream;
import static com.alibaba.testable.core.matcher.InvocationVerifier.verifyInvoked;
/**
* @author jim
*/
public class InvokeInterfaceDemoTest {
private final InvokeInterfaceDemo instance = new InvokeInterfaceDemo();
public static class Mock {
@MockInvoke(targetClass = InvokeInterfaceDemo.ILambda.class, targetMethod = "run")
private void mockILambdaRun() {
}
@MockInvoke(targetClass = InvokeInterfaceDemo.ILambda.class, targetMethod = "function1")
private void mockILambdaFunction1(String s) {
}
@SuppressWarnings("unchecked")
@MockInvoke(targetClass = Collection.class, targetMethod = "stream")
<E> Stream<E> mockStream() {
List<E> fooList = new ArrayList<>();
fooList.add((E) "123");
fooList.add((E) "456");
return fooList.stream();
}
@MockInvoke(targetClass = Boolean.class, targetMethod = "logicalAnd")
public static boolean mockLogicalAnd(boolean a, boolean b) {
return false;
}
}
@Test
public void shouldMockCollectionStream() {
instance.collectionInterfaceDefaultOrStatic();
verifyInvoked("mockStream").withTimes(1);
}
@Test
public void shouldMockInterfaceDefault() {
instance.interfaceDefault();
verifyInvoked("mockILambdaRun").withTimes(1);
verifyInvoked("mockILambdaFunction1").withTimes(1);
}
@Test
public void shouldMockObjectStaticMethodReference() {
instance.objectStaticMethodReference();
verifyInvoked("mockLogicalAnd").withTimes(2);
}
@Test
public void shouldMockInterfaceStatic() {
instance.interfaceStatic();
}
}

View File

@@ -8,8 +8,8 @@ import static com.alibaba.testable.core.matcher.InvocationVerifier.verifyInvoked
/**
* @author zcbbpo
*/
public class ExternalLambdaDemoTest {
private final ExternalLambdaDemo lambdaDemo = new ExternalLambdaDemo();
public class InvokeVirtualDemoTest {
private final InvokeVirtualDemo lambdaDemo = new InvokeVirtualDemo();
@SuppressWarnings("unused")
public static class Mock {
@@ -34,7 +34,7 @@ public class ExternalLambdaDemoTest {
return "";
}
@MockInvoke(targetClass = ExternalLambdaDemo.class, targetMethod = "f3")
@MockInvoke(targetClass = InvokeVirtualDemo.class, targetMethod = "f3")
public Boolean mockF3(String s1, Long l) {
return true;
}

View File

@@ -0,0 +1,33 @@
package com.alibaba.demo.lambda;
import com.alibaba.testable.core.annotation.MockInvoke;
import org.junit.jupiter.api.Test;
import static com.alibaba.testable.core.matcher.InvocationVerifier.verifyInvoked;
/**
* @author jim
*/
public class StaticInstanceReferenceTest {
private final StaticInstanceReference instance = new StaticInstanceReference();
public static class Mock {
@MockInvoke(targetClass = StaticInstanceReference.StaticClassA.class, targetMethod = "doIt")
private void mockDoIt() {
}
@MockInvoke(targetClass = StaticInstanceReference.StaticClassA.class)
private Integer function2(String s) {
return 2;
}
}
@Test
public void shouldMockDoIt() {
instance.staticMethodReference();
verifyInvoked("mockDoIt").withTimes(1);
verifyInvoked("function2").withTimes(1);
}
}

View File

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

View File

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

View File

@@ -14,8 +14,8 @@ repositories {
dependencies {
testImplementation 'org.codehaus.groovy:groovy-all:3.0.7'
testImplementation 'org.spockframework:spock-core:2.0-M5-groovy-3.0'
testImplementation('com.alibaba.testable:testable-all:0.6.10')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.6.10')
testImplementation('com.alibaba.testable:testable-all:0.7.8')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.7.8')
}
tasks.withType(JavaCompile) {

View File

@@ -12,7 +12,7 @@
<java.version>1.8</java.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<testable.version>0.6.10</testable.version>
<testable.version>0.7.8</testable.version>
</properties>
<dependencyManagement>

View File

@@ -25,7 +25,7 @@ In the code, there are often cases of using <u>interface variables or parent cla
At this time, follow a principle that the type of the first parameter of the mock method is always the same as the type of the variable that initiated the call.
Therefore, regardless of whether the actually called method comes from the parent class or the subclass, and whether the subclass overrides the method. If the calling variable is of the parent type (or interface type), the first parameter type of the mock method should use the corresponding parent type (or interface) type.
Therefore, regardless of whether the actually called method comes from the parent class or the subclass, and whether the subclass overrides the method. If the calling variable is of the parent type (or interface type), the `targetClass` value of `@MockInvoke` annotation (or the first parameter type of the mock method) should use the corresponding parent type / interface type.
See the use case of the `DemoInheritTest` test class in the Java and Kotlin examples.
@@ -62,3 +62,9 @@ This framework will run Android unit tests on a standard JVM virtual machine, wh
This problem is caused by the system `Class Path` content is too long, and has nothing to do with `TestableMock`. However, it should be noted that IntelliJ provides two auxiliary solutions: `JAR manifest` and `classpath file`. If `TestableMock` is used in the test, please select `JAR manifest`.
![jar-manifest.png](https://img.alicdn.com/imgextra/i2/O1CN01hfC5YE1Kw0gBIlB2x_!!6000000001227-2-tps-752-171.png)
#### 10. Getting "no unique public constructor for [xxx]" error with `OmniConstructor`?
When the `omni.constructor.enhance.enable` configuration is enabled, `TestableMock` will dynamically add a constructor with a parameter of `java.lang.Void` when each class is loaded into memory to avoid the original type constructor exceptions that may be thrown. The above error occurs when some frameworks do not allow multiple constructors for some classes.
The solution is to add the package path where the error class is located to the `omni.constructor.enhance.pkgPrefix.excludes` configuration, which tell `TestableMock` not to dynamically add constructors to these classes. For details, please refer to the [Global Run Arguments](en-us/doc/javaagent-args.md) document.

View File

@@ -35,9 +35,7 @@ Parent parent = OmniConstructor.newInstance(Parent.class);
parent.getChild().getGrandChild().getContent();
```
> **Note 1**: In the current version, the member fields of type interface or abstract class will still be initialized to `null`, this problem will be fixed in the future.
>
> **Note 2**: Based on the light-weight principle, in the default mode, `OmniConstructor` will only uses the original constructor of the class to create objects. For POJO and most model layer objects, this mode has been able to meet the needs.
> **Note**: Based on the light-weight principle, in the default mode, `OmniConstructor` will only uses the original constructor of the class to create objects. For POJO and most model layer objects, this mode has been able to meet the needs.
> But for more complex situations, such as when certain class have constructors throwing exceptions or contain other statements that hinder the normal execution of the construction, the object construction may fail.
> In those situations, you can use the [Testable global configuration](en-us/doc/javaagent-args.md) `omni.constructor.enhance.enable = true` to enable bytecode enhancement mode of `OmniConstructor`, in this mode, any Java class can be constructed.

View File

@@ -1,5 +1,35 @@
# Release Note
## 0.7.8
- support use `OmniConstructor` class to create instance of interface type or abstract class
- provide a `CollectionUtil` class which contains many utility collection creation methods
- fix a compatibility issue with jdk 1.6
## 0.7.7
- skip bytecode process for classes which not associated to any mock class, speed up test initialization
- only generate substitution method for mocked method references
- fix compatibility issues with jdk 17
## 0.7.6
- define package mapping rule according to order in config, to avoid a short path overwrite longer ones
- fix compatibility issue with jdk 9+
- skip classes generated by mockito during `OmniConstructor` bytecode enhancing
## 0.7.5
- fix an issue cause mocking method with array return type fails
## 0.7.4
- fix an `OmniConstructor.newInstance()` fail issue caused by `java.net.URL` class cannot be instrumented
## 0.7.3
- fix an issue cause mock failed with referring to method defined in interface
## 0.7.2
- fix an issue cause mock failed with static method reference
## 0.7.1
- fix an issue cause mock class unrecognized after package-mapping
## 0.7.0
- fix an improper exception issue when mock method with `associated` scope invoked
- rename type `InvokeVerifier`/`InvockeMatcher` to `InvocationVerifier`/`InvocationMatcher`

View File

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

View File

@@ -1,6 +1,15 @@
Upgrade Guide
---
#### Upgrade to 0.7
In version `0.7` mock related annotations and methods are renamed, in order to avoid naming conflict and misunderstanding.
1. The `verify()` used for check mock method invocation status has been renamed to `verifyInvoked()`, as there are several static methods also named `verify()` in `com.sun` package.
2. The annotation `@MockMethod` and `@MockConstructor` used for mark mock replacement method has been renamed to `@MockInvoke` and `@MockNew`, which is more suitable for its real usage.
The usage of those annotations and method has no change for below and above version `0.7.0`, so during the upgrading only rename is required.
### Upgrade to v0.6
In version `0.6``TestableMock` provided a new [quick complicated class construction](en-us/doc/omni-constructor.md) capability. Meanwhile, it includes a modification that is incompatible with the `0.5` version: class `PrivateAccessor` have been moved from `com.alibaba.testable.core.accessor` package to `com.alibaba.testable.core.tool` package.

View File

@@ -0,0 +1,46 @@
Use IntelliJ Plugin
---
There are some typical operations in the process of using`TestableMock` library, such as define a mock class, copy signature of the method invoked and convert it to mock method definition. While the mock result does not match expectation, we have to debug the mock affection via [Self-Help Troubleshooting](zh-cn/doc/troubleshooting.md) guide during runtime. With the help of IDE plugin, part of the patterned operations can be automatically completed to further optimize the experience of `TestableMock`.
Currently `TestableMock` provided only [IntelliJ IDEA](https://www.jetbrains.com/idea/) plugin. (According to the [2020](https://www.jrebel.com/blog/2020-java-technology-report#IDE) and [2021](https://www.jrebel.com/blog/2021-java-technology-report) Java Technology Trends Report released by JRebel, IntelliJ IDEA has become the IDE of choice with the highest percentage of Java developers)
## Usage
Open preference dialog, search "Testable-Mock" in plugin market, reach `Testable-Mock Helper` plugin and click "install", then restart IDE.
![install](https://img.alicdn.com/imgextra/i2/O1CN013twjqN2803NVtvJEP_!!6000000007869-0-tps-2342-566.jpg)
#### 1. **Generate Mock Class**
After plugin installed, right click in any java class, choose "Generate Testable TestClass"
![mock-class](https://img.alicdn.com/imgextra/i1/O1CN01gNsunh21jfVufv6EU_!!6000000007021-0-tps-546-291.jpg)
A test class containing a mock container class will be automatically generated in the same package path location as the current class in the `test` directory of the project (named after `CurrentClass + Test`)
#### 2. **Create Mock Method**
Select any method invocation, right click and choose "Copy Testable Mock-Method" or "Copy Mock-Method To TestClass"
![mock-method](https://img.alicdn.com/imgextra/i4/O1CN01K38Zsh1NrNYLlrUZ9_!!6000000001623-0-tps-642-264.jpg)
The former will convert the method signature of selected invocation into a mock method definition and store it on the clipboard; the latter will directly insert the converted mock method into the corresponding mock class.
## Build from source code
Beside install plugin from market, you can also build it from source.
Make sure you have jdk 11 installed, execute below command:
```bash
git clone https://github.com/zcbbpo/testable-idea
cd testable-idea
./gradlew clean build
```
After build, the packaged zip file is lay in `build/distributions/` folder, you can install it by "Install Plugin From Disk" menu.
![from-local](https://img.alicdn.com/imgextra/i3/O1CN01YMHefk26MNS4pH4ZI_!!6000000007647-0-tps-2342-516.jpg)
Finally, special thanks to the contribution of `TestableMock` IntelliJ IDEA plugin made by @[zcbbpo](https://github.com/zcbbpo).

View File

@@ -165,7 +165,7 @@ For complete code examples, see the `should_get_source_method_name()` and `shoul
In test cases, you can use the `InvocationVerifier.verifyInvoked()` method, and cooperate with `with()`, `withInOrder()`, `without()`, `withTimes()` and other methods to verify the mock call situation.
For details, please refer to the [Check Mock Call](en-us/doc/matcher.md) document.
For details, please refer to the [Check Mock Call](en-us/doc/invoke-matcher.md) document.
### 4. Special instructions

View File

@@ -11,6 +11,7 @@
- [Reusing Mock Methods](en-us/doc/mock-method-reusing.md)
- [Use MockWith Annotation](en-us/doc/use-mock-with.md)
- [Use Package Mapping](en-us/doc/use-package-mapping.md)
- [Use IntelliJ Plugin](en-us/doc/use-intellij-plugin.md)
- [Frequently Asked Questions](en-us/doc/frequently-asked-questions.md)
- [Mock Invocation In Thread Pool](zh-cn/doc/with-thread-pool.md)
- [Use TestableMock In IDE](en-us/doc/use-in-ide.md)

View File

@@ -9,7 +9,7 @@
| Spock | 动态代理 | 类 | 不能Mock私有/静态和构造方法 | 较复杂 | 一般 |
| PowerMock | 自定义类加载器 | 类 | **任何方法皆可** | 较复杂 | **较好** |
| JMockit | 运行时字节码修改 | 类 | 不能Mock构造方法(new操作符) | 较复杂 | 一般 |
| TestableMock | 运行时字节码修改 | 方法 | **任何方法皆可** | **容易** | 一般 |
| TestableMock | 运行时字节码修改 | 方法 | **任何方法皆可** | **容易** | **较好** |
`Mockito`是Java最老牌的Mock工具稳定性和易用性较好IntelliJ和Eclipse都有专用插件支持。相对不足之处在于Mock功能稍弱在必要情况下需与其他Mock工具配合使用。
@@ -19,8 +19,12 @@
`JMockit`是一款功能性与易用性均居于`Mockito``PowerMock`之间的Mock工具较好的弥补了两者各自的不足。该项目在2017年尝试推出JMockit2重写版本但未能完成目前处于不活跃的维护状态。
相比之下,`TestabledMock`的功能与`PowerMock`基本平齐,且极易上手,只需掌握`@MockInvoke`注解就可以完成绝大多数任务
相比之下,`TestabledMock`的功能与`PowerMock`基本平齐,且极易上手,只需掌握一个`@MockInvoke`注解就可以完成绝大多数Mock操作
当前`TestableMock`的主要不足在于编写Mock方法时IDE无法即时提示方法参数是否正确匹配。若发现匹配效果不符合预期需要通过[自助问题排查](zh-cn/doc/troubleshooting.md)文档提供的方法在运行期进行校验。这个功能理论上能够通过扩展主流IDE插件来补充但目前暂无相关开发计划参见[Issue-104](https://github.com/alibaba/testable-mock/issues/104)。
## TestableMock的不足
此外,由于`TestableMock`独辟蹊径的采用“每个业务类拥有一个专属Mock容器类”的思维方式将Mock方法定义与单元测试用例解耦一方面使得Mock方法具有默认可复用性单元测试用例也因此变得更干净纯粹另一方面也导致Mock方法定义变得零散生命周期管理起来相对困难对现有开发者的Mock编写习惯会带来一定改变。
为了便于开发者进行Mock工具的合理选型我们认为客观的强调`TestabledMock`存在的不足,与阐述它的优点同样重要。
任何事物都有两面性。由于`TestableMock`独辟蹊径采用“每个业务类拥有一个专属Mock容器类”的思维方式将Mock方法定义与单元测试用例解耦一方面使得Mock方法具有默认可复用性单元测试用例也因此变得更干净纯粹另一方面也导致Mock方法定义变得零散生命周期管理起来相对困难对现有开发者的Mock编写习惯会带来一定改变。
此外,`TestableMock`最初的设计主要面向标准单元测试场景(每个类有自己的测试用例,测试类与被测类基本逐一对应)。为了最大限度的提升易用性,把需要用到的注解、操作步骤都减少到最少,`TestableMock`采用了许多“约定优于配置”的设计。譬如被测量、测试类与Mock容器类在同一包路径且分别按约定命名等。对于单元测试较规范的项目而言`TestableMock`将极大降低Mock引入的成本对于规范不足的项目依然可以结合`@MockWith`、包路径映射等功能完成相同的Mock效果但应当尤其注意这些“花哨”的功能对代码可读性和可维护性带来的负面影响。

View File

@@ -29,7 +29,7 @@
这时候遵循一个原则Mock方法的首个参数类型**始终与发起调用的变量类型一致**。
因此不论实际被调用方法来自父类还是子类也不论子类是否覆写该方法。若发起调用的变量为父类型或接口类型则Mock方法的首个参数类型都应该使用相应的父类(或接口)类型。
因此,不论实际被调用方法来自父类还是子类,也不论子类是否覆写该方法。若发起调用的变量为父类型(或接口类型),则`@MockInvoke`注解的`targetClass`参数值(或Mock方法的首个参数类型都应该使用相应的父类/接口类的类型。
参见Java和Kotlin示例中`DemoInheritTest`测试类的用例。
@@ -64,3 +64,9 @@ Kotlin语言中的`String`类型实际上是`kotlin.String`,而非`java.lang.S
这个问题是由于系统ClassPath包含太多路径所致与是否使用`TestableMock`无关。但需要注意的是IntelliJ提供了两种辅助解决机制`JAR manifest``classpath file`,若测试中使用了`TestableMock`,请选择`JAR manifest`
![jar-manifest.png](https://img.alicdn.com/imgextra/i2/O1CN01hfC5YE1Kw0gBIlB2x_!!6000000001227-2-tps-752-171.png)
#### 10. 使用`OmniConstructor`遇到"no unique public constructor for [xxx]"错误?
当启用`omni.constructor.enhance.enable`配置时,`TestableMock`会在每个类加载进内存时动态添加一个参数为`java.lang.Void`的构造方法,用于避开原类型构造方法里可能抛出的异常。若遇到某些框架不允许类型存在多个构造方法的时候,就会出现上述错误。
解决办法是将报错类所在的包路径添加到`omni.constructor.enhance.pkgPrefix.excludes`配置中,告知`TestableMock`不要对这些类动态添加构造方法即可。配置方法详见[全局运行参数](zh-cn/doc/javaagent-args.md)文档。

View File

@@ -35,9 +35,7 @@ Parent parent = OmniConstructor.newInstance(Parent.class);
parent.getChild().getGrandChild().getContent();
```
> **注意 1** :在当前版本中,类型为接口或抽象类的成员字段依然会被初始化为`null`,此问题将在后续版本修复
>
> **注意 2** :基于轻量优先原则,默认模式下,`OmniConstructor`仅利用类型原有的构造方法来创建对象对于绝大多数POJO和`Model`层对象而言这种模式已经能够满足需要。
> **注意**:基于轻量优先原则,默认模式下,`OmniConstructor`仅利用类型原有的构造方法来创建对象对于绝大多数POJO和`Model`层对象而言这种模式已经能够满足需要。
> 但对于更复杂的情形,譬如遇到某些类型的构造方法会抛出异常或包含其他妨碍构造正常执行的语句时,对象构造可能会失败。
> 此时可通过[Testable全局配置](zh-cn/doc/javaagent-args.md)`omni.constructor.enhance.enable = true`启用`OmniConstructor`的字节码增强模式在该模式下任何Java类型皆可构造。

View File

@@ -1,5 +1,35 @@
# Release Note
## 0.7.8
- 支持使用`OmniConstructor`工具类自动创建接口和抽象类的实例
- 提供`CollectionUtil`工具类,包含在测试中常用的集合创建方法
- 修复一处JDK 1.6的兼容问题
## 0.7.7
- 未关联Mock类的类型自动跳过字节码处理提升大型项目测试启动速度
- 仅对被Mock的方法引用生成替代方法 (issue-280)
- 修复JDK 17的兼容性问题 (issue-283)
## 0.7.6
- 依据定义顺序匹配包映射规则防止短路径覆盖长路径issue-275
- 修复JDK 9+的兼容性问题issue-270 / pr-274
-`OmniConstructor`字节码增强时忽略Mockito生成的类型issue-197
## 0.7.5
- 修复Mock数组类型返回值方法报错的问题issue-267
## 0.7.4
- 修复`java.net.URL`类无法被instrument导致使用`OmniConstructor`构造报错的问题issue-261
## 0.7.3
- 修复接口方法引用导致Mock报错的问题issue-254
## 0.7.2
- 修复静态方法引用导致Mock报错的问题issue-250
## 0.7.1
- 修复包路径映射时后识别不到Mock类的问题issue-248
## 0.7.0
- 修复当`scope``associated`的Mock方法被`null`对象调用且上下文与测试用例未关联时抛错不合理的问题issue-163
- 类型`InvokeVerifier``InvockeMatcher`更名为`InvocationVerifier``InvocationMatcher`

View File

@@ -16,7 +16,7 @@
```xml
<properties>
<testable.version>0.6.10</testable.version>
<testable.version>0.7.8</testable.version>
</properties>
```
@@ -63,8 +63,8 @@
```groovy
dependencies {
testImplementation('com.alibaba.testable:testable-all:0.6.10')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.6.10')
testImplementation('com.alibaba.testable:testable-all:0.7.8')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.7.8')
}
```
@@ -110,4 +110,6 @@ test {
> sourceCompatibility = '6' // 或7/8/9/...
> ```
>
> **注意**:由于`Spock`采用基于字符串解析的特殊执行方式,`TestableTool`工具类在`Spock`框架中无法使用。
>
> 完整代码可参考`demo/spock-demo`示例项目。

View File

@@ -1,6 +1,15 @@
版本升级说明
---
#### 升级到0.7.x版本
`0.7`版本重命名了Mock相关的注解和方法以避免命名冲突和由于名字带来的理解歧义包括两部分变化。
1. 用于验证Mock方法调用的`verify()`方法与包括`com.sun`包在内的多个JVM内置静态方法重名在IDE自动添加`import`时会有误选的情况,该方法在新版本中更名为`verifyInvoked()`,同时所属的类型更名为`InvocationVerifier`
2. 注解`@MockMethod``@MockConstructor`的实际作用是对方法调用和New操作进行Mock而其名称容易使人误解为是对方法和构造器本身进行了Mock在新版本中这两个注解分别已被分别更名为`@MockInvoke``@MockNew`
这些注解和方法的使用与之前版本并无差异,从`0.6.x`版本升级到`0.7.0`及以上版本时,请全局替换相关名称即可。
### 升级到0.6.x版本
`0.6`版本中,`TestableMock`提供了[快速构造复杂参数对象](zh-cn/doc/omni-constructor.md)的能力,同时包含一处与`0.5`版本不兼容的修改,`PrivateAccessor`类型的包路径从`com.alibaba.testable.core.accessor`移到了`com.alibaba.testable.core.tool`

View File

@@ -0,0 +1,46 @@
使用IntelliJ IDEA插件
---
在使用`TestableMock`工具的过程中经常涉及一些固定操作比如定义Mock容器类、复制需Mock的调用定义并创建Mock方法在执行测试时若发现Mock匹配的效果不符合预期则需要通过[自助问题排查](zh-cn/doc/troubleshooting.md)方法在运行期进行校验。通过IDE插件辅助能够将部分模式化的操作自动完成进一步优化`TestableMock`的使用体验。
目前`TestableMock`提供了 [IntelliJ IDEA](https://www.jetbrains.com/idea/) 的插件支持根据JRebel发布的 [2020](https://www.jrebel.com/blog/2020-java-technology-report#IDE) 和 [2021](https://www.jrebel.com/blog/2021-java-technology-report) Java技术趋势报告IntelliJ IDEA已成为Java开发者使用比例最高的首选IDE
## 使用方法
打开配置项在插件市场中搜索“Testable-Mock”选中`Testable-Mock Helper`插件点击“安装”然后重启IDE。
![install](https://img.alicdn.com/imgextra/i2/O1CN013twjqN2803NVtvJEP_!!6000000007869-0-tps-2342-566.jpg)
#### 1. **创建Mock容器类**
安装完插件后在任意Java类点击右键点击“生成Testable测试类”英文版为“Generate Testable TestClass”
![mock-class](https://img.alicdn.com/imgextra/i1/O1CN01gNsunh21jfVufv6EU_!!6000000007021-0-tps-546-291.jpg)
在项目`test`目录下与当前类相同的包路径位置会自动生成一个包含Mock容器类的测试类`当前类+Test`命名)。
#### 2. **创建Mock方法**
选中任意方法调用点击右键点击“复制方法为Testable-Mock”英文版为“Copy Testable Mock-Method”或“复制Testable-Mock方法到测试类”英文版为“Copy Mock-Method To TestClass”
![mock-method](https://img.alicdn.com/imgextra/i4/O1CN01K38Zsh1NrNYLlrUZ9_!!6000000001623-0-tps-642-264.jpg)
前者会将被选中调用的方法签名并转换为Mock方法定义存放到剪贴板后者则会将转换好的Mock方法直接插入到相应的Mock类里。
## 源码构建
除了通过插件市场安装,也可以直接从源码构建`Testable-Mock Helper`插件。
本地准备JDK 11环境然后执行以下命令
```bash
git clone https://github.com/zcbbpo/testable-idea
cd testable-idea
./gradlew clean build
```
构建完成后,在`build/distributions/`目录下可以找到构建好的插件zip包通过“从本地磁盘安装”菜单加载该插件。
![from-local](https://img.alicdn.com/imgextra/i3/O1CN01YMHefk26MNS4pH4ZI_!!6000000007647-0-tps-2342-516.jpg)
在此,特别感谢 @[zcbbpo](https://github.com/zcbbpo) 对`TestableMock`IntelliJ IDEA插件的贡献。

View File

@@ -165,7 +165,7 @@ private Data mockDemo() {
在测试用例中可用通过`InvocationVerifier.verifyInvoked()`方法,配合`with()``withInOrder()``without()``withTimes()`等方法实现对Mock调用情况的验证。
详见[校验Mock调用](zh-cn/doc/matcher.md)文档。
详见[校验Mock调用](zh-cn/doc/invoke-matcher.md)文档。
### 4. 特别说明

View File

@@ -11,6 +11,7 @@
- [复用Mock类与方法](zh-cn/doc/mock-method-reusing.md)
- [使用MockWith注解](zh-cn/doc/use-mock-with.md)
- [使用包路径映射](zh-cn/doc/use-package-mapping.md)
- [使用IntelliJ插件](zh-cn/doc/use-intellij-plugin.md)
- [常见使用问题](zh-cn/doc/frequently-asked-questions.md)
- [Mock线程池内的调用](zh-cn/doc/with-thread-pool.md)
- [在IDE运行单元测试](zh-cn/doc/use-in-ide.md)

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-parent</artifactId>
<version>0.6.10</version>
<version>0.7.8</version>
<relativePath>../testable-parent</relativePath>
</parent>
<artifactId>testable-agent</artifactId>
@@ -14,8 +14,8 @@
<name>testable-agent</name>
<properties>
<asm.lib.version>8.0.1</asm.lib.version>
<junit.version>5.6.2</junit.version>
<asm.lib.version>9.3</asm.lib.version>
<junit.version>5.8.2</junit.version>
</properties>
<dependencies>
@@ -123,6 +123,9 @@
<goals>
<goal>jar</goal>
</goals>
<configuration>
<additionalOptions>-Xdoclint:none</additionalOptions>
</configuration>
</execution>
</executions>
</plugin>
@@ -137,6 +140,13 @@
<goals>
<goal>sign</goal>
</goals>
<configuration>
<!-- Prevent `gpg` from using pinentry programs -->
<gpgArguments>
<arg>--pinentry-mode</arg>
<arg>loopback</arg>
</gpgArguments>
</configuration>
</execution>
</executions>
</plugin>

View File

@@ -190,7 +190,10 @@ public class MockClassHandler extends BaseClassWithContextHandler {
if (VOID_RES.equals(returnType)) {
il.add(new InsnNode(POP));
il.add(new InsnNode(RETURN));
} else if (returnType.charAt(0) == TYPE_ARRAY || returnType.charAt(0) == TYPE_CLASS) {
} else if (returnType.charAt(0) == TYPE_ARRAY) {
il.add(new TypeInsnNode(CHECKCAST, returnType));
il.add(new InsnNode(ARETURN));
} else if (returnType.charAt(0) == TYPE_CLASS) {
il.add(new TypeInsnNode(CHECKCAST, ClassUtil.toSlashSeparateJavaStyleName(returnType)));
il.add(new InsnNode(ARETURN));
} else {

View File

@@ -3,7 +3,7 @@ package com.alibaba.testable.agent.handler;
import com.alibaba.testable.agent.handler.test.JUnit4Framework;
import com.alibaba.testable.agent.handler.test.JUnit5Framework;
import com.alibaba.testable.agent.util.ClassUtil;
import com.alibaba.testable.agent.util.CollectionUtil;
import com.alibaba.testable.core.util.CollectionUtil;
import org.objectweb.asm.Label;
import org.objectweb.asm.tree.*;
@@ -38,7 +38,7 @@ public class OmniClassHandler extends BaseClassHandler {
@Override
protected void transform(ClassNode cn) {
if (isInterfaceOrAtom(cn) || isJunitTestClass(cn) || isUninstantiableClass(cn) || hasSpecialAnnotation(cn)) {
if (isInterfaceOrAtom(cn) || isUniqueConstructorClass(cn) || isUninstantiableClass(cn) || hasSpecialAnnotation(cn)) {
return;
}
addConstructorWithVoidTypeParameter(cn);
@@ -89,7 +89,15 @@ public class OmniClassHandler extends BaseClassHandler {
return (cn.access & ACC_INTERFACE) != 0 || cn.superName == null || VOID_TYPE.equals(cn.name);
}
private boolean isJunitTestClass(ClassNode cn) {
private boolean isUniqueConstructorClass(ClassNode cn) {
// elastic plugin class should contain only one constructor
if ("org/elasticsearch/plugins/Plugin".equals(cn.superName)) {
return true;
}
// should skip mockito generated class
if (cn.name.contains("$MockitoMock$")) {
return true;
}
// junit require test class contains only one constructor
for (MethodNode mn : cn.methods) {
if (mn.visibleAnnotations == null) {
@@ -131,7 +139,7 @@ public class OmniClassHandler extends BaseClassHandler {
}
private List<LocalVariableNode> createLocalVariables(ClassNode cn, LabelNode start, LabelNode end) {
return CollectionUtil.listOf(
return CollectionUtil.mutableListOf(
new LocalVariableNode(THIS_REF, ClassUtil.toByteCodeClassName(cn.name), null, start, end, 0),
new LocalVariableNode(IGNORE, ClassUtil.toByteCodeClassName(VOID_TYPE), null, start, end, 1)
);

View File

@@ -1,27 +1,21 @@
package com.alibaba.testable.agent.handler;
import com.alibaba.testable.agent.model.BasicType;
import com.alibaba.testable.agent.model.BsmArg;
import com.alibaba.testable.agent.model.MethodInfo;
import com.alibaba.testable.agent.model.TravelStatus;
import com.alibaba.testable.agent.util.BytecodeUtil;
import com.alibaba.testable.agent.util.ClassUtil;
import com.alibaba.testable.agent.util.MethodUtil;
import com.alibaba.testable.core.util.LogUtil;
import org.objectweb.asm.Handle;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.*;
import org.objectweb.asm.tree.*;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import static com.alibaba.testable.core.constant.ConstPool.CONSTRUCTOR;
import static com.alibaba.testable.core.constant.ConstPool.DOLLAR;
/**
* @author flin
@@ -51,6 +45,10 @@ public class SourceClassHandler extends BaseClassHandler {
@Override
protected void transform(ClassNode cn) {
LogUtil.diagnose("Found source class %s", cn.name);
if (injectMethods.isEmpty()) {
return;
}
Set<MethodInfo> memberInjectMethods = new HashSet<MethodInfo>();
Set<MethodInfo> newOperatorInjectMethods = new HashSet<MethodInfo>();
for (MethodInfo im : injectMethods) {
@@ -61,17 +59,19 @@ public class SourceClassHandler extends BaseClassHandler {
}
}
resolveMethodReference(cn);
if (!memberInjectMethods.isEmpty()) {
resolveMethodReference(cn, memberInjectMethods);
}
for (MethodNode m : cn.methods) {
transformMethod(m, memberInjectMethods, newOperatorInjectMethods, cn);
transformMethod(m, memberInjectMethods, newOperatorInjectMethods);
}
}
private void transformMethod(MethodNode mn, Set<MethodInfo> memberInjectMethods,
Set<MethodInfo> newOperatorInjectMethods, ClassNode cn) {
Set<MethodInfo> newOperatorInjectMethods) {
LogUtil.verbose(" Found method %s", mn.name);
if (mn.name.startsWith("$")) {
if (mn.name.startsWith(DOLLAR)) {
// skip methods e.g. "$jacocoInit"
return;
}
@@ -341,85 +341,57 @@ public class SourceClassHandler extends BaseClassHandler {
return Opcodes.INVOKEVIRTUAL == opcode && ClassUtil.isCompanionClassName(ownerClass);
}
private void setFinalValue(Field ownerField, Object obj, Object value) throws Exception {
ownerField.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(ownerField, ownerField.getModifiers() & ~Modifier.FINAL);
ownerField.set(obj, value);
}
private List<Handle> fetchInvokeDynamicHandle(MethodNode mn) {
List<Handle> handleList = new ArrayList<Handle>();
private List<BsmArg> fetchInvokeDynamicHandle(MethodNode mn) {
List<BsmArg> handleList = new ArrayList<BsmArg>();
for (AbstractInsnNode instruction : mn.instructions) {
if (instruction.getOpcode() == Opcodes.INVOKEDYNAMIC) {
InvokeDynamicInsnNode invokeDynamicInsnNode = (InvokeDynamicInsnNode) instruction;
handleList.add((Handle) invokeDynamicInsnNode.bsmArgs[1]);
// java9+ String makeConcatWithConstants will ues INVOKEDYNAMIC to optimize.
// When the mock source method has like <code>"abc" + "def"</code> the bsmArgs length was 1
// https://openjdk.java.net/jeps/280
if (invokeDynamicInsnNode.bsmArgs.length < 3) {
continue;
}
BsmArg bsmArg = new BsmArg(invokeDynamicInsnNode.bsmArgs);
handleList.add(bsmArg);
}
}
return handleList;
}
private void resolveMethodReference(ClassNode cn) {
List<Handle> invokeDynamicList = new ArrayList<Handle>();
private void resolveMethodReference(ClassNode cn, Set<MethodInfo> mockedMethods) {
List<BsmArg> invokeDynamicList = new ArrayList<BsmArg>();
for (MethodNode method : cn.methods) {
List<Handle> handleList = fetchInvokeDynamicHandle(method);
invokeDynamicList.addAll(handleList);
List<BsmArg> handleList = fetchInvokeDynamicHandle(method);
for (BsmArg arg : handleList) {
for (MethodInfo mi : mockedMethods) {
if (isMethodMocked(arg.getHandle(), mi)) {
invokeDynamicList.add(arg);
}
}
}
}
// process for method reference
for (Handle handle : invokeDynamicList) {
for (BsmArg bsmArg : invokeDynamicList) {
// the jdk auto generation method
if (handle.getName().startsWith("lambda$")) {
if (bsmArg.getHandle().getName().startsWith("lambda$")) {
continue;
}
int tag = handle.getTag();
int tag = bsmArg.getHandle().getTag();
if (tag == Opcodes.H_NEWINVOKESPECIAL) {
// lambda new method reference
continue;
}
// external mean:
// public void foo() {
// String s = "";
// consumes(s::contains);
//}
boolean external = tag == Opcodes.H_INVOKEVIRTUAL;
boolean isStatic = bsmArg.isStatic();
Handle handle = bsmArg.getHandle();
Type handleDesc = bsmArg.getHandleDesc();
boolean isStatic = tag == Opcodes.H_INVOKESTATIC || external;
String desc = handle.getDesc();
String parameters = desc.substring(desc.indexOf("(") + 1, desc.lastIndexOf(")"));
String returnType = desc.substring(desc.indexOf(")") + 1);
String[] parameterArray = parameters.split(";");
int len = parameterArray.length;
for (String s : parameterArray) {
if (s.isEmpty()) {
len--;
}
}
len = external ? len + 1 : len;
String[] refineParameterArray = new String[len];
int index = external ? 1 : 0;
if (external) {
// The type should was reference type
refineParameterArray[0] = "L" + handle.getOwner();
}
for (String s : parameterArray) {
if (!s.isEmpty()) {
refineParameterArray[index] = s;
index++;
}
}
String externalDesc = buildDesc(refineParameterArray, returnType);
String lambdaName = String.format("Lambda$_%s_%d", handle.getName(), atomicInteger.incrementAndGet());
MethodVisitor mv = cn.visitMethod(isStatic ? ACC_PUBLIC + ACC_STATIC : ACC_PUBLIC, lambdaName, external ? externalDesc : desc, null, null);
String lambdaName = String.format("_Lambda$_%s_%d", handle.getName(), atomicInteger.incrementAndGet());
MethodVisitor mv = cn.visitMethod(isStatic ? ACC_PUBLIC + ACC_STATIC : ACC_PUBLIC, lambdaName, handleDesc.getDescriptor(), null, null);
mv.visitCode();
Label l0 = new Label();
@@ -428,101 +400,65 @@ public class SourceClassHandler extends BaseClassHandler {
// add this
mv.visitVarInsn(ALOAD, 0);
}
for (int i = 0; i < refineParameterArray.length; i++) {
String arg = refineParameterArray[i];
mv.visitVarInsn(getLoadType(arg), isStatic ? i : i + 1);
Type[] argumentTypes = handleDesc.getArgumentTypes();
int nextVar = isStatic ? 0 : 1;
for (Type argumentType : argumentTypes) {
String arg = argumentType.getDescriptor();
mv.visitVarInsn(getLoadType(arg), nextVar);
nextVar = isLongByte(argumentType) ? nextVar + 2 : nextVar + 1;
}
mv.visitMethodInsn(isStatic ? INVOKESTATIC : INVOKEVIRTUAL, handle.getOwner(), handle.getName(), desc, false);
if (tag == H_INVOKEINTERFACE) {
mv.visitMethodInsn(INVOKEINTERFACE, handle.getOwner(), handle.getName(), bsmArg.getOriginalHandleDesc(), handle.isInterface());
} else {
mv.visitMethodInsn(Opcodes.H_INVOKESTATIC == tag ? INVOKESTATIC : INVOKEVIRTUAL, handle.getOwner(), handle.getName(), bsmArg.getOriginalHandleDesc(), handle.isInterface());
}
mv.visitInsn(getReturnType(returnType));
mv.visitInsn(getReturnType(handleDesc.getReturnType().getDescriptor()));
Label l1 = new Label();
mv.visitLabel(l1);
// static function was not required add this to first parameter
if (isStatic) {
for (int i = 0; i < refineParameterArray.length; i++) {
String localVar = refineParameterArray[i];
if (!isPrimitive(localVar)) {
// primitive type and reference type difference
localVar = localVar.endsWith(";") ? localVar : localVar + ";";
}
if (localVar.isEmpty()) {
continue;
}
// add local var
mv.visitLocalVariable(String.format("o%d", i), localVar, null, l0, l1, i);
}
visitLocalVariableByArguments(mv, 0, argumentTypes, l0, l1);
} else {
mv.visitLocalVariable("this", "L" + handle.getOwner() + ";", null, l0, l1, 0);
for (int i = 0; i < refineParameterArray.length; i++) {
String localVar = refineParameterArray[i];
if (!isPrimitive(localVar) && !isPrimitiveArray(localVar)) {
localVar = localVar.endsWith(";") ? localVar : localVar + ";";
}
if (localVar.isEmpty()) {
continue;
}
mv.visitLocalVariable(String.format("o%d", i), localVar, null, l0, l1, i + 1);
}
visitLocalVariableByArguments(mv, 1, argumentTypes, l0, l1);
}
// auto compute max
mv.visitMaxs(-1, -1);
mv.visitEnd();
try {
// modify handle to the generation method
setFinalValue(handle.getClass().getDeclaredField("name"), handle, lambdaName);
// mark: should merge the below two if.
if (!handle.getOwner().equals(cn.name) && isStatic) {
setFinalValue(handle.getClass().getDeclaredField("owner"), handle, cn.name);
}
if (external) {
setFinalValue(handle.getClass().getDeclaredField("owner"), handle, cn.name);
setFinalValue(handle.getClass().getDeclaredField("descriptor"), handle, externalDesc);
setFinalValue(handle.getClass().getDeclaredField("tag"), handle, H_INVOKESTATIC);
}
} catch (Exception ignore) {
}
bsmArg.complete(cn.name, lambdaName);
}
}
private String buildDesc(String[] refineParameterArray, String returnType) {
StringBuilder sb = new StringBuilder();
sb.append("(");
for (String s : refineParameterArray) {
sb.append(s);
if (!isPrimitive(s)) {
sb.append(";");
}
private boolean isMethodMocked(Handle targetHandle, MethodInfo mockMethodInfo) {
if (targetHandle.getTag() == Opcodes.H_INVOKEINTERFACE || targetHandle.getTag() == Opcodes.H_INVOKEVIRTUAL) {
String targetMockDesc = MethodUtil.addParameterAtBegin(mockMethodInfo.getDesc(),
ClassUtil.toByteCodeClassName(mockMethodInfo.getClazz()));
return mockMethodInfo.getClazz().equals(targetHandle.getOwner()) &&
mockMethodInfo.getName().equals(targetHandle.getName()) &&
targetMockDesc.equals(targetHandle.getDesc());
}
sb.append(")");
sb.append(returnType);
return sb.toString();
return mockMethodInfo.getClazz().equals(targetHandle.getOwner()) &&
mockMethodInfo.getName().equals(targetHandle.getName()) &&
mockMethodInfo.getDesc().equals(targetHandle.getDesc());
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
private boolean isPrimitive(String type) {
if (type.endsWith(";")) {
type = type.substring(0, type.length() - 1);
private void visitLocalVariableByArguments(MethodVisitor mv, final int initVar, Type[] argumentTypes, Label l0, Label l1) {
int nextLocalVar = initVar;
for (int i = 0; i < argumentTypes.length; i++) {
Type argumentType = argumentTypes[i];
String localVar = argumentType.getDescriptor();
mv.visitLocalVariable(String.format("o%d", i), localVar, null, l0, l1, nextLocalVar);
nextLocalVar = isLongByte(argumentType) ? nextLocalVar + 2 : nextLocalVar + 1;
}
return BasicType.basicType(type.charAt(0)).isPrimitive();
}
private boolean isPrimitiveArray(String type) {
if (!type.startsWith("[")) {
return false;
}
if (type.endsWith(";")) {
type = type.substring(0, type.length() - 1);
}
type = type.replace("[", "");
return BasicType.basicType(type.charAt(0)).isPrimitive();
private boolean isLongByte(Type argumentType) {
return double.class.getName().equals(argumentType.getClassName()) || long.class.getName().equals(argumentType.getClassName());
}
private int getReturnType(String returnType) {

View File

@@ -2,7 +2,7 @@ package com.alibaba.testable.agent.handler.test;
import com.alibaba.testable.agent.model.TestCaseMethodType;
import com.alibaba.testable.agent.util.ClassUtil;
import com.alibaba.testable.agent.util.CollectionUtil;
import com.alibaba.testable.core.util.CollectionUtil;
import org.objectweb.asm.Label;
import org.objectweb.asm.tree.*;

View File

@@ -1,7 +1,7 @@
package com.alibaba.testable.agent.handler.test;
import com.alibaba.testable.agent.model.TestCaseMethodType;
import com.alibaba.testable.agent.util.CollectionUtil;
import com.alibaba.testable.core.util.CollectionUtil;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.MethodNode;

View File

@@ -0,0 +1,84 @@
package com.alibaba.testable.agent.model;
import com.alibaba.testable.agent.util.StringUtil;
import org.objectweb.asm.Handle;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
/**
* @author jim
*/
public class BsmArg {
private Type handleDesc;
private Type methodDesc;
private Handle handle;
private final Object[] bsmArgs;
private final String originalHandleDesc;
public BsmArg(Object[] bsmArgs) {
this.bsmArgs = bsmArgs;
handle = (Handle) bsmArgs[1];
methodDesc = (Type) bsmArgs[2];
handleDesc = Type.getType(handle.getDesc());
originalHandleDesc = handle.getDesc();
// H_INVOKEVIRTUAL: String s = "";consumes(s::contains);
// H_INVOKEINTERFACE: list.stream().flatMap(Collection::stream)
if (handle.getTag() == Opcodes.H_INVOKEVIRTUAL || handle.getTag() == Opcodes.H_INVOKEINTERFACE) {
Type[] argumentTypes = handleDesc.getArgumentTypes();
Type thisArgument = Type.getType("L" + handle.getOwner() + ";");
String handleDescString = refineHandle(thisArgument, argumentTypes);
handle = new Handle(handle.getTag(), handle.getOwner(), handle.getName(), handleDescString, handle.isInterface());
handleDesc = Type.getType(handleDescString);
if (/*handle.getTag() == Opcodes.H_INVOKEVIRTUAL || */methodDesc.getArgumentTypes().length == handleDesc.getArgumentTypes().length - 1) {
Type[] methodArguments = methodDesc.getArgumentTypes();
String methodDescString = refineHandle(thisArgument, methodArguments);
methodDesc = Type.getType(methodDescString);
}
}
}
private String refineHandle(Type thisArgument, Type[] argumentTypes) {
Type[] handleArguments = new Type[argumentTypes.length + 1];
String[] handleArgs = new String[argumentTypes.length + 1];
handleArguments[0] = thisArgument;
System.arraycopy(argumentTypes, 0, handleArguments, 1, argumentTypes.length);
for (int i = 0; i < handleArguments.length; i++) {
handleArgs[i] = handleArguments[i].getDescriptor();
}
return "(" + StringUtil.join("", handleArgs) + ")" + handleDesc.getReturnType().getDescriptor();
}
public Type getHandleDesc() {
return handleDesc;
}
public Type getMethodDesc() {
return methodDesc;
}
public Handle getHandle() {
return handle;
}
public String getOriginalHandleDesc() {
return originalHandleDesc;
}
public boolean isStatic() {
int tag = handle.getTag();
return tag == Opcodes.H_INVOKESTATIC || tag == Opcodes.H_INVOKEVIRTUAL || tag == Opcodes.H_INVOKEINTERFACE;
}
public void complete(String owner, String methodName) {
bsmArgs[1] = new Handle(isStatic()? Opcodes.H_INVOKESTATIC : Opcodes.H_INVOKEVIRTUAL, owner, methodName, handleDesc.getDescriptor(), false);
}
}

View File

@@ -63,15 +63,14 @@ public class TestableClassTransformer implements ClassFileTransformer {
}
private byte[] transformMock(byte[] bytes, ClassNode cn) {
String className = (GlobalConfig.getMockPackageMapping() == null) ? cn.name : mapPackage(cn.name);
try {
if (mockClassParser.isMockClass(cn)) {
// it's a mock class
bytes = new MockClassHandler(className).getBytes(bytes);
bytes = new MockClassHandler(cn.name).getBytes(bytes);
BytecodeUtil.dumpByte(cn, GlobalConfig.getDumpPath(), bytes);
return bytes;
}
String mockClass = foundMockForSourceClass(className);
String mockClass = foundMockForSourceClass(cn.name);
if (mockClass != null) {
// it's a source class with testable enabled
List<MethodInfo> injectMethods = mockClassParser.getTestableMockMethods(mockClass);
@@ -92,7 +91,7 @@ public class TestableClassTransformer implements ClassFileTransformer {
LogUtil.error("Invalid mock method %s::%s - %s", e.getClassName(), e.getMethodName(), e.getMessage());
System.exit(0);
} catch (Throwable t) {
LogUtil.warn("Failed to transform class " + className);
LogUtil.warn("Failed to transform class " + cn.name);
LogUtil.warn(t.toString());
LogUtil.warn(ThreadUtil.getFirstRelatedStackLine(t));
} finally {
@@ -102,18 +101,8 @@ public class TestableClassTransformer implements ClassFileTransformer {
return bytes;
}
private String mapPackage(String name) {
String dotSeparatedName = ClassUtil.toDotSeparatedName(name);
for (String prefix : GlobalConfig.getMockPackageMapping().keySet()) {
if (dotSeparatedName.startsWith(prefix)) {
return ClassUtil.toSlashSeparatedName(GlobalConfig.getMockPackageMapping().get(prefix))
+ name.substring(prefix.length());
}
}
return name;
}
private String foundMockForSourceClass(String className) {
private String foundMockForSourceClass(String name) {
String className = (GlobalConfig.getMockPackageMapping() == null) ? name : mapPackage(name);
String mockClass = lookForMockWithAnnotationAsSourceClass(className);
if (mockClass != null) {
return mockClass;
@@ -125,6 +114,17 @@ public class TestableClassTransformer implements ClassFileTransformer {
return foundMockForInnerSourceClass(className);
}
private String mapPackage(String name) {
String dotSeparatedName = ClassUtil.toDotSeparatedName(name);
for (String prefix : GlobalConfig.getMockPackageMapping().keySet()) {
if (dotSeparatedName.startsWith(prefix)) {
return ClassUtil.toSlashSeparatedName(GlobalConfig.getMockPackageMapping().get(prefix))
+ name.substring(prefix.length());
}
}
return name;
}
private String foundMockForInnerSourceClass(String className) {
return (className.contains(DOLLAR) && !className.endsWith(KOTLIN_POSTFIX_COMPANION)) ?
foundMockForTestClass(ClassUtil.getTestClassName(className.substring(0, className.indexOf(DOLLAR)))) : null;

View File

@@ -239,7 +239,7 @@ public class ClassUtil {
* @return outer class name
*/
public static String toOuterClassName(String name) {
int pos = name.lastIndexOf("$");
int pos = name.lastIndexOf(DOLLAR);
return (pos > 0) ? name.substring(0, pos) : name;
}

View File

@@ -1,39 +0,0 @@
package com.alibaba.testable.agent.util;
import java.util.*;
/**
* @author flin
*/
public class CollectionUtil {
/**
* Check two collection has any equaled item
* @param collectionLeft the first collection
* @param collectionRight the second collection
* @return found or not
*/
public static boolean containsAny(Collection<?> collectionLeft, Collection<?> collectionRight) {
for (Object o : collectionLeft) {
for (Object i : collectionRight) {
if (o.equals(i)) {
return true;
}
}
}
return false;
}
/**
* Generate a list of item
* @param items elements to add
* @param <T> type of element
* @return a ArrayList of provided elements
*/
public static <T> List<T> listOf(T... items) {
List<T> list = new ArrayList<T>(items.length);
Collections.addAll(list, items);
return list;
}
}

View File

@@ -6,10 +6,7 @@ import com.alibaba.testable.core.util.LogUtil;
import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import static com.alibaba.testable.agent.constant.ConstPool.PROPERTY_TEMP_DIR;
import static com.alibaba.testable.agent.constant.ConstPool.PROPERTY_USER_DIR;
@@ -114,7 +111,7 @@ public class GlobalConfig {
public static void addMockPackageMapping(String originPkg, String mockClassPkg) {
if (mockPkgMapping == null) {
mockPkgMapping = new HashMap<String, String>(5);
mockPkgMapping = new LinkedHashMap<String, String>(5);
}
mockPkgMapping.put(originPkg + DOT, mockClassPkg + DOT);
}

View File

@@ -19,4 +19,15 @@ public class StringUtil {
return sb.toString();
}
public static String join(String delimiter, String[] s) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < s.length; i++) {
stringBuilder.append(s[i]);
if (i != s.length - 1) {
stringBuilder.append(delimiter);
}
}
return stringBuilder.toString();
}
}

View File

@@ -3,7 +3,7 @@ package com.alibaba.testable.agent.util;
import org.junit.jupiter.api.Test;
import org.objectweb.asm.tree.AnnotationNode;
import static com.alibaba.testable.agent.util.CollectionUtil.listOf;
import static com.alibaba.testable.core.util.CollectionUtil.listOf;
import static org.junit.jupiter.api.Assertions.*;
class AnnotationUtilTest {

View File

@@ -1,19 +0,0 @@
package com.alibaba.testable.agent.util;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CollectionUtilTest {
@Test
void should_check_collection_contains_any_element() {
assertTrue(CollectionUtil.containsAny(
CollectionUtil.listOf("a", "b"), CollectionUtil.listOf("b", "c")
));
assertFalse(CollectionUtil.containsAny(
CollectionUtil.listOf("a", "b"), CollectionUtil.listOf("c", "d")
));
}
}

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-parent</artifactId>
<version>0.6.10</version>
<version>0.7.8</version>
<relativePath>../testable-parent</relativePath>
</parent>
<artifactId>testable-all</artifactId>
@@ -59,6 +59,9 @@
<goals>
<goal>jar</goal>
</goals>
<configuration>
<additionalOptions>-Xdoclint:none</additionalOptions>
</configuration>
</execution>
</executions>
</plugin>
@@ -73,6 +76,13 @@
<goals>
<goal>sign</goal>
</goals>
<configuration>
<!-- Prevent `gpg` from using pinentry programs -->
<gpgArguments>
<arg>--pinentry-mode</arg>
<arg>loopback</arg>
</gpgArguments>
</configuration>
</execution>
</executions>
</plugin>

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-parent</artifactId>
<version>0.6.10</version>
<version>0.7.8</version>
<relativePath>../testable-parent</relativePath>
</parent>
<artifactId>testable-core</artifactId>
@@ -15,7 +15,7 @@
<properties>
<junit.version>5.6.2</junit.version>
<transmittable.thread.local.version>2.12.1</transmittable.thread.local.version>
<transmittable.thread.local.version>2.12.4</transmittable.thread.local.version>
</properties>
<dependencies>
@@ -80,6 +80,9 @@
<goals>
<goal>jar</goal>
</goals>
<configuration>
<additionalOptions>-Xdoclint:none</additionalOptions>
</configuration>
</execution>
</executions>
</plugin>
@@ -94,6 +97,13 @@
<goals>
<goal>sign</goal>
</goals>
<configuration>
<!-- Prevent `gpg` from using pinentry programs -->
<gpgArguments>
<arg>--pinentry-mode</arg>
<arg>loopback</arg>
</gpgArguments>
</configuration>
</execution>
</executions>
</plugin>

View File

@@ -0,0 +1,10 @@
package com.alibaba.testable.core.compile;
public class CompilationException extends RuntimeException {
private static final long serialVersionUID = 5272588827551900536L;
public CompilationException(String msg) {
super(msg);
}
}

View File

@@ -0,0 +1,33 @@
package com.alibaba.testable.core.compile;
import javax.tools.SimpleJavaFileObject;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
/**
* Created by trung on 5/3/15.
*/
public class CompiledCode extends SimpleJavaFileObject {
private ByteArrayOutputStream baos = new ByteArrayOutputStream();
private String className;
public CompiledCode(String className) throws Exception {
super(new URI(className), Kind.CLASS);
this.className = className;
}
public String getClassName() {
return className;
}
@Override
public OutputStream openOutputStream() throws IOException {
return baos;
}
public byte[] getByteCode() {
return baos.toByteArray();
}
}

View File

@@ -0,0 +1,27 @@
package com.alibaba.testable.core.compile;
import java.util.HashMap;
import java.util.Map;
public class DynamicClassLoader extends ClassLoader {
private Map<String, CompiledCode> customCompiledCode = new HashMap<String, CompiledCode>();
public DynamicClassLoader(ClassLoader parent) {
super(parent);
}
public void addCode(CompiledCode cc) {
customCompiledCode.put(cc.getName(), cc);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
CompiledCode cc = customCompiledCode.get(name);
if (cc == null) {
return super.findClass(name);
}
byte[] byteCode = cc.getByteCode();
return defineClass(name, byteCode, 0, byteCode.length);
}
}

View File

@@ -0,0 +1,56 @@
package com.alibaba.testable.core.compile;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.tools.FileObject;
import javax.tools.ForwardingJavaFileManager;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
/**
* Created by trung on 5/3/15. Edited by turpid-monkey on 9/25/15, completed
* support for multiple compile units.
*/
public class ExtendedStandardJavaFileManager extends
ForwardingJavaFileManager<JavaFileManager> {
private List<CompiledCode> compiledCode = new ArrayList<CompiledCode>();
private DynamicClassLoader cl;
/**
* Creates a new instance of ForwardingJavaFileManager.
*
* @param fileManager
* delegate to this file manager
* @param cl
*/
protected ExtendedStandardJavaFileManager(JavaFileManager fileManager,
DynamicClassLoader cl) {
super(fileManager);
this.cl = cl;
}
@Override
public JavaFileObject getJavaFileForOutput(
JavaFileManager.Location location, String className,
JavaFileObject.Kind kind, FileObject sibling) throws IOException {
try {
CompiledCode innerClass = new CompiledCode(className);
compiledCode.add(innerClass);
cl.addCode(innerClass);
return innerClass;
} catch (Exception e) {
throw new RuntimeException(
"Error while creating in-memory output file for "
+ className, e);
}
}
@Override
public ClassLoader getClassLoader(JavaFileManager.Location location) {
return cl;
}
}

View File

@@ -0,0 +1,140 @@
package com.alibaba.testable.core.compile;
import java.util.*;
import javax.tools.*;
/**
* Compile Java sources in-memory
*/
public class InMemoryJavaCompiler {
private JavaCompiler javac;
private DynamicClassLoader classLoader;
private Iterable<String> options;
boolean ignoreWarnings = false;
private Map<String, SourceCode> sourceCodes = new HashMap<String, SourceCode>();
public static InMemoryJavaCompiler newInstance() {
return new InMemoryJavaCompiler();
}
private InMemoryJavaCompiler() {
this.javac = ToolProvider.getSystemJavaCompiler();
this.classLoader = new DynamicClassLoader(ClassLoader.getSystemClassLoader());
}
public InMemoryJavaCompiler useParentClassLoader(ClassLoader parent) {
this.classLoader = new DynamicClassLoader(parent);
return this;
}
/**
* @return the class loader used internally by the compiler
*/
public ClassLoader getClassloader() {
return classLoader;
}
/**
* Options used by the compiler, e.g. '-Xlint:unchecked'.
*
* @param options
* @return
*/
public InMemoryJavaCompiler useOptions(String... options) {
this.options = Arrays.asList(options);
return this;
}
/**
* Ignore non-critical compiler output, like unchecked/unsafe operation
* warnings.
*
* @return
*/
public InMemoryJavaCompiler ignoreWarnings() {
ignoreWarnings = true;
return this;
}
/**
* Compile all sources
*
* @return Map containing instances of all compiled classes
* @throws Exception
*/
public Map<String, Class<?>> compileAll() throws Exception {
if (sourceCodes.size() == 0) {
throw new CompilationException("No source code to compile");
}
Collection<SourceCode> compilationUnits = sourceCodes.values();
CompiledCode[] code = new CompiledCode[compilationUnits.size()];
Iterator<SourceCode> iter = compilationUnits.iterator();
for (int i = 0; i < code.length; i++) {
code[i] = new CompiledCode(iter.next().getClassName());
}
DiagnosticCollector<JavaFileObject> collector = new DiagnosticCollector<JavaFileObject>();
ExtendedStandardJavaFileManager fileManager = new ExtendedStandardJavaFileManager(javac.getStandardFileManager(null, null, null), classLoader);
JavaCompiler.CompilationTask task = javac.getTask(null, fileManager, collector, options, null, compilationUnits);
boolean result = task.call();
if (!result || collector.getDiagnostics().size() > 0) {
StringBuilder exceptionMsg = new StringBuilder();
exceptionMsg.append("Unable to compile the source");
boolean hasWarnings = false;
boolean hasErrors = false;
for (Diagnostic<? extends JavaFileObject> d : collector.getDiagnostics()) {
switch (d.getKind()) {
case NOTE:
case MANDATORY_WARNING:
case WARNING:
hasWarnings = true;
break;
case OTHER:
case ERROR:
default:
hasErrors = true;
break;
}
exceptionMsg.append("\n").append("[kind=").append(d.getKind());
exceptionMsg.append(", ").append("line=").append(d.getLineNumber());
exceptionMsg.append(", ").append("message=").append(d.getMessage(Locale.US)).append("]");
}
if (hasWarnings && !ignoreWarnings || hasErrors) {
throw new CompilationException(exceptionMsg.toString());
}
}
Map<String, Class<?>> classes = new HashMap<String, Class<?>>();
for (String className : sourceCodes.keySet()) {
classes.put(className, classLoader.loadClass(className));
}
return classes;
}
/**
* Compile single source
*
* @param className
* @param sourceCode
* @return
* @throws Exception
*/
public Class<?> compile(String className, String sourceCode) throws Exception {
return addSource(className, sourceCode).compileAll().get(className);
}
/**
* Add source code to the compiler
*
* @param className
* @param sourceCode
* @return
* @throws Exception
* @see {@link #compileAll()}
*/
public InMemoryJavaCompiler addSource(String className, String sourceCode) throws Exception {
sourceCodes.put(className, new SourceCode(className, sourceCode));
return this;
}
}

View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,9 @@
This part of code is under [Apache License Version 2.0](LICENSE), modified from [InMemoryJavaCompiler](https://github.com/trung/InMemoryJavaCompiler) with modification (e.g. dropping unnecessary dependence and avoid throwing raw Exception).
Many thanks to the origin author and contributors of this lovely library:
- [trung](https://github.com/trung)
- [turpid-monkey](https://github.com/turpid-monkey)
- [PKeidel](https://github.com/PKeidel)
- [fossabot](https://github.com/fossabot)
- [KOLANICH](https://github.com/KOLANICH)
- [hammadirshad](https://github.com/hammadirshad)

View File

@@ -0,0 +1,28 @@
package com.alibaba.testable.core.compile;
import javax.tools.SimpleJavaFileObject;
import java.net.URI;
/**
* Created by trung on 5/3/15.
*/
public class SourceCode extends SimpleJavaFileObject {
private final String contents;
private final String className;
public SourceCode(String className, String contents) {
super(URI.create("string:///" + className.replace('.', '/')
+ Kind.SOURCE.extension), Kind.SOURCE);
this.contents = contents;
this.className = className;
}
public String getClassName() {
return className;
}
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
return contents;
}
}

View File

@@ -2,22 +2,28 @@ package com.alibaba.testable.core.model;
import com.alibaba.testable.core.util.UnnullableMap;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
public class MockContext {
/**
* dot separated class name
* current test class (dot separated) name
*/
public final String testClassName;
/**
* current test case name
*/
public final String testCaseName;
/**
* store of MOCK_CONTEXT.get() and MOCK_CONTEXT.set()
*/
public final Map<String, Object> parameters;
/**
* invocation record of mock method and arguments
*/
public final Map<String, List<Object[]>> invokeRecord;
public MockContext(String testClassName, String testCaseName) {

View File

@@ -0,0 +1,33 @@
package com.alibaba.testable.core.model;
import java.io.Serializable;
/**
* @author flin
*/
public class Pair<L, R> implements Serializable {
private static final long serialVersionUID = -5197546316467446976L;
/** Left object */
private L left;
/** Right object */
private R right;
public Pair(L left, R right) {
this.left = left;
this.right = right;
}
public L getLeft() {
return left;
}
public R getRight() {
return right;
}
public static <L, R> Pair<L, R> of(L l, R r) {
return new Pair<L, R>(l, r);
}
}

View File

@@ -3,6 +3,7 @@ package com.alibaba.testable.core.tool;
import com.alibaba.testable.core.exception.ClassConstructionException;
import com.alibaba.testable.core.model.ConstructionOption;
import com.alibaba.testable.core.util.CollectionUtil;
import com.alibaba.testable.core.util.ConstructionUtil;
import com.alibaba.testable.core.util.LogUtil;
import com.alibaba.testable.core.util.TypeUtil;
@@ -113,11 +114,11 @@ public class OmniConstructor {
return array;
}
private static <T> T newAbstractClass(Class<T> clazz) {
return null;
private static <T> T newAbstractClass(Class<T> clazz) throws InstantiationException {
return ConstructionUtil.generateSubClassOf(clazz);
}
private static <T> T newInterface(Class<T> clazz) {
private static <T> T newInterface(Class<T> clazz) throws InstantiationException {
if (clazz.equals(List.class)) {
return (T)Collections.emptyList();
} else if (clazz.equals(Map.class)) {
@@ -125,7 +126,7 @@ public class OmniConstructor {
} else if (clazz.equals(Set.class)) {
return (T)Collections.emptySet();
}
return null;
return ConstructionUtil.generateSubClassOf(clazz);
}
private static <T> T newEnum(Class<T> clazz)
@@ -180,7 +181,7 @@ public class OmniConstructor {
}
classPool.put(type, instance);
for (Field f : TypeUtil.getAllFields(type)) {
if (f.getName().startsWith("$") || isStaticFinalField(f)) {
if (f.getName().startsWith(DOLLAR) || isStaticFinalField(f)) {
// skip static-final fields and fields e.g. "$jacocoData"
continue;
}
@@ -227,6 +228,10 @@ public class OmniConstructor {
private static Object createInstance(Class<?> clazz, Set<Class<?>> classPool)
throws InstantiationException, IllegalAccessException, InvocationTargetException {
Object ins = createSpecialClass(clazz);
if (ins != null) {
return ins;
}
Constructor<?> constructor = getBestConstructor(clazz);
if (constructor == null) {
throw new ClassConstructionException("Fail to invoke constructor of " + clazz.getName());
@@ -244,6 +249,18 @@ public class OmniConstructor {
}
}
private static Object createSpecialClass(Class<?> clazz) throws InstantiationException, IllegalAccessException, InvocationTargetException {
try {
// java.net.URL is loaded before OmniClassHandler, cannot be instrumented
if (clazz.getName().equals("java.net.URL")) {
return clazz.getDeclaredConstructor(String.class).newInstance("https://");
}
} catch (NoSuchMethodException e) {
return null;
}
return null;
}
private static Constructor<?> getBestConstructor(Class<?> clazz) {
Constructor<?> bestConstructor = null;
int minimalExceptionCount = 999;

View File

@@ -1,23 +1,47 @@
package com.alibaba.testable.core.util;
import java.util.Collection;
import java.util.Iterator;
import com.alibaba.testable.core.model.Pair;
import java.util.*;
public class CollectionUtil {
public static Object[] slice(Object[] args, int pos) {
int size = args.length - pos;
/**
* Get slice of args[startPos, args.length]
* @param args original item array
* @param startPos index of the first item to keep
* @return a new array with sliced items
*/
public static Object[] slice(Object[] args, int startPos) {
return slice(args, startPos, args.length - 1);
}
/**
* Get slice of args[startPos, endPos]
* @param args original item array
* @param startPos index of the first item to keep
* @param endPos index of the last item to keep
* @return a new array with sliced items
*/
public static Object[] slice(Object[] args, int startPos, int endPos) {
int size = endPos - startPos + 1;
if (size <= 0) {
return new Object[0];
}
Object[] slicedArgs = new Object[size];
System.arraycopy(args, pos, slicedArgs, 0, size);
System.arraycopy(args, startPos, slicedArgs, 0, size);
return slicedArgs;
}
/**
* Join a collection into string
* @param collection many items with proper toString() method
* @param joinSymbol splitter of echo items
* @return a joined string
*/
public static String join(Collection<?> collection, String joinSymbol) {
StringBuilder sb = new StringBuilder();
for(Iterator<?> i = collection.iterator(); i.hasNext(); sb.append((String)i.next())) {
for(Iterator<?> i = collection.iterator(); i.hasNext(); sb.append(i.next().toString())) {
if (sb.length() != 0) {
sb.append(joinSymbol);
}
@@ -25,6 +49,12 @@ public class CollectionUtil {
return sb.toString();
}
/**
* Check whether target exist in collection
* @param collection many items to find from
* @param target an item to be found
* @return whether target exist in collection
*/
public static <T> boolean contains(T[] collection, T target) {
for (T item : collection) {
if (target.equals(item)) {
@@ -33,4 +63,84 @@ public class CollectionUtil {
}
return false;
}
/**
* Check two collection has any equaled item
* @param collectionLeft the first collection
* @param collectionRight the second collection
* @return whether any equaled item found
*/
public static boolean containsAny(Collection<?> collectionLeft, Collection<?> collectionRight) {
for (Object o : collectionLeft) {
for (Object i : collectionRight) {
if (o.equals(i)) {
return true;
}
}
}
return false;
}
/**
* Create an array
* @param items elements to add
* @return array of the provided items
*/
public static <T> T[] arrayOf(T... items) {
return items;
}
/**
* Create a list
* @param items elements to add
* @return list of the provided items
*/
public static <T> List<T> listOf(T... items) {
return Arrays.asList(items);
}
/**
* Generate a list of item
* @param items elements to add
* @return mutable list of the provided items
*/
public static <T> List<T> mutableListOf(T... items) {
List<T> list = new ArrayList<T>(items.length);
Collections.addAll(list, items);
return list;
}
/**
* Create a set
* @param items elements to add
* @return set of the provided items
*/
public static <T> Set<T> setOf(T... items) {
return new HashSet<T>(Arrays.asList(items));
}
/**
* Create a map
* @param pair elements to add
* @return map of the provided items
*/
public static <K, V> Map<K, V> mapOf(Pair<K, V>... pair) {
return mapOf(new HashMap<K, V>(pair.length), pair);
}
/**
* Create an ordered map
* @param pair elements to add
* @return ordered map of the provided items
*/
public static <K, V> Map<K, V> orderMapOf(Pair<K, V>... pair) {
return mapOf(new LinkedHashMap<K, V>(pair.length), pair);
}
private static <K, V> Map<K, V> mapOf(Map<K, V> kvs, Pair<K, V>[] pair) {
for (Pair<K, V> p : pair) {
kvs.put(p.getLeft(), p.getRight());
}
return kvs;
}
}

View File

@@ -0,0 +1,98 @@
package com.alibaba.testable.core.util;
import com.alibaba.testable.core.compile.InMemoryJavaCompiler;
import com.alibaba.testable.core.tool.OmniConstructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import static com.alibaba.testable.core.constant.ConstPool.DOLLAR;
import static com.alibaba.testable.core.constant.ConstPool.DOT;
public class ConstructionUtil {
private static final String TESTABLE_IMPL = "$TestableImpl";
public static <T> T generateSubClassOf(Class<T> clazz) throws InstantiationException {
StringBuilder sourceCode = new StringBuilder();
sourceCode.append("package ").append(clazz.getPackage().getName()).append(";\n")
.append("public class ").append(getSubclassName(clazz))
.append(clazz.isInterface() ? " implements " : " extends ")
.append(getClassName(clazz))
.append(" {\n");
for (Method m : clazz.getMethods()) {
if (!Modifier.isStatic(m.getModifiers()) && !Modifier.isFinal(m.getModifiers())) {
sourceCode.append("\tpublic ");
TypeVariable<Method>[] typeParameters = m.getTypeParameters();
if (typeParameters.length > 0) {
sourceCode.append("<");
for (int i = 0; i < typeParameters.length; i++) {
sourceCode.append(typeParameters[i].getName()).append(" extends ");
Type[] bounds = typeParameters[i].getBounds();
for (int j = 0; j < bounds.length; j++) {
sourceCode.append(getClassName(bounds[j]));
if (j < bounds.length - 1) {
sourceCode.append(" & ");
}
}
if (i < typeParameters.length - 1) {
sourceCode.append(", ");
}
}
sourceCode.append("> ");
}
sourceCode.append(getClassName(m.getGenericReturnType())).append(" ")
.append(m.getName()).append("(");
Type[] parameters = m.getGenericParameterTypes();
for (int i = 0; i < parameters.length; i++) {
sourceCode.append(getParameterName(parameters[i])).append(" p").append(i);
if (i < parameters.length - 1) {
sourceCode.append(", ");
}
}
sourceCode.append(") {\n");
if (!m.getReturnType().equals(void.class)) {
sourceCode.append("\t\treturn (").append(getClassName(m.getGenericReturnType())).append(") ")
.append(getClassName(OmniConstructor.class)).append(".")
.append("newInstance(").append(getClassName(m.getReturnType())).append(".class);\n");
}
sourceCode.append("\t}\n");
}
}
sourceCode.append("}");
try {
return (T) InMemoryJavaCompiler.newInstance()
.useParentClassLoader(clazz.getClassLoader())
.useOptions("-Xlint:unchecked")
.ignoreWarnings()
.compile(clazz.getPackage().getName() + DOT + getSubclassName(clazz), sourceCode.toString())
.newInstance();
} catch (Exception e) {
throw new InstantiationException(e.toString());
}
}
private static String getClassName(Type clazz) {
if (clazz instanceof Class) {
return ((Class<?>)clazz).getName().replace(DOLLAR, DOT);
} else if (clazz instanceof TypeVariable) {
return ((TypeVariable<?>)clazz).getName();
}
return clazz.toString().replaceAll("@.*$", "").replaceAll("^.* ", "");
}
private static String getParameterName(Type parameter) {
if (parameter instanceof Class && ((Class<?>)parameter).isArray()) {
return getParameterName(((Class<?>)parameter).getComponentType()) + "[]";
}
return getClassName(parameter);
}
private static String getSubclassName(Class<?> clazz) {
return clazz.getSimpleName() + TESTABLE_IMPL;
}
}

View File

@@ -0,0 +1,118 @@
package com.alibaba.testable.core.compile;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.function.Executable;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
class InMemoryJavaCompilerTest {
@Test
public void compile_WhenTypical() throws Exception {
final StringBuffer sourceCode = new StringBuffer();
sourceCode.append("package org.mdkt;\n");
sourceCode.append("public class HelloClass {\n");
sourceCode.append(" public String hello() { return \"hello\"; }");
sourceCode.append("}");
Class<?> helloClass = InMemoryJavaCompiler.newInstance().compile("org.mdkt.HelloClass", sourceCode.toString());
assertNotNull(helloClass);
assertEquals(1, helloClass.getDeclaredMethods().length);
}
@Test
public void compileAll_WhenTypical() throws Exception {
String cls1 = "public class A{ public B b() { return new B(); }}";
String cls2 = "public class B{ public String toString() { return \"B!\"; }}";
Map<String, Class<?>> compiled = InMemoryJavaCompiler.newInstance().addSource("A", cls1).addSource("B", cls2).compileAll();
assertNotNull(compiled.get("A"));
assertNotNull(compiled.get("B"));
Class<?> aClass = compiled.get("A");
Object a = aClass.newInstance();
assertEquals("B!", aClass.getMethod("b").invoke(a).toString());
}
@Test
public void compile_WhenSourceContainsInnerClasses() throws Exception {
final StringBuffer sourceCode = new StringBuffer();
sourceCode.append("package org.mdkt;\n");
sourceCode.append("public class HelloClass {\n");
sourceCode.append(" private static class InnerHelloWorld { int inner; }\n");
sourceCode.append(" public String hello() { return \"hello\"; }");
sourceCode.append("}");
Class<?> helloClass = InMemoryJavaCompiler.newInstance().compile("org.mdkt.HelloClass", sourceCode.toString());
assertNotNull(helloClass);
assertEquals(1, helloClass.getDeclaredMethods().length);
}
@Test
public void compile_whenError() throws Exception {
final StringBuffer sourceCode = new StringBuffer();
sourceCode.append("package org.mdkt;\n");
sourceCode.append("public classHelloClass {\n");
sourceCode.append(" public String hello() { return \"hello\"; }");
sourceCode.append("}");
assertThrows(CompilationException.class, new Executable() {
@Override
public void execute() throws Throwable {
InMemoryJavaCompiler.newInstance().compile("org.mdkt.HelloClass", sourceCode.toString());
}
}, "Unable to compile the source");
}
@Test
public void compile_WhenFailOnWarnings() throws Exception {
final StringBuffer sourceCode = new StringBuffer();
sourceCode.append("package org.mdkt;\n");
sourceCode.append("public class HelloClass {\n");
sourceCode.append(" public java.util.List<String> hello() { return new java.util.ArrayList(); }");
sourceCode.append("}");
assertThrows(CompilationException.class, new Executable() {
@Override
public void execute() throws Throwable {
InMemoryJavaCompiler.newInstance().compile("org.mdkt.HelloClass", sourceCode.toString());
}
});
}
@Test
public void compile_WhenIgnoreWarnings() throws Exception {
final StringBuffer sourceCode = new StringBuffer();
sourceCode.append("package org.mdkt;\n");
sourceCode.append("public class HelloClass {\n");
sourceCode.append(" public java.util.List<String> hello() { return new java.util.ArrayList(); }");
sourceCode.append("}");
Class<?> helloClass = InMemoryJavaCompiler.newInstance().ignoreWarnings().compile("org.mdkt.HelloClass", sourceCode.toString());
List<?> res = (List<?>) helloClass.getMethod("hello").invoke(helloClass.newInstance());
assertEquals(0, res.size());
}
@Test
public void compile_WhenWarningsAndErrors() throws Exception {
final StringBuffer sourceCode = new StringBuffer();
sourceCode.append("package org.mdkt;\n");
sourceCode.append("public class HelloClass extends xxx {\n");
sourceCode.append(" public java.util.List<String> hello() { return new java.util.ArrayList(); }");
sourceCode.append("}");
assertThrows(CompilationException.class, new Executable() {
@Override
public void execute() throws Throwable {
InMemoryJavaCompiler.newInstance().compile("org.mdkt.HelloClass", sourceCode.toString());
}
});
}
}

View File

@@ -3,16 +3,31 @@ package com.alibaba.testable.core.util;
import org.junit.jupiter.api.Test;
import static com.alibaba.testable.core.tool.PrivateAccessor.invokeStatic;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.*;
class CollectionUtilTest {
@Test
void should_slice_array() {
Object[] args = new Object[]{"1", "2", "3"};
Object[] slicedArgs = invokeStatic(CollectionUtil.class, "slice", args, 1);
Object[] args = new Object[]{"1", "2", "3", "4"};
Object[] slicedArgs = CollectionUtil.slice(args, 1);
assertEquals(3, slicedArgs.length);
assertEquals("2", slicedArgs[0]);
assertEquals("3", slicedArgs[1]);
assertEquals("4", slicedArgs[2]);
slicedArgs = CollectionUtil.slice(args, 1, 2);
assertEquals(2, slicedArgs.length);
assertEquals("2", slicedArgs[0]);
assertEquals("3", slicedArgs[1]);
}
@Test
void should_check_collection_contains_any_element() {
assertTrue(CollectionUtil.containsAny(
CollectionUtil.listOf("a", "b"), CollectionUtil.listOf("b", "c")
));
assertFalse(CollectionUtil.containsAny(
CollectionUtil.listOf("a", "b"), CollectionUtil.listOf("c", "d")
));
}
}

View File

@@ -0,0 +1,42 @@
package com.alibaba.testable.core.util;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class ConstructionUtilTest {
public interface EmptyInterface {}
public interface RealInterface {
void fun1();
int func2(double d, boolean b);
String fun3(String s, byte[] b);
EmptyInterface fun4(RealInterface i);
<T> T fun5(T i);
}
public static abstract class AbstractClazz implements RealInterface {
@Override
public void fun1() {}
public static <T> T useless() { return null; }
}
@Test
void should_generate_empty_interface() throws Exception {
EmptyInterface ins = ConstructionUtil.generateSubClassOf(EmptyInterface.class);
assertNotNull(ins);
}
@Test
void should_generate_real_interface() throws Exception {
RealInterface ins = ConstructionUtil.generateSubClassOf(RealInterface.class);
assertNotNull(ins);
}
@Test
void should_generate_abstract_class() throws Exception {
RealInterface ins = ConstructionUtil.generateSubClassOf(AbstractClazz.class);
assertNotNull(ins);
}
}

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-parent</artifactId>
<version>0.6.10</version>
<version>0.7.8</version>
<relativePath>../testable-parent</relativePath>
</parent>
<artifactId>testable-maven-plugin</artifactId>
@@ -85,6 +85,9 @@
<goals>
<goal>jar</goal>
</goals>
<configuration>
<additionalOptions>-Xdoclint:none</additionalOptions>
</configuration>
</execution>
</executions>
</plugin>
@@ -99,6 +102,13 @@
<goals>
<goal>sign</goal>
</goals>
<configuration>
<!-- Prevent `gpg` from using pinentry programs -->
<gpgArguments>
<arg>--pinentry-mode</arg>
<arg>loopback</arg>
</gpgArguments>
</configuration>
</execution>
</executions>
</plugin>

View File

@@ -2,6 +2,7 @@ package com.alibaba.testable;
import com.google.common.base.Strings;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
@@ -34,6 +35,12 @@ public class TestableMojo extends AbstractMojo
@Parameter(property = "plugin.artifactMap", required = true, readonly = true)
private Map<String, Artifact> pluginArtifactMap;
/**
* Location of local maven repository
*/
@Parameter(defaultValue = "${localRepository}", readonly = true, required = true)
private ArtifactRepository localRepository;
/**
* JavaAgent log level (mute/debug/verbose)
*/
@@ -56,6 +63,11 @@ public class TestableMojo extends AbstractMojo
* Name of the Testable Agent artifact.
*/
private static final String AGENT_ARTIFACT_NAME = "com.alibaba.testable:testable-agent";
/**
* Testable agent jar path in local repository
*/
private static final String AGENT_LOCAL_REPOSITORY_PATH =
"/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar";
/**
* Name of the property used in maven-osgi-test-plugin.
*/
@@ -104,7 +116,12 @@ public class TestableMojo extends AbstractMojo
getLog().error("failed to find testable agent jar");
return "";
}
return " -javaagent:" + testableAgentArtifact.getFile().getAbsolutePath();
String agentJarPath = testableAgentArtifact.getFile().getAbsolutePath();
if (agentJarPath.endsWith("target/classes")) {
// Just for testcases within testable project itself
agentJarPath = localRepository.getBasedir() + AGENT_LOCAL_REPOSITORY_PATH;
}
return " -javaagent:" + agentJarPath;
}
private String getEffectivePropertyKey() {

View File

@@ -5,7 +5,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-parent</artifactId>
<version>0.6.10</version>
<version>0.7.8</version>
<packaging>pom</packaging>
<name>testable-parent</name>
<description>Unit test enhancement toolkit</description>
@@ -33,16 +33,16 @@
<java.version>1.6</java.version>
<project.compiler.level>1.6</project.compiler.level>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<plugin.compiler.version>3.8.1</plugin.compiler.version>
<plugin.surefire.version>3.0.0-M5</plugin.surefire.version>
<plugin.jar.version>3.2.0</plugin.jar.version>
<plugin.shade.version>3.2.4</plugin.shade.version>
<plugin.source.version>3.2.0</plugin.source.version>
<plugin.javadoc.version>3.2.0</plugin.javadoc.version>
<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.6.10</testable.version>
<plugin.compiler.version>3.10.1</plugin.compiler.version>
<plugin.surefire.version>3.0.0-M7</plugin.surefire.version>
<plugin.jar.version>3.3.0</plugin.jar.version>
<plugin.shade.version>3.4.1</plugin.shade.version>
<plugin.source.version>3.2.1</plugin.source.version>
<plugin.javadoc.version>3.4.1</plugin.javadoc.version>
<plugin.gpg.version>3.0.1</plugin.gpg.version>
<plugin.staging.version>1.6.13</plugin.staging.version>
<plugin.maven.version>3.7.0</plugin.maven.version>
<testable.version>0.7.8</testable.version>
</properties>
<profiles>
@@ -73,6 +73,9 @@
<goals>
<goal>jar</goal>
</goals>
<configuration>
<additionalOptions>-Xdoclint:none</additionalOptions>
</configuration>
</execution>
</executions>
</plugin>
@@ -87,6 +90,13 @@
<goals>
<goal>sign</goal>
</goals>
<configuration>
<!-- Prevent `gpg` from using pinentry programs -->
<gpgArguments>
<arg>--pinentry-mode</arg>
<arg>loopback</arg>
</gpgArguments>
</configuration>
</execution>
</executions>
</plugin>

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-parent</artifactId>
<version>0.6.10</version>
<version>0.7.8</version>
<relativePath>../testable-parent</relativePath>
</parent>
<artifactId>testable-processor</artifactId>
@@ -92,6 +92,9 @@
<goals>
<goal>jar</goal>
</goals>
<configuration>
<additionalOptions>-Xdoclint:none</additionalOptions>
</configuration>
</execution>
</executions>
</plugin>
@@ -106,6 +109,13 @@
<goals>
<goal>sign</goal>
</goals>
<configuration>
<!-- Prevent `gpg` from using pinentry programs -->
<gpgArguments>
<arg>--pinentry-mode</arg>
<arg>loopback</arg>
</gpgArguments>
</configuration>
</execution>
</executions>
</plugin>