com.google.template.soy.jbcsrc.ExpressionTester.java Source code

Java tutorial

Introduction

Here is the source code for com.google.template.soy.jbcsrc.ExpressionTester.java

Source

/*
 * Copyright 2015 Google Inc.
 *
 * 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.
 */

package com.google.template.soy.jbcsrc;

import static com.google.common.base.Preconditions.checkArgument;

import com.google.common.base.Joiner;
import com.google.common.base.Objects;
import com.google.common.collect.ImmutableList;
import com.google.common.truth.FailureStrategy;
import com.google.common.truth.Subject;
import com.google.common.truth.SubjectFactory;
import com.google.common.truth.Truth;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.Label;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.Method;
import org.objectweb.asm.util.CheckClassAdapter;

import java.io.PrintWriter;
import java.io.StringWriter;

/**
 * Test-Only utility for testing Expression instances.
 * 
 * <p>Since {@link Expression expressions} are fully encapsulated we can represent them as simple
 * nullary interface methods.  For each expression we will compile an appropriately typed 
 * implementation of an invoker interface.
 */
public final class ExpressionTester {
    private interface Invoker {
        void voidInvoke();
    }

    // These need to be public so that our memory classloader can access them across protection 
    // domains
    public interface IntInvoker extends Invoker {
        int invoke();
    }

    public interface CharInvoker extends Invoker {
        char invoke();
    }

    public interface BooleanInvoker extends Invoker {
        boolean invoke();
    }

    public interface FloatInvoker extends Invoker {
        float invoke();
    }

    public interface LongInvoker extends Invoker {
        long invoke();
    }

    public interface DoubleInvoker extends Invoker {
        double invoke();
    }

    public interface ObjectInvoker extends Invoker {
        Object invoke();
    }

    /**
     * Returns a truth subject that can be used to assert on an {@link Expression}.
     */
    public static ExpressionSubject assertThatExpression(Expression resp) {
        return Truth.assertAbout(FACTORY).that(resp);
    }

    static final class ExpressionSubject extends Subject<ExpressionSubject, Expression> {
        private ClassData compiledClass;
        private Invoker invoker;

        private ExpressionSubject(FailureStrategy strategy, Expression subject) {
            super(strategy, subject);
        }

        ExpressionSubject evaluatesTo(int expected) {
            compile();
            if (((IntInvoker) invoker).invoke() != expected) {
                fail("evaluatesTo", expected);
            }
            return this;
        }

        /**
         * Asserts on the literal code of the expression, use sparingly since it may lead to overly
         * coupled tests.
         */
        void hasCode(String... instructions) {
            compile();
            String formatted = Joiner.on('\n').join(instructions);
            if (!formatted.equals(getSubject().trace().trim())) {
                fail("hasCode", formatted);
            }
        }

        ExpressionSubject evaluatesTo(boolean expected) {
            compile();
            boolean actual;
            try {
                actual = ((BooleanInvoker) invoker).invoke();
            } catch (Throwable t) {
                failWithBadResults("evalutes to", expected, "fails with", t);
                return this;
            }
            if (actual != expected) {
                failWithBadResults("evaluates to", expected, "evaluates to", actual);
            }
            return this;
        }

        ExpressionSubject evaluatesTo(double expected) {
            compile();
            double actual;
            try {
                actual = ((DoubleInvoker) invoker).invoke();
            } catch (Throwable t) {
                failWithBadResults("evalutes to", expected, "fails with", t);
                return this;
            }
            if (actual != expected) {
                failWithBadResults("evaluates to", expected, "evaluates to", actual);
            }
            return this;
        }

        ExpressionSubject evaluatesTo(long expected) {
            compile();
            long actual;
            try {
                actual = ((LongInvoker) invoker).invoke();
            } catch (Throwable t) {
                failWithBadResults("evalutes to", expected, "fails with", t);
                return this;
            }
            if (actual != expected) {
                failWithBadResults("evaluates to", expected, "evaluates to", actual);
            }
            return this;
        }

        ExpressionSubject evaluatesTo(char expected) {
            compile();
            char actual;
            try {
                actual = ((CharInvoker) invoker).invoke();
            } catch (Throwable t) {
                failWithBadResults("evalutes to", expected, "fails with", t);
                return this;
            }
            if (actual != expected) {
                failWithBadResults("evaluates to", expected, "evaluates to", actual);
            }
            return this;
        }

        ExpressionSubject evaluatesTo(Object expected) {
            compile();
            Object actual;
            try {
                actual = ((ObjectInvoker) invoker).invoke();
            } catch (Throwable t) {
                failWithBadResults("evalutes to", expected, "fails with", t);
                return this;
            }
            if (!Objects.equal(actual, expected)) {
                failWithBadResults("evaluates to", expected, "evaluates to", actual);
            }
            return this;
        }

        ExpressionSubject evaluatesToInstanceOf(Class<?> expected) {
            compile();
            Object actual;
            try {
                actual = ((ObjectInvoker) invoker).invoke();
            } catch (Throwable t) {
                failWithBadResults("evalutes to instance of", expected, "fails with", t);
                return this;
            }
            if (!expected.isInstance(actual)) {
                failWithBadResults("evaluates to instance of", expected, "evaluates to", actual);
            }
            return this;
        }

