it works but somehow not

This commit is contained in:
Shautvast 2023-07-21 23:23:20 +02:00
parent 974165580f
commit b48a51fc74
11 changed files with 272 additions and 1 deletions

View file

@ -1,3 +1,4 @@
# Apples # Apples
* universal compare tool * universal compare tool
* compares any to any and shows the diff * compares any to any and shows the diff
* no reflection

View file

@ -15,6 +15,11 @@
<version>5.9.3</version> <version>5.9.3</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-tree</artifactId>
<version>9.4</version>
</dependency>
</dependencies> </dependencies>
<build> <build>

View file

@ -0,0 +1,134 @@
package nl.sander.apples;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.tree.*;
import java.lang.reflect.Modifier;
import java.util.LinkedList;
import java.util.UUID;
import static org.objectweb.asm.Opcodes.*;
class AppleFactory extends ClassVisitor {
public static final String SUPER = javaName(BaseApple.class.getName());
public static final String INIT = "<init>";
public static final String ZERO_ARGS_VOID = "()V";
private boolean isRecord = false;
public AppleFactory() {
super(ASM9);
}
final ClassNode classNode = new ClassNode();
private String classToMap;
private MethodNode compareMethod;
private int localVarIndex = 0;
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
if (superName.equals("java/lang/Record")) {
isRecord = true;
}
this.classToMap = name;
classNode.name = "Apple" + UUID.randomUUID();
classNode.superName = SUPER;
classNode.version = V20;
classNode.access = ACC_PUBLIC;
MethodNode constructor = new MethodNode(ACC_PUBLIC, INIT, ZERO_ARGS_VOID, null, null);
constructor.instructions.add(new VarInsnNode(ALOAD, 0));
constructor.instructions.add(new MethodInsnNode(INVOKESPECIAL, SUPER, INIT, ZERO_ARGS_VOID));
constructor.instructions.add(new InsnNode(RETURN));
classNode.methods.add(constructor);
compareMethod = new MethodNode(ACC_PUBLIC,
"compare", "(Ljava/lang/Object;Ljava/lang/Object;)Lnl/sander/apples/Result;", null, null);
classNode.methods.add(compareMethod);
add(new VarInsnNode(ALOAD,0));
}
public MethodVisitor visitMethod(int access, String methodname,
String desc, String signature, String[] exceptions) {
if (!hasArgs(desc) && access == Modifier.PUBLIC && isRecord ||
(methodname.startsWith("get") || (methodname.startsWith("is")) && desc.equals("()Z"))) {
int startIndex;
if (isRecord) {
startIndex = 0;
} else {
if (methodname.startsWith("is")) {
startIndex = 2;
} else {
startIndex = 3;
}
}
visitGetter(correctName(methodname, startIndex), getReturnType(desc));
}
return null;
}
private void visitGetter(String getterMethodName, String returnType) {
add(new VarInsnNode(ALOAD, 1));
add(new TypeInsnNode(CHECKCAST, javaName(classToMap)));
add(new MethodInsnNode(INVOKEVIRTUAL, classToMap, getterMethodName, "()" + returnType));
add(new VarInsnNode(ALOAD, 2));
add(new TypeInsnNode(CHECKCAST, javaName(classToMap)));
add(new MethodInsnNode(INVOKEVIRTUAL, classToMap, getterMethodName, "()" + returnType));
add(new MethodInsnNode(INVOKESTATIC, "nl/sander/apples/Apples", "compare", "(Ljava/lang/Object;Ljava/lang/Object;)Lnl/sander/apples/Result;"));
add(new VarInsnNode(ASTORE, 3 + (localVarIndex++)));
}
private String correctName(String getterMethodName, int startIndex) {
String tmp = getterMethodName.substring(startIndex);
return tmp.substring(0, 1).toLowerCase() + tmp.substring(1);
}
@Override
public void visitEnd() {
if (localVarIndex < 6) {
add(new InsnNode(3 + localVarIndex));
} else {
add(new LdcInsnNode(localVarIndex));
}
add(new TypeInsnNode(ANEWARRAY, "nl/sander/apples/Result"));
for (int i = 0; i < localVarIndex; i++) {
add(new InsnNode(DUP));
if (i < 6) {
add(new InsnNode(3 + i));
} else {
add(new LdcInsnNode(i));
}
add(new VarInsnNode(ALOAD, 3 + i));
add(new InsnNode(AASTORE));
}
add(new MethodInsnNode(INVOKESTATIC, "nl/sander/apples/Result", "merge", "([Lnl/sander/apples/Result;)Lnl/sander/apples/Result;"));
add(new InsnNode(ARETURN));
}
private void add(AbstractInsnNode ins) {
compareMethod.instructions.add(ins);
}
private String getReturnType(String desc) {
return desc.substring(2);
}
private boolean hasArgs(String desc) {
return desc.charAt(1) != ')';
}
private static String javaName(String className) {
return className.replaceAll("\\.", "/");
}
}

View file

@ -1,10 +1,16 @@
package nl.sander.apples; package nl.sander.apples;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import java.io.FileOutputStream;
import java.util.Map; import java.util.Map;
public class Apples { public class Apples {
private final static Map<Character, String> CHAR_ESCAPES = Map.of('\t', "\\t", '\b', "\\b", '\n', "\\n", '\r', "\\r", '\f', "\\f", '\\', "\\\\"); private final static Map<Character, String> CHAR_ESCAPES = Map.of('\t', "\\t", '\b', "\\b", '\n', "\\n", '\r', "\\r", '\f', "\\f", '\\', "\\\\");
private static final ByteClassLoader generatedClassesLoader = new ByteClassLoader();
public static Result compare(Object left, Object right) { public static Result compare(Object left, Object right) {
if (left == null) { if (left == null) {
return Result.from(right == null, "null != " + asString(right)); return Result.from(right == null, "null != " + asString(right));
@ -22,7 +28,27 @@ public class Apples {
return Result.unequal(asString(left) + " != " + asString(right)); return Result.unequal(asString(left) + " != " + asString(right));
} }
return Result.SAME; if (left instanceof String) {
return Result.from(left.equals(right), () -> asString(left) + " != " + asString(right));
}
try {
ClassReader cr = new ClassReader(left.getClass().getName());
AppleFactory appleFactory = new AppleFactory();
cr.accept(appleFactory, ClassReader.SKIP_FRAMES);
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES );
appleFactory.classNode.accept(classWriter);
byte[] byteArray = classWriter.toByteArray();
try (FileOutputStream f = new FileOutputStream("B.class")) {
f.write(byteArray);
}
generatedClassesLoader.addClass(appleFactory.classNode.name, byteArray);
BaseApple apple = (BaseApple) generatedClassesLoader.loadClass(appleFactory.classNode.name).getConstructor().newInstance();
return apple.compare(left, right);
} catch (Exception e) {
throw new RuntimeException(e);
}
} }
public static Result compare(long left, long right) { public static Result compare(long left, long right) {

View file

@ -0,0 +1,6 @@
package nl.sander.apples;
abstract class BaseApple<T> {
public abstract Result compare(T left, T right);
}

View file

@ -0,0 +1,23 @@
package nl.sander.apples;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
class ByteClassLoader extends ClassLoader {
private final ConcurrentMap<String, Class<?>> classes = new ConcurrentHashMap<>();
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class<?> instance = classes.get(name);
if (instance == null) {
throw new ClassNotFoundException(name);
}
return instance;
}
public void addClass(String name, byte[] bytecode) {
Class<?> classDef = defineClass(name, bytecode, 0, bytecode.length);
classes.put(name, classDef);
}
}

View file

@ -1,6 +1,9 @@
package nl.sander.apples; package nl.sander.apples;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;
public record Result(boolean areEqual, List<String> diffs) { public record Result(boolean areEqual, List<String> diffs) {
public static Result SAME = new Result(true, List.of()); public static Result SAME = new Result(true, List.of());
@ -13,8 +16,24 @@ public record Result(boolean areEqual, List<String> diffs) {
} }
} }
public static Result from(boolean areEqual, Supplier<String> messageSupplier) {
if (!areEqual) {
return new Result(areEqual, List.of(messageSupplier.get()));
} else {
return SAME;
}
}
public static Result unequal(String message) { public static Result unequal(String message) {
return from(false, message); return from(false, message);
} }
public static Result merge(Result... result) {
boolean areEqual = Arrays.stream(result).allMatch(r -> r.areEqual);
List<String> diffs = Arrays.stream(result)
.map(Result::diffs)
.flatMap(List::stream)
.collect(Collectors.toList());
return new Result(areEqual, diffs);
}
} }

View file

@ -0,0 +1,4 @@
package nl.sander.apples;
public record Plum(String core, String peel, boolean juicy, int number, float price, Storage storage) {
}

View file

@ -0,0 +1,14 @@
package nl.sander.apples;
public class PlumApple extends BaseApple<Plum> {
public Result compare(Plum left, Plum right) {
Result core = Apples.compare(left.core(), right.core());
Result peel = Apples.compare(left.peel(), right.peel());
Result juicy = Apples.compare(left.juicy(), right.juicy());
Result price = Apples.compare(left.price(), right.price());
Result number = Apples.compare(left.number(), right.number());
Result storage = Apples.compare(left.storage(), right.storage());
return Result.merge(core, peel, juicy, price, number, storage);
}
}

View file

@ -0,0 +1,33 @@
package nl.sander.apples;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
class RecordsTest {
@Test
void testExample() {
Result comparison = new PlumApple().compare(new Plum("small", "red",true, 1, 1.0F,Storage.HIGH),
new Plum("large", "green",true, 1, 1.0F,Storage.HIGH));
assertFalse(comparison.areEqual());
assertFalse(comparison.diffs().isEmpty());
assertEquals(2, comparison.diffs().size());
assertEquals("\"small\" != \"large\"", comparison.diffs().get(0));
assertEquals("\"red\" != \"green\"", comparison.diffs().get(1));
}
@Test
void testRecords() {
Result comparison = Apples.compare(new Plum("small", "red",true, 1, 1.0F,Storage.HIGH),
new Plum("large", "green",true, 1, 1.0F,Storage.HIGH));
assertFalse(comparison.areEqual());
assertFalse(comparison.diffs().isEmpty());
assertEquals(2, comparison.diffs().size());
assertEquals("\"small\" != \"large\"", comparison.diffs().get(0));
assertEquals("\"red\" != \"green\"", comparison.diffs().get(1));
}
}

View file

@ -0,0 +1,6 @@
package nl.sander.apples;
public enum Storage {
HIGH,
LOW
}