/* * 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.tofu.internal; import static com.google.common.truth.Truth.assertThat; import static com.google.common.util.concurrent.Futures.immediateFailedFuture; import static org.junit.Assert.fail; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSortedSet; import com.google.common.util.concurrent.Futures; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.template.soy.SoyFileSetParserBuilder; import com.google.template.soy.SoyModule; import com.google.template.soy.data.SoyDict; import com.google.template.soy.data.SoyFutureException; import com.google.template.soy.data.SoyValueConverter; import com.google.template.soy.shared.restricted.SoyPrintDirective; import com.google.template.soy.tofu.SoyTofu; import com.google.template.soy.tofu.SoyTofuException; import com.google.template.soy.tofu.internal.BaseTofu.BaseTofuFactory; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Unit tests for exception behavior of Tofu. */ @RunWith(JUnit4.class) public final class TofuExceptionsTest { private static final SoyValueConverter CONVERTER = SoyValueConverter.UNCUSTOMIZED_INSTANCE; private static final Injector INJECTOR = Guice.createInjector(new SoyModule()); private static final String SOY_FILE = Joiner.on('\n') .join( "{namespace ns}", "", "/** */", "{template .callerTemplate}", " {call .calleeTemplate data=\"all\" /}", "{/template}", "", // line 7 "{template .calleeTemplate}", " {@param foo: [boo: int, bad: string]}", " {$foo.boo}", " {$foo.bad}", "{/template}", "", // line 13 "{template .transclusionCaller}", " {@param foo: int}", " {call .transclusionCallee}", " {param content}{$foo}{/param}", " {/call}", "{/template}", "", // line 20 "{template .transclusionCallee}", " {@param content: string}", " {$content}", "{/template}"); private SoyTofu tofu; @Before public void setUp() throws Exception { tofu = INJECTOR .getInstance(BaseTofuFactory.class) .create( SoyFileSetParserBuilder.forFileContents(SOY_FILE).parse().registry(), ImmutableMap.<String, ImmutableSortedSet<String>>of(), ImmutableMap.<String, SoyPrintDirective>of()); } @Test public void testExceptions_undefined() throws Exception { SoyDict data = CONVERTER.newDict("foo.boo", 42); // This is an exception that occurs during expression evaluation try { tofu.newRenderer("ns.callerTemplate").setData(data).render(); fail(); } catch (SoyTofuException ste) { assertThat(ste.getCause()).isNull(); assertThat(ste) .hasMessageThat() .isEqualTo("In 'print' tag, expression \"$foo.bad\" evaluates to undefined."); assertThat(ste.getStackTrace()[0].toString()).isEqualTo("ns.calleeTemplate(no-path:11)"); assertThat(ste.getStackTrace()[1].toString()).isEqualTo("ns.callerTemplate(no-path:5)"); } } @Test public void testExceptions_badType() throws Exception { SoyDict data = CONVERTER.newDict("foo", "not a record"); // This is an exception that occurs during template calling due to a type checkin try { tofu.newRenderer("ns.callerTemplate").setData(data).render(); fail(); } catch (SoyTofuException ste) { assertThat(ste.getCause()).isNull(); assertThat(ste) .hasMessageThat() .isEqualTo( "Parameter type mismatch: attempt to bind value 'not a record' to parameter " + "'foo' which has declared type '[bad: string, boo: int]'."); assertThat(ste.getStackTrace()[0].toString()).isEqualTo("ns.calleeTemplate(no-path:8)"); assertThat(ste.getStackTrace()[1].toString()).isEqualTo("ns.callerTemplate(no-path:5)"); } } @Test public void testExceptions_failedFuture() { Exception futureFailureCause = new Exception("boom"); SoyDict data = CONVERTER.newDict("foo", immediateFailedFuture(futureFailureCause)); // This error occurs due to a failed future. try { tofu.newRenderer("ns.callerTemplate").setData(data).render(); fail(); } catch (SoyTofuException ste) { assertThat(ste) .hasMessageThat() .isEqualTo("When evaluating \"$foo.boo\": Error dereferencing future"); SoyFutureException sfe = (SoyFutureException) ste.getCause(); assertThat(sfe).hasMessageThat().isEqualTo("Error dereferencing future"); assertThat(sfe.getCause()).isEqualTo(futureFailureCause); assertThat(ste.getStackTrace()[0].toString()).isEqualTo("ns.calleeTemplate(no-path:10)"); assertThat(ste.getStackTrace()[1].toString()).isEqualTo("ns.callerTemplate(no-path:5)"); } } @Test public void testExceptions_wrongTypeFuture() { SoyDict data = CONVERTER.newDict("foo", Futures.immediateFuture("not a record")); // This error occurs due to data of the wrong type, hidden behind a future. try { tofu.newRenderer("ns.callerTemplate").setData(data).render(); fail(); } catch (SoyTofuException ste) { assertThat(ste.getCause()).isNull(); assertThat(ste) .hasMessageThat() .isEqualTo( "When evaluating \"$foo.boo\": Parameter type mismatch: attempt to bind value " + "'not a record' to parameter 'foo' which has declared type " + "'[bad: string, boo: int]'."); assertThat(ste.getStackTrace()[0].toString()).isEqualTo("ns.calleeTemplate(no-path:8)"); assertThat(ste.getStackTrace()[1].toString()).isEqualTo("ns.calleeTemplate(no-path:10)"); assertThat(ste.getStackTrace()[2].toString()).isEqualTo("ns.callerTemplate(no-path:5)"); } } @Test public void testExceptions_transclusion_wrongTypeFuture() { SoyDict data = CONVERTER.newDict("foo", Futures.immediateFuture("not an int")); try { tofu.newRenderer("ns.transclusionCaller").setData(data).render(); fail(); } catch (SoyTofuException ste) { assertThat(ste.getCause()).isNull(); assertThat(ste) .hasMessageThat() .isEqualTo( "When evaluating \"$foo\": Parameter type mismatch: attempt to bind value " + "'not an int' to parameter 'foo' which has declared type 'int'."); assertThat(ste.getStackTrace()[0].toString()).isEqualTo("ns.transclusionCaller(no-path:14)"); assertThat(ste.getStackTrace()[1].toString()).isEqualTo("ns.transclusionCaller(no-path:17)"); assertThat(ste.getStackTrace()[2].toString()).isEqualTo("ns.transclusionCallee(no-path:23)"); assertThat(ste.getStackTrace()[3].toString()).isEqualTo("ns.transclusionCaller(no-path:16)"); } } @Test public void testExceptions_transclusion_failedFuture() { Exception futureFailureCause = new Exception("boom"); SoyDict data = CONVERTER.newDict("foo", immediateFailedFuture(futureFailureCause)); try { tofu.newRenderer("ns.transclusionCaller").setData(data).render(); fail(); } catch (SoyTofuException ste) { SoyFutureException sfe = (SoyFutureException) ste.getCause(); assertThat(sfe).hasMessageThat().isEqualTo("Error dereferencing future"); assertThat(sfe.getCause()).isEqualTo(futureFailureCause); assertThat(ste) .hasMessageThat() .isEqualTo("When evaluating \"$foo\": Error dereferencing future"); assertThat(ste.getStackTrace()[0].toString()).isEqualTo("ns.transclusionCaller(no-path:17)"); assertThat(ste.getStackTrace()[1].toString()).isEqualTo("ns.transclusionCallee(no-path:23)"); assertThat(ste.getStackTrace()[2].toString()).isEqualTo("ns.transclusionCaller(no-path:16)"); } } }