diff options
Diffstat (limited to 'src/test/java')
3 files changed, 232 insertions, 47 deletions
diff --git a/src/test/java/com/google/escapevelocity/MethodFinderTest.java b/src/test/java/com/google/escapevelocity/MethodFinderTest.java new file mode 100644 index 0000000..66b8948 --- /dev/null +++ b/src/test/java/com/google/escapevelocity/MethodFinderTest.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2019 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.escapevelocity; + +import static com.google.common.truth.Truth.assertThat; +import static java.util.stream.Collectors.toSet; + +import com.google.common.collect.ImmutableMap; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Collections; +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; + +@RunWith(JUnit4.class) +public class MethodFinderTest { + @Test + public void visibleMethodFromClass() throws Exception { + Map<String, String> map = Collections.singletonMap("foo", "bar"); + Class<?> mapClass = map.getClass(); + assertThat(Modifier.isPublic(mapClass.getModifiers())).isFalse(); + + Method size = mapClass.getMethod("size"); + Method visibleSize = MethodFinder.visibleMethod(size, mapClass); + assertThat(visibleSize.getDeclaringClass().isInterface()).isFalse(); + assertThat(visibleSize.invoke(map)).isEqualTo(1); + } + + @Test + public void visibleMethodFromInterface() throws Exception { + Map<String, String> map = ImmutableMap.of("foo", "bar"); + Map.Entry<String, String> entry = map.entrySet().iterator().next(); + Class<?> entryClass = entry.getClass(); + assertThat(Modifier.isPublic(entryClass.getModifiers())).isFalse(); + + Method getValue = entryClass.getMethod("getValue"); + Method visibleGetValue = MethodFinder.visibleMethod(getValue, entryClass); + assertThat(visibleGetValue.getDeclaringClass().isInterface()).isTrue(); + assertThat(visibleGetValue.invoke(entry)).isEqualTo("bar"); + } + + @Test + public void publicMethodsWithName() { + List<String> list = Collections.singletonList("foo"); + Class<?> listClass = list.getClass(); + assertThat(Modifier.isPublic(listClass.getModifiers())).isFalse(); + + MethodFinder methodFinder = new MethodFinder(); + Set<Method> methods = methodFinder.publicMethodsWithName(listClass, "remove"); + // This should find at least remove(int) and remove(Object). + assertThat(methods.size()).isAtLeast(2); + assertThat(methods.stream().map(Method::getName).collect(toSet())).containsExactly("remove"); + assertThat(methods.stream().allMatch(MethodFinderTest::isPublic)).isTrue(); + + // We should cache the result, meaning we get back the same result if we ask a second time. + Set<Method> methods2 = methodFinder.publicMethodsWithName(listClass, "remove"); + assertThat(methods2).isSameInstanceAs(methods); + } + + @Test + public void publicMethodsWithName_Nonexistent() { + List<String> list = Collections.singletonList("foo"); + Class<?> listClass = list.getClass(); + assertThat(Modifier.isPublic(listClass.getModifiers())).isFalse(); + + MethodFinder methodFinder = new MethodFinder(); + Set<Method> methods = methodFinder.publicMethodsWithName(listClass, "nonexistentMethod"); + assertThat(methods).isEmpty(); + } + + private static boolean isPublic(Method method) { + return Modifier.isPublic(method.getModifiers()) + && Modifier.isPublic(method.getDeclaringClass().getModifiers()); + } +} diff --git a/src/test/java/com/google/escapevelocity/ReferenceNodeTest.java b/src/test/java/com/google/escapevelocity/ReferenceNodeTest.java index 3e784f6..b1759bd 100644 --- a/src/test/java/com/google/escapevelocity/ReferenceNodeTest.java +++ b/src/test/java/com/google/escapevelocity/ReferenceNodeTest.java @@ -17,15 +17,11 @@ package com.google.escapevelocity; import static com.google.common.truth.Truth.assertThat; -import com.google.escapevelocity.ReferenceNode.MethodReferenceNode; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.primitives.Primitives; import com.google.common.truth.Expect; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.Collections; -import java.util.Map; +import com.google.escapevelocity.ReferenceNode.MethodReferenceNode; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -85,22 +81,12 @@ public class ReferenceNodeTest { MethodReferenceNode.primitiveTypeIsAssignmentCompatible(to, from); expect .withMessage(from + " assignable to " + to) - .that(expected).isEqualTo(actual); + .that(actual).isEqualTo(expected); } } } @Test - public void testVisibleMethod() throws Exception { - Map<String, String> map = Collections.singletonMap("foo", "bar"); - Class<?> mapClass = map.getClass(); - assertThat(Modifier.isPublic(mapClass.getModifiers())).isFalse(); - Method size = map.getClass().getMethod("size"); - Method visibleSize = ReferenceNode.visibleMethod(size, mapClass); - assertThat(visibleSize.invoke(map)).isEqualTo(1); - } - - @Test public void testCompatibleArgs() { assertThat(MethodReferenceNode.compatibleArgs( new Class<?>[]{int.class}, ImmutableList.of((Object) 5))).isTrue(); diff --git a/src/test/java/com/google/escapevelocity/TemplateTest.java b/src/test/java/com/google/escapevelocity/TemplateTest.java index 04bad8e..0503125 100644 --- a/src/test/java/com/google/escapevelocity/TemplateTest.java +++ b/src/test/java/com/google/escapevelocity/TemplateTest.java @@ -16,10 +16,12 @@ package com.google.escapevelocity; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.fail; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSetMultimap; import com.google.common.truth.Expect; import java.io.ByteArrayInputStream; import java.io.FileNotFoundException; @@ -27,6 +29,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.StringReader; import java.io.StringWriter; +import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; @@ -36,6 +39,7 @@ import java.util.function.Supplier; import org.apache.commons.collections.ExtendedProperties; import org.apache.velocity.VelocityContext; import org.apache.velocity.exception.ResourceNotFoundException; +import org.apache.velocity.exception.VelocityException; import org.apache.velocity.runtime.RuntimeConstants; import org.apache.velocity.runtime.RuntimeInstance; import org.apache.velocity.runtime.log.NullLogChute; @@ -45,7 +49,6 @@ import org.apache.velocity.runtime.resource.loader.ResourceLoader; import org.junit.Before; import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.rules.TestName; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -57,7 +60,6 @@ import org.junit.runners.JUnit4; public class TemplateTest { @Rule public TestName testName = new TestName(); @Rule public Expect expect = Expect.create(); - @Rule public ExpectedException thrown = ExpectedException.none(); private RuntimeInstance velocityRuntimeInstance; @@ -128,6 +130,31 @@ public class TemplateTest { return writer.toString(); } + private void expectParseException( + String template, + String expectedMessageSubstring) { + Exception velocityException = null; + try { + SimpleNode parsedTemplate = + velocityRuntimeInstance.parse(new StringReader(template), testName.getMethodName()); + VelocityContext velocityContext = new VelocityContext(new TreeMap<>()); + velocityRuntimeInstance.render( + velocityContext, new StringWriter(), parsedTemplate.getTemplateName(), parsedTemplate); + fail("Velocity did not throw an exception for this template"); + } catch (org.apache.velocity.runtime.parser.ParseException | VelocityException expected) { + velocityException = expected; + } + try { + Template.parseFrom(new StringReader(template)); + fail("Velocity generated an exception, but EscapeVelocity did not: " + velocityException); + } catch (IOException e) { + throw new UncheckedIOException(e); + } catch (ParseException expected) { + assertWithMessage("Got expected exception, but message did not match") + .that(expected).hasMessageThat().contains(expectedMessageSubstring); + } + } + @Test public void empty() { compare(""); @@ -210,13 +237,6 @@ public class TemplateTest { compare("$foo.!", ImmutableMap.of("foo", false)); } - /* TODO(emcmanus): make this work. - @Test - public void substituteNotPropertyId() { - compare("$foo.!", ImmutableMap.of("foo", false)); - } - */ - @Test public void substituteNestedProperty() { compare("\n$t.name.empty\n", ImmutableMap.of("t", Thread.currentThread())); @@ -228,23 +248,64 @@ public class TemplateTest { } @Test + public void substituteMethodNoArgsSyntheticOverride() { + compare("<$c.isEmpty()>", ImmutableMap.of("c", ImmutableSetMultimap.of())); + } + + @Test public void substituteMethodOneArg() { compare("<$list.get(0)>", ImmutableMap.of("list", ImmutableList.of("foo"))); } @Test + public void substituteMethodOneNullArg() { + // This should evaluate map.containsKey(map.get("absent")), which is map.containsKey(null). + compare("<$map.containsKey($map.get(\"absent\"))>", ImmutableMap.of("map", ImmutableMap.of())); + } + + @Test public void substituteMethodTwoArgs() { compare("\n$s.indexOf(\"bar\", 2)\n", ImmutableMap.of("s", "barbarbar")); } @Test - public void substituteMethodNoSynthetic() { + public void substituteMethodSyntheticOverloads() { // If we aren't careful, we'll see both the inherited `Set<K> keySet()` from Map // and the overridden `ImmutableSet<K> keySet()` in ImmutableMap. compare("$map.keySet()", ImmutableMap.of("map", ImmutableMap.of("foo", "bar"))); } @Test + public void substituteStaticMethod() { + compare("$Integer.toHexString(23)", ImmutableMap.of("Integer", Integer.class)); + } + + @Test + public void substituteStaticMethodAsInstanceMethod() { + compare("$i.toHexString(23)", ImmutableMap.of("i", 0)); + } + + @Test + public void substituteClassMethod() { + // This is Class.getName(). + compare("$Integer.getName()", ImmutableMap.of("Integer", Integer.class)); + } + + /** See {@link #substituteClassOrInstanceMethod}. */ + public static class GetName { + public static String getName() { + return "Noddy"; + } + } + + @Test + public void substituteClassOrInstanceMethod() { + // If the method exists as both an instance method on Class and a static method on the named + // class, it's the instance method that wins, so this is still Class.getName(). + compare("$GetName.getName()", ImmutableMap.of("GetName", GetName.class)); + } + + @Test public void substituteIndexNoBraces() { compare("<$map[\"x\"]>", ImmutableMap.of("map", ImmutableMap.of("x", "y"))); } @@ -254,6 +315,14 @@ public class TemplateTest { compare("<${map[\"x\"]}>", ImmutableMap.of("map", ImmutableMap.of("x", "y"))); } + // Velocity allows you to write $map.foo instead of $map["foo"]. + @Test + public void substituteMapProperty() { + compare("$map.foo", ImmutableMap.of("map", ImmutableMap.of("foo", "bar"))); + // $map.empty is always equivalent to $map["empty"], never Map.isEmpty(). + compare("$map.empty", ImmutableMap.of("map", ImmutableMap.of("empty", "foo"))); + } + @Test public void substituteIndexThenProperty() { compare("<$map[2].name>", ImmutableMap.of("map", ImmutableMap.of(2, getClass()))); @@ -291,6 +360,29 @@ public class TemplateTest { } @Test + public void substituteInString() { + String template = + "#foreach ($a in $list)" + + "#set ($s = \"THING_${foreach.index}\")" + + "$s,$s;" + + "#end"; + compare(template, ImmutableMap.of("list", ImmutableList.of(1, 2, 3))); + compare("#set ($s = \"$x\") <$s>", ImmutableMap.of("x", "fred")); + compare("#set ($s = \"==$x$y\") <$s>", ImmutableMap.of("x", "fred", "y", "jim")); + compare("#set ($s = \"$x$y==\") <$s>", ImmutableMap.of("x", "fred", "y", "jim")); + } + + @Test + public void stringOperationsOnSubstitution() { + compare("#set ($s = \"a${b}c\") $s.length()", ImmutableMap.of("b", 23)); + } + + @Test + public void singleQuoteNoSubstitution() { + compare("#set ($s = 'a${b}c') x${s}y", ImmutableMap.of("b", 23)); + } + + @Test public void simpleSet() { compare("$x#set ($x = 17)#set ($y = 23) ($x, $y)", ImmutableMap.of("x", 1)); } @@ -506,6 +598,18 @@ public class TemplateTest { } @Test + public void forEachIndex() { + String template = + "#foreach ($x in $list)" + + "[$foreach.index]" + + "#foreach ($y in $list)" + + "($foreach.index)==$x.$y==" + + "#end" + + "#end"; + compare(template, ImmutableMap.of("list", ImmutableList.of("blim", "blam", "blum"))); + } + + @Test public void setSpacing() { // The spacing in the output from #set is eccentric. compare("x#set ($x = 0)x"); @@ -550,6 +654,18 @@ public class TemplateTest { compare(template, ImmutableMap.of("x", "tiddly")); } + @Test + public void macroWithCommaSeparatedArgs() { + String template = + "$x\n" + + "#macro (m, $x, $y)\n" + + " #if ($x < $y) less #else greater #end\n" + + "#end\n" + + "#m(17 23) #m(23 17) #m(17 17)\n" + + "$x"; + compare(template, ImmutableMap.of("x", "tiddly")); + } + /** * Tests defining a macro inside a conditional. This proves that macros are not evaluated in the * main control flow, but rather are extracted at parse time. It also tests what happens if there @@ -706,45 +822,36 @@ public class TemplateTest { } @Test - public void badBraceReference() throws IOException { + public void badBraceReference() { String template = "line 1\nline 2\nbar${foo.!}baz"; - thrown.expect(ParseException.class); - thrown.expectMessage("Expected }, on line 3, at text starting: .!}baz"); - Template.parseFrom(new StringReader(template)); + expectParseException(template, "Expected }, on line 3, at text starting: .!}baz"); } @Test - public void undefinedMacro() throws IOException { + public void undefinedMacro() { String template = "#oops()"; - thrown.expect(ParseException.class); - thrown.expectMessage("#oops is neither a standard directive nor a macro that has been defined"); - Template.parseFrom(new StringReader(template)); + expectParseException( + template, + "#oops is neither a standard directive nor a macro that has been defined"); } @Test - public void macroArgumentMismatch() throws IOException { + public void macroArgumentMismatch() { String template = "#macro (twoArgs $a $b) $a $b #end\n" + "#twoArgs(23)\n"; - thrown.expect(ParseException.class); - thrown.expectMessage("Wrong number of arguments to #twoArgs: expected 2, got 1"); - Template.parseFrom(new StringReader(template)); + expectParseException(template, "Wrong number of arguments to #twoArgs: expected 2, got 1"); } @Test - public void unclosedBlockQuote() throws IOException { + public void unclosedBlockQuote() { String template = "foo\nbar #[[\nblah\nblah"; - thrown.expect(ParseException.class); - thrown.expectMessage("Unterminated #[[ - did not see matching ]]#, on line 2"); - Template.parseFrom(new StringReader(template)); + expectParseException(template, "Unterminated #[[ - did not see matching ]]#, on line 2"); } @Test - public void unclosedBlockComment() throws IOException { - String template = "foo\nbar #*\nblah\nblah"; - thrown.expect(ParseException.class); - thrown.expectMessage("Unterminated #* - did not see matching *#, on line 2"); - Template.parseFrom(new StringReader(template)); + public void unclosedBlockComment() { + compare("foo\nbar #*\nblah\nblah"); } /** |