/* * Copyright 2008 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.soytree; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; import com.google.template.soy.SoyFileSetParserBuilder; import com.google.template.soy.base.SoySyntaxException; import com.google.template.soy.data.SanitizedContent.ContentKind; import com.google.template.soy.error.ErrorReporter; import com.google.template.soy.error.ExplodingErrorReporter; import com.google.template.soy.error.FormattingErrorReporter; import com.google.template.soy.exprtree.BooleanNode; import com.google.template.soy.exprtree.ExprRootNode; import com.google.template.soy.exprtree.GlobalNode; import com.google.template.soy.exprtree.IntegerNode; import com.google.template.soy.exprtree.StringNode; import com.google.template.soy.soytree.defn.SoyDocParam; import com.google.template.soy.soytree.defn.TemplateParam; import com.google.template.soy.soytree.defn.TemplateParam.DeclLoc; import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** * Unit tests for TemplateNode. * */ @RunWith(JUnit4.class) public class TemplateNodeTest { @Test public void testParseSoyDoc() { String soyDoc = "" + "/**\n" + " * Test template.\n" + " *\n" + " * @param foo Foo to print.\n" + " * @param? goo\n" + " * Goo to print.\n" + " */"; TemplateNode tn = parse( "{namespace ns}\n" + "/**\n" + " * Test template.\n" + " *\n" + " * @param foo Foo to print.\n" + " * @param? goo\n" + " * Goo to print.\n" + " */" + "{template .boo}{$foo}{$goo}{/template}"); assertEquals(soyDoc, tn.getSoyDoc()); assertEquals("Test template.", tn.getSoyDocDesc()); List<TemplateParam> params = tn.getParams(); assertEquals(2, params.size()); SoyDocParam soyDocParam0 = (SoyDocParam) params.get(0); assertEquals(DeclLoc.SOY_DOC, soyDocParam0.declLoc()); assertEquals("foo", soyDocParam0.name()); assertEquals(true, soyDocParam0.isRequired()); assertEquals("Foo to print.", soyDocParam0.desc()); SoyDocParam soyDocParam1 = (SoyDocParam) params.get(1); assertEquals(DeclLoc.SOY_DOC, soyDocParam1.declLoc()); assertEquals("goo", soyDocParam1.name()); assertEquals(false, soyDocParam1.isRequired()); assertEquals("Goo to print.", soyDocParam1.desc()); } @Test public void testEscapeSoyDoc() { TemplateNode tn = parse("{namespace ns}\n" + "/**@deprecated */\n" + "{template .boo}{/template}"); assertEquals("@deprecated", tn.getSoyDocDesc()); } @Test public void testParseHeaderDecls() { TemplateNode tn = parse("{namespace ns}\n" + "/**@param foo */\n" + "{template .boo}{$foo}{/template}"); List<TemplateParam> params = tn.getParams(); assertThat(params).hasSize(1); SoyDocParam soyDocParam0 = (SoyDocParam) params.get(0); assertEquals("foo", soyDocParam0.name()); assertThat(ImmutableList.copyOf(tn.getAllParams())).hasSize(1); } @Test public void testInvalidParamNames() { FormattingErrorReporter errorReporter = new FormattingErrorReporter(); parse("{namespace ns}\n" + "/**@param ij */\n" + "{template .boo}{/template}", errorReporter); assertThat(errorReporter.getErrorMessages()) .containsExactly("Invalid param name 'ij' ('ij' is for injected data)."); errorReporter = new FormattingErrorReporter(); parse( "{namespace ns}\n" + "{template .boo}\n" + "{@param ij : int}\n" + "{/template}", errorReporter); assertThat(errorReporter.getErrorMessages()) .containsExactly("Invalid param name 'ij' ('ij' is for injected data)."); } @Test public void testParamsAlreadyDeclared() { FormattingErrorReporter errorReporter = new FormattingErrorReporter(); parse( "{namespace ns}\n" + "/**@param foo @param goo @param? foo */\n" + "{template .boo}{/template}", errorReporter); assertThat(errorReporter.getErrorMessages()).containsExactly("Param 'foo' already declared"); errorReporter = new FormattingErrorReporter(); parse( "{namespace ns}\n" + "{template .boo}\n" + "{@param goo : null}{@param foo:string}{@param foo : int}\n" + "{/template}", errorReporter); assertThat(errorReporter.getErrorMessages()).containsExactly("Param 'foo' already declared"); errorReporter = new FormattingErrorReporter(); parse( "{namespace ns}\n" + "/** @param foo a soydoc param */\n" + "{template .boo}\n" + "{@param foo : string}\n" + "{/template}", errorReporter); assertThat(errorReporter.getErrorMessages()).containsExactly("Param 'foo' already declared"); } @Test public void testCommandTextErrors() { FormattingErrorReporter errorReporter = new FormattingErrorReporter(); parse("{namespace ns}\n{template autoescape=\"strict\"}{/template}", errorReporter); assertThat(errorReporter.getErrorMessages()) .containsExactly( "Template name 'autoescape' must be relative to the file namespace, i.e. a dot " + "followed by an identifier.", "parse error at '=': expected }, identifier, or ."); errorReporter = new FormattingErrorReporter(); parse("{namespace ns}\n{template .foo autoescape=\"}{/template}", errorReporter); assertThat(errorReporter.getErrorMessages()) .containsExactly( "Unexpected end of file. Did you forget to close an attribute value or a comment?"); errorReporter = new FormattingErrorReporter(); parse("{namespace ns}\n{template .foo autoescape=\"false\"}{/template}", errorReporter); assertThat(errorReporter.getErrorMessages()) .containsExactly( "Invalid attribute value, expected one of [deprecated-contextual, " + "deprecated-noncontextual, strict]."); // assertion inside no-arg templateBasicNode() is that there is no exception. parse("{namespace ns}\n{template .foo autoescape=\n\t\r \"strict\"}{/template}"); } @Test public void testValidStrictTemplates() { TemplateNode node; node = parse( "{namespace ns autoescape=\"deprecated-noncontextual\"}\n" + "{template .boo kind=\"text\" autoescape=\"strict\"}{/template}"); assertEquals(AutoescapeMode.STRICT, node.getAutoescapeMode()); assertEquals(ContentKind.TEXT, node.getContentKind()); node = parse( "{namespace ns autoescape=\"deprecated-noncontextual\"}\n" + "{template .boo kind=\"html\" autoescape=\"strict\"}{/template}"); assertEquals(AutoescapeMode.STRICT, node.getAutoescapeMode()); assertEquals(ContentKind.HTML, node.getContentKind()); // "kind" is optional, defaults to HTML node = parse( "{namespace ns autoescape=\"deprecated-noncontextual\"}\n" + "{template .boo autoescape=\"strict\"}{/template}"); assertEquals(AutoescapeMode.STRICT, node.getAutoescapeMode()); assertEquals(ContentKind.HTML, node.getContentKind()); } @Test public void testInvalidStrictTemplates() { FormattingErrorReporter errorReporter = new FormattingErrorReporter(); parse( "{namespace ns autoescape=\"deprecated-noncontextual\"}\n" + "{template .boo kind=\"text\"}{/template}", errorReporter); assertThat(errorReporter.getErrorMessages()) .containsExactly("kind=\"...\" attribute is only valid with autoescape=\"strict\"."); } @Test public void testValidRequiredCss() { TemplateNode node; node = parse("{namespace ns}\n{template .boo requirecss=\"foo.boo\"}{/template}"); assertEquals(ImmutableList.<String>of("foo.boo"), node.getRequiredCssNamespaces()); node = parse("{namespace ns}\n{template .boo requirecss=\"foo, bar\"}{/template}"); assertEquals(ImmutableList.<String>of("foo", "bar"), node.getRequiredCssNamespaces()); node = parse("{namespace ns}\n{template .boo requirecss=\"foo.boo, foo.moo\"}{/template}"); assertEquals(ImmutableList.<String>of("foo.boo", "foo.moo"), node.getRequiredCssNamespaces()); // Now for deltemplates. node = parse( "{namespace ns}\n" + "{deltemplate namespace.boo requirecss=\"foo.boo, moo.hoo\"}{/deltemplate}"); assertEquals(ImmutableList.<String>of("foo.boo", "moo.hoo"), node.getRequiredCssNamespaces()); } @Test public void testValidVariant() { // Variant is a string literal: There's no expression and the value is already resolved. TemplateDelegateNode node = (TemplateDelegateNode) parse( join( "{namespace ns}", "{deltemplate namespace.boo variant=\"'abc'\"}", "{/deltemplate}")); assertEquals("namespace.boo", node.getDelTemplateName()); assertEquals("abc", node.getDelTemplateVariant()); assertEquals("abc", node.getDelTemplateKey().variant()); // Variant is a global, that was not yet resolved. node = (TemplateDelegateNode) parse( join( "{namespace ns}", "{deltemplate namespace.boo variant=\"test.GLOBAL_CONSTANT\"}", "{/deltemplate}")); assertEquals("namespace.boo", node.getDelTemplateName()); assertEquals("test.GLOBAL_CONSTANT", node.getDelTemplateVariant()); assertEquals("test.GLOBAL_CONSTANT", node.getDelTemplateKey().variant()); // Verify the global expression. List<ExprRootNode> exprs = node.getExprList(); assertEquals(1, exprs.size()); ExprRootNode expr = exprs.get(0); assertEquals("test.GLOBAL_CONSTANT", expr.toSourceString()); assertEquals(1, expr.numChildren()); assertTrue(expr.getRoot() instanceof GlobalNode); // Substitute the global expression. expr.replaceChild(0, new IntegerNode(123, expr.getRoot().getSourceLocation())); // Check the new values. assertEquals("123", node.getDelTemplateVariant()); assertEquals("123", node.getDelTemplateKey().variant()); // Resolve a global to a string. node = (TemplateDelegateNode) parse( join( "{namespace ns}", "{deltemplate namespace.boo variant=\"test.GLOBAL_CONSTANT\"}", "{/deltemplate}")); node.getExprList().get(0).replaceChild(0, new StringNode("variant", node.getSourceLocation())); assertEquals("variant", node.getDelTemplateVariant()); assertEquals("variant", node.getDelTemplateKey().variant()); } @Test public void testInvalidVariant() { // Try to resolve a global to an invalid type. TemplateDelegateNode node = (TemplateDelegateNode) parse( join( "{namespace ns}", "{deltemplate namespace.boo variant=\"test.GLOBAL_CONSTANT\"}", "{/deltemplate}")); node.getExprList().get(0).replaceChild(0, new BooleanNode(true, node.getSourceLocation())); try { node.getDelTemplateVariant(); fail("An error is expected when an invalid node type is used."); } catch (AssertionError e) { assertTrue(e.getMessage().contains("Invalid expression for deltemplate")); } // Try to resolve a global to an invalid string node = (TemplateDelegateNode) parse( join( "{namespace ns}", "{deltemplate namespace.boo variant=\"test.GLOBAL_CONSTANT\"}", "{/deltemplate}")); node.getExprList() .get(0) .replaceChild(0, new StringNode("Not and Identifier!", node.getSourceLocation())); try { node.getDelTemplateVariant(); fail("An error is expected when a global string value is not an identifier."); } catch (SoySyntaxException e) { assertTrue(e.getMessage().contains("a string literal is used, value must be an identifier")); } } @Test public void testInvalidRequiredCss() { FormattingErrorReporter errorReporter = new FormattingErrorReporter(); parse("{namespace ns}\n{template .boo requirecss=\"\"}{/template}", errorReporter); assertThat(errorReporter.getErrorMessages()) .containsExactly("Invalid required CSS namespace name '', expected an identifier."); errorReporter = new FormattingErrorReporter(); parse("{namespace ns}\n{template .boo requirecss=\"foo boo\"}{/template}", errorReporter); assertThat(errorReporter.getErrorMessages()) .containsExactly("Invalid required CSS namespace name 'foo boo', expected an identifier."); errorReporter = new FormattingErrorReporter(); parse("{namespace ns}\n{template .boo requirecss=\"9vol\"}{/template}", errorReporter); assertThat(errorReporter.getErrorMessages()) .containsExactly("Invalid required CSS namespace name '9vol', expected an identifier."); errorReporter = new FormattingErrorReporter(); parse("{namespace ns}\n{deltemplate foo.boo requirecss=\"5ham\"}{/deltemplate}", errorReporter); assertThat(errorReporter.getErrorMessages()) .containsExactly("Invalid required CSS namespace name '5ham', expected an identifier."); } @Test public void testToSourceString() { TemplateNode tn = parse( join( "{namespace ns}", "/**", " * Test template.", " *", " * @param foo Foo to print.", " * @param goo", " * Goo to print.", " */", "{template .boo}", " /** Something milky. */", " {@param moo : bool}", " {@param? too : string}", "{sp}{sp}{$foo}{$goo}{$moo ? 'moo' : ''}{$too}\n", "{/template}")); assertEquals( "" + "/**\n" + " * Test template.\n" + " *\n" + " * @param foo Foo to print.\n" + " * @param goo\n" + " * Goo to print.\n" + " */\n" + "{template .boo}\n" + " {@param moo: bool} /** Something milky. */\n" + " {@param? too: null|string}\n" + "{sp} {$foo}{$goo}{$moo ? 'moo' : ''}{$too}\n" + "{/template}\n", tn.toSourceString()); } private static String join(String... lines) { return Joiner.on("\n").join(lines); } private static TemplateNode parse(String file) { return parse(file, ExplodingErrorReporter.get()); } private static TemplateNode parse(String file, ErrorReporter errorReporter) { SoyFileSetNode node = SoyFileSetParserBuilder.forFileContents(file) .errorReporter(errorReporter) .allowUnboundGlobals(true) // for the delvariant tests .parse() .fileSet(); // if parsing fails, templates/files will be missing. just return null in that case. if (node.numChildren() > 0) { SoyFileNode filenode = node.getChild(0); if (filenode.numChildren() > 0) { return filenode.getChild(0); } } return null; } }