/* * 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.template.soy.jbcsrc.BytecodeUtils.STRING_TYPE; import static com.google.template.soy.jbcsrc.BytecodeUtils.constant; import static com.google.template.soy.jbcsrc.BytecodeUtils.constantNull; import static com.google.template.soy.jbcsrc.ExpressionTester.assertThatExpression; import static com.google.template.soy.jbcsrc.SoyExpression.forList; import static com.google.template.soy.jbcsrc.SoyExpression.forSanitizedString; import static com.google.template.soy.jbcsrc.SoyExpression.forString; import com.google.template.soy.data.SanitizedContent.ContentKind; import com.google.template.soy.data.UnsafeSanitizedContentOrdainer; import com.google.template.soy.data.internal.ListImpl; import com.google.template.soy.data.restricted.BooleanData; import com.google.template.soy.data.restricted.FloatData; import com.google.template.soy.data.restricted.IntegerData; import com.google.template.soy.data.restricted.StringData; import com.google.template.soy.types.aggregate.ListType; import com.google.template.soy.types.primitive.AnyType; import com.google.template.soy.types.primitive.FloatType; import com.google.template.soy.types.primitive.IntType; import com.google.template.soy.types.primitive.StringType; import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.objectweb.asm.Type; /** Tests for {@link SoyExpression} */ @RunWith(JUnit4.class) public class SoyExpressionTest { @Test public void testIntExpressions() { SoyExpression expr = SoyExpression.forInt(constant(12L)); assertThatExpression(expr).evaluatesTo(12L); assertThatExpression(expr.box()).evaluatesTo(IntegerData.forValue(12)); assertThatExpression(expr.box().unboxAs(long.class)).evaluatesTo(12L); assertThatExpression(expr.coerceToDouble()).evaluatesTo(12D); assertThatExpression( SoyExpression.forSoyValue(AnyType.getInstance(), expr.box()).coerceToDouble()) .evaluatesTo(12D); assertThatExpression(expr.coerceToBoolean()).evaluatesTo(true); assertThatExpression(SoyExpression.forInt(constant(0L)).coerceToBoolean()).evaluatesTo(false); assertThatExpression(expr.coerceToString()).evaluatesTo("12"); } @Test public void testFloatExpressions() { SoyExpression expr = SoyExpression.forFloat(constant(12.34D)); assertThatExpression(expr).evaluatesTo(12.34D); assertThatExpression(expr.box()).evaluatesTo(FloatData.forValue(12.34D)); assertThatExpression(expr.box().coerceToDouble()).evaluatesTo(12.34D); assertThatExpression( SoyExpression.forSoyValue(FloatType.getInstance(), expr.box()).coerceToString()) .evaluatesTo("12.34"); assertThatExpression(expr.coerceToBoolean()).evaluatesTo(true); assertThatExpression(SoyExpression.forFloat(constant(0D)).coerceToBoolean()).evaluatesTo(false); assertThatExpression(expr.coerceToString()).evaluatesTo("12.34"); } @Test public void testBooleanExpressions() { SoyExpression expr = SoyExpression.FALSE; assertThatExpression(expr).evaluatesTo(false); // sanity assertThatExpression(expr.box()).evaluatesTo(BooleanData.FALSE); assertThatExpression(expr.box().coerceToBoolean()).evaluatesTo(false); assertThatExpression(expr.coerceToString()).evaluatesTo("false"); expr = SoyExpression.TRUE; assertThatExpression(expr).evaluatesTo(true); assertThatExpression(expr.box()).evaluatesTo(BooleanData.TRUE); assertThatExpression(expr.box().coerceToBoolean()).evaluatesTo(true); assertThatExpression(expr.coerceToString()).evaluatesTo("true"); } @Test public void testNullExpression() { assertThatExpression(SoyExpression.NULL).evaluatesTo(null); assertThatExpression(SoyExpression.NULL.box()).evaluatesTo(null); assertThatExpression(SoyExpression.NULL.box().unboxAs(Object.class)).evaluatesTo(null); assertThatExpression(SoyExpression.NULL.coerceToBoolean()).evaluatesTo(false); assertThatExpression(SoyExpression.NULL.coerceToString()).evaluatesTo("null"); } @Test public void testSanitizedExpressions() { assertThatExpression(forSanitizedString(constant("foo"), ContentKind.ATTRIBUTES).box()) .evaluatesTo(UnsafeSanitizedContentOrdainer.ordainAsSafe("foo", ContentKind.ATTRIBUTES)); assertThatExpression( forSanitizedString(constant("foo"), ContentKind.ATTRIBUTES).coerceToBoolean()) .evaluatesTo(true); } @Test public void testStringExpression() { assertThatExpression(forString(constantNull(STRING_TYPE)).coerceToBoolean()).evaluatesTo(false); assertThatExpression(forString(constantNull(STRING_TYPE)).box()).evaluatesTo(null); assertThatExpression(forString(constant("")).coerceToBoolean()).evaluatesTo(false); assertThatExpression(forString(constant("")).box()).evaluatesTo(StringData.EMPTY_STRING); assertThatExpression(forString(constant("truthy")).coerceToBoolean()).evaluatesTo(true); } @Test public void testListExpression() { ListType list = ListType.of(IntType.getInstance()); assertThatExpression(forList(list, constantNull(Type.getType(List.class))).coerceToBoolean()) .evaluatesTo(false); assertThatExpression(forList(list, constantNull(Type.getType(List.class))).box()) .evaluatesTo(null); assertThatExpression(forList(list, MethodRef.IMMUTABLE_LIST_OF.invoke()).coerceToBoolean()) .evaluatesTo(true); // SoyList uses Object identity for equality so we can't really assert on the value. assertThatExpression(forList(list, MethodRef.IMMUTABLE_LIST_OF.invoke()).box()) .evaluatesToInstanceOf(ListImpl.class); } // Tests for a bug where the generic boxing code would cause ASM to emit an invalid frame. // // for example, assume a nullable string is at the top of the stack. Then SoyExpression would // emit the following code to box it while preserving null. // // DUP // IFNULL L1 // INVOKESTATIC StringData.forValue // L1: // // So when execution arrives at L1 there should either be a nullreference or a StringData object // at the top of the stack. Howerver L1 is also the target of a jump (the IFNULL condition), so // ASM will generate a stack frame at this location. // (see https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.7.4 for background // on stack frames). // So ASM tries to determine the type of the top item on the stack and to do so it compares the // type at the DUP instruction to the type at the INVOKESTATIC. The former is String and the // latter is StringData. The common supertype of this is Object, so ASM outputs a stack frame // that says the top of the stack is an Object instead of StringData. This means if we happen to // invoke a StringData method next we will get a verification error. @Test public void testBoxNullable() { MethodRef stringDataGetValue = MethodRef.create(StringData.class, "getValue"); SoyExpression nullableString = SoyExpression.forString(constant("hello").asNullable()); assertThatExpression(nullableString).evaluatesTo("hello"); assertThatExpression(nullableString.box().invoke(stringDataGetValue)).evaluatesTo("hello"); } // similar to the above, but in the unboxing codepath @Test public void testUnboxNullable() { SoyExpression nullableString = SoyExpression.forSoyValue( StringType.getInstance(), MethodRef.STRING_DATA_FOR_VALUE.invoke(constant("hello")).asNullable()); assertThatExpression(nullableString).evaluatesTo(StringData.forValue("hello")); assertThatExpression(nullableString.unboxAs(String.class).invoke(MethodRef.STRING_IS_EMPTY)) .evaluatesTo(false); } }