/* * 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.truth.Truth.assertThat; import static com.google.template.soy.data.SoyValueConverter.EMPTY_DICT; import static com.google.template.soy.jbcsrc.TemplateTester.asRecord; import static com.google.template.soy.jbcsrc.TemplateTester.assertThatTemplateBody; import static com.google.template.soy.jbcsrc.TemplateTester.compileTemplateBody; import static com.google.template.soy.jbcsrc.TemplateTester.getDefaultContext; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.util.concurrent.SettableFuture; import com.google.template.soy.jbcsrc.TemplateTester.CompiledTemplateSubject; import com.google.template.soy.jbcsrc.api.AdvisingStringBuilder; import com.google.template.soy.jbcsrc.api.RenderResult; import com.google.template.soy.jbcsrc.shared.CompiledTemplate; import com.google.template.soy.jbcsrc.shared.CompiledTemplates; import com.google.template.soy.jbcsrc.shared.RenderContext; import java.io.IOException; import java.lang.reflect.Constructor; import java.util.Arrays; import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link LazyClosureCompiler}. */ @RunWith(JUnit4.class) public class LazyClosureCompilerTest { @Test public void testLetContentNode() { assertThatTemplateBody("{let $foo}", " foo bar baz", "{/let}", "{$foo}") .rendersAs("foo bar baz"); } @Test public void testLetContentNode_typed() { assertThatTemplateBody("{let $foo kind=\"html\"}", " foo bar baz", "{/let}", "{$foo}") .rendersAs("foo bar baz"); } @Test public void testLetNodes_nested() { assertThatTemplateBody( "{let $foo}", " {let $foo}foo bar baz{/let}", " {$foo}", "{/let}", "{$foo}") .rendersAs("foo bar baz"); } @Test public void testLetContentNode_detaching() throws IOException { SettableFuture<String> bar = SettableFuture.create(); CompiledTemplates templates = compileTemplateBody( "{@param bar : string }", "{let $foo}", " hello {$bar}", "{/let}", "{$foo}"); CompiledTemplate.Factory factory = templates.getTemplateFactory("ns.foo"); RenderContext context = getDefaultContext(templates); CompiledTemplate template = factory.create(asRecord(ImmutableMap.of("bar", bar)), EMPTY_DICT); AdvisingStringBuilder output = new AdvisingStringBuilder(); RenderResult result = template.render(output, context); assertEquals(RenderResult.Type.DETACH, result.type()); assertSame(bar, result.future()); // we found bar! assertEquals("hello ", output.toString()); // make sure no progress is made result = template.render(output, context); assertEquals(RenderResult.Type.DETACH, result.type()); assertSame(bar, result.future()); assertEquals("hello ", output.toString()); bar.set("bar"); assertEquals(RenderResult.done(), template.render(output, context)); assertEquals("hello bar", output.toString()); } @Test public void testLetValueNode() { assertThatTemplateBody("{let $foo : 1+2 /}", "{$foo}").rendersAs("3"); assertThatTemplateBody("{let $null : null /}", "{$null}").rendersAs("null"); assertThatTemplateBody("{let $bar : 'a' /}", "{let $foo : $bar + 'b' /}", "{$foo}") .rendersAs("ab"); } @Test public void testLetValueNode_captureParameter() { assertThatTemplateBody("{@param param: string}", "{let $foo : $param + '_suffix' /}", "{$foo}") .rendersAs("string_suffix", ImmutableMap.of("param", "string")); } // Regression test for a bug where captures of synthetic variables wouldn't be deduped properly // and we would recapture the same synthetic multiple times. @Test public void testLetValueNode_captureSyntheticParameter() { // make sure that if we capture a synthetic we only capture it once CompiledTemplates templates = compileTemplateBody( "{@param l : list<string>}", "{foreach $s in $l}", // the index function is implemented via a synthetic loop index " {let $bar : index($s) + index($s) /}", " {$bar}", "{/foreach}"); CompiledTemplate.Factory factory = templates.getTemplateFactory("ns.foo"); CompiledTemplate template = factory.create(EMPTY_DICT, EMPTY_DICT); List<Class<?>> innerClasses = Lists.newArrayList(template.getClass().getDeclaredClasses()); innerClasses.remove(factory.getClass()); Class<?> let = Iterables.getOnlyElement(innerClasses); assertEquals("let_bar", let.getSimpleName()); // the closures capture variables as constructor parameters. // in this case since index() always returns an unboxed integer the parameter should be a single // int. In a previous version, we passed 2 ints. assertThat(let.getDeclaredConstructors()).hasLength(1); Constructor<?> cStruct = let.getDeclaredConstructors()[0]; assertThat(Arrays.asList(cStruct.getParameterTypes())).isEqualTo(Arrays.asList(int.class)); } @Test public void testLetValueNode_nullableParameter() { CompiledTemplateSubject tester = assertThatTemplateBody( "{@param? param : bool}", "{let $paramWithDefault : $param ?: true /}", "{$paramWithDefault ? 'true' : 'false'}"); tester.rendersAs("true", ImmutableMap.<String, Object>of()); tester.rendersAs("true", ImmutableMap.<String, Object>of("param", true)); tester.rendersAs("false", ImmutableMap.<String, Object>of("param", false)); } @Test public void testLetValueNode_nullableString() { CompiledTemplateSubject tester = assertThatTemplateBody( "{@param? param : string}", "{@param? param2 : string}", "{let $paramWithDefault : $param ?: $param2 /}", "{$paramWithDefault}"); tester.rendersAs("null", ImmutableMap.<String, Object>of()); tester.rendersAs("1", ImmutableMap.<String, Object>of("param", "1")); tester.rendersAs("1", ImmutableMap.<String, Object>of("param", "1", "param2", "2")); tester.rendersAs("2", ImmutableMap.<String, Object>of("param2", "2")); } @Test public void testLetValueNode_optionalInts() { CompiledTemplateSubject tester = assertThatTemplateBody( "{@param comments: list<string>}", "{@param? numComments: number}", " {let $numNotShown: ", " isNonnull($numComments) and length($comments) > $numComments + 2 ?", " length($comments) - $numComments : 0 /}", " {$numNotShown}"); tester.rendersAs("0", ImmutableMap.of("comments", ImmutableList.of(), "numComments", 2)); tester.rendersAs("0", ImmutableMap.of("comments", ImmutableList.of())); tester.rendersAs( "3", ImmutableMap.of("comments", ImmutableList.of("a", "b", "c", "d"), "numComments", 1)); } @Test public void testDetachOnFutureLazily() throws IOException { SettableFuture<String> bar = SettableFuture.create(); CompiledTemplates templates = compileTemplateBody( "{@param bar : string }", "{let $foo : $bar + $bar /}", "before use", "{$foo}"); CompiledTemplate.Factory factory = templates.getTemplateFactory("ns.foo"); RenderContext context = getDefaultContext(templates); CompiledTemplate template = factory.create(asRecord(ImmutableMap.of("bar", bar)), EMPTY_DICT); AdvisingStringBuilder output = new AdvisingStringBuilder(); RenderResult result = template.render(output, context); assertEquals(RenderResult.Type.DETACH, result.type()); assertSame(bar, result.future()); // we found bar! assertEquals("before use", output.toString()); // make sure no progress is made result = template.render(output, context); assertEquals(RenderResult.Type.DETACH, result.type()); assertSame(bar, result.future()); assertEquals("before use", output.toString()); bar.set(" bar"); assertEquals(RenderResult.done(), template.render(output, context)); assertEquals("before use bar bar", output.toString()); } @Test public void testLetValueNodeStructure() { // make sure we don't break normal reflection apis CompiledTemplates templates = compileTemplateBody("{let $bar : 'a' /}", "{let $foo : $bar + 1 /}"); CompiledTemplate.Factory factory = templates.getTemplateFactory("ns.foo"); CompiledTemplate template = factory.create(EMPTY_DICT, EMPTY_DICT); assertThat(template.getClass().getDeclaredClasses()).asList().hasSize(2); List<Class<?>> innerClasses = Lists.newArrayList(template.getClass().getDeclaredClasses()); innerClasses.remove(factory.getClass()); Class<?> let = Iterables.getOnlyElement(innerClasses); assertEquals("let_foo", let.getSimpleName()); assertEquals(template.getClass(), let.getDeclaringClass()); } }