jaroslav@273: /** jaroslav@273: * Back 2 Browser Bytecode Translator jaroslav@273: * Copyright (C) 2012 Jaroslav Tulach jaroslav@273: * jaroslav@273: * This program is free software: you can redistribute it and/or modify jaroslav@273: * it under the terms of the GNU General Public License as published by jaroslav@273: * the Free Software Foundation, version 2 of the License. jaroslav@273: * jaroslav@273: * This program is distributed in the hope that it will be useful, jaroslav@273: * but WITHOUT ANY WARRANTY; without even the implied warranty of jaroslav@273: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the jaroslav@273: * GNU General Public License for more details. jaroslav@273: * jaroslav@273: * You should have received a copy of the GNU General Public License jaroslav@273: * along with this program. Look for COPYING file in the top folder. jaroslav@273: * If not, see http://opensource.org/licenses/GPL-2.0. jaroslav@273: */ jaroslav@346: package org.apidesign.bck2brwsr.vmtest; jaroslav@273: jaroslav@346: import java.io.File; jaroslav@346: import java.io.FileWriter; jaroslav@346: import java.io.IOException; jaroslav@346: import java.io.InputStream; jaroslav@273: import java.lang.reflect.Method; jaroslav@346: import java.net.URL; jaroslav@346: import java.util.Enumeration; jaroslav@297: import java.util.Map; jaroslav@297: import java.util.WeakHashMap; jaroslav@350: import java.util.logging.Level; jaroslav@350: import java.util.logging.Logger; jaroslav@273: import javax.script.Invocable; jaroslav@273: import javax.script.ScriptEngine; jaroslav@273: import javax.script.ScriptEngineManager; jaroslav@349: import org.apidesign.bck2brwsr.launcher.Bck2BrwsrLauncher; jaroslav@346: import org.apidesign.vm4brwsr.Bck2Brwsr; jaroslav@273: import org.testng.Assert; jaroslav@273: import org.testng.ITest; jaroslav@350: import org.testng.annotations.BeforeTest; jaroslav@273: import org.testng.annotations.Factory; jaroslav@273: import org.testng.annotations.Test; jaroslav@273: jaroslav@273: /** A TestNG {@link Factory} that seeks for {@link Compare} annotations jaroslav@273: * in provided class and builds set of tests that compare the computations jaroslav@273: * in real as well as JavaScript virtual machines. Use as:
jaroslav@273:  * {@code @}{@link Factory} public static create() {
jaroslav@346:  *   return @{link VMTest}.{@link #create(YourClass.class);
jaroslav@273:  * }
jaroslav@273: * jaroslav@273: * @author Jaroslav Tulach jaroslav@273: */ jaroslav@346: public final class VMTest implements ITest { jaroslav@280: private final Run first, second; jaroslav@273: private final Method m; jaroslav@273: jaroslav@346: private VMTest(Method m, Run first, Run second) { jaroslav@273: this.first = first; jaroslav@273: this.second = second; jaroslav@273: this.m = m; jaroslav@273: } jaroslav@273: jaroslav@346: /** Inspects clazz and for each {@lik Compare} method creates jaroslav@347: * instances of tests. Each instance runs the test in different virtual jaroslav@346: * machine and at the end they compare the results. jaroslav@346: * jaroslav@346: * @param clazz the class to inspect jaroslav@346: * @return the set of created tests jaroslav@346: */ jaroslav@273: public static Object[] create(Class clazz) { jaroslav@273: Method[] arr = clazz.getMethods(); jaroslav@349: Object[] ret = new Object[5 * arr.length]; jaroslav@273: int cnt = 0; jaroslav@273: for (Method m : arr) { jaroslav@273: Compare c = m.getAnnotation(Compare.class); jaroslav@273: if (c == null) { jaroslav@273: continue; jaroslav@273: } jaroslav@349: final Run real = new Run(m, 0); jaroslav@349: final Run js = new Run(m, 1); jaroslav@349: final Run brwsr = new Run(m, 2); jaroslav@273: ret[cnt++] = real; jaroslav@273: ret[cnt++] = js; jaroslav@349: ret[cnt++] = brwsr; jaroslav@346: ret[cnt++] = new VMTest(m, real, js); jaroslav@349: ret[cnt++] = new VMTest(m, real, brwsr); jaroslav@273: } jaroslav@273: Object[] r = new Object[cnt]; jaroslav@273: for (int i = 0; i < cnt; i++) { jaroslav@273: r[i] = ret[i]; jaroslav@273: } jaroslav@273: return r; jaroslav@273: } jaroslav@346: jaroslav@346: /** Test that compares the previous results. jaroslav@346: * @throws Throwable jaroslav@346: */ jaroslav@280: @Test(dependsOnGroups = "run") public void compareResults() throws Throwable { jaroslav@280: Object v1 = first.value; jaroslav@280: Object v2 = second.value; jaroslav@280: if (v1 instanceof Number) { jaroslav@280: v1 = ((Number)v1).doubleValue(); jaroslav@280: } jaroslav@296: Assert.assertEquals(v2, v1, "Comparing results"); jaroslav@280: } jaroslav@280: jaroslav@346: /** Test name. jaroslav@346: * @return name of the tested method followed by a suffix jaroslav@346: */ jaroslav@280: @Override jaroslav@280: public String getTestName() { jaroslav@349: return m.getName() + "[Compare " + second.typeName() + "]"; jaroslav@280: } jaroslav@346: jaroslav@346: /** Helper method that inspects the classpath and loads given resource jaroslav@346: * (usually a class file). Used while running tests in Rhino. jaroslav@346: * jaroslav@346: * @param name resource name to find jaroslav@346: * @return the array of bytes in the given resource jaroslav@346: * @throws IOException I/O in case something goes wrong jaroslav@346: */ jaroslav@346: public static byte[] read(String name) throws IOException { jaroslav@346: URL u = null; jaroslav@346: Enumeration en = VMTest.class.getClassLoader().getResources(name); jaroslav@346: while (en.hasMoreElements()) { jaroslav@346: u = en.nextElement(); jaroslav@346: } jaroslav@346: if (u == null) { jaroslav@346: throw new IOException("Can't find " + name); jaroslav@346: } jaroslav@346: try (InputStream is = u.openStream()) { jaroslav@346: byte[] arr; jaroslav@346: arr = new byte[is.available()]; jaroslav@346: int offset = 0; jaroslav@346: while (offset < arr.length) { jaroslav@346: int len = is.read(arr, offset, arr.length - offset); jaroslav@346: if (len == -1) { jaroslav@346: throw new IOException("Can't read " + name); jaroslav@346: } jaroslav@346: offset += len; jaroslav@346: } jaroslav@346: return arr; jaroslav@346: } jaroslav@346: } jaroslav@346: jaroslav@280: public static final class Run implements ITest { jaroslav@280: private final Method m; jaroslav@349: private final int type; jaroslav@280: Object value; jaroslav@297: private Invocable code; jaroslav@297: private CharSequence codeSeq; jaroslav@347: private static final Map compiled = new WeakHashMap<>(); jaroslav@350: private Object inst; jaroslav@280: jaroslav@349: private Run(Method m, int type) { jaroslav@280: this.m = m; jaroslav@349: this.type = type; jaroslav@350: try { jaroslav@350: initialize(); jaroslav@350: } catch (Throwable ex) { jaroslav@350: Logger.getLogger(VMTest.class.getName()).log(Level.SEVERE, null, ex); jaroslav@350: } jaroslav@280: } jaroslav@280: jaroslav@297: private void compileTheCode(Class clazz) throws Exception { jaroslav@297: final Object[] data = compiled.get(clazz); jaroslav@297: if (data != null) { jaroslav@297: code = (Invocable) data[0]; jaroslav@297: codeSeq = (CharSequence) data[1]; jaroslav@280: return; jaroslav@273: } jaroslav@280: StringBuilder sb = new StringBuilder(); jaroslav@346: Bck2Brwsr.generate(sb, VMTest.class.getClassLoader()); jaroslav@280: jaroslav@280: ScriptEngineManager sem = new ScriptEngineManager(); jaroslav@347: ScriptEngine mach = sem.getEngineByExtension("js"); jaroslav@303: jaroslav@347: sb.append( jaroslav@347: "\nvar vm = bck2brwsr(org.apidesign.bck2brwsr.vmtest.VMTest.read);" jaroslav@347: + "\nfunction initVM() { return vm; };" jaroslav@347: + "\n"); jaroslav@280: jaroslav@347: Object res = mach.eval(sb.toString()); jaroslav@347: Assert.assertTrue(mach instanceof Invocable, "It is invocable object: " + res); jaroslav@347: code = (Invocable) mach; jaroslav@280: codeSeq = sb; jaroslav@297: compiled.put(clazz, new Object[] { code, codeSeq }); jaroslav@280: } jaroslav@350: jaroslav@350: private void initialize() throws Throwable { jaroslav@350: if (type == 1) { jaroslav@350: compileTheCode(m.getDeclaringClass()); jaroslav@350: Object vm = code.invokeFunction("initVM"); jaroslav@350: inst = code.invokeMethod(vm, "loadClass", m.getDeclaringClass().getName()); jaroslav@350: } else if (type == 2) { jaroslav@350: inst = addBrowserMethod(m.getDeclaringClass(), m.getName()); jaroslav@350: } jaroslav@350: } jaroslav@280: jaroslav@280: @Test(groups = "run") public void executeCode() throws Throwable { jaroslav@349: if (type == 1) { jaroslav@273: try { jaroslav@296: value = code.invokeMethod(inst, m.getName() + "__" + computeSignature(m)); jaroslav@273: } catch (Exception ex) { jaroslav@346: throw new AssertionError(dumpJS(codeSeq)).initCause(ex); jaroslav@273: } jaroslav@349: } else if (type == 2) { jaroslav@350: Bck2BrwsrLauncher.MethodInvocation c = (Bck2BrwsrLauncher.MethodInvocation) inst; jaroslav@350: execBrowser(); jaroslav@349: value = c.toString(); jaroslav@273: } else { jaroslav@273: value = m.invoke(m.getDeclaringClass().newInstance()); jaroslav@273: } jaroslav@273: } jaroslav@280: @Override jaroslav@280: public String getTestName() { jaroslav@349: return m.getName() + "[" + typeName() + "]"; jaroslav@349: } jaroslav@349: jaroslav@349: final String typeName() { jaroslav@349: switch (type) { jaroslav@349: case 0: return "Java"; jaroslav@349: case 1: return "JavaScript"; jaroslav@349: case 2: return "Browser"; jaroslav@349: default: return "Unknown type " + type; jaroslav@349: } jaroslav@273: } jaroslav@296: jaroslav@296: private static String computeSignature(Method m) { jaroslav@296: StringBuilder sb = new StringBuilder(); jaroslav@296: appendType(sb, m.getReturnType()); jaroslav@296: for (Class c : m.getParameterTypes()) { jaroslav@296: appendType(sb, c); jaroslav@296: } jaroslav@296: return sb.toString(); jaroslav@296: } jaroslav@296: jaroslav@296: private static void appendType(StringBuilder sb, Class t) { jaroslav@296: if (t == null) { jaroslav@296: sb.append('V'); jaroslav@296: return; jaroslav@296: } jaroslav@296: if (t.isPrimitive()) { jaroslav@296: int ch = -1; jaroslav@296: if (t == int.class) { jaroslav@296: ch = 'I'; jaroslav@296: } jaroslav@296: if (t == short.class) { jaroslav@296: ch = 'S'; jaroslav@296: } jaroslav@296: if (t == byte.class) { jaroslav@296: ch = 'B'; jaroslav@296: } jaroslav@296: if (t == boolean.class) { jaroslav@296: ch = 'Z'; jaroslav@296: } jaroslav@296: if (t == long.class) { jaroslav@296: ch = 'J'; jaroslav@296: } jaroslav@296: if (t == float.class) { jaroslav@296: ch = 'F'; jaroslav@296: } jaroslav@296: if (t == double.class) { jaroslav@296: ch = 'D'; jaroslav@296: } jaroslav@296: assert ch != -1 : "Unknown primitive type " + t; jaroslav@296: sb.append((char)ch); jaroslav@296: return; jaroslav@296: } jaroslav@296: if (t.isArray()) { jaroslav@296: sb.append("_3"); jaroslav@296: appendType(sb, t.getComponentType()); jaroslav@296: return; jaroslav@296: } jaroslav@296: sb.append('L'); jaroslav@296: sb.append(t.getName().replace('.', '_')); jaroslav@296: sb.append("_2"); jaroslav@296: } jaroslav@273: } jaroslav@346: jaroslav@346: static StringBuilder dumpJS(CharSequence sb) throws IOException { jaroslav@346: File f = File.createTempFile("execution", ".js"); jaroslav@347: try (FileWriter w = new FileWriter(f)) { jaroslav@347: w.append(sb); jaroslav@347: } jaroslav@346: return new StringBuilder(f.getPath()); jaroslav@346: } jaroslav@350: jaroslav@350: private static Bck2BrwsrLauncher launcher; jaroslav@350: jaroslav@350: private static synchronized Bck2BrwsrLauncher.MethodInvocation addBrowserMethod( jaroslav@350: Class clazz, String name jaroslav@350: ) { jaroslav@350: if (launcher == null) { jaroslav@350: launcher = new Bck2BrwsrLauncher(); jaroslav@350: launcher.setTimeout(5000); jaroslav@350: } jaroslav@350: return launcher.addMethod(clazz, name); jaroslav@350: } jaroslav@350: jaroslav@350: private static void execBrowser() throws Exception { jaroslav@350: Bck2BrwsrLauncher l = clearBrowser(); jaroslav@350: if (l != null) { jaroslav@350: l.execute(); jaroslav@350: } jaroslav@350: } jaroslav@350: private static synchronized Bck2BrwsrLauncher clearBrowser() { jaroslav@350: Bck2BrwsrLauncher l = launcher; jaroslav@350: launcher = null; jaroslav@350: return l; jaroslav@350: } jaroslav@273: }