/*
* 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.data.SoyValueConverter.EMPTY_LIST;
import static com.google.template.soy.jbcsrc.TemplateTester.assertThatFile;
import static com.google.template.soy.jbcsrc.TemplateTester.assertThatTemplateBody;
import static com.google.template.soy.jbcsrc.TemplateTester.getDefaultContext;
import static org.junit.Assert.assertEquals;
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.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.template.soy.SoyFileSetParserBuilder;
import com.google.template.soy.data.SanitizedContent.ContentKind;
import com.google.template.soy.data.SanitizedContents;
import com.google.template.soy.data.SoyDict;
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.internal.BasicParamStore;
import com.google.template.soy.data.internal.ParamStore;
import com.google.template.soy.data.restricted.IntegerData;
import com.google.template.soy.data.restricted.StringData;
import com.google.template.soy.error.ExplodingErrorReporter;
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 com.google.template.soy.jbcsrc.shared.TemplateMetadata;
import com.google.template.soy.shared.SoyCssRenamingMap;
import com.google.template.soy.shared.restricted.SoyJavaFunction;
import com.google.template.soy.soytree.CallDelegateNode;
import com.google.template.soy.soytree.SoyFileSetNode;
import com.google.template.soy.soytree.TemplateRegistry;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** A test for the template compiler, notably {@link BytecodeCompiler} and its collaborators. */
@RunWith(JUnit4.class)
public class BytecodeCompilerTest {
@Test
public void testDelCall_delPackageSelections() throws IOException {
String soyFileContent1 =
Joiner.on("\n")
.join(
"{namespace ns1 autoescape=\"strict\"}",
"",
"/***/",
"{template .callerTemplate}",
" {delcall myApp.myDelegate}",
" {param boo: 'aaaaaah' /}",
" {/delcall}",
"{/template}",
"",
"/** */",
"{deltemplate myApp.myDelegate}", // default implementation (doesn't use $boo)
" {@param boo : string}",
" default",
"{/deltemplate}",
"");
String soyFileContent2 =
Joiner.on("\n")
.join(
"{delpackage SecretFeature}",
"{namespace ns2 autoescape=\"strict\"}",
"",
"/** */",
"{deltemplate myApp.myDelegate}", // implementation in SecretFeature
" {@param boo : string}",
" SecretFeature {$boo}",
"{/deltemplate}",
"");
String soyFileContent3 =
Joiner.on("\n")
.join(
"{delpackage AlternateSecretFeature}",
"{namespace ns3 autoescape=\"strict\"}",
"",
"/** */",
"{deltemplate myApp.myDelegate}", // implementation in AlternateSecretFeature
" {@param boo : string}",
" AlternateSecretFeature {call .helper data=\"all\" /}",
"{/deltemplate}",
"");
String soyFileContent4 =
Joiner.on("\n")
.join(
"{namespace ns3 autoescape=\"strict\"}",
"",
"/** */",
"{template .helper private=\"true\"}",
" {@param boo : string}",
" {$boo}",
"{/template}",
"");
SoyFileSetNode soyTree =
SoyFileSetParserBuilder.forFileContents(
soyFileContent1, soyFileContent2, soyFileContent3, soyFileContent4)
.parse()
.fileSet();
TemplateRegistry templateRegistry = new TemplateRegistry(soyTree, ExplodingErrorReporter.get());
CompiledTemplates templates =
BytecodeCompiler.compile(templateRegistry, false, ExplodingErrorReporter.get()).get();
CompiledTemplate.Factory factory = templates.getTemplateFactory("ns1.callerTemplate");
Predicate<String> activePackages = Predicates.alwaysFalse();
assertThat(renderWithContext(factory, getDefaultContext(templates, activePackages)))
.isEqualTo("default");
activePackages = Predicates.equalTo("SecretFeature");
assertThat(renderWithContext(factory, getDefaultContext(templates, activePackages)))
.isEqualTo("SecretFeature aaaaaah");
activePackages = Predicates.equalTo("AlternateSecretFeature");
assertThat(renderWithContext(factory, getDefaultContext(templates, activePackages)))
.isEqualTo("AlternateSecretFeature aaaaaah");
activePackages = Predicates.equalTo("NonexistentFeature");
assertThat(renderWithContext(factory, getDefaultContext(templates, activePackages)))
.isEqualTo("default");
}
private static String renderWithContext(CompiledTemplate.Factory factory, RenderContext context)
throws IOException {
AdvisingStringBuilder builder = new AdvisingStringBuilder();
assertEquals(
RenderResult.done(), factory.create(EMPTY_DICT, EMPTY_DICT).render(builder, context));
String string = builder.toString();
return string;
}
@Test
public void testDelCall_delVariant() throws IOException {
String soyFileContent1 =
Joiner.on("\n")
.join(
"{namespace ns1 autoescape=\"strict\"}",
"",
"/***/",
"{template .callerTemplate}",
" {@param variant : string}",
" {delcall ns1.del variant=\"$variant\" allowemptydefault=\"true\"/}",
"{/template}",
"",
"/** */",
"{deltemplate ns1.del variant=\"'v1'\"}",
" v1",
"{/deltemplate}",
"",
"/** */",
"{deltemplate ns1.del variant=\"'v2'\"}",
" v2",
"{/deltemplate}",
"");
CompiledTemplates templates = compileFiles(soyFileContent1);
CompiledTemplate.Factory factory = templates.getTemplateFactory("ns1.callerTemplate");
RenderContext context = getDefaultContext(templates);
AdvisingStringBuilder builder = new AdvisingStringBuilder();
assertEquals(
RenderResult.done(),
factory
.create(TemplateTester.asRecord(ImmutableMap.of("variant", "v1")), EMPTY_DICT)
.render(builder, context));
assertThat(builder.getAndClearBuffer()).isEqualTo("v1");
assertEquals(
RenderResult.done(),
factory
.create(TemplateTester.asRecord(ImmutableMap.of("variant", "v2")), EMPTY_DICT)
.render(builder, context));
assertThat(builder.getAndClearBuffer()).isEqualTo("v2");
assertEquals(
RenderResult.done(),
factory
.create(TemplateTester.asRecord(ImmutableMap.of("variant", "unknown")), EMPTY_DICT)
.render(builder, context));
assertThat(builder.toString()).isEmpty();
TemplateMetadata templateMetadata = getTemplateMetadata(templates, "ns1.callerTemplate");
assertThat(templateMetadata.callees()).isEmpty();
assertThat(templateMetadata.delCallees()).asList().containsExactly("ns1.del");
}
@Test
public void testCallBasicNode() throws IOException {
CompiledTemplates templates =
TemplateTester.compileFile(
"{namespace ns autoescape=\"strict\"}",
"",
"/** */",
"{template .callerDataAll}",
" {@param foo : string}",
" {call .callee data=\"all\" /}",
"{/template}",
"",
"/** */",
"{template .callerDataExpr}",
" {@param rec : [foo : string]}",
" {call .callee data=\"$rec\" /}",
"{/template}",
"",
"/** */",
"{template .callerParams}",
" {@param p1 : string}",
" {call .callee}",
" {param foo : $p1 /}",
" {param boo : 'a' + 1 + 'b' /}",
" {/call}",
"{/template}",
"",
"/** */",
"{template .callerParamsAndData}",
" {@param p1 : string}",
" {call .callee data=\"all\"}",
" {param foo : $p1 /}",
" {/call}",
"{/template}",
"",
"/** */",
"{template .callee}",
" {@param foo : string}",
" {@param? boo : string}",
"Foo: {$foo}{\\n}",
"Boo: {$boo}{\\n}",
"{/template}",
"");
ParamStore params = new BasicParamStore(2);
params.setField("foo", StringData.forValue("foo"));
assertThat(render(templates, params, "ns.callerDataAll")).isEqualTo("Foo: foo\nBoo: null\n");
params.setField("boo", StringData.forValue("boo"));
assertThat(render(templates, params, "ns.callerDataAll")).isEqualTo("Foo: foo\nBoo: boo\n");
assertThat(getTemplateMetadata(templates, "ns.callerDataAll").callees())
.asList()
.containsExactly("ns.callee");
params = new BasicParamStore(2);
params.setField("rec", new BasicParamStore(2).setField("foo", StringData.forValue("foo")));
assertThat(render(templates, params, "ns.callerDataExpr")).isEqualTo("Foo: foo\nBoo: null\n");
((ParamStore) params.getField("rec")).setField("boo", StringData.forValue("boo"));
assertThat(render(templates, params, "ns.callerDataExpr")).isEqualTo("Foo: foo\nBoo: boo\n");
assertThat(getTemplateMetadata(templates, "ns.callerDataExpr").callees())
.asList()
.containsExactly("ns.callee");
params = new BasicParamStore(2);
params.setField("p1", StringData.forValue("foo"));
assertThat(render(templates, params, "ns.callerParams")).isEqualTo("Foo: foo\nBoo: a1b\n");
assertThat(getTemplateMetadata(templates, "ns.callerParams").callees())
.asList()
.containsExactly("ns.callee");
params = new BasicParamStore(2);
params.setField("p1", StringData.forValue("foo"));
params.setField("boo", StringData.forValue("boo"));
assertThat(render(templates, params, "ns.callerParamsAndData"))
.isEqualTo("Foo: foo\nBoo: boo\n");
assertThat(getTemplateMetadata(templates, "ns.callerParamsAndData").callees())
.asList()
.containsExactly("ns.callee");
}
private static TemplateMetadata getTemplateMetadata(CompiledTemplates templates, String name) {
return templates
.getTemplateFactory(name)
.getClass()
.getDeclaringClass()
.getAnnotation(TemplateMetadata.class);
}
private String render(CompiledTemplates templates, SoyRecord params, String name)
throws IOException {
CompiledTemplate caller = templates.getTemplateFactory(name).create(params, EMPTY_DICT);
AdvisingStringBuilder sb = new AdvisingStringBuilder();
assertEquals(RenderResult.done(), caller.render(sb, getDefaultContext(templates)));
String output = sb.toString();
return output;
}
@Test
public void testForNode() {
// empty loop
assertThatTemplateBody("{for $i in range(2, 2)}", " {$i}", "{/for}").rendersAs("");
assertThatTemplateBody("{for $i in range(10)}", " {$i}", "{/for}").rendersAs("0123456789");
assertThatTemplateBody("{for $i in range(2, 10)}", " {$i}", "{/for}").rendersAs("23456789");
assertThatTemplateBody("{for $i in range(2, 10, 2)}", " {$i}", "{/for}").rendersAs("2468");
assertThatTemplateBody("{for $i in range(0, 10, 65536 * 65536 * 65536)}", " {$i}", "{/for}")
.failsToRenderWith(IllegalArgumentException.class);
assertThatTemplateBody(
"{for $i in range(65536 * 65536 * 65536, 65536 * 65536 * 65536 + 1)}",
" {$i}",
"{/for}")
.failsToRenderWith(IllegalArgumentException.class);
}
@Test
public void testForEachNode() {
// empty loop
assertThatTemplateBody(
"{@param list: list<int>}", "{foreach $i in $list}", " {$i}", "{/foreach}")
.rendersAs("", ImmutableMap.of("list", EMPTY_LIST));
assertThatTemplateBody(
"{@param list: list<int>}",
"{foreach $i in $list}",
" {$i}",
"{ifempty}",
" empty",
"{/foreach}")
.rendersAs("empty", ImmutableMap.of("list", EMPTY_LIST));
assertThatTemplateBody("{foreach $i in [1,2,3,4,5]}", " {$i}", "{/foreach}")
.rendersAs("12345");
assertThatTemplateBody(
"{foreach $i in [1,2,3,4,5]}",
" {if isFirst($i)}",
" first!{\\n}",
" {/if}",
" {$i}-{index($i)}{\\n}",
" {if isLast($i)}",
" last!",
" {/if}",
"{/foreach}")
.rendersAs(Joiner.on('\n').join("first!", "1-0", "2-1", "3-2", "4-3", "5-4", "last!"));
}
@Test
public void testForEachNode_mapKeys() {
assertThatTemplateBody(
"{@param map : map<string, int>}",
"{foreach $key in keys($map)}",
" {$key} - {$map[$key]}{if not isLast($key)}{\\n}{/if}",
"{/foreach}")
.rendersAs("a - 1\nb - 2", ImmutableMap.of("map", ImmutableMap.of("a", 1, "b", 2)));
}
@Test
public void testForEachNode_nullableList() {
// The compiler should be rejected this :(
assertThatTemplateBody(
"{@param map : map<string, list<int>>}",
"{foreach $item in $map?['key']}",
" {$item}",
"{/foreach}")
.rendersAs(
"123", ImmutableMap.of("map", ImmutableMap.of("key", ImmutableList.of(1, 2, 3))));
}
@Test
public void testSwitchNode() {
assertThatTemplateBody(
"{switch 1}",
" {case 1}",
" one",
" {case 2}",
" two",
" {default}",
" default",
"{/switch}")
.rendersAs("one");
assertThatTemplateBody(
"{switch 2}",
" {case 1}",
" one",
" {case 2}",
" two",
" {default}",
" default",
"{/switch}")
.rendersAs("two");
assertThatTemplateBody(
"{switch 'asdf'}",
" {case 1}",
" one",
" {case 2}",
" two",
" {default}",
" default",
"{/switch}")
.rendersAs("default");
}
@Test
public void testSwitchNode_empty() {
assertThatTemplateBody("{switch 1}", "{/switch}").rendersAs("");
}
@Test
public void testSwitchNode_defaultOnly() {
assertThatTemplateBody("{switch 1}", " {default}Hello", "{/switch}").rendersAs("Hello");
}
@Test
public void testNestedSwitch() {
assertThatTemplateBody(
"{switch 'a'}",
" {case 'a'}",
" {switch 1} {case 1} sub {default} sub default {/switch}",
" {case 2}",
" two",
" {default}",
" default",
"{/switch}")
.rendersAs(" sub ");
}
@Test
public void testIfNode() {
assertThatTemplateBody("{if true}", " hello", "{/if}").rendersAs("hello");
assertThatTemplateBody("{if false}", " hello", "{/if}").rendersAs("");
assertThatTemplateBody("{if false}", " one", "{elseif false}", " two", "{/if}").rendersAs("");
assertThatTemplateBody("{if true}", " one", "{elseif false}", " two", "{/if}")
.rendersAs("one");
assertThatTemplateBody("{if false}", " one", "{elseif true}", " two", "{/if}")
.rendersAs("two");
assertThatTemplateBody(
"{if true}", " one", "{elseif true}", " two", "{else}", " three", "{/if}")
.rendersAs("one");
assertThatTemplateBody(
"{if false}", " one", "{elseif true}", " two", "{else}", " three", "{/if}")
.rendersAs("two");
assertThatTemplateBody(
"{if false}", " one", "{elseif false}", " two", "{else}", " three", "{/if}")
.rendersAs("three");
}
@Test
public void testIfNode_nullableBool() {
CompiledTemplateSubject tester =
assertThatTemplateBody(
"{@param? cond1 : bool}",
"{@param cond2 : bool}",
"{if $cond2 or $cond1}",
" hello",
"{else}",
" goodbye",
"{/if}");
tester.rendersAs("goodbye", ImmutableMap.of("cond2", false));
tester.rendersAs("hello", ImmutableMap.of("cond2", true));
tester.rendersAs("goodbye", ImmutableMap.of("cond1", false, "cond2", false));
tester.rendersAs("hello", ImmutableMap.of("cond1", true, "cond2", false));
}
@Test
public void testPrintNode() {
assertThatTemplateBody("{1 + 2}").rendersAs("3");
assertThatTemplateBody("{'asdf'}").rendersAs("asdf");
}
@Test
public void testLogNode() {
assertThatTemplateBody("{log}", " hello{sp}", " {'world'}", "{/log}")
.logsOutput("hello world");
}
@Test
public void testRawTextNode() {
assertThatTemplateBody("hello raw text world").rendersAs("hello raw text world");
}
@Test
public void testRawTextNode_largeText() {
// This string is larger than the max constant pool entry size
String largeString = Strings.repeat("x", 1 << 17);
assertThatTemplateBody(largeString).rendersAs(largeString);
assertThatTemplateBody("{@param foo:?}\n{'" + largeString + "' + $foo}")
.rendersAs(largeString + "hello", ImmutableMap.of("foo", "hello"));
}
@Test
public void testCssNode() {
FakeRenamingMap renamingMap = new FakeRenamingMap(ImmutableMap.of("foo", "bar"));
assertThatTemplateBody("{css foo}").withCssRenamingMap(renamingMap).rendersAs("bar");
assertThatTemplateBody("{css foo2}").withCssRenamingMap(renamingMap).rendersAs("foo2");
assertThatTemplateBody("{css 1+2, foo2}").withCssRenamingMap(renamingMap).rendersAs("3-foo2");
}
@Test
public void testXidNode() {
FakeRenamingMap renamingMap = new FakeRenamingMap(ImmutableMap.of("foo", "bar"));
assertThatTemplateBody("{xid foo}").withXidRenamingMap(renamingMap).rendersAs("bar");
assertThatTemplateBody("{xid foo2}").withXidRenamingMap(renamingMap).rendersAs("foo2_");
}
@Test
public void testCallCustomFunction() {
SoyJavaFunction plusOneFunction =
new SoyJavaFunction() {
@Override
public Set<Integer> getValidArgsSizes() {
return ImmutableSet.of(1);
}
@Override
public String getName() {
return "plusOne";
}
@Override
public SoyValue computeForJava(List<SoyValue> args) {
return IntegerData.forValue(args.get(0).integerValue() + 1);
}
};
assertThatTemplateBody("{plusOne(1)}").withSoyFunction(plusOneFunction).rendersAs("2");
}
@Test
public void testIsNonNull() {
assertThatTemplateBody("{@param foo : [a : [ b : string]] }", "{isNonnull($foo.a)}")
.rendersAs(
"false", ImmutableMap.<String, Object>of("foo", ImmutableMap.<String, String>of()));
}
// Tests for a bug in an integration test where unnecessary float unboxing conversions happened.
@Test
public void testBoxedIntComparisonFromFunctions() {
assertThatTemplateBody(
"{@param list : list<int>}",
"{foreach $item in $list}",
"{if index($item) == ceiling(length($list) / 2) - 1}",
" Middle.",
"{/if}",
"{/foreach}",
"")
.rendersAs("Middle.", ImmutableMap.of("list", ImmutableList.of(1, 2, 3)));
}
@Test
public void testOptionalListIteration() {
CompiledTemplateSubject tester =
assertThatTemplateBody(
"{@param? list : list<int>}",
"{if $list}",
" {foreach $item in $list}",
" {$item}",
" {/foreach}",
"{/if}",
"");
tester.rendersAs("123", ImmutableMap.of("list", ImmutableList.of(1, 2, 3)));
tester.rendersAs("");
}
@Test
public void testPrintDirectives() {
assertThatTemplateBody("{' blah &&blahblahblah' |escapeHtml|insertWordBreaks:8}")
.rendersAs(" blah &&blahbl<wbr>ahblah");
}
@Test
public void testParam() {
assertThatTemplateBody("{@param foo : int }", "{$foo + 1}")
.rendersAs("2", ImmutableMap.of("foo", 1))
.rendersAs("3", ImmutableMap.of("foo", 2))
.rendersAs("4", ImmutableMap.of("foo", 3));
}
@Test
public void testParam_headerDocParam() {
assertThatFile(
"{namespace ns autoescape=\"strict\"}",
"/** ",
" * @param foo A foo",
"*/ ",
"{template .foo}",
" {$foo + 1}",
"{/template}",
"")
.rendersAs("2", ImmutableMap.of("foo", 1))
.rendersAs("3", ImmutableMap.of("foo", 2))
.rendersAs("4", ImmutableMap.of("foo", 3));
}
@Test
public void testInjectParam() {
assertThatTemplateBody("{@inject foo : int }", "{$foo + 1}")
.rendersAs("2", ImmutableMap.<String, Object>of(), ImmutableMap.of("foo", 1))
.rendersAs("3", ImmutableMap.<String, Object>of(), ImmutableMap.of("foo", 2))
.rendersAs("4", ImmutableMap.<String, Object>of(), ImmutableMap.of("foo", 3));
}
@Test
public void testInjectParam_legacyIj() {
assertThatTemplateBody("{$ij.foo + 1}")
.rendersAs("2", ImmutableMap.<String, Object>of(), ImmutableMap.of("foo", 1))
.rendersAs("3", ImmutableMap.<String, Object>of(), ImmutableMap.of("foo", 2))
.rendersAs("4", ImmutableMap.<String, Object>of(), ImmutableMap.of("foo", 3));
}
@Test
public void testParamValidation() throws Exception {
CompiledTemplates templates =
TemplateTester.compileTemplateBody("{@param foo : int}", "{$foo ?: -1}");
CompiledTemplate.Factory singleParam = templates.getTemplateFactory("ns.foo");
RenderContext context = getDefaultContext(templates);
AdvisingStringBuilder builder = new AdvisingStringBuilder();
SoyDict params =
SoyValueConverter.UNCUSTOMIZED_INSTANCE.newDict("foo", IntegerData.forValue(1));
singleParam.create(params, EMPTY_DICT).render(builder, context);
assertEquals("1", builder.getAndClearBuffer());
singleParam.create(EMPTY_DICT, EMPTY_DICT).render(builder, context);
assertEquals("-1", builder.getAndClearBuffer());
templates = TemplateTester.compileTemplateBody("{@inject foo : int}", "{$foo}");
CompiledTemplate.Factory singleIj = templates.getTemplateFactory("ns.foo");
context = getDefaultContext(templates);
params = SoyValueConverter.UNCUSTOMIZED_INSTANCE.newDict("foo", IntegerData.forValue(1));
singleIj.create(SoyValueConverter.EMPTY_DICT, params).render(builder, context);
assertEquals("1", builder.getAndClearBuffer());
params = SoyValueConverter.UNCUSTOMIZED_INSTANCE.newDict();
singleIj.create(SoyValueConverter.EMPTY_DICT, params).render(builder, context);
assertEquals("null", builder.getAndClearBuffer());
}
@Test
public void testParamFields() throws Exception {
CompiledTemplate.Factory multipleParams =
TemplateTester.compileTemplateBody(
"{@param foo : string}",
"{@param baz : string}",
"{@inject bar : string}",
"{$foo + $baz + $bar}")
.getTemplateFactory("ns.foo");
SoyDict params =
SoyValueConverter.UNCUSTOMIZED_INSTANCE.newDict(
"foo", StringData.forValue("foo"),
"bar", StringData.forValue("bar"),
"baz", StringData.forValue("baz"));
CompiledTemplate template = multipleParams.create(params, params);
assertEquals(StringData.forValue("foo"), getField("foo", template));
assertEquals(StringData.forValue("bar"), getField("bar", template));
assertEquals(StringData.forValue("baz"), getField("baz", template));
TemplateMetadata templateMetadata = template.getClass().getAnnotation(TemplateMetadata.class);
assertThat(templateMetadata.injectedParams()).asList().containsExactly("bar");
assertThat(templateMetadata.callees()).isEmpty();
assertThat(templateMetadata.delCallees()).isEmpty();
}
@Test
public void testPassHtmlAsNullableString() throws Exception {
CompiledTemplateSubject subject =
TemplateTester.assertThatFile(
"{namespace ns}",
"{template .foo}",
" {@param? content : string}",
" {$content ?: 'empty' |escapeHtml}",
"{/template}");
subject.rendersAs("empty");
subject.rendersAs(
"<b>hello</b>", ImmutableMap.of("content", SanitizedContents.constantHtml("<b>hello</b>")));
}
private Object getField(String name, CompiledTemplate template) throws Exception {
Field declaredField = template.getClass().getDeclaredField(name);
declaredField.setAccessible(true);
return declaredField.get(template);
}
@Test
public void testBasicFunctionality() {
// make sure we don't break standard reflection access
CompiledTemplate.Factory factory =
TemplateTester.compileTemplateBody("hello world").getTemplateFactory("ns.foo");
assertEquals("com.google.template.soy.jbcsrc.gen.ns.foo$Factory", factory.getClass().getName());
assertEquals("Factory", factory.getClass().getSimpleName());
CompiledTemplate templateInstance = factory.create(EMPTY_DICT, EMPTY_DICT);
Class<? extends CompiledTemplate> templateClass = templateInstance.getClass();
assertEquals("com.google.template.soy.jbcsrc.gen.ns.foo", templateClass.getName());
assertEquals("foo", templateClass.getSimpleName());
TemplateMetadata templateMetadata = templateClass.getAnnotation(TemplateMetadata.class);
assertEquals("HTML", templateMetadata.contentKind());
assertEquals(ContentKind.HTML, templateInstance.kind());
assertThat(templateMetadata.injectedParams()).isEmpty();
assertThat(templateMetadata.callees()).isEmpty();
assertThat(templateMetadata.delCallees()).isEmpty();
// ensure that the factory is an inner class of the template.
assertEquals(templateClass, factory.getClass().getEnclosingClass());
assertEquals(templateClass, factory.getClass().getDeclaringClass());
assertThat(templateClass.getDeclaredClasses()).asList().contains(factory.getClass());
}
@Test
public void testContentKindNonStrict() {
assertThat(
TemplateTester.compileFile(
"{namespace ns autoescape=\"deprecated-contextual\"}",
"/** foo */",
"{template .foo}",
"{/template}")
.getTemplateFactory("ns.foo")
.getClass()
.getDeclaringClass()
.getAnnotation(TemplateMetadata.class)
.contentKind())
.isEmpty();
}
@Test
public void testRenderMsgStmt() throws Exception {
assertThatTemplateBody(
"{@param quota : int}",
"{@param url : string}",
"{msg desc=\"msg with placeholders.\"}",
" You're currently using {$quota} MB of your quota.{sp}",
" <a href=\"{$url}\">Learn more</A>",
" <br /><br />",
"{/msg}",
"{msg meaning=\"noun\" desc=\"\" hidden=\"true\"}Archive{/msg}",
"{msg meaning=\"noun\" desc=\"The archive (noun).\"}Archive{/msg}",
"{msg meaning=\"verb\" desc=\"\"}Archive{/msg}",
"{msg desc=\"\"}Archive{/msg}",
"")
.rendersAs(
"You're currently using 26 MB of your quota. "
+ "<a href=\"http://foo.com\">Learn more</A>"
+ "<br /><br />"
+ "ArchiveArchiveArchiveArchive",
ImmutableMap.of("quota", 26, "url", "http://foo.com"));
}
@Test
public void testGenders() {
CompiledTemplateSubject tester =
assertThatTemplateBody(
"{@param userGender : string}",
"{@param targetName : string}",
"{@param targetGender : string}",
"{msg genders=\"$userGender, $targetGender\" desc=\"...\"}",
" You replied to {$targetName}.",
"{/msg}",
"");
tester.rendersAs(
"You replied to bender the offender.",
ImmutableMap.of(
"userGender", "male",
"targetName", "bender the offender",
"targetGender", "male"));
tester.rendersAs(
"You replied to gender bender.",
ImmutableMap.of(
"userGender", "male",
"targetName", "gender bender",
"targetGender", "female"));
}
@Test
public void testPlurals() {
CompiledTemplateSubject tester =
assertThatTemplateBody(
"{@param items: list<[foo: string]>}",
"{msg desc=\"...\"}",
" {plural length($items)}",
" {case 0}Unused plural form",
" {case 1}{$items[0].foo}",
" {case 2}{$items[1]?.foo}, {$items[0]?.foo}",
" {default}{$items[2]?.foo} and some more",
" {/plural}",
"{/msg}",
"");
tester.rendersAs(
"hello", ImmutableMap.of("items", ImmutableList.of(ImmutableMap.of("foo", "hello"))));
}
// Tests for a bug where we would overescape deltemplates at the call site when the strict
// content kind of the deltemplate was unknown at compile time.
@Test
public void testDelCallEscaping_separateCompilation() throws IOException {
String soyFileContent1 =
Joiner.on("\n")
.join(
"{namespace ns}",
"",
"{template .callerTemplate}",
" {delcall myApp.myDelegate/}",
"{/template}",
"");
SoyFileSetNode soyTree =
SoyFileSetParserBuilder.forFileContents(soyFileContent1).parse().fileSet();
// apply an escaping directive to the callsite, just like the autoescaper would
CallDelegateNode cdn = (CallDelegateNode) soyTree.getChild(0).getChild(0).getChild(0);
cdn.setEscapingDirectiveNames(ImmutableList.of("|escapeHtml"));
TemplateRegistry templateRegistry = new TemplateRegistry(soyTree, ExplodingErrorReporter.get());
CompiledTemplates templates =
BytecodeCompiler.compile(templateRegistry, false, ExplodingErrorReporter.get()).get();
CompiledTemplate.Factory caller = templates.getTemplateFactory("ns.callerTemplate");
try {
renderWithContext(caller, getDefaultContext(templates));
fail();
} catch (IllegalArgumentException iae) {
assertThat(iae)
.hasMessageThat()
.isEqualTo(
"Found no active impl for delegate call to 'myApp.myDelegate' "
+ "(and no attribute allowemptydefault=\"true\").");
}
String soyFileContent2 =
Joiner.on("\n")
.join(
"{namespace ns2}",
"",
"{deltemplate myApp.myDelegate}",
" <span>Hello</span>",
"{/deltemplate}",
"");
CompiledTemplates templatesWithDeltemplate = compileFiles(soyFileContent2);
// By passing an alternate context, we ensure the deltemplate selector contains the delegate
assertThat(renderWithContext(caller, getDefaultContext(templatesWithDeltemplate)))
.isEqualTo("<span>Hello</span>");
}
private static final class FakeRenamingMap implements SoyCssRenamingMap {
private final Map<String, String> renamingMap;
FakeRenamingMap(Map<String, String> renamingMap) {
this.renamingMap = renamingMap;
}
@Override
public String get(String key) {
return renamingMap.get(key);
}
}
private CompiledTemplates compileFiles(String... soyFileContents) {
SoyFileSetNode soyTree =
SoyFileSetParserBuilder.forFileContents(soyFileContents).parse().fileSet();
TemplateRegistry templateRegistry = new TemplateRegistry(soyTree, ExplodingErrorReporter.get());
CompiledTemplates templates =
BytecodeCompiler.compile(templateRegistry, false, ExplodingErrorReporter.get()).get();
return templates;
}
}