        ExpressionSubject throwsException(Class<? extends Throwable> clazz) {
            return throwsException(clazz, null);
        }

        ExpressionSubject throwsException(Class<? extends Throwable> clazz, String message) {
            compile();
            try {
                invoker.voidInvoke();
            } catch (Throwable t) {
                if (!clazz.isInstance(t)) {
                    failWithBadResults("throws an exception of type", clazz, "fails with", t);
                }
                if (message != null && !t.getMessage().equals(message)) {
                    failWithBadResults("throws an exception with message", message, "fails with", t);
                }
                return this;
            }
            fail("throws an exception");
            return this; // dead code, but the compiler can't prove it
        }

        private void compile() {
            if (invoker == null) {
                try {
                    Class<? extends Invoker> invokerClass = invokerForType(getSubject().resultType());
                    this.compiledClass = createClass(invokerClass, getSubject());
                    this.invoker = load(invokerClass, compiledClass);
                } catch (Throwable t) {
                    throw new RuntimeException("Compilation of" + getDisplaySubject() + " failed", t);
                }
            }
        }
    }

    private static final SubjectFactory<ExpressionSubject, Expression> FACTORY = new SubjectFactory<ExpressionSubject, Expression>() {
        @Override
        public ExpressionSubject getSubject(FailureStrategy fs, Expression that) {
            return new ExpressionSubject(fs, that);
        }
    };

    static <T> T createInvoker(Class<T> clazz, Expression expr) {
        Class<? extends Invoker> expected = invokerForType(expr.resultType());
        checkArgument(clazz.equals(expected), "%s isn't an appropriate invoker type for %s, expected %s", clazz,
                expr.resultType(), expected);
        ClassData data = createClass(clazz.asSubclass(Invoker.class), expr);
        return load(clazz, data);
    }

    private static <T> T load(Class<T> clazz, ClassData data) {
        MemoryClassLoader loader = new MemoryClassLoader(ImmutableList.of(data));
        Class<?> generatedClass;
        try {
            generatedClass = loader.loadClass(data.type().className());
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
        try {
            return generatedClass.asSubclass(clazz).newInstance();
        } catch (InstantiationException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    private static ClassData createClass(Class<? extends Invoker> targetInterface, Expression expr) {
        java.lang.reflect.Method invokeMethod;
        try {
            invokeMethod = targetInterface.getMethod("invoke");
        } catch (NoSuchMethodException | SecurityException e) {
            throw new RuntimeException(e);
        }
        Class<?> returnType = invokeMethod.getReturnType();
        if (!Type.getType(returnType).equals(expr.resultType())) {
            if (!returnType.equals(Object.class) || expr.resultType().getSort() != Type.OBJECT) {
                throw new IllegalArgumentException(targetInterface + " is not appropriate for this expression");
            }
        }
        TypeInfo generatedType = TypeInfo.create(
                ExpressionTester.class.getPackage().getName() + "." + targetInterface.getSimpleName() + "Impl");
        SoyClassWriter cw = SoyClassWriter.builder(generatedType)
                .setAccess(Opcodes.ACC_FINAL | Opcodes.ACC_SUPER | Opcodes.ACC_PUBLIC)
                .implementing(TypeInfo.create(targetInterface)).build();
        BytecodeUtils.defineDefaultConstructor(cw, generatedType);
        Method invoke = Method.getMethod(invokeMethod);
        Statement.returnExpression(expr).writeMethod(Opcodes.ACC_PUBLIC, invoke, cw);

        Method voidInvoke;
        try {
            voidInvoke = Method.getMethod(Invoker.class.getMethod("voidInvoke"));
        } catch (NoSuchMethodException | SecurityException e) {
            throw new RuntimeException(e); // this method definitely exists
        }
        Statement.concat(LocalVariable.createThisVar(generatedType, new Label(), new Label())
                .invoke(MethodRef.create(invokeMethod)).toStatement(), new Statement() {
                    @Override
                    void doGen(CodeBuilder adapter) {
                        adapter.visitInsn(Opcodes.RETURN);
                    }
                }).writeMethod(Opcodes.ACC_PUBLIC, voidInvoke, cw);
        ClassData data = cw.toClassData();
        checkClassData(data);
        return data;
    }

    /**
     * Utility to run the {@link CheckClassAdapter} on the class and print it to a string. for 
     * debugging.
     */
    private static void checkClassData(ClassData clazz) {
        StringWriter sw = new StringWriter();
        CheckClassAdapter.verify(new ClassReader(clazz.data()), ExpressionTester.class.getClassLoader(), false,
                new PrintWriter(sw));
        String result = sw.toString();
        if (!result.isEmpty()) {
            throw new IllegalStateException(result);
        }
    }

    private static Class<? extends Invoker> invokerForType(Type type) {
        switch (type.getSort()) {
        case Type.INT:
            return IntInvoker.class;
        case Type.CHAR:
            return CharInvoker.class;
        case Type.BOOLEAN:
            return BooleanInvoker.class;
        case Type.FLOAT:
            return FloatInvoker.class;
        case Type.LONG:
            return LongInvoker.class;
        case Type.DOUBLE:
            return DoubleInvoker.class;
        case Type.ARRAY:
        case Type.OBJECT:
            return ObjectInvoker.class;
        }
        throw new AssertionError("unsupported type" + type);
    }
}