/* * 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.sharedpasses.render; import static com.google.common.truth.Truth.assertThat; import static com.google.template.soy.shared.SharedTestUtils.untypedTemplateBodyForExpression; import static org.junit.Assert.fail; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.template.soy.SoyFileSetParserBuilder; import com.google.template.soy.SoyModule; import com.google.template.soy.base.SourceLocation; import com.google.template.soy.data.SoyDataException; import com.google.template.soy.data.SoyDict; import com.google.template.soy.data.SoyList; import com.google.template.soy.data.SoyRecord; import com.google.template.soy.data.SoyValue; import com.google.template.soy.data.SoyValueConverter; import com.google.template.soy.data.SoyValueProvider; import com.google.template.soy.data.restricted.FloatData; import com.google.template.soy.data.restricted.NullData; import com.google.template.soy.data.restricted.StringData; import com.google.template.soy.data.restricted.UndefinedData; import com.google.template.soy.error.FormattingErrorReporter; import com.google.template.soy.exprparse.ExpressionParser; import com.google.template.soy.exprparse.SoyParsingContext; import com.google.template.soy.exprtree.ExprNode; import com.google.template.soy.exprtree.FunctionNode; import com.google.template.soy.internal.i18n.BidiGlobalDir; import com.google.template.soy.shared.SharedTestUtils; import com.google.template.soy.shared.restricted.SoyFunction; import com.google.template.soy.sharedpasses.render.EvalVisitor.EvalVisitorFactory; import com.google.template.soy.soytree.PrintNode; import java.util.Map; import java.util.Set; import javax.annotation.Nullable; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** * Unit tests for EvalVisitor. * */ @RunWith(JUnit4.class) public class EvalVisitorTest { private static final Injector INJECTOR = Guice.createInjector(new SoyModule()); protected static final SoyValueConverter CONVERTER = INJECTOR.getInstance(SoyValueConverter.class); private SoyRecord testData; private static final SoyRecord TEST_IJ_DATA = CONVERTER.newDict("ijBool", true, "ijInt", 26, "ijStr", "injected"); private final Map<String, SoyValueProvider> locals = Maps.newHashMap( ImmutableMap.<String, SoyValueProvider>of( "zoo", StringData.forValue("loo"), "woo", FloatData.forValue(-1.618))); @Before public void setUp() { testData = createTestData(); SharedTestUtils.simulateNewApiCall(INJECTOR, null, BidiGlobalDir.LTR); } protected SoyRecord createTestData() { SoyList tri = CONVERTER.newList(1, 3, 6, 10, 15, 21); return CONVERTER.newDict( "boo", 8, "foo.bar", "baz", "foo.goo2", tri, "goo", tri, "moo", 3.14, "t", true, "f", false, "n", null, "map0", CONVERTER.newDict(), "list0", CONVERTER.newList(), "longNumber", 1000000000000000001L, "floatNumber", 1.5); } /** * Evaluates the given expression and returns the result. * * @param expression The expression to evaluate. * @return The expression result. * @throws Exception If there's an error. */ private SoyValue eval(String expression) throws Exception { PrintNode code = (PrintNode) SoyFileSetParserBuilder.forTemplateContents( // wrap in a function so we don't run into the 'can't print bools' error message untypedTemplateBodyForExpression("fakeFunction(" + expression + ")")) .addSoyFunction( new SoyFunction() { @Override public String getName() { return "fakeFunction"; } @Override public Set<Integer> getValidArgsSizes() { return ImmutableSet.of(1); } }) .parse() .fileSet() .getChild(0) .getChild(0) .getChild(0); ExprNode expr = ((FunctionNode) code.getExpr().getChild(0)).getChild(0); EvalVisitor evalVisitor = INJECTOR .getInstance(EvalVisitorFactory.class) .create(TEST_IJ_DATA, TestingEnvironment.createForTest(testData, locals)); return evalVisitor.exec(expr); } /** * Asserts that the given expression evaluates to the given result. * * @param expression The expression to evaluate. * @param result The expected expression result. * @throws Exception If the assertion is not true or if there's an error. */ private void assertEval(String expression, boolean result) throws Exception { assertThat(eval(expression).booleanValue()).isEqualTo(result); } /** * Asserts that the given expression evaluates to the given result. * * @param expression The expression to evaluate. * @param result The expected result. * @throws Exception If the assertion is not true or if there's an error. */ private void assertEval(String expression, long result) throws Exception { assertThat(eval(expression).longValue()).isEqualTo(result); } /** * Asserts that the given expression evaluates to the given result. * * @param expression The expression to evaluate. * @param result The expected result. * @throws Exception If the assertion is not true or if there's an error. */ private void assertEval(String expression, double result) throws Exception { assertThat(eval(expression).floatValue()).isEqualTo(result); } /** * Asserts that the given expression evaluates to the given result. * * @param expression The expression to evaluate. * @param result The expected result. * @throws Exception If the assertion is not true or if there's an error. */ private void assertEval(String expression, String result) throws Exception { assertThat(eval(expression).stringValue()).isEqualTo(result); } /** * Asserts that evaluating the given expression causes a ParseException. * * @param expression The expression to evaluate. * @return the error messages */ private static ImmutableList<String> assertParseError(String expression) { FormattingErrorReporter errorReporter = new FormattingErrorReporter(); new ExpressionParser( expression, SourceLocation.UNKNOWN, SoyParsingContext.empty(errorReporter, "fake.namespace")) .parseExpression(); ImmutableList<String> errorMessages = errorReporter.getErrorMessages(); if (errorMessages.isEmpty()) { fail("expected parse error, got none"); } return errorMessages; } /** * Asserts that evaluating the given expression causes a RenderException. * * @param expression The expression to evaluate. * @throws Exception If the assertion is not true or if there's an error. */ private void assertRenderException(String expression, @Nullable String errorMsgSubstring) throws Exception { try { eval(expression); fail(); } catch (RenderException re) { if (errorMsgSubstring != null) { assertThat(re.getMessage()).contains(errorMsgSubstring); } // Test passes. } } /** * Asserts that evaluating the given expression causes a SoyDataException. * * @param expression The expression to evaluate. * @throws Exception If the assertion is not true or if there's an error. */ private void assertDataException(String expression, @Nullable String errorMsgSubstring) throws Exception { try { eval(expression); fail(); } catch (SoyDataException e) { if (errorMsgSubstring != null) { assertThat(e.getMessage()).contains(errorMsgSubstring); } // Test passes. } } // ----------------------------------------------------------------------------------------------- // Tests begin here. @Test public void testEvalPrimitives() throws Exception { assertThat(eval("null")).isInstanceOf(NullData.class); assertEval("true", true); assertEval("false", false); assertEval("26", 26); assertEval("8.27", 8.27); assertEval("'boo'", "boo"); } @Test public void testEvalListLiteral() throws Exception { SoyList result = (SoyList) eval("['blah', 123, $boo]"); assertThat(result.length()).isEqualTo(3); assertThat(result.get(0).stringValue()).isEqualTo("blah"); assertThat(result.get(1).integerValue()).isEqualTo(123); assertThat(result.get(2).integerValue()).isEqualTo(8); result = (SoyList) eval("['blah', 123, $boo,]"); // trailing comma assertThat(result.length()).isEqualTo(3); assertThat(result.get(0).stringValue()).isEqualTo("blah"); assertThat(result.get(1).integerValue()).isEqualTo(123); assertThat(result.get(2).integerValue()).isEqualTo(8); result = (SoyList) eval("[]"); assertThat(result.length()).isEqualTo(0); assertParseError("[,]"); } @Test public void testEvalMapLiteral() throws Exception { SoyDict result = (SoyDict) eval("[:]"); assertThat(result.getItemKeys()).isEmpty(); result = (SoyDict) eval("['aaa': 'blah', 'bbb': 123, $foo.bar: $boo]"); assertThat(result.getItemKeys()).hasSize(3); assertThat(result.getField("aaa").stringValue()).isEqualTo("blah"); assertThat(result.getField("bbb").integerValue()).isEqualTo(123); assertThat(result.getField("baz").integerValue()).isEqualTo(8); result = (SoyDict) eval("['aaa': 'blah', 'bbb': 123, $foo.bar: $boo,]"); // trailing comma assertThat(result.getItemKeys()).hasSize(3); assertThat(result.getField("aaa").stringValue()).isEqualTo("blah"); assertThat(result.getField("bbb").integerValue()).isEqualTo(123); assertThat(result.getField("baz").integerValue()).isEqualTo(8); result = (SoyDict) eval("quoteKeysIfJs([:])"); assertThat(result.getItemKeys()).isEmpty(); result = (SoyDict) eval("quoteKeysIfJs( ['aaa': 'blah', 'bbb': 123, $foo.bar: $boo] )"); assertThat(result.getItemKeys()).hasSize(3); assertThat(result.getField("aaa").stringValue()).isEqualTo("blah"); assertThat(result.getField("bbb").integerValue()).isEqualTo(123); assertThat(result.getField("baz").integerValue()).isEqualTo(8); assertParseError("[:,]"); assertParseError("[,:]"); // Test last value overwrites earlier value for the same key. result = (SoyDict) eval("['baz': 'blah', $foo.bar: 'bluh']"); assertThat(result.getField("baz").stringValue()).isEqualTo("bluh"); } @Test public void testEvalDataRefBasic() throws Exception { assertEval("$zoo", "loo"); assertEval("$woo", -1.618); assertEval("$boo", 8); assertEval("$foo.bar", "baz"); assertEval("$goo[2]", 6); assertEval("$ij.ijBool", true); assertEval("$ij.ijInt", 26); assertEval("$ij.ijStr", "injected"); assertThat(eval("$too")).isInstanceOf(UndefinedData.class); assertThat(eval("$foo.too")).isInstanceOf(UndefinedData.class); assertThat(eval("$foo.goo2[22]")).isInstanceOf(UndefinedData.class); assertThat(eval("$ij.boo")).isInstanceOf(UndefinedData.class); // TODO: If enabling exception for undefined LHS (see EvalVisitor), uncomment tests below. //assertRenderException( // "$foo.bar.moo.tar", "encountered undefined LHS just before accessing \".tar\""); assertThat(eval("$foo.bar.moo.tar")).isInstanceOf(UndefinedData.class); //assertRenderException( // "$foo.baz.moo.tar", "encountered undefined LHS just before accessing \".moo\""); assertThat(eval("$foo.baz.moo.tar")).isInstanceOf(UndefinedData.class); assertRenderException("$boo?[2]", "encountered non-map/list just before accessing \"?[2]\""); assertRenderException( "$boo?['xyz']", "encountered non-map/list just before accessing \"?['xyz']\""); assertDataException( "$foo[2]", "SoyDict accessed with non-string key (got key type" + " com.google.template.soy.data.restricted.IntegerData)."); assertThat(eval("$moo.too")).isInstanceOf(UndefinedData.class); //assertRenderException( // "$roo.too", "encountered undefined LHS just before accessing \".too\""); assertThat(eval("$roo.too")).isInstanceOf(UndefinedData.class); //assertRenderException("$roo[2]", "encountered undefined LHS just before accessing \"[2]\""); assertThat(eval("$roo[2]")).isInstanceOf(UndefinedData.class); assertThat(eval("$ij.ijInt.boo")).isInstanceOf(UndefinedData.class); //assertRenderException( // "$ij.ijZoo.boo", "encountered undefined LHS just before accessing \".boo\""); assertThat(eval("$ij.ijZoo.boo")).isInstanceOf(UndefinedData.class); } @Test public void testEvalDataRefWithNullSafeAccess() throws Exception { // Note: Null-safe access only helps when left side is undefined or null, not when it's the // wrong type. assertRenderException( "$foo?.bar?.moo.tar", "encountered non-record just before accessing \"?.moo\""); assertThat(eval("$foo?.baz?.moo.tar")).isInstanceOf(NullData.class); assertDataException( "$foo[2]", "SoyDict accessed with non-string key (got key type" + " com.google.template.soy.data.restricted.IntegerData)."); assertRenderException("$moo?.too", "encountered non-record just before accessing \"?.too\""); assertThat(eval("$roo?.too")).isInstanceOf(NullData.class); assertThat(eval("$roo?[2]")).isInstanceOf(NullData.class); assertRenderException( "$ij.ijInt?.boo", "encountered non-record just before accessing \"?.boo\""); assertThat(eval("$ij.ijZoo?.boo")).isInstanceOf(NullData.class); } @Test public void testEvalNumericalOperators() throws Exception { assertEval("-$boo", -8); assertEval("$goo[3]*3", 30); assertEval("2 * $moo", 6.28); assertEval("$goo[0] / 4", 0.25); assertEval("$woo/-0.8090", 2.0); assertEval("$boo % 3", 2); assertEval("-99+-111", -210); assertEval("$moo + $goo[5]", 24.14); assertEval("$ij.ijInt + $boo", 34); assertEval("'boo'+'hoo'", "boohoo"); // string concatenation assertEval("$foo.bar + $ij.ijStr", "bazinjected"); // string concatenation assertEval("8 + $zoo + 8.0", "8loo8"); // coercion to string type assertEval("$goo[4] - $boo", 7); assertEval("1.002- $woo", 2.62); // Ensure longs work. assertEval("$longNumber + $longNumber", 2000000000000000002L); assertEval("$longNumber * 4 - $longNumber", 3000000000000000003L); assertEval("$longNumber / $longNumber", 1.0); // NOTE: Division is on floats. assertEval("$longNumber < ($longNumber + 1)", true); assertEval("$longNumber < ($longNumber - 1)", false); } @Test public void testEvalDataRefWithExpressions() throws Exception { assertEval("$foo['bar']", "baz"); assertEval("$goo[2]", 6); assertEval("$foo['goo' + 2][2+2]", 15); assertEval("$foo['goo'+2][4]", 15); assertEval("$foo.goo2[2 + 2]", 15); } @Test public void testEvalBooleanOperators() throws Exception { assertEval("not $t", false); assertEval("not null", true); assertEval("not $boo", false); assertEval("not $ij.ijBool", false); assertEval("not 0.0", true); assertEval("not $foo.bar", false); assertEval("not ''", true); assertEval("not $foo", false); assertEval("not $map0", false); assertEval("not $goo", false); assertEval("not $list0", false); assertEval("false and $undefinedName", false); // short-circuit evaluation assertEval("$t and -1 and $goo and $foo.bar", true); assertEval("true or $undefinedName", true); // short-circuit evaluation assertEval("$f or 0.0 or ''", false); } @Test public void testEvalComparisonOperators() throws Exception { assertEval("1<1", false); assertEval("$woo < 0", true); assertEval("$goo[0]>0", true); assertEval("$moo> 11.1111", false); assertEval("0 <= 0", true); assertEval("$moo <= -$woo", false); assertEval("2 >= $goo[2]", false); assertEval("4 >=$moo", true); assertEval("15==$goo[4]", true); assertEval("$woo == 1.61", false); assertEval("4.0 ==4", true); assertEval("$f == true", false); assertEval("null== null", true); assertEval("'$foo.bar' == $foo.bar", false); assertEval("$foo.bar == 'b' + 'a'+'z'", true); assertEval("$foo == $map0", false); assertEval("$foo.goo2 == $goo", true); assertEval("'22' == 22", true); assertEval("'22' == '' + 22", true); assertEval("$goo[4]!=15", false); assertEval("1.61 != $woo", true); assertEval("4 !=4.0", false); assertEval("true != $f", true); assertEval("null!= null", false); assertEval("$foo.bar != '$foo.bar'", true); assertEval("'b' + 'a'+'z' != $foo.bar", false); assertEval("$map0 != $foo", true); assertEval("$goo != $foo.goo2", false); assertEval("22 != '22'", false); assertEval("'' + 22 != '22'", false); assertEval("$longNumber < $longNumber", false); assertEval("$longNumber < ($longNumber - 1)", false); assertEval("($longNumber - 1) < $longNumber", true); assertEval("$longNumber <= $longNumber", true); assertEval("$longNumber <= ($longNumber - 1)", false); assertEval("($longNumber - 1) <= $longNumber", true); assertEval("$longNumber > $longNumber", false); assertEval("$longNumber > ($longNumber - 1)", true); assertEval("($longNumber - 1) > $longNumber", false); assertEval("$longNumber >= $longNumber", true); assertEval("$longNumber >= ($longNumber - 1)", true); assertEval("($longNumber - 1) >= $longNumber", false); assertEval("$floatNumber < $floatNumber", false); assertEval("$floatNumber < ($floatNumber - 1)", false); assertEval("($floatNumber - 1) < $floatNumber", true); assertEval("$floatNumber <= $floatNumber", true); assertEval("$floatNumber <= ($floatNumber - 1)", false); assertEval("($floatNumber - 1) <= $floatNumber", true); assertEval("$floatNumber > $floatNumber", false); assertEval("$floatNumber > ($floatNumber - 1)", true); assertEval("($floatNumber - 1) > $floatNumber", false); assertEval("$floatNumber >= $floatNumber", true); assertEval("$floatNumber >= ($floatNumber - 1)", true); assertEval("($floatNumber - 1) >= $floatNumber", false); } @Test public void testEvalConditionalOperator() throws Exception { assertEval("($f and 0)?4 : '4'", "4"); assertEval("$goo ? $goo[1]:1", 3); } @Test public void testEvalFunctions() throws Exception { assertEval("isNonnull(null)", false); assertEval("isNonnull(0)", true); assertEval("isNonnull(1)", true); assertEval("isNonnull(false)", true); assertEval("isNonnull(true)", true); assertEval("isNonnull('')", true); assertEval("isNonnull($undefined)", false); assertEval("isNonnull($n)", false); assertEval("isNonnull($boo)", true); assertEval("isNonnull($foo.goo2)", true); assertEval("isNonnull($map0)", true); } }