/*
* 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 org.junit.Assert.fail;
import com.google.common.base.Joiner;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.AbstractFuture;
import com.google.common.util.concurrent.Futures;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.template.soy.SoyFileSetParser.ParseResult;
import com.google.template.soy.SoyFileSetParserBuilder;
import com.google.template.soy.SoyModule;
import com.google.template.soy.data.SanitizedContent;
import com.google.template.soy.data.SoyAbstractValue;
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.UnsafeSanitizedContentOrdainer;
import com.google.template.soy.error.ErrorReporter;
import com.google.template.soy.error.ExplodingErrorReporter;
import com.google.template.soy.internal.i18n.BidiGlobalDir;
import com.google.template.soy.msgs.SoyMsgBundle;
import com.google.template.soy.msgs.internal.MsgUtils;
import com.google.template.soy.msgs.restricted.SoyMsg;
import com.google.template.soy.msgs.restricted.SoyMsgBundleImpl;
import com.google.template.soy.msgs.restricted.SoyMsgPart;
import com.google.template.soy.msgs.restricted.SoyMsgRawTextPart;
import com.google.template.soy.passes.RewriteRemaindersVisitor;
import com.google.template.soy.shared.SharedTestUtils;
import com.google.template.soy.shared.SoyCssRenamingMap;
import com.google.template.soy.shared.SoyGeneralOptions;
import com.google.template.soy.shared.SoyIdRenamingMap;
import com.google.template.soy.soytree.MsgFallbackGroupNode;
import com.google.template.soy.soytree.MsgNode;
import com.google.template.soy.soytree.SoyFileNode;
import com.google.template.soy.soytree.SoyFileSetNode;
import com.google.template.soy.soytree.SoyNode;
import com.google.template.soy.soytree.TemplateNode;
import com.google.template.soy.soytree.TemplateRegistry;
import java.io.ByteArrayOutputStream;
import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.Flushable;
import java.io.IOException;
import java.io.PrintStream;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicReference;
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 RenderVisitor.
*
*/
@RunWith(JUnit4.class)
public class RenderVisitorTest {
private static final Injector INJECTOR = Guice.createInjector(new SoyModule());
private static final SoyValueConverter CONVERTER = INJECTOR.getInstance(SoyValueConverter.class);
private static final SoyRecord TEST_DATA;
static {
SoyList tri = CONVERTER.newList(1, 3, 6, 10, 15, 21);
TEST_DATA =
CONVERTER.newDict(
"boo",
8,
"foo.bar",
"baz",
"foo.goo2",
tri,
"goo",
tri,
"moo",
3.14,
"t",
true,
"f",
false,
"map0",
CONVERTER.newDict(),
"list0",
CONVERTER.newList(),
"list1",
CONVERTER.newList(1, 2, 3),
"component",
"comp",
"plainText",
"<plaintext id=foo>",
"sanitizedContent",
UnsafeSanitizedContentOrdainer.ordainAsSafe(
"<plaintext id=foo>", SanitizedContent.ContentKind.HTML),
"greekA",
"alpha",
"greekB",
"beta",
"toStringTestValue",
createToStringTestValue());
}
private static final SoyRecord TEST_IJ_DATA =
CONVERTER.newDict("ijBool", true, "ijInt", 26, "ijStr", "injected");
private static final SoyIdRenamingMap TEST_XID_RENAMING_MAP =
new SoyIdRenamingMap() {
@Override
public String get(String key) {
return key + "_id_renamed";
}
};
private static final SoyCssRenamingMap TEST_CSS_RENAMING_MAP =
new SoyCssRenamingMap() {
@Override
public String get(String key) {
return key + "_renamed";
}
};
private static final ErrorReporter FAIL = ExplodingErrorReporter.get();
private SoyIdRenamingMap xidRenamingMap = null;
private SoyCssRenamingMap cssRenamingMap = null;
@Before
public void setUp() {
SharedTestUtils.simulateNewApiCall(INJECTOR, null, BidiGlobalDir.LTR);
}
/**
* Asserts that the given input string (should be a template body) renders to the given result.
*
* @param templateBody The input string to render (should be a template body).
* @param result The expected rendered result.
* @throws Exception If the assertion is not true or if there's an error.
*/
private void assertRender(String templateBody, String result) throws Exception {
assertThat(render(templateBody)).isEqualTo(result);
}
/**
* Asserts that the given input string (should be a template body) renders to the given result.
*
* @param templateBody The input string to render (should be a template body).
* @param data The SoyRecord data used for testing.
* @param result The expected rendered result.
* @throws Exception If the assertion is not true or if there's an error.
*/
private void assertRenderWithData(String templateBody, SoyRecord data, String result)
throws Exception {
assertThat(renderWithData(templateBody, data)).isEqualTo(result);
}
/**
* Asserts that the given input string (should be a template body) renders to the given result.
*
* @param templateBody The input string to render (should be a template body).
* @param errorMessage The expected error message.
* @throws Exception If an expected exception is not thrown, or the error message is different
* from expected.
*/
private void assertRenderException(String templateBody, String errorMessage) throws Exception {
assertRenderExceptionWithDataAndMsgBundle(templateBody, TEST_DATA, null, errorMessage);
}
/**
* Asserts that the given input string (should be a template body) renders to the given result.
*
* @param templateBody The input string to render (should be a template body).
* @param data The SoyRecord data used for testing.
* @param errorMessage The expected error message.
* @throws Exception If an expected exception is not thrown, or the error message is different
* from expected.
*/
private void assertRenderExceptionWithData(
String templateBody, SoyRecord data, String errorMessage) throws Exception {
assertRenderExceptionWithDataAndMsgBundle(templateBody, data, null, errorMessage);
}
/**
* Asserts that the given input string (should be a template body) renders to the given result.
*
* @param templateBody The input string to render (should be a template body).
* @param data The SoyRecord data used for testing.
* @param msgBundle The bundle of translated messages.
* @param errorMessage The expected error message.
* @throws Exception If an expected exception is not thrown, or the error message is different
* from expected.
*/
private void assertRenderExceptionWithDataAndMsgBundle(
String templateBody, SoyRecord data, @Nullable SoyMsgBundle msgBundle, String errorMessage)
throws Exception {
try {
String result = renderWithDataAndMsgBundle(templateBody, data, msgBundle);
fail(
"Invalid template body didn't throw exception. Template body was:\n"
+ templateBody
+ "\n result was:\n"
+ result);
} catch (RenderException e) {
assertThat(e.getMessage()).contains(errorMessage);
}
}
/**
* Renders the given input string (should be a template body) and returns the result.
*
* @param templateBody The input string to render (should be a template body).
* @return The rendered result.
* @throws Exception If there's an error.
*/
private String render(String templateBody) throws Exception {
return renderWithData(templateBody, TEST_DATA);
}
/**
* Renders the given input string (should be a template body) and returns the result.
*
* @param templateBody The input string to render (should be a template body).
* @param data The soy data as a map of variables to objects.
* @return The rendered result.
* @throws Exception If there's an error.
*/
private String renderWithData(String templateBody, SoyRecord data) throws Exception {
return renderWithDataAndMsgBundle(templateBody, data, null);
}
/**
* Renders the given input string (should be a template body) and returns the result.
*
* @param templateBody The input string to render (should be a template body).
* @param data The soy data as a map of variables to objects.
* @param msgBundle The bundle of translated messages.
* @return The rendered result.
* @throws Exception If there's an error.
*/
private String renderWithDataAndMsgBundle(
String templateBody, SoyRecord data, @Nullable SoyMsgBundle msgBundle) throws Exception {
ErrorReporter boom = ExplodingErrorReporter.get();
SoyFileSetNode soyTree =
SoyFileSetParserBuilder.forTemplateContents(templateBody)
.errorReporter(boom)
.parse()
.fileSet();
TemplateNode templateNode = (TemplateNode) SharedTestUtils.getNode(soyTree);
StringBuilder outputSb = new StringBuilder();
RenderVisitor rv =
INJECTOR
.getInstance(RenderVisitorFactory.class)
.create(
outputSb,
null,
data,
TEST_IJ_DATA,
Predicates.<String>alwaysFalse(),
msgBundle,
xidRenamingMap,
cssRenamingMap);
for (SoyNode node : templateNode.getChildren()) {
new RewriteRemaindersVisitor(boom).exec(node);
}
rv.exec(templateNode);
return outputSb.toString();
}
// -----------------------------------------------------------------------------------------------
// Tests begin here.
@Test
public void testRenderRawText() throws Exception {
String templateBody =
" {sp} aaa bbb \n"
+ " ccc {lb}{rb} ddd {\\n}\n"
+ " {literal}eee\n"
+ "fff }{ {/literal} \n"
+ " \u2222\uEEEE\u9EC4\u607A\n";
assertRender(templateBody, " aaa bbb ccc {} ddd \neee\nfff }{ \u2222\uEEEE\u9EC4\u607A");
}
@Test
public void testRenderComments() throws Exception {
String templateBody =
" {sp} // {sp}\n"
+ // first {sp} outside of comments
" /* {sp} {sp} */ // {sp}\n"
+ " /* {sp} */{sp}/* {sp} */\n"
+ // middle {sp} outside of comments
" /* {sp}\n"
+ " {sp} */{sp}\n"
+ // last {sp} outside of comments
" // {sp} /* {sp} */\n";
assertRender(templateBody, " ");
}
@Test
public void testRenderPrintStmt() throws Exception {
String templateBody =
"{@param foo : ? }\n"
+ "{@param boo : ? }\n"
+ "{@param f : ? }\n"
+ "{@param goo : ? }\n"
+ "{@param undefined : ? }\n"
+ "{@param toStringTestValue : ? }\n"
+ " {$boo} {$foo.bar}{sp}\n"
+ " {$ij.ijStr}\n"
+ " {$goo[5] + 1}{sp}\n"
+ " {$f ?: ''} {$undefined ?: -1}{sp}\n"
+ " {print ' blah &&blahblahblah' |escapeHtml|insertWordBreaks:8}{sp}\n"
+ " {$toStringTestValue |noAutoescape}\n";
assertRender(
templateBody,
"8 baz injected22 false -1 blah &&blahbl<wbr>ahblah coerceToString()");
}
@Test
public void testRenderMsgStmt() throws Exception {
String templateBody =
"{@param foo: ?}\n"
+ " {msg desc=\"Tells user's quota usage.\"}\n"
+ " You're currently using {$ij.ijInt} MB of your quota.{sp}\n"
+ " <a href=\"{$foo.bar}\">Learn more</A>\n"
+ " <br /><br />\n"
+ " {/msg}\n"
+ " {msg meaning=\"noun\" desc=\"\" hidden=\"true\"}Archive{/msg}\n"
+ " {msg meaning=\"noun\" desc=\"The archive (noun).\"}Archive{/msg}\n"
+ " {msg meaning=\"verb\" desc=\"\"}Archive{/msg}\n"
+ " {msg desc=\"\"}Archive{/msg}\n";
assertRender(
templateBody,
"You're currently using 26 MB of your quota. "
+ "<a href=\"baz\">Learn more</A>"
+ "<br /><br />"
+ "ArchiveArchiveArchiveArchive");
}
@Test
public void testRenderMsgStmtWithFallback() throws Exception {
String templateBody =
""
+ " {msg desc=\"\"}\n"
+ " blah\n"
+ " {fallbackmsg desc=\"\"}\n"
+ " bleh\n"
+ " {/msg}\n";
// Without msg bundle.
assertRender(templateBody, "blah");
// With msg bundle.
SoyFileNode file =
SoyFileSetParserBuilder.forFileContents(
"{namespace test}\n{template .foo}\n" + templateBody + "{/template}")
.parse()
.fileSet()
.getChild(0);
MsgNode fallbackMsg =
((MsgFallbackGroupNode) file.getChildren().get(0).getChildren().get(0)).getChild(1);
SoyMsg translatedFallbackMsg =
SoyMsg.builder()
.setId(MsgUtils.computeMsgIdForDualFormat(fallbackMsg))
.setLocaleString("x-zz")
.setParts(ImmutableList.<SoyMsgPart>of(SoyMsgRawTextPart.of("zbleh")))
.build();
SoyMsgBundle msgBundle =
new SoyMsgBundleImpl("x-zz", Lists.newArrayList(translatedFallbackMsg));
assertThat(renderWithDataAndMsgBundle(templateBody, TEST_DATA, msgBundle)).isEqualTo("zbleh");
}
@Test
public void testRenderSimpleSelect() throws Exception {
String templateBody =
"{@param person: ?}\n"
+ "{@param gender: ?}\n"
+ " {msg desc=\"Simple select message\"}\n"
+ " {select $gender}\n"
+ " {case 'female'}{$person} shared her photos.\n"
+ " {default}{$person} shared his photos.\n"
+ " {/select}\n"
+ " {/msg}\n";
SoyDict data = CONVERTER.newDict("person", "The president", "gender", "female");
assertRenderWithData(templateBody, data, "The president shared her photos.");
data = CONVERTER.newDict("person", "The president", "gender", "male");
assertRenderWithData(templateBody, data, "The president shared his photos.");
}
@Test
public void testRenderSimplePlural() throws Exception {
String templateBody =
"{@param n_people: ?}\n"
+ "{@param person: ?}\n"
+ " {msg desc=\"Simple plural message\"}\n"
+ " {plural $n_people offset=\"1\"}\n"
+ " {case 0}Nobody shared photos.\n"
+ " {case 1}Only {$person} shared photos.\n"
+ " {default}{$person} and {remainder($n_people)} others shared photos.\n"
+ " {/plural}\n"
+ " {/msg}\n";
SoyDict data = CONVERTER.newDict("person", "Bob", "n_people", 0);
assertRenderWithData(templateBody, data, "Nobody shared photos.");
data = CONVERTER.newDict("person", "Bob", "n_people", 1);
assertRenderWithData(templateBody, data, "Only Bob shared photos.");
data = CONVERTER.newDict("person", "Bob", "n_people", 10);
assertRenderWithData(templateBody, data, "Bob and 9 others shared photos.");
}
@Test
public void testRenderNestedSelects() throws Exception {
String templateBody =
"{@param gender1: ?}\n"
+ "{@param gender2: ?}\n"
+ "{@param person1: ?}\n"
+ "{@param person2: ?}\n"
+ " {msg desc=\"Nested selects\"}\n"
+ " {select $gender1}\n"
+ " {case 'female'}\n"
+ " {select $gender2}\n"
+ " {case 'female'}\n"
+ " {$person1} shared her photos with {$person2} and her friends.\n"
+ " {default}\n"
+ " {$person1} shared her photos with {$person2} and his friends.\n"
+ " {/select}\n"
+ " {default}\n"
+ " {select $gender2}\n"
+ " {case 'female'}"
+ " {$person1} shared his photos with {$person2} and her friends.\n"
+ " {default}"
+ " {$person1} shared his photos with {$person2} and his friends.\n"
+ " {/select}\n"
+ " {/select}\n"
+ " {/msg}\n";
SoyDict data =
CONVERTER.newDict(
"person1", "Alice", "gender1", "female", "person2", "Lara", "gender2", "female");
assertRenderWithData(templateBody, data, "Alice shared her photos with Lara and her friends.");
data =
CONVERTER.newDict(
"person1", "Alice", "gender1", "female", "person2", "Mark", "gender2", "male");
assertRenderWithData(templateBody, data, "Alice shared her photos with Mark and his friends.");
data =
CONVERTER.newDict(
"person1", "Bob", "gender1", "male", "person2", "Mark", "gender2", "male");
assertRenderWithData(
templateBody, data, " Bob shared his photos with Mark and his friends.");
data =
CONVERTER.newDict(
"person1", "Bob", "gender1", "male", "person2", "Lara", "gender2", "female");
assertRenderWithData(
templateBody, data, " Bob shared his photos with Lara and her friends.");
}
@Test
public void testRenderPluralNestedInSelect() throws Exception {
String templateBody =
" {@param person : ?}\n"
+ " {@param n_people : ?}\n"
+ " {@param gender : ?}\n"
+ " {msg desc=\"Plural nested inside select\"}\n"
+ " {select $gender}\n"
+ " {case 'female'}\n"
+ " {plural $n_people}\n"
+ " {case 0}{$person} added nobody to her circle.\n"
+ " {case 1}{$person} added one person to her circle.\n"
+ " {default}{$person} added {$n_people} people to her circle.\n"
+ " {/plural}\n"
+ " {default}\n"
+ " {plural $n_people}\n"
+ " {case 0}{$person} added nobody to his circle.\n"
+ " {case 1}{$person} added one person to his circle.\n"
+ " {default}{$person} added {$n_people} people to his circle.\n"
+ " {/plural}\n"
+ " {/select}\n"
+ " {/msg}\n";
SoyDict data = CONVERTER.newDict("person", "Alice", "gender", "female", "n_people", 0);
assertRenderWithData(templateBody, data, "Alice added nobody to her circle.");
data = CONVERTER.newDict("person", "Alice", "gender", "female", "n_people", 1);
assertRenderWithData(templateBody, data, "Alice added one person to her circle.");
data = CONVERTER.newDict("person", "Alice", "gender", "female", "n_people", 10);
assertRenderWithData(templateBody, data, "Alice added 10 people to her circle.");
data = CONVERTER.newDict("person", "Bob", "gender", "male", "n_people", 10);
assertRenderWithData(templateBody, data, "Bob added 10 people to his circle.");
}
@Test
public void testRenderPlrselComplexConstructs() throws Exception {
String templateBody =
" {@param person : [gender: string, name: string]}\n"
+ " {@param invitees : list<string>}\n"
+ " {msg desc=\"[ICU Syntax] A sample nested message with complex constructs\"}\n"
+ " {select $person.gender}\n"
+ " {case 'female'}\n"
+ " {plural length($invitees) offset=\"1\"}\n"
+ " {case 0}{$person.name} added nobody to her circle.\n"
+ " {case 1}{$person.name} added {$invitees[0]} to her circle.\n"
+ " {default}{$person.name} added {$invitees[0]} and "
+ "{remainder(length($invitees))} others to her circle.\n"
+ " {/plural}\n"
+ " {default}\n"
+ " {plural length($invitees) offset=\"1\"}\n"
+ " {case 0}{$person.name} added nobody to his circle.\n"
+ " {case 1}{$person.name} added {$invitees[0]} to his circle.\n"
+ " {default}{$person.name} added {$invitees[0]} and "
+ "{remainder(length($invitees))} others to his circle.\n"
+ " {/plural}\n"
+ " {/select}\n"
+ " {/msg}\n";
SoyDict data =
CONVERTER.newDict(
"person", CONVERTER.newDict("name", "Alice", "gender", "female"),
"invitees", CONVERTER.newList("Anna", "Brent", "Chris", "Darin"));
assertRenderWithData(templateBody, data, "Alice added Anna and 3 others to her circle.");
data =
CONVERTER.newDict(
"person", CONVERTER.newDict("name", "Bob", "gender", "male"),
"invitees", CONVERTER.newList("Anna", "Brent", "Chris", "Darin"));
assertRenderWithData(templateBody, data, "Bob added Anna and 3 others to his circle.");
}
@Test
public void testRenderPluralWithEmbeddedHtmlElements() throws Exception {
/**
* Link to open up the email options dialog.
*
* @param num {number} Number of people who will be notified via email.
*/
String templateBody =
"{@param num: ?}\n"
+ "{msg desc=\"[ICU Syntax] Explanatory text saying that with current\n"
+ " settings, $num people will be notified via email\"}\n"
+ " {plural $num}\n"
+ " {case 0}\n"
+ " Notify people via email ›\n"
+ " {case 1}\n"
+ " Notify{sp}\n"
+ " <span class=\"{css sharebox-id-email-number}\">{$num}</span>{sp}\n"
+ " person via email ›\n"
+ " {default}\n"
+ " Notify{sp}\n"
+ " <span class=\"{css sharebox-id-email-number}\">{$num}</span>{sp}\n"
+ " people via email ›\n"
+ " {/plural}\n"
+ " {/msg}\n";
SoyDict data = CONVERTER.newDict("num", 1);
assertRenderWithData(
templateBody,
data,
"Notify <span class=\"sharebox-id-email-number\">1</span> person via email ›");
data = CONVERTER.newDict("num", 10);
assertRenderWithData(
templateBody,
data,
"Notify <span class=\"sharebox-id-email-number\">10</span> people via email ›");
}
@Test
public void testRenderSelectWithRuntimeErrors() throws Exception {
String templateBody =
"{@param gender: ?}\n"
+ "{@param person: ?}\n"
+ " {msg desc=\"Simple select message\"}\n"
+ " {select $gender}\n"
+ " {case 'female'}{$person} shared her photos.\n"
+ " {default}{$person} shared his photos.\n"
+ " {/select}\n"
+ " {/msg}\n";
SoyDict data = CONVERTER.newDict("person", "The president", "gender", 100);
assertRenderExceptionWithData(
templateBody, data, "Select expression \"$gender\" doesn't evaluate to string.");
}
@Test
public void testRenderPluralWithRuntimeErrors() throws Exception {
String templateBody =
"{@param n_people: ?}\n"
+ "{@param person: ?}\n"
+ " {msg desc=\"Simple plural message\"}\n"
+ " {plural $n_people offset=\"1\"}\n"
+ " {case 0}Nobody shared photos.\n"
+ " {case 1}Only {$person} shared photos.\n"
+ " {default}{$person} and {remainder($n_people)} others shared photos.\n"
+ " {/plural}\n"
+ " {/msg}\n";
SoyDict data = CONVERTER.newDict("person", "Bob", "n_people", "nobody");
assertRenderExceptionWithData(
templateBody, data, "Plural expression \"$n_people\" doesn't evaluate to number.");
}
@Test
public void testRenderLetStmt() throws Exception {
String templateBody =
"{@param foo: ?}\n"
+ " {let $alpha: $foo.goo2[1] /}\n"
+ " {let $beta}Boo!{/let}\n"
+ " {let $gamma}\n"
+ " {for $i in range($alpha)}\n"
+ " {$i}{$beta}\n"
+ " {/for}\n"
+ " {/let}\n"
+ " {$alpha}{$beta}{$gamma}\n";
assertRender(templateBody, "3Boo!0Boo!1Boo!2Boo!");
}
@Test
public void testRenderIfStmt() throws Exception {
String templateBody =
"{@param boo: ?}\n"
+ "{@param goo: ?}\n"
+ "{@param moo: ?}\n"
+ "{@param f: ?}\n"
+ " {if $boo}{$boo}{/if}\n"
+ " {if ''}-{else}+{/if}\n"
+ " {if $f or 0.0}\n"
+ " Blah\n"
+ " {elseif $goo[2] > 2 and $ij.ijBool}\n"
+ " {$moo}\n"
+ " {else}\n"
+ " Blah {$moo}\n"
+ " {/if}\n";
assertRender(templateBody, "8+3.14");
}
@Test
public void testRenderSwitchStmt() throws Exception {
String templateBody =
"{@param foo: ?}\n"
+ "{@param boo: ?}\n"
+ "{@param goo: ?}\n"
+ "{@param t: ?}\n"
+ " {switch $boo} {case 0}Blah\n"
+ " {case $goo[1]+1}\n"
+ " Bleh\n"
+ " {case -1, 1, $goo[2]+2}\n"
+ " Bluh\n"
+ " {default}\n"
+ " Bloh\n"
+ " {/switch}{sp}\n"
+ " {switch $foo.bar}{case 'baz',$boo}baz{default}zab{/switch}\n"
+ " {switch true}{case not $t}daz{default}zad{/switch}\n";
assertRender(templateBody, "Bluh bazzad");
}
@Test
public void testRenderForeachStmt() throws Exception {
String templateBody =
""
+ "{@param goo : list<?> }\n"
+ "{@param list0 : list<?> }\n"
+ "{@param foo : ? }\n"
+ "{@param boo : ? }\n"
+ " {foreach $n in $goo}\n"
+ " {if not isFirst($n)}{\\n}{/if}\n"
+ " {$n} = Sum of 1 through {index($n) + 1}.\n"
+ " {/foreach}\n"
+ " {\\n}\n"
+ " {foreach $i in $goo}\n"
+ " {foreach $j in $foo.goo2}\n"
+ " {if $i == $j} {$i + $j}{/if}\n"
+ " {/foreach}\n"
+ " {/foreach}\n"
+ " {sp}\n"
+ " {foreach $item in $list0}\n"
+ " Blah\n"
+ " {ifempty}\n"
+ " Bluh\n"
+ " {/foreach}\n"
+ " {foreach $item in $list0}\n"
+ " Blah\n"
+ " {/foreach}\n"
+ " {foreach $item in ['blah', 123, $boo]}\n"
+ " {sp}{$item}\n"
+ " {/foreach}\n";
assertRender(
templateBody,
""
+ "1 = Sum of 1 through 1.\n"
+ "3 = Sum of 1 through 2.\n"
+ "6 = Sum of 1 through 3.\n"
+ "10 = Sum of 1 through 4.\n"
+ "15 = Sum of 1 through 5.\n"
+ "21 = Sum of 1 through 6.\n"
+ " 2 6 12 20 30 42 Bluh blah 123 8");
// Test iteration over map keys.
templateBody =
""
+ "{@param myMap : map<string, ?> }\n"
+ " {foreach $key in keys($myMap)}\n"
+ " {if isFirst($key)}\n"
+ " [\n"
+ " {/if}\n"
+ " {$key}: {$myMap[$key]}\n"
+ " {if isLast($key)}\n"
+ " ]\n"
+ " {else}\n"
+ " ,{sp}\n"
+ " {/if}\n"
+ " {/foreach}\n";
SoyDict data = CONVERTER.newDict("myMap", CONVERTER.newDict("aaa", "Blah", "bbb", 17));
String output = renderWithData(templateBody, data);
assertThat(ImmutableSet.of("[aaa: Blah, bbb: 17]", "[bbb: 17, aaa: Blah]")).contains(output);
}
@Test
public void testRenderForStmt() throws Exception {
String templateBody =
"{@param goo : list<?> }\n"
+ " {foreach $n in $goo}\n"
+ " {if not isFirst($n)}{\\n}{/if}\n"
+ " {$n} ={sp}\n"
+ " {for $i in range(1, index($n)+2)}\n"
+ " {if $i != 1} + {/if}\n"
+ " {$i}\n"
+ " {/for}\n"
+ " {/foreach}\n";
assertRender(
templateBody,
"1 = 1\n"
+ "3 = 1 + 2\n"
+ "6 = 1 + 2 + 3\n"
+ "10 = 1 + 2 + 3 + 4\n"
+ "15 = 1 + 2 + 3 + 4 + 5\n"
+ "21 = 1 + 2 + 3 + 4 + 5 + 6");
}
@Test
public void testRenderWithXidRenaming() throws Exception {
xidRenamingMap = TEST_XID_RENAMING_MAP;
String templateBody = "..{xid ident}";
assertRender(templateBody, "..ident_id_renamed");
}
@Test
public void testRenderWithoutXidRenaming() throws Exception {
String templateBody = "..{xid ident}";
assertRender(templateBody, "..ident_");
}
@Test
public void testRenderWithCssRenaming() throws Exception {
cssRenamingMap = TEST_CSS_RENAMING_MAP;
String templateBody =
"{@param component : ? }\n" + "..{css class}" + "..{css $component, selector}";
assertRender(templateBody, "..class_renamed" + "..comp-selector_renamed");
}
@Test
public void testRenderWithoutCssRenaming() throws Exception {
String templateBody =
"{@param component : ? }\n" + "..{css class}" + "..{css $component, selector}";
assertRender(templateBody, "..class" + "..comp-selector");
}
@Test
public void testRenderPcdataWithKnownSafeHtml() throws Exception {
String templateBody =
"{@param plainText : ?}\n"
+ "{@param sanitizedContent : ?}\n"
+ "plain: {$plainText |escapeHtml}{\\n}"
+ "html: {$sanitizedContent |escapeHtml}{\\n}"
+ "The end.";
assertRender(
templateBody,
"plain: <plaintext id=foo>\n" + "html: <plaintext id=foo>\n" + "The end.");
}
@Test
public void testRenderBasicCall() throws Exception {
String soyFileContent =
"{namespace ns autoescape=\"deprecated-noncontextual\"}\n"
+ "\n"
+ "/** @param boo @param foo @param goo */\n"
+ "{template .callerTemplate}\n"
+ " {call .calleeTemplate data=\"all\" /}\n"
+ " {call .calleeTemplate data=\"$foo\" /}\n"
+ " {call .calleeTemplate data=\"all\"}\n"
+ " {param boo: $foo.boo /}\n"
+ " {/call}\n"
+ " {call .calleeTemplate data=\"all\"}\n"
+ " {param boo: 'moo' /}\n"
+ " {/call}\n"
+ " {call .calleeTemplate data=\"$foo\"}\n"
+ " {param boo}moo{/param}\n"
+ " {/call}\n"
+ " {call .calleeTemplate}\n"
+ " {param boo}zoo{/param}\n"
+ " {param goo: $foo.goo /}\n"
+ " {/call}\n"
+ "{/template}\n"
+ "\n"
+ "/**\n"
+ " * @param boo\n"
+ " * @param goo\n"
+ " */\n"
+ "{template .calleeTemplate}\n"
+ " {$boo}\n"
+ " {foreach $n in $goo} {$n}{/foreach}{\\n}\n"
+ "{/template}\n";
TemplateRegistry templateRegistry =
SoyFileSetParserBuilder.forFileContents(soyFileContent)
.errorReporter(FAIL)
.parse()
.registry();
SoyDict foo = CONVERTER.newDict("boo", "foo", "goo", CONVERTER.newList(3, 2, 1));
SoyDict data = CONVERTER.newDict("boo", "boo", "foo", foo, "goo", CONVERTER.newList(1, 2, 3));
StringBuilder outputSb = new StringBuilder();
RenderVisitor rv =
INJECTOR
.getInstance(RenderVisitorFactory.class)
.create(
outputSb,
templateRegistry,
data,
TEST_IJ_DATA,
Predicates.<String>alwaysFalse(),
null,
xidRenamingMap,
cssRenamingMap);
rv.exec(templateRegistry.getBasicTemplate("ns.callerTemplate"));
String expectedOutput =
"boo 1 2 3\n"
+ "foo 3 2 1\n"
+ "foo 1 2 3\n"
+ "moo 1 2 3\n"
+ "moo 3 2 1\n"
+ "zoo 3 2 1\n";
assertThat(outputSb.toString()).isEqualTo(expectedOutput);
}
private static class TestFuture extends AbstractFuture<String> {
private int isDoneCounter;
private final StringBuilder progress;
TestFuture(String val, StringBuilder progress) {
this.set(val);
this.progress = progress;
}
@Override
public boolean isDone() {
// Return false twice. We check each future once whether we need to check upon rendering
// whether it is done and once when actually rendering.
if (isDoneCounter >= 2) {
return true;
}
isDoneCounter++;
return false;
}
@Override
public String get() throws InterruptedException, ExecutionException {
String val = super.get();
progress.append(val);
return val;
}
}
@Test
public void testRenderFuture() throws Exception {
final StringBuilder progress = new StringBuilder();
Flushable flushable =
new Flushable() {
@Override
public void flush() {
progress.append("flush;");
}
};
String soyFileContent =
"{namespace ns autoescape=\"deprecated-noncontextual\"}\n"
+ "\n"
+ "/** @param boo @param foo @param goo */\n"
+ "{template .callerTemplate}\n"
+ " {call .calleeTemplate data=\"all\" /}\n"
+ " {call .calleeTemplate data=\"$foo\" /}\n"
+ " {call .calleeTemplate data=\"all\"}\n"
+ " {param boo: $foo.boo /}\n"
+ " {/call}\n"
+ " {call .calleeTemplate data=\"all\"}\n"
+ " {param boo: 'moo' /}\n"
+ " {/call}\n"
+ " {call .calleeTemplate data=\"$foo\"}\n"
+ " {param boo}moo{/param}\n"
+ " {/call}\n"
+ " {call .calleeTemplate}\n"
+ " {param boo}zoo{/param}\n"
+ " {param goo: $foo.goo /}\n"
+ " {/call}\n"
+ "{/template}\n"
+ "\n"
+ "/**\n"
+ " * @param boo\n"
+ " * @param goo\n"
+ " */\n"
+ "{template .calleeTemplate}\n"
+ " {$boo}{$ij.future}\n"
+ " {foreach $n in $goo} {$n}{/foreach}{\\n}\n"
+ "{/template}\n";
TemplateRegistry templateRegistry =
SoyFileSetParserBuilder.forFileContents(soyFileContent)
.errorReporter(FAIL)
.parse()
.registry();
SoyDict foo =
CONVERTER.newDict(
"boo", new TestFuture("foo", progress), "goo", CONVERTER.newList(3, 2, 1));
SoyDict data =
CONVERTER.newDict(
"boo", new TestFuture("boo", progress), "foo", foo, "goo", CONVERTER.newList(1, 2, 3));
SoyRecord testIj = CONVERTER.newDict("future", new TestFuture("ij", progress));
StringBuilder outputSb = new StringBuilder();
CountingFlushableAppendable output = new CountingFlushableAppendable(outputSb, flushable);
RenderVisitor rv =
INJECTOR
.getInstance(RenderVisitorFactory.class)
.create(
output,
templateRegistry,
data,
testIj,
Predicates.<String>alwaysFalse(),
null,
xidRenamingMap,
cssRenamingMap);
rv.exec(templateRegistry.getBasicTemplate("ns.callerTemplate"));
String expectedOutput =
"booij 1 2 3\n"
+ "fooij 3 2 1\n"
+ "fooij 1 2 3\n"
+ "mooij 1 2 3\n"
+ "mooij 3 2 1\n"
+ "zooij 3 2 1\n";
assertThat(outputSb.toString()).isEqualTo(expectedOutput);
assertThat(progress.toString()).isEqualTo("booflush;ijflush;foo");
}
@Test
public void testRenderDelegateCall() throws Exception {
String soyFileContent1 =
"{namespace ns1 autoescape=\"deprecated-noncontextual\"}\n"
+ "\n"
+ "/***/\n"
+ "{template .callerTemplate}\n"
+ " {delcall myApp.myDelegate}\n"
+ " {param boo: 'aaaaaah' /}\n"
+ " {/delcall}\n"
+ "{/template}\n"
+ "\n"
+ "/** @param boo */\n"
+ "{deltemplate myApp.myDelegate}\n"
+ // default implementation (doesn't use $boo)
" 000\n"
+ "{/deltemplate}\n";
String soyFileContent2 =
"{delpackage SecretFeature}\n"
+ "{namespace ns2 autoescape=\"deprecated-noncontextual\"}\n"
+ "\n"
+ "/** @param boo */\n"
+ "{deltemplate myApp.myDelegate}\n"
+ // implementation in SecretFeature
" 111 {$boo}\n"
+ "{/deltemplate}\n";
String soyFileContent3 =
"{delpackage AlternateSecretFeature}\n"
+ "{namespace ns3 autoescape=\"deprecated-noncontextual\"}\n"
+ "\n"
+ "/** @param boo */\n"
+ "{deltemplate myApp.myDelegate}\n"
+ // implementation in AlternateSecretFeature
" 222 {call .helper data=\"all\" /}\n"
+ "{/deltemplate}\n";
String soyFileContent4 =
"{delpackage AlternateSecretFeature}\n"
+ "{namespace ns3 autoescape=\"deprecated-noncontextual\"}\n"
+ "\n"
+ "/** @param boo */\n"
+ "{template .helper private=\"true\"}\n"
+ " {$boo} {$ij.ijStr}\n"
+ "{/template}\n";
TemplateRegistry templateRegistry =
SoyFileSetParserBuilder.forFileContents(
soyFileContent1, soyFileContent2, soyFileContent3, soyFileContent4)
.errorReporter(FAIL)
.parse()
.registry();
TemplateNode callerTemplate = templateRegistry.getBasicTemplate("ns1.callerTemplate");
Predicate<String> activeDelPackageNames = Predicates.alwaysFalse();
StringBuilder outputSb = new StringBuilder();
INJECTOR
.getInstance(RenderVisitorFactory.class)
.create(
outputSb,
templateRegistry,
CONVERTER.newDict(),
TEST_IJ_DATA,
activeDelPackageNames,
null,
xidRenamingMap,
cssRenamingMap)
.exec(callerTemplate);
assertThat(outputSb.toString()).isEqualTo("000");
activeDelPackageNames = Predicates.equalTo("SecretFeature");
outputSb = new StringBuilder();
INJECTOR
.getInstance(RenderVisitorFactory.class)
.create(
outputSb,
templateRegistry,
CONVERTER.newDict(),
TEST_IJ_DATA,
activeDelPackageNames,
null,
xidRenamingMap,
cssRenamingMap)
.exec(callerTemplate);
assertThat(outputSb.toString()).isEqualTo("111 aaaaaah");
activeDelPackageNames = Predicates.equalTo("AlternateSecretFeature");
outputSb = new StringBuilder();
INJECTOR
.getInstance(RenderVisitorFactory.class)
.create(
outputSb,
templateRegistry,
CONVERTER.newDict(),
TEST_IJ_DATA,
activeDelPackageNames,
null,
xidRenamingMap,
cssRenamingMap)
.exec(callerTemplate);
assertThat(outputSb.toString()).isEqualTo("222 aaaaaah injected");
activeDelPackageNames = Predicates.equalTo("NonexistentFeature");
outputSb = new StringBuilder();
INJECTOR
.getInstance(RenderVisitorFactory.class)
.create(
outputSb,
templateRegistry,
CONVERTER.newDict(),
TEST_IJ_DATA,
activeDelPackageNames,
null,
xidRenamingMap,
cssRenamingMap)
.exec(callerTemplate);
assertThat(outputSb.toString()).isEqualTo("000");
activeDelPackageNames =
Predicates.in(ImmutableSet.of("NonexistentFeature", "AlternateSecretFeature"));
outputSb = new StringBuilder();
INJECTOR
.getInstance(RenderVisitorFactory.class)
.create(
outputSb,
templateRegistry,
CONVERTER.newDict(),
TEST_IJ_DATA,
activeDelPackageNames,
null,
xidRenamingMap,
cssRenamingMap)
.exec(callerTemplate);
assertThat(outputSb.toString()).isEqualTo("222 aaaaaah injected");
activeDelPackageNames =
Predicates.in(ImmutableSet.of("SecretFeature", "AlternateSecretFeature"));
outputSb = new StringBuilder();
try {
INJECTOR
.getInstance(RenderVisitorFactory.class)
.create(
outputSb,
templateRegistry,
CONVERTER.newDict(),
TEST_IJ_DATA,
activeDelPackageNames,
null,
xidRenamingMap,
cssRenamingMap)
.exec(callerTemplate);
fail();
} catch (RenderException e) {
assertThat(e.getMessage())
.contains(
"For delegate template 'myApp.myDelegate', found two active implementations with"
+ " equal priority");
}
}
@Test
public void testRenderDelegateVariantCall() throws Exception {
String soyFileContent1 =
""
+ "{namespace ns1 autoescape=\"deprecated-noncontextual\"}\n"
+ "\n"
+ "/** @param greekB */\n"
+ "{template .callerTemplate}\n"
+ " {delcall myApp.myDelegate variant=\"'alpha'\"}\n"
+ // variant is string
" {param boo: 'zzz' /}\n"
+ " {/delcall}\n"
+ " {delcall myApp.myDelegate variant=\"$greekB\"}\n"
+ // variant is expression
" {param boo: 'zzz' /}\n"
+ " {/delcall}\n"
+ " {delcall myApp.myDelegate variant=\"'gamma'\"}\n"
+ // variant "gamma" not implemented
" {param boo: 'zzz' /}\n"
+ " {/delcall}\n"
+ " {delcall myApp.myDelegate variant=\"test.GLOBAL\"}\n"
+ // variant is a global expression
" {param boo: 'zzz' /}\n"
+ " {/delcall}\n"
+ "{/template}\n"
+ "\n"
+ "/** @param boo */\n"
+ "{deltemplate myApp.myDelegate}\n"
+ // variant "" default
" 000empty\n"
+ "{/deltemplate}\n"
+ "\n"
+ "/** @param boo */\n"
+ "{deltemplate myApp.myDelegate variant=\"'alpha'\"}\n"
+ // variant "alpha" default
" 000alpha\n"
+ "{/deltemplate}\n"
+ "\n"
+ "/** @param boo */\n"
+ "{deltemplate myApp.myDelegate variant=\"'beta'\"}\n"
+ // variant "beta" default
" 000beta\n"
+ "{/deltemplate}\n"
+ "/** @param boo */\n"
+ "{deltemplate myApp.myDelegate variant=\"test.GLOBAL\"}\n"
+ // variant using global
" 000global\n"
+ "{/deltemplate}\n";
String soyFileContent2 =
""
+ "{delpackage SecretFeature}\n"
+ "{namespace ns2 autoescape=\"deprecated-noncontextual\"}\n"
+ "\n"
+ "/** @param boo */\n"
+ "{deltemplate myApp.myDelegate}\n"
+ // variant "" in SecretFeature
" 111empty\n"
+ "{/deltemplate}\n"
+ "\n"
+ "/** @param boo */\n"
+ "{deltemplate myApp.myDelegate variant=\"'alpha'\"}\n"
+ // "alpha" in SecretFeature
" 111alpha\n"
+ "{/deltemplate}\n"
+ "\n"
+ "/** @param boo */\n"
+ "{deltemplate myApp.myDelegate variant=\"'beta'\"}\n"
+ // "beta" in SecretFeature
" 111beta\n"
+ "{/deltemplate}\n"
+ "/** @param boo */\n"
+ "{deltemplate myApp.myDelegate variant=\"test.GLOBAL\"}\n"
+ // variant using global
" 111global\n"
+ "{/deltemplate}\n";
String soyFileContent3 =
""
+ "{delpackage AlternateSecretFeature}\n"
+ "{namespace ns3 autoescape=\"deprecated-noncontextual\"}\n"
+ "\n"
+ "/** @param boo */\n"
+ "{deltemplate myApp.myDelegate}\n"
+ // variant "" in AlternateSecretFeature
" 222empty\n"
+ "{/deltemplate}\n"
+ "\n"
+ "/** @param boo */\n"
+ "{deltemplate myApp.myDelegate variant=\"'alpha'\"}\n"
+ // variant "alpha" in Alternate
" 222alpha\n"
+ "{/deltemplate}\n"
+ "/** @param boo */\n"
+ "{deltemplate myApp.myDelegate variant=\"test.GLOBAL\"}\n"
+ // variant using global
" 222global\n"
+ "{/deltemplate}\n"; // Note: No variant "beta" in AlternateSecretFeature.
SoyGeneralOptions options = new SoyGeneralOptions();
options.setCompileTimeGlobals(ImmutableMap.<String, Object>of("test.GLOBAL", 1));
ParseResult result =
SoyFileSetParserBuilder.forFileContents(soyFileContent1, soyFileContent2, soyFileContent3)
.options(options)
.errorReporter(FAIL)
.parse();
TemplateRegistry templateRegistry = result.registry();
TemplateNode callerTemplate = templateRegistry.getBasicTemplate("ns1.callerTemplate");
Predicate<String> activeDelPackageNames = Predicates.alwaysFalse();
StringBuilder outputSb = new StringBuilder();
INJECTOR
.getInstance(RenderVisitorFactory.class)
.create(
outputSb,
templateRegistry,
TEST_DATA,
TEST_IJ_DATA,
activeDelPackageNames,
null,
xidRenamingMap,
cssRenamingMap)
.exec(callerTemplate);
assertThat(outputSb.toString()).isEqualTo("000alpha000beta000empty000global");
activeDelPackageNames = Predicates.equalTo("SecretFeature");
outputSb = new StringBuilder();
INJECTOR
.getInstance(RenderVisitorFactory.class)
.create(
outputSb,
templateRegistry,
TEST_DATA,
TEST_IJ_DATA,
activeDelPackageNames,
null,
xidRenamingMap,
cssRenamingMap)
.exec(callerTemplate);
assertThat(outputSb.toString()).isEqualTo("111alpha111beta111empty111global");
activeDelPackageNames = Predicates.equalTo("AlternateSecretFeature");
outputSb = new StringBuilder();
INJECTOR
.getInstance(RenderVisitorFactory.class)
.create(
outputSb,
templateRegistry,
TEST_DATA,
TEST_IJ_DATA,
activeDelPackageNames,
null,
xidRenamingMap,
cssRenamingMap)
.exec(callerTemplate);
assertThat(outputSb.toString()).isEqualTo("222alpha000beta222empty222global");
activeDelPackageNames = Predicates.equalTo("NonexistentFeature");
outputSb = new StringBuilder();
INJECTOR
.getInstance(RenderVisitorFactory.class)
.create(
outputSb,
templateRegistry,
TEST_DATA,
TEST_IJ_DATA,
activeDelPackageNames,
null,
xidRenamingMap,
cssRenamingMap)
.exec(callerTemplate);
assertThat(outputSb.toString()).isEqualTo("000alpha000beta000empty000global");
activeDelPackageNames =
Predicates.in(ImmutableSet.of("NonexistentFeature", "AlternateSecretFeature"));
outputSb = new StringBuilder();
INJECTOR
.getInstance(RenderVisitorFactory.class)
.create(
outputSb,
templateRegistry,
TEST_DATA,
TEST_IJ_DATA,
activeDelPackageNames,
null,
xidRenamingMap,
cssRenamingMap)
.exec(callerTemplate);
assertThat(outputSb.toString()).isEqualTo("222alpha000beta222empty222global");
activeDelPackageNames =
Predicates.in(ImmutableSet.of("SecretFeature", "AlternateSecretFeature"));
outputSb = new StringBuilder();
try {
INJECTOR
.getInstance(RenderVisitorFactory.class)
.create(
outputSb,
templateRegistry,
TEST_DATA,
TEST_IJ_DATA,
activeDelPackageNames,
null,
xidRenamingMap,
cssRenamingMap)
.exec(callerTemplate);
fail();
} catch (RenderException e) {
assertThat(e.getMessage())
.contains(
"For delegate template 'myApp.myDelegate:alpha', found two active implementations with"
+ " equal priority");
}
}
@Test
public void testRenderDelegateCallWithoutDefault() throws Exception {
String soyFileContent1a =
"{namespace ns1 autoescape=\"deprecated-noncontextual\"}\n"
+ "\n"
+ "/***/\n"
+ "{template .callerTemplate}\n"
+ " {delcall myApp.myDelegate allowemptydefault=\"true\"}\n"
+ " {param boo: 'aaaaaah' /}\n"
+ " {/delcall}\n"
+ "{/template}\n";
String soyFileContent1b =
"{namespace ns1 autoescape=\"deprecated-noncontextual\"}\n"
+ "\n"
+ "/***/\n"
+ "{template .callerTemplate}\n"
+ " {delcall myApp.myDelegate allowemptydefault=\"false\"}\n"
+ " {param boo: 'aaaaaah' /}\n"
+ " {/delcall}\n"
+ "{/template}\n";
String soyFileContent2 =
"{delpackage SecretFeature}\n"
+ "{namespace ns2 autoescape=\"deprecated-noncontextual\"}\n"
+ "\n"
+ "/** @param boo */\n"
+ "{deltemplate myApp.myDelegate}\n"
+ // implementation in SecretFeature
" 111 {$boo}\n"
+ "{/deltemplate}\n";
// ------ Test with only file 1a in bundle. ------
TemplateRegistry templateRegistry =
SoyFileSetParserBuilder.forFileContents(soyFileContent1a).parse().registry();
TemplateNode callerTemplate = templateRegistry.getBasicTemplate("ns1.callerTemplate");
Predicate<String> activeDelPackageNames = Predicates.alwaysFalse();
StringBuilder outputSb = new StringBuilder();
INJECTOR
.getInstance(RenderVisitorFactory.class)
.create(
outputSb,
templateRegistry,
CONVERTER.newDict(),
null,
activeDelPackageNames,
null,
xidRenamingMap,
cssRenamingMap)
.exec(callerTemplate);
assertThat(outputSb.toString()).isEmpty();
activeDelPackageNames = Predicates.equalTo("SecretFeature");
outputSb = new StringBuilder();
INJECTOR
.getInstance(RenderVisitorFactory.class)
.create(
outputSb,
templateRegistry,
CONVERTER.newDict(),
null,
activeDelPackageNames,
null,
xidRenamingMap,
cssRenamingMap)
.exec(callerTemplate);
assertThat(outputSb.toString()).isEmpty();
// ------ Test with both files 1a and 2 in bundle. ------
templateRegistry =
SoyFileSetParserBuilder.forFileContents(soyFileContent1a, soyFileContent2)
.parse()
.registry();
callerTemplate = templateRegistry.getBasicTemplate("ns1.callerTemplate");
activeDelPackageNames = Predicates.alwaysFalse();
outputSb = new StringBuilder();
INJECTOR
.getInstance(RenderVisitorFactory.class)
.create(
outputSb,
templateRegistry,
CONVERTER.newDict(),
null,
activeDelPackageNames,
null,
xidRenamingMap,
cssRenamingMap)
.exec(callerTemplate);
assertThat(outputSb.toString()).isEmpty();
activeDelPackageNames = Predicates.equalTo("SecretFeature");
outputSb = new StringBuilder();
INJECTOR
.getInstance(RenderVisitorFactory.class)
.create(
outputSb,
templateRegistry,
CONVERTER.newDict(),
null,
activeDelPackageNames,
null,
xidRenamingMap,
cssRenamingMap)
.exec(callerTemplate);
assertThat(outputSb.toString()).isEqualTo("111 aaaaaah");
activeDelPackageNames = Predicates.equalTo("NonexistentFeature");
outputSb = new StringBuilder();
INJECTOR
.getInstance(RenderVisitorFactory.class)
.create(
outputSb,
templateRegistry,
CONVERTER.newDict(),
null,
activeDelPackageNames,
null,
xidRenamingMap,
cssRenamingMap)
.exec(callerTemplate);
assertThat(outputSb.toString()).isEmpty();
activeDelPackageNames = Predicates.in(ImmutableSet.of("NonexistentFeature", "SecretFeature"));
outputSb = new StringBuilder();
INJECTOR
.getInstance(RenderVisitorFactory.class)
.create(
outputSb,
templateRegistry,
CONVERTER.newDict(),
null,
activeDelPackageNames,
null,
xidRenamingMap,
cssRenamingMap)
.exec(callerTemplate);
assertThat(outputSb.toString()).isEqualTo("111 aaaaaah");
// ------ Test with only file 1b in bundle. ------
templateRegistry =
SoyFileSetParserBuilder.forFileContents(soyFileContent1b)
.errorReporter(FAIL)
.parse()
.registry();
callerTemplate = templateRegistry.getBasicTemplate("ns1.callerTemplate");
activeDelPackageNames = Predicates.alwaysFalse();
try {
INJECTOR
.getInstance(RenderVisitorFactory.class)
.create(
new StringBuilder(),
templateRegistry,
CONVERTER.newDict(),
null,
activeDelPackageNames,
null,
xidRenamingMap,
cssRenamingMap)
.exec(callerTemplate);
fail();
} catch (RenderException re) {
assertThat(re.getMessage()).contains("Found no active impl for delegate call");
}
// ------ Test with both files 1b and 2 in bundle. ------
templateRegistry =
SoyFileSetParserBuilder.forFileContents(soyFileContent1b, soyFileContent2)
.errorReporter(FAIL)
.parse()
.registry();
callerTemplate = templateRegistry.getBasicTemplate("ns1.callerTemplate");
activeDelPackageNames = Predicates.alwaysFalse();
try {
INJECTOR
.getInstance(RenderVisitorFactory.class)
.create(
new StringBuilder(),
templateRegistry,
CONVERTER.newDict(),
null,
activeDelPackageNames,
null,
xidRenamingMap,
cssRenamingMap)
.exec(callerTemplate);
fail();
} catch (RenderException re) {
assertThat(re.getMessage()).contains("Found no active impl for delegate call");
}
activeDelPackageNames = Predicates.equalTo("SecretFeature");
outputSb = new StringBuilder();
INJECTOR
.getInstance(RenderVisitorFactory.class)
.create(
outputSb,
templateRegistry,
CONVERTER.newDict(),
null,
activeDelPackageNames,
null,
xidRenamingMap,
cssRenamingMap)
.exec(callerTemplate);
assertThat(outputSb.toString()).isEqualTo("111 aaaaaah");
}
@Test
public void testRenderLogStmt() throws Exception {
String templateBody =
""
+ "{@param foo: ?}\n"
+ "{@param boo: ?}\n"
+ "{@param moo: ?}\n"
+ "{if true}\n"
+ " {$foo.bar}\n"
+ " {log}Blah {$boo}.{/log}\n"
+ " {$moo}\n"
+ "{/if}\n";
// Send stdout to my own buffer.
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
System.setOut(new PrintStream(buffer));
assertRender(templateBody, "baz3.14");
assertThat(buffer.toString()).isEqualTo("Blah 8.\n");
// Restore stdout.
System.setOut(new PrintStream(new FileOutputStream(FileDescriptor.out)));
}
@Test
public void testRenderLogStmtOrdering() throws Exception {
String templateBody =
""
+ "{let $gamma}\n"
+ " {log}let-block{/log}\n"
+ " let-block\n"
+ "{/let}\n"
+ "before{sp}{log}before{/log}\n"
+ "{$gamma}\n"
+ "{sp}after{log}after{/log}\n"
+ "{sp}{$gamma}\n";
// Send stdout to my own buffer.
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
System.setOut(new PrintStream(buffer));
// the let block is evaluated exactly once.
assertRender(templateBody, "before let-block after let-block");
assertThat(buffer.toString()).isEqualTo("before\nlet-block\nafter\n");
System.setOut(new PrintStream(new FileOutputStream(FileDescriptor.out)));
}
@Test
public void testRenderCallLazyParamContentNode() throws Exception {
String soyFileContent =
"{namespace ns autoescape=\"deprecated-noncontextual\"}\n"
+ "\n"
+ "/** */\n"
+ "{template .callerTemplate}\n"
+ " {call .calleeTemplate}\n"
+ " {param foo}\n"
+ " param{log}param{/log}\n"
+ " {/param}\n"
+ " {/call}\n"
+ "{/template}\n"
+ "\n"
+ "/**\n"
+ " * @param foo\n"
+ " */\n"
+ "{template .calleeTemplate}\n"
+ " callee{log}callee{/log}\n"
+ " {sp}{$foo}{sp}{$foo}\n"
+ "{/template}\n";
TemplateRegistry templateRegistry =
SoyFileSetParserBuilder.forFileContents(soyFileContent)
.errorReporter(FAIL)
.parse()
.registry();
StringBuilder outputSb = new StringBuilder();
// Send stdout to my own buffer.
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
System.setOut(new PrintStream(buffer));
RenderVisitor rv =
INJECTOR
.getInstance(RenderVisitorFactory.class)
.create(
outputSb,
templateRegistry,
CONVERTER.newDict(),
null,
Predicates.<String>alwaysFalse(),
null,
xidRenamingMap,
cssRenamingMap);
rv.exec(templateRegistry.getBasicTemplate("ns.callerTemplate"));
assertThat(outputSb.toString()).isEqualTo("callee param param");
assertThat(buffer.toString()).isEqualTo("callee\nparam\n");
// Restore stdout.
System.setOut(new PrintStream(new FileOutputStream(FileDescriptor.out)));
}
@Test
public void testRenderExceptionsHaveExtraInfo() throws Exception {
assertRenderException(
"{@param undefined: ?}\n" + " Hello {$undefined}\n",
"In 'print' tag, expression \"$undefined\" evaluates to undefined.");
assertRenderException(
"{@param undefined: ?}\n" + " Hello {$undefined + 'foo'}\n",
"When evaluating \"$undefined + 'foo'\":");
assertRenderException(
"{@param undefined: ?}\n" + " Hello {$undefined + 3}\n",
"When evaluating \"$undefined + 3\":");
}
@Test
public void testParamTypeCheckSuccess() throws Exception {
assertRender("{@param boo: int}\n{$boo}\n", "8");
assertRender("{@param list1: list<int>}\n{$list1[0]}\n", "1");
}
@Test
public void testInjectedParamTypeCheckSuccess() throws Exception {
assertRender("{@inject ijInt: int}\n{$ijInt}\n", "26");
assertRender("{@inject ijStr: string}\n{$ijStr}\n", "injected");
}
@Test
public void testDelayedCheckingOfCachingProviders() {
String soyFileContent =
"{namespace ns autoescape=\"deprecated-noncontextual\"}\n"
+ "\n"
+ "{template .template}\n"
+ " {@param foo: int}\n"
+ " Before: {$foo}\n"
+ "{/template}\n";
ErrorReporter boom = ExplodingErrorReporter.get();
TemplateRegistry templateRegistry =
SoyFileSetParserBuilder.forFileContents(soyFileContent)
.errorReporter(boom)
.parse()
.registry();
TemplateNode callerTemplate = templateRegistry.getBasicTemplate("ns.template");
final StringBuilder outputSb = new StringBuilder();
final AtomicReference<String> outputAtFutureGetTime = new AtomicReference<>();
AbstractFuture<Integer> fooFuture =
new AbstractFuture<Integer>() {
{
set(1);
}
@Override
public Integer get() throws InterruptedException, ExecutionException {
outputAtFutureGetTime.set(outputSb.toString());
return super.get();
}
};
SoyRecord data = CONVERTER.newDict("foo", fooFuture);
RenderVisitor rv =
INJECTOR
.getInstance(RenderVisitorFactory.class)
.create(
outputSb,
templateRegistry,
data,
TEST_IJ_DATA,
Predicates.<String>alwaysFalse(),
null,
xidRenamingMap,
cssRenamingMap);
rv.exec(callerTemplate);
assertThat(outputAtFutureGetTime.get()).isEqualTo("Before: ");
assertThat(outputSb.toString()).isEqualTo("Before: 1");
}
@Test
public void testDelayedCheckingOfCachingProviders_typeCheckFailure() {
String soyFileContent =
"{namespace ns autoescape=\"deprecated-noncontextual\"}\n"
+ "\n"
+ "{template .template}\n"
+ " {@param foo: int}\n"
+ " Before: {$foo}\n"
+ "{/template}\n";
TemplateRegistry templateRegistry =
SoyFileSetParserBuilder.forFileContents(soyFileContent)
.errorReporter(FAIL)
.parse()
.registry();
TemplateNode callerTemplate = templateRegistry.getBasicTemplate("ns.template");
final StringBuilder outputSb = new StringBuilder();
SoyRecord data = CONVERTER.newDict("foo", Futures.immediateFuture("hello world"));
RenderVisitor rv =
INJECTOR
.getInstance(RenderVisitorFactory.class)
.create(
outputSb,
templateRegistry,
data,
TEST_IJ_DATA,
Predicates.<String>alwaysFalse(),
null,
xidRenamingMap,
cssRenamingMap);
try {
rv.exec(callerTemplate);
fail();
} catch (RenderException exception) {
assertThat(outputSb.toString()).isEqualTo("Before: ");
assertThat(exception.getMessage())
.contains(
"Parameter type mismatch: attempt to bind value 'hello world' to parameter "
+ "'foo' which has declared type 'int'");
}
}
@Test
public void testStreamLazyParamsToOutputStreamDirectly() {
String soyFileContent =
Joiner.on("\n")
.join(
"{namespace ns autoescape=\"deprecated-noncontextual\"}",
"",
"{template .callee}",
" {@param body: html}",
" <div>",
" {$body}",
" </div>",
"{/template}",
"",
"{template .caller}",
" {@param future: string}",
" {call .callee}",
" {param body kind=\"html\"}",
" static-content{sp}",
" {$future}",
" {/param}",
" {/call}",
"{/template}");
TemplateRegistry templateRegistry =
SoyFileSetParserBuilder.forFileContents(soyFileContent)
.errorReporter(FAIL)
.parse()
.registry();
TemplateNode callerTemplate = templateRegistry.getBasicTemplate("ns.caller");
final StringBuilder outputSb = new StringBuilder();
final AtomicReference<String> outputAtFutureGetTime = new AtomicReference<>();
AbstractFuture<String> future =
new AbstractFuture<String>() {
{
set("future-content");
}
@Override
public String get() throws InterruptedException, ExecutionException {
outputAtFutureGetTime.set(outputSb.toString());
return super.get();
}
};
SoyRecord data = CONVERTER.newDict("future", future);
RenderVisitor rv =
INJECTOR
.getInstance(RenderVisitorFactory.class)
.create(
outputSb,
templateRegistry,
data,
TEST_IJ_DATA,
Predicates.<String>alwaysFalse(),
null,
xidRenamingMap,
cssRenamingMap);
rv.exec(callerTemplate);
assertThat(outputAtFutureGetTime.get()).isEqualTo("<div>static-content ");
assertThat(outputSb.toString()).isEqualTo("<div>static-content future-content</div>");
}
@Test
public void testParamTypeCheckFailed() throws Exception {
assertRenderException("{@param boo: string}\n{$boo}\n", "Parameter type mismatch");
assertRenderException("{@param list1: list<string>}\n{$list1[0]}\n", "Expected value of type");
assertRenderException("{@inject ijInt: string}\n{$ijInt}\n", "Parameter type mismatch");
}
private static SoyValue createToStringTestValue() {
return new SoyAbstractValue() {
@Override
public String toString() {
// NOTE: Soy should not print the toString() values, only the coerceToString() values.
return "toString()";
}
@Override
public String coerceToString() {
return "coerceToString()";
}
@Override
public void render(Appendable appendable) throws IOException {
appendable.append(coerceToString());
}
@Override
public boolean coerceToBoolean() {
return true;
}
@Override
public boolean equals(Object other) {
return this.getClass() == other.getClass();
}
@Override
public int hashCode() {
return this.getClass().hashCode();
}
};
}
}