
net.amygdalum.testrecorder.fakeio.FakeIO Maven / Gradle / Ivy
The newest version!
package net.amygdalum.testrecorder.fakeio;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toSet;
import static net.amygdalum.testrecorder.asm.ByteCode.argumentTypesFrom;
import static net.amygdalum.testrecorder.runtime.GenericObject.copyArrayValues;
import static net.amygdalum.testrecorder.runtime.GenericObject.copyField;
import static net.amygdalum.testrecorder.util.Types.allFields;
import static net.amygdalum.testrecorder.util.Types.getDeclaredMethod;
import static net.amygdalum.testrecorder.util.Types.isLiteral;
import static org.hamcrest.CoreMatchers.equalTo;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.instrument.Instrumentation;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.stream.Stream;
import org.hamcrest.Matcher;
import org.hamcrest.StringDescription;
import net.amygdalum.testrecorder.runtime.Aspect;
import net.amygdalum.testrecorder.runtime.GenericObjectException;
import net.amygdalum.testrecorder.runtime.Invocation;
import net.bytebuddy.agent.ByteBuddyAgent;
public class FakeIO {
public static final Object NO_RESULT = new Object();
private static FakeIOTransformer fakeIOTransformer = install();
private static Map faked = new HashMap<>();
private Class> clazz;
private List interactions;
private FakeIO(Class> clazz) {
this.clazz = clazz;
this.interactions = new ArrayList<>();
}
public static Object callFake(String name, Object instance, String methodName, String methodDesc, Object... varargs) {
FakeIO fake = faked.get(name);
if (fake == null) {
return NO_RESULT;
}
Invocation invocation = Invocation.capture(instance, fake.clazz, methodName, methodDesc);
return fake.call(invocation, varargs);
}
public Class> getClazz() {
return clazz;
}
public static FakeIO fake(Object... instances) {
Stream> classes = Arrays.stream(instances)
.map(instance -> instance.getClass());
Class> clazz = classes
.distinct()
.findFirst()
.orElse(Proxy.class);
return faked.computeIfAbsent(clazz.getName(), key -> new FakeIO(clazz));
}
public static FakeIO fake(Class> clazz) {
return faked.computeIfAbsent(clazz.getName(), key -> new FakeIO(clazz));
}
private static FakeIOTransformer install() {
Instrumentation inst = ByteBuddyAgent.install();
installBridge(inst);
return (FakeIOTransformer) new FakeIOTransformer().attach(inst);
}
private static void installBridge(Instrumentation inst) {
try {
String bridgeClassName = "net.amygdalum.testrecorder.fakeio.bridge.BridgedFakeIO";
inst.appendToBootstrapClassLoaderSearch(jarfile(bridgeClassName));
Class> bridgedFakeIOClass = Class.forName(bridgeClassName, true, null);
MethodHandle callFakeMethod = MethodHandles.lookup().findStatic(FakeIO.class, "callFake", MethodType.methodType(Object.class, String.class, Object.class, String.class, String.class, Object[].class));
bridgedFakeIOClass.getField("callFake").set(null, callFakeMethod);
bridgedFakeIOClass.getField("NO_RESULT").set(null, NO_RESULT);
} catch (ReflectiveOperationException | IOException e) {
throw new RuntimeException("failed installing fake bridge", e);
}
}
private static JarFile jarfile(String bridgeClassName) throws IOException {
String bridge = bridgeClassName.replace('.', '/') + ".class";
InputStream resourceStream = FakeIO.class.getResourceAsStream("/" + bridge);
if (resourceStream == null) {
throw new FileNotFoundException(bridge);
}
try (InputStream inputStream = resourceStream) {
File agentJar = File.createTempFile("agent", "jar");
agentJar.deleteOnExit();
Manifest manifest = new Manifest();
try (JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(agentJar), manifest)) {
jarOutputStream.putNextEntry(new JarEntry(bridge));
byte[] buffer = new byte[4096];
int index;
while ((index = inputStream.read(buffer)) != -1) {
jarOutputStream.write(buffer, 0, index);
}
jarOutputStream.closeEntry();
}
return new JarFile(agentJar);
}
}
public Input fakeInput(Aspect aspect) {
Input input = new Input(this, aspect.getName(), aspect.getDesc());
interactions.add(input);
return input;
}
public boolean matches(Object instance, Class> staticClass) {
return instance != null && clazz.isInstance(instance)
|| staticClass == clazz;
}
public Output fakeOutput(Aspect aspect) {
Output output = new Output(this, aspect.getName(), aspect.getDesc());
interactions.add(output);
return output;
}
public FakeIO setup() {
Set> classes = resolveClasses();
Set methods = resolveMethods();
fakeIOTransformer.addClasses(classes);
fakeIOTransformer.addMethods(methods);
fakeIOTransformer.restart(classes.toArray(new Class[0]));
return this;
}
private Set> resolveClasses() {
return interactions.stream().map(interaction -> interaction.resolve(clazz)).collect(toSet());
}
private Set resolveMethods() {
return interactions.stream().map(Interaction::getMethod).collect(toSet());
}
private Optional findInteraction(Invocation invocation) {
return interactions.stream()
.filter(interaction -> interaction.matches(invocation))
.findFirst();
}
public Object call(Invocation invocation, Object... varargs) {
Optional interaction = findInteraction(invocation);
if (interaction.isPresent()) {
return interaction.get().call(invocation, varargs);
} else {
return NO_RESULT;
}
}
public void verify() {
try {
for (Interaction interaction : interactions) {
interaction.verify();
}
} finally {
interactions.clear();
faked.remove(clazz.getName(), this);
reset();
}
}
public static void reset() {
faked.clear();
fakeIOTransformer.reset();
}
public abstract static class Interaction {
protected FakeIO io;
protected String methodName;
protected String methodDesc;
protected List invocationData;
protected Map bindableInvocations;
public Interaction(FakeIO io, String methodName, String methodDesc) {
this.io = io;
this.methodName = methodName;
this.methodDesc = methodDesc;
this.invocationData = new ArrayList<>();
this.bindableInvocations = new HashMap<>();
}
public Class> resolve(Class> clazz) {
try {
Method method = getDeclaredMethod(clazz, methodName, argumentTypesFrom(methodDesc, clazz.getClassLoader()));
return method.getDeclaringClass();
} catch (ReflectiveOperationException e) {
throw new RuntimeException("failed to resolve class of virtual call", e);
}
}
public boolean matches(Invocation invocation) {
return io.matches(invocation.instance, invocation.clazz)
&& methodName.equals(invocation.methodName)
&& methodDesc.equals(invocation.methodDesc);
}
public Interaction addStatic(Object result, Object... args) {
StaticInvocation invocation = new StaticInvocation();
InvocationData data = new InvocationData(invocation, result, args);
invocationData.add(data);
return this;
}
public Interaction addVirtual(Object instance, Object result, Object... args) {
BoundInvocation invocation = new BoundInvocation(instance);
InvocationData data = new InvocationData(invocation, result, args);
invocationData.add(data);
return this;
}
public Interaction addFreeVirtual(int id, Object result, Object... args) {
BindableInvocation invocation = bindableInvocations.computeIfAbsent(id, key -> new BindableInvocation());
InvocationData data = new InvocationData(invocation, result, args);
invocationData.add(data);
return this;
}
public Object call(Invocation invocation, Object[] arguments) {
Iterator invocationDataIterator = invocationData.iterator();
while (invocationDataIterator.hasNext()) {
InvocationData next = invocationDataIterator.next();
if (next.matches(invocation, arguments)) {
Object result = call(next, arguments);
invocationDataIterator.remove();
return result;
}
}
String methodInvocation = invocation.clazz.getSimpleName() + "." + signatureFor(arguments);
if (invocationData.isEmpty()) {
throw new AssertionError("surplus invocation " + methodInvocation + "\n\nIf the input was recorded ensure that all call sites are recorded");
} else {
throw new AssertionError("mismatching invocation " + methodInvocation + "\n\nIf the input was recorded ensure that all call sites are recorded");
}
}
public abstract Object call(InvocationData data, Object[] arguments);
public String getMethod() {
return methodName + methodDesc;
}
public Input fakeInput(Aspect aspect) {
return io.fakeInput(aspect);
}
public Output fakeOutput(Aspect aspect) {
return io.fakeOutput(aspect);
}
public FakeIO setup() {
return io.setup();
}
protected String signatureFor(Object[] args) {
return Arrays.stream(args)
.map(arg -> arg instanceof Matcher> ? (Matcher>) arg : equalTo(arg))
.map(matcher -> StringDescription.toString(matcher))
.collect(joining(", ", methodName + "(", ")"));
}
public abstract void verify();
}
public static class Input extends Interaction {
public Input(FakeIO io, String methodName, String methodDesc) {
super(io, methodName, methodDesc);
}
@Override
public Object call(InvocationData data, Object[] arguments) {
try {
sync(data.args, arguments);
return data.result;
} catch (GenericObjectException e) {
throw new AssertionError("failed synchronizing input " + signatureFor(data.args) + " with " + signatureFor(arguments) + ": " + e.getMessage());
}
}
public void sync(Object[] fromArgs, Object[] toArgs) {
for (int i = 0; i < toArgs.length; i++) {
sync(fromArgs[i], toArgs[i]);
}
}
public void sync(Object from, Object to) {
if (from == to) {
return;
} else if (from == null) {
throw new GenericObjectException();
} else if (to == null) {
throw new GenericObjectException();
}
Class> current = from.getClass();
if (isLiteral(current)) {
if (from.equals(to)) {
return;
} else {
throw new GenericObjectException();
}
}
if (current.isArray()) {
copyArrayValues(from, to);
return;
}
for (Field field : allFields(current)) {
copyField(field, from, to);
}
}
@Override
public void verify() {
if (!invocationData.isEmpty()) {
StringBuilder msg = new StringBuilder("expected but not found:");
for (InvocationData invocationDataItem : invocationData) {
String expected = io.getClazz().getSimpleName() + "." + signatureFor(invocationDataItem.args);
msg.append("\nexpected but not received call " + expected);
}
throw new AssertionError(msg);
}
}
}
public static class Output extends Interaction {
public Output(FakeIO io, String methodName, String methodDesc) {
super(io, methodName, methodDesc);
}
@Override
public Object call(InvocationData data, Object[] arguments) {
Object[] args = data.args;
if (!verify(args, arguments)) {
String expected = signatureFor(args);
String found = signatureFor(arguments);
throw new AssertionError("expected output:\n" + expected + "\nbut found:\n" + found);
}
return data.result;
}
private boolean verify(Object[] fromArgs, Object[] toArgs) {
for (int i = 0; i < fromArgs.length; i++) {
Object from = fromArgs[i];
Object to = toArgs[i];
if (from instanceof Matcher>) {
if (!((Matcher>) from).matches(to)) {
return false;
}
} else if (from == null) {
if (to != null) {
return false;
}
} else if (to == null) {
return false;
} else if (from instanceof boolean[]) {
if (!(to instanceof boolean[] && Arrays.equals((boolean[]) from, (boolean[]) to))) {
return false;
}
} else if (from instanceof byte[]) {
if (!(to instanceof byte[] && Arrays.equals((byte[]) from, (byte[]) to))) {
return false;
}
} else if (from instanceof short[]) {
if (!(to instanceof short[] && Arrays.equals((short[]) from, (short[]) to))) {
return false;
}
} else if (from instanceof int[]) {
if (!(to instanceof int[] && Arrays.equals((int[]) from, (int[]) to))) {
return false;
}
} else if (from instanceof long[]) {
if (!(to instanceof long[] && Arrays.equals((long[]) from, (long[]) to))) {
return false;
}
} else if (from instanceof float[]) {
if (!(to instanceof float[] && Arrays.equals((float[]) from, (float[]) to))) {
return false;
}
} else if (from instanceof double[]) {
if (!(to instanceof double[] && Arrays.equals((double[]) from, (double[]) to))) {
return false;
}
} else if (from instanceof char[]) {
if (!(to instanceof char[] && Arrays.equals((char[]) from, (char[]) to))) {
return false;
}
} else if (from instanceof Object[]) {
if (!(to instanceof Object[] && Arrays.equals((Object[]) from, (Object[]) to))) {
return false;
}
} else if (!from.equals(to)) {
return false;
}
}
return true;
}
@Override
public void verify() {
if (!invocationData.isEmpty()) {
StringBuilder msg = new StringBuilder("expected but not found:");
for (InvocationData invocationDataItem : invocationData) {
String expected = signatureFor(invocationDataItem.args);
msg.append("\nexpected but not received call " + expected);
}
throw new AssertionError(msg);
}
}
}
protected static class InvocationData {
public SelfSpecification self;
public Object result;
public Object[] args;
public InvocationData(SelfSpecification self, Object result, Object[] args) {
this.self = self;
this.result = result;
this.args = args;
}
public boolean matches(Invocation invocation, Object[] arguments) {
return self.matches(invocation.instance)
&& args != null
&& arguments != null
&& args.length == arguments.length;
}
}
protected interface SelfSpecification {
boolean matches(Object instance);
}
protected static class AnyInvocation implements SelfSpecification {
@Override
public boolean matches(Object instance) {
return true;
}
}
protected static class StaticInvocation implements SelfSpecification {
@Override
public boolean matches(Object instance) {
return instance == null;
}
}
protected static class BoundInvocation implements SelfSpecification {
private Object instance;
public BoundInvocation(Object instance) {
this.instance = instance;
}
@Override
public boolean matches(Object instance) {
return this.instance == instance;
}
}
protected static class BindableInvocation implements SelfSpecification {
private Object instance;
public BindableInvocation() {
}
@Override
public boolean matches(Object instance) {
if (this.instance == null) {
this.instance = instance;
}
return this.instance == instance;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy