/* * 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.getDefaultContext; import static org.junit.Assert.assertEquals; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.util.concurrent.SettableFuture; import com.google.template.soy.data.SoyRecord; import com.google.template.soy.jbcsrc.api.AdvisingAppendable; 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.util.List; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link DetachState}. */ @RunWith(JUnit4.class) public final class DetachStateTest { static final class TestAppendable implements AdvisingAppendable { private final StringBuilder delegate = new StringBuilder(); boolean softLimitReached; @Override public TestAppendable append(CharSequence s) { delegate.append(s); return this; } @Override public TestAppendable append(CharSequence s, int start, int end) { delegate.append(s, start, end); return this; } @Override public TestAppendable append(char c) { delegate.append(c); return this; } @Override public boolean softLimitReached() { return softLimitReached; } @Override public String toString() { return delegate.toString(); } } @Test public void testDetach_singleRawTextNode() throws IOException { CompiledTemplates templates = TemplateTester.compileTemplateBody("hello world"); CompiledTemplate.Factory factory = templates.getTemplateFactory("ns.foo"); RenderContext context = getDefaultContext(templates); CompiledTemplate template = factory.create(EMPTY_DICT, EMPTY_DICT); // Basic stuff works TestAppendable output = new TestAppendable(); assertEquals(RenderResult.done(), template.render(output, context)); assertEquals("hello world", output.toString()); output = new TestAppendable(); output.softLimitReached = true; // detached!!! assertEquals(RenderResult.limited(), template.render(output, context)); assertEquals("hello world", output.toString()); assertEquals(RenderResult.done(), template.render(output, context)); assertEquals("hello world", output.toString()); // nothing was added } @Test public void testDetach_multipleNodes() throws IOException { CompiledTemplates templates = TemplateTester.compileTemplateBody( "hello", // this print node inserts a space character and ensures that our raw text nodes don't // get merged "{' '}", "world"); CompiledTemplate.Factory factory = templates.getTemplateFactory("ns.foo"); RenderContext context = getDefaultContext(templates); CompiledTemplate template = factory.create(EMPTY_DICT, EMPTY_DICT); // Basic stuff works TestAppendable output = new TestAppendable(); assertEquals(RenderResult.done(), template.render(output, context)); assertEquals("hello world", output.toString()); output = new TestAppendable(); output.softLimitReached = true; // detached!!! assertEquals(RenderResult.limited(), template.render(output, context)); assertEquals("hello", output.toString()); assertEquals(RenderResult.limited(), template.render(output, context)); assertEquals("hello ", output.toString()); assertEquals(RenderResult.limited(), template.render(output, context)); assertEquals("hello world", output.toString()); assertEquals(RenderResult.done(), template.render(output, context)); assertEquals("hello world", output.toString()); // nothing was added } // ensure that when we call back in, locals are restored @Test public void testDetach_saveRestore() throws IOException { CompiledTemplates templates = TemplateTester.compileTemplateBody("{for $i in range(10)}", " {$i}", "{/for}"); CompiledTemplate.Factory factory = templates.getTemplateFactory("ns.foo"); RenderContext context = getDefaultContext(templates); CompiledTemplate template = factory.create(EMPTY_DICT, EMPTY_DICT); // Basic stuff works TestAppendable output = new TestAppendable(); assertEquals(RenderResult.done(), template.render(output, context)); assertEquals("0123456789", output.toString()); output = new TestAppendable(); output.softLimitReached = true; for (int i = 0; i < 10; i++) { assertEquals(RenderResult.limited(), template.render(output, context)); assertEquals(String.valueOf(i), output.toString()); output.delegate.setLength(0); } assertEquals(RenderResult.done(), template.render(output, context)); assertThat(output.toString()).isEmpty(); // last render was empty } @Test public void testDetachOnUnResolvedProvider() throws IOException { SettableFuture<String> future = SettableFuture.create(); CompiledTemplates templates = TemplateTester.compileTemplateBody("{@param foo : string}", "prefix{sp}{$foo}{sp}suffix"); CompiledTemplate.Factory factory = templates.getTemplateFactory("ns.foo"); RenderContext context = getDefaultContext(templates); CompiledTemplate template = factory.create(asRecord(ImmutableMap.of("foo", future)), EMPTY_DICT); AdvisingStringBuilder output = new AdvisingStringBuilder(); RenderResult result = template.render(output, context); assertEquals(RenderResult.Type.DETACH, result.type()); assertEquals(future, result.future()); assertEquals("prefix ", output.toString()); // No progress is made, our caller is an idiot and didn't wait for the future result = template.render(output, context); assertEquals(RenderResult.Type.DETACH, result.type()); assertEquals(future, result.future()); assertEquals("prefix ", output.toString()); future.set("future"); result = template.render(output, context); assertEquals(RenderResult.done(), result); assertEquals("prefix future suffix", output.toString()); } @Test public void testDetachOnEachIteration() throws IOException { CompiledTemplates templates = TemplateTester.compileTemplateBody( "{@param list : list<string>}", "prefix{\\n}", "{foreach $item in $list}", " loop-prefix{\\n}", " {$item}{\\n}", " loop-suffix{\\n}", "{/foreach}", "suffix"); CompiledTemplate.Factory factory = templates.getTemplateFactory("ns.foo"); RenderContext context = getDefaultContext(templates); List<SettableFuture<String>> futures = ImmutableList.of( SettableFuture.<String>create(), SettableFuture.<String>create(), SettableFuture.<String>create()); CompiledTemplate template = factory.create(asRecord(ImmutableMap.of("list", futures)), EMPTY_DICT); AdvisingStringBuilder output = new AdvisingStringBuilder(); RenderResult result = template.render(output, context); assertEquals(RenderResult.Type.DETACH, result.type()); assertEquals(futures.get(0), result.future()); assertEquals("prefix\nloop-prefix\n", output.getAndClearBuffer()); futures.get(0).set("first"); result = template.render(output, context); assertEquals(RenderResult.Type.DETACH, result.type()); assertEquals(futures.get(1), result.future()); assertEquals("first\nloop-suffix\nloop-prefix\n", output.getAndClearBuffer()); futures.get(1).set("second"); result = template.render(output, context); assertEquals(RenderResult.Type.DETACH, result.type()); assertEquals(futures.get(2), result.future()); assertEquals("second\nloop-suffix\nloop-prefix\n", output.getAndClearBuffer()); futures.get(2).set("third"); result = template.render(output, context); assertEquals(RenderResult.done(), result); assertEquals("third\nloop-suffix\nsuffix", output.toString()); } // This test is for a bug where we were generating one detach logic block for a full expressions // but it caused stack merge errors because the runtime stack wasn't consistent across all detach // points. See http://mail.ow2.org/wws/arc/asm/2015-04/msg00001.html @Test public void testDetachOnMultipleParamsInOneExpression() throws IOException { CompiledTemplates templates = TemplateTester.compileTemplateBody( "{@param list : list<int>}", "{@param foo : int}", "{foreach $item in $list}", " {$item + $foo}", "{/foreach}"); CompiledTemplate.Factory factory = templates.getTemplateFactory("ns.foo"); RenderContext context = getDefaultContext(templates); SoyRecord params = asRecord(ImmutableMap.of("list", ImmutableList.of(1, 2, 3, 4), "foo", 1)); AdvisingStringBuilder output = new AdvisingStringBuilder(); assertEquals(RenderResult.done(), factory.create(params, EMPTY_DICT).render(output, context)); assertEquals("2345", output.toString()); } @Test public void testDetachOnCall() throws IOException { CompiledTemplates templates = TemplateTester.compileFile( "{namespace ns}", "", "{template .caller}", " {@param callerParam : string}", " {call .callee}", " {param calleeParam: $callerParam /}", " {/call}", "{/template}", "", "{template .callee}", " {@param calleeParam : string}", " prefix {$calleeParam} suffix", "{/template}", ""); CompiledTemplate.Factory factory = templates.getTemplateFactory("ns.caller"); SettableFuture<String> param = SettableFuture.create(); SoyRecord params = asRecord(ImmutableMap.of("callerParam", param)); CompiledTemplate template = factory.create(params, EMPTY_DICT); AdvisingStringBuilder output = new AdvisingStringBuilder(); assertEquals( RenderResult.continueAfter(param), template.render(output, getDefaultContext(templates))); assertEquals("prefix ", output.toString()); param.set("foo"); assertEquals(RenderResult.done(), template.render(output, getDefaultContext(templates))); assertEquals("prefix foo suffix", output.toString()); } @Test public void testDetachOnParamTransclusion() throws IOException { CompiledTemplates templates = TemplateTester.compileFile( "{namespace ns autoescape=\"strict\"}", "", "/** */", "{template .caller}", " {@param callerParam : string}", " {call .callee}", " {param calleeParam}", " prefix {$callerParam} suffix", " {/param}", " {/call}", "{/template}", "", "/** */", "{template .callee}", " {@param calleeParam : string}", " {$calleeParam}", "{/template}", ""); CompiledTemplate.Factory factory = templates.getTemplateFactory("ns.caller"); RenderContext context = getDefaultContext(templates); SettableFuture<String> param = SettableFuture.create(); SoyRecord params = asRecord(ImmutableMap.of("callerParam", param)); CompiledTemplate template = factory.create(params, EMPTY_DICT); AdvisingStringBuilder output = new AdvisingStringBuilder(); assertEquals(RenderResult.continueAfter(param), template.render(output, context)); assertEquals("prefix ", output.toString()); param.set("foo"); assertEquals(RenderResult.done(), template.render(output, context)); assertEquals("prefix foo suffix", output.toString()); } }