You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@freemarker.apache.org by dd...@apache.org on 2017/05/14 10:52:46 UTC

[03/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/test/java/org/apache/freemarker/core/CustomAttributeTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/test/java/org/apache/freemarker/core/CustomAttributeTest.java b/freemarker-core/src/test/java/org/apache/freemarker/core/CustomAttributeTest.java
new file mode 100644
index 0000000..726a20c
--- /dev/null
+++ b/freemarker-core/src/test/java/org/apache/freemarker/core/CustomAttributeTest.java
@@ -0,0 +1,163 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.freemarker.core;
+
+import static org.junit.Assert.*;
+
+import java.math.BigDecimal;
+import java.util.Arrays;
+
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+
+@SuppressWarnings("boxing")
+public class CustomAttributeTest {
+    
+    private static final String KEY_1 = "key1";
+    private static final String KEY_2 = "key2";
+    private static final String KEY_3 = "key3";
+    private static final Integer KEY_4 = 4;
+
+    private static final Integer VALUE_1 = 1; // Serializable
+    private static final Object VALUE_2 = new Object();
+    private static final Object VALUE_3 = new Object();
+    private static final Object VALUE_4 = new Object();
+    private static final Object VALUE_LIST = ImmutableList.<Object>of(
+            "s", BigDecimal.valueOf(2), Boolean.TRUE, ImmutableMap.of("a", "A"));
+    private static final Object VALUE_BIGDECIMAL = BigDecimal.valueOf(22);
+
+    private static final Object CUST_ATT_KEY = new Object();
+
+    @Test
+    public void testStringKey() throws Exception {
+        // Need some MutableProcessingConfiguration:
+        TemplateConfiguration.Builder mpc = new TemplateConfiguration.Builder();
+
+        assertEquals(0, mpc.getCustomAttributeNames().length);
+        assertNull(mpc.getCustomAttribute(KEY_1));
+        
+        mpc.setCustomAttribute(KEY_1, VALUE_1);
+        assertArrayEquals(new String[] { KEY_1 }, mpc.getCustomAttributeNames());
+        assertSame(VALUE_1, mpc.getCustomAttribute(KEY_1));
+        
+        mpc.setCustomAttribute(KEY_2, VALUE_2);
+        assertArrayEquals(new String[] { KEY_1, KEY_2 }, sort(mpc.getCustomAttributeNames()));
+        assertSame(VALUE_1, mpc.getCustomAttribute(KEY_1));
+        assertSame(VALUE_2, mpc.getCustomAttribute(KEY_2));
+
+        mpc.setCustomAttribute(KEY_1, VALUE_2);
+        assertArrayEquals(new String[] { KEY_1, KEY_2 }, sort(mpc.getCustomAttributeNames()));
+        assertSame(VALUE_2, mpc.getCustomAttribute(KEY_1));
+        assertSame(VALUE_2, mpc.getCustomAttribute(KEY_2));
+
+        mpc.setCustomAttribute(KEY_1, null);
+        assertArrayEquals(new String[] { KEY_1, KEY_2 }, sort(mpc.getCustomAttributeNames()));
+        assertNull(mpc.getCustomAttribute(KEY_1));
+        assertSame(VALUE_2, mpc.getCustomAttribute(KEY_2));
+
+        mpc.removeCustomAttribute(KEY_1);
+        assertArrayEquals(new String[] { KEY_2 }, mpc.getCustomAttributeNames());
+        assertNull(mpc.getCustomAttribute(KEY_1));
+        assertSame(VALUE_2, mpc.getCustomAttribute(KEY_2));
+    }
+
+    @Test
+    public void testRemoveFromEmptySet() throws Exception {
+        // Need some MutableProcessingConfiguration:
+        TemplateConfiguration.Builder mpc = new TemplateConfiguration.Builder();
+
+        mpc.removeCustomAttribute(KEY_1);
+        assertEquals(0, mpc.getCustomAttributeNames().length);
+        assertNull(mpc.getCustomAttribute(KEY_1));
+
+        mpc.setCustomAttribute(KEY_1, VALUE_1);
+        assertArrayEquals(new String[] { KEY_1 }, mpc.getCustomAttributeNames());
+        assertSame(VALUE_1, mpc.getCustomAttribute(KEY_1));
+    }
+
+    @Test
+    public void testAttrsFromFtlHeaderOnly() throws Exception {
+        Template t = new Template(null, "<#ftl attributes={"
+                + "'" + KEY_1 + "': [ 's', 2, true, {  'a': 'A' } ], "
+                + "'" + KEY_2 + "': " + VALUE_BIGDECIMAL + " "
+                + "}>",
+                new Configuration.Builder(Configuration.VERSION_3_0_0).build());
+
+        assertEquals(ImmutableSet.of(KEY_1, KEY_2), t.getCustomAttributes().keySet());
+        assertEquals(VALUE_LIST, t.getCustomAttribute(KEY_1));
+        assertEquals(VALUE_BIGDECIMAL, t.getCustomAttribute(KEY_2));
+
+        t.setCustomAttribute(KEY_1, VALUE_1);
+        assertEquals(VALUE_1, t.getCustomAttribute(KEY_1));
+        assertEquals(VALUE_BIGDECIMAL, t.getCustomAttribute(KEY_2));
+
+        t.setCustomAttribute(KEY_1, null);
+        assertEquals(ImmutableSet.of(KEY_1, KEY_2), t.getCustomAttributes().keySet());
+        assertNull(t.getCustomAttribute(KEY_1));
+    }
+
+    @Test
+    public void testAttrsFromFtlHeaderAndFromTemplateConfiguration() throws Exception {
+        TemplateConfiguration.Builder tcb = new TemplateConfiguration.Builder();
+        tcb.setCustomAttribute(KEY_3, VALUE_3);
+        tcb.setCustomAttribute(KEY_4, VALUE_4);
+        Template t = new Template(null, "<#ftl attributes={"
+                + "'" + KEY_1 + "': 'a', "
+                + "'" + KEY_2 + "': 'b', "
+                + "'" + KEY_3 + "': 'c' "
+                + "}>",
+                new Configuration.Builder(Configuration.VERSION_3_0_0).build(),
+                tcb.build());
+
+        assertEquals(ImmutableSet.of(KEY_1, KEY_2, KEY_3, KEY_4), t.getCustomAttributes().keySet());
+        assertEquals("a", t.getCustomAttribute(KEY_1));
+        assertEquals("b", t.getCustomAttribute(KEY_2));
+        assertEquals("c", t.getCustomAttribute(KEY_3)); // Has overridden TC attribute
+        assertEquals(VALUE_4, t.getCustomAttribute(KEY_4)); // Inherited TC attribute
+
+        t.setCustomAttribute(KEY_3, null);
+        assertEquals(ImmutableSet.of(KEY_1, KEY_2, KEY_3, KEY_4), t.getCustomAttributes().keySet());
+        assertNull("null value shouldn't cause fallback to TC attribute", t.getCustomAttribute(KEY_3));
+    }
+
+
+    @Test
+    public void testAttrsFromTemplateConfigurationOnly() throws Exception {
+        TemplateConfiguration.Builder tcb = new TemplateConfiguration.Builder();
+        tcb.setCustomAttribute(KEY_3, VALUE_3);
+        tcb.setCustomAttribute(KEY_4, VALUE_4);
+        Template t = new Template(null, "",
+                new Configuration.Builder(Configuration.VERSION_3_0_0).build(),
+                tcb.build());
+
+        assertEquals(ImmutableSet.of(KEY_3, KEY_4), t.getCustomAttributes().keySet());
+        assertEquals(VALUE_3, t.getCustomAttribute(KEY_3));
+        assertEquals(VALUE_4, t.getCustomAttribute(KEY_4));
+    }
+
+    private Object[] sort(String[] customAttributeNames) {
+        Arrays.sort(customAttributeNames);
+        return customAttributeNames;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/test/java/org/apache/freemarker/core/DateFormatTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/test/java/org/apache/freemarker/core/DateFormatTest.java b/freemarker-core/src/test/java/org/apache/freemarker/core/DateFormatTest.java
new file mode 100644
index 0000000..4ad5937
--- /dev/null
+++ b/freemarker-core/src/test/java/org/apache/freemarker/core/DateFormatTest.java
@@ -0,0 +1,464 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.freemarker.core;
+
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.impl.SimpleDate;
+import org.apache.freemarker.core.templateresolver.ConditionalTemplateConfigurationFactory;
+import org.apache.freemarker.core.templateresolver.FileNameGlobMatcher;
+import org.apache.freemarker.core.userpkg.AppMetaTemplateDateFormatFactory;
+import org.apache.freemarker.core.userpkg.EpochMillisDivTemplateDateFormatFactory;
+import org.apache.freemarker.core.userpkg.EpochMillisTemplateDateFormatFactory;
+import org.apache.freemarker.core.userpkg.HTMLISOTemplateDateFormatFactory;
+import org.apache.freemarker.core.userpkg.LocAndTZSensitiveTemplateDateFormatFactory;
+import org.apache.freemarker.core.valueformat.TemplateDateFormat;
+import org.apache.freemarker.core.valueformat.TemplateDateFormatFactory;
+import org.apache.freemarker.core.valueformat.UndefinedCustomFormatException;
+import org.apache.freemarker.core.valueformat.impl.AliasTemplateDateFormatFactory;
+import org.apache.freemarker.test.TemplateTest;
+import org.apache.freemarker.test.TestConfigurationBuilder;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableMap;
+
+public class DateFormatTest extends TemplateTest {
+    
+    /** 2015-09-06T12:00:00Z */
+    private static long T = 1441540800000L;
+    private static TemplateDateModel TM = new SimpleDate(new Date(T), TemplateDateModel.DATETIME);
+    
+    private TestConfigurationBuilder createConfigurationBuilder() {
+        return new TestConfigurationBuilder()
+                .locale(Locale.US)
+                .timeZone(TimeZone.getTimeZone("GMT+01:00"))
+                .sqlDateAndTimeTimeZone(TimeZone.getTimeZone("UTC"))
+                .customDateFormats(ImmutableMap.of(
+                        "epoch", EpochMillisTemplateDateFormatFactory.INSTANCE,
+                        "loc", LocAndTZSensitiveTemplateDateFormatFactory.INSTANCE,
+                        "div", EpochMillisDivTemplateDateFormatFactory.INSTANCE,
+                        "appMeta", AppMetaTemplateDateFormatFactory.INSTANCE,
+                        "htmlIso", HTMLISOTemplateDateFormatFactory.INSTANCE));
+    }
+
+    @Override
+    protected Configuration createDefaultConfiguration() throws Exception {
+        return createConfigurationBuilder().build();
+    }
+
+    @Test
+    public void testCustomFormat() throws Exception {
+        addToDataModel("d", new Date(123456789));
+        assertOutput(
+                "${d?string.@epoch} ${d?string.@epoch} <#setting locale='de_DE'>${d?string.@epoch}",
+                "123456789 123456789 123456789");
+
+        setConfigurationWithDateTimeFormat("@epoch");
+        assertOutput(
+                "<#assign d = d?datetime>"
+                + "${d} ${d?string} <#setting locale='de_DE'>${d}",
+                "123456789 123456789 123456789");
+
+        setConfigurationWithDateTimeFormat("@htmlIso");
+        assertOutput(
+                "<#assign d = d?datetime>"
+                + "${d} ${d?string} <#setting locale='de_DE'>${d}",
+                "1970-01-02<span class='T'>T</span>10:17:36Z "
+                + "1970-01-02T10:17:36Z "
+                + "1970-01-02<span class='T'>T</span>10:17:36Z");
+    }
+
+    @Test
+    public void testLocaleChange() throws Exception {
+        addToDataModel("d", new Date(123456789));
+        assertOutput(
+                "${d?string.@loc} ${d?string.@loc} "
+                + "<#setting locale='de_DE'>"
+                + "${d?string.@loc} ${d?string.@loc} "
+                + "<#setting locale='en_US'>"
+                + "${d?string.@loc} ${d?string.@loc}",
+                "123456789@en_US:GMT+01:00 123456789@en_US:GMT+01:00 "
+                + "123456789@de_DE:GMT+01:00 123456789@de_DE:GMT+01:00 "
+                + "123456789@en_US:GMT+01:00 123456789@en_US:GMT+01:00");
+
+        setConfigurationWithDateTimeFormat("@loc");
+        assertOutput(
+                "<#assign d = d?datetime>"
+                + "${d} ${d?string} "
+                + "<#setting locale='de_DE'>"
+                + "${d} ${d?string} "
+                + "<#setting locale='en_US'>"
+                + "${d} ${d?string}",
+                "123456789@en_US:GMT+01:00 123456789@en_US:GMT+01:00 "
+                + "123456789@de_DE:GMT+01:00 123456789@de_DE:GMT+01:00 "
+                + "123456789@en_US:GMT+01:00 123456789@en_US:GMT+01:00");
+    }
+
+    @Test
+    public void testTimeZoneChange() throws Exception {
+        addToDataModel("d", new Date(123456789));
+        setConfigurationWithDateTimeFormat("iso");
+        assertOutput(
+                "${d?string.@loc} ${d?string.@loc} ${d?datetime?isoLocal} "
+                + "<#setting timeZone='GMT+02:00'>"
+                + "${d?string.@loc} ${d?string.@loc} ${d?datetime?isoLocal} "
+                + "<#setting timeZone='GMT+01:00'>"
+                + "${d?string.@loc} ${d?string.@loc} ${d?datetime?isoLocal}",
+                "123456789@en_US:GMT+01:00 123456789@en_US:GMT+01:00 1970-01-02T11:17:36+01:00 "
+                + "123456789@en_US:GMT+02:00 123456789@en_US:GMT+02:00 1970-01-02T12:17:36+02:00 "
+                + "123456789@en_US:GMT+01:00 123456789@en_US:GMT+01:00 1970-01-02T11:17:36+01:00");
+
+        setConfigurationWithDateTimeFormat("@loc");
+        assertOutput(
+                "<#assign d = d?datetime>"
+                + "${d} ${d?string} "
+                + "<#setting timeZone='GMT+02:00'>"
+                + "${d} ${d?string} "
+                + "<#setting timeZone='GMT+01:00'>"
+                + "${d} ${d?string}",
+                "123456789@en_US:GMT+01:00 123456789@en_US:GMT+01:00 "
+                + "123456789@en_US:GMT+02:00 123456789@en_US:GMT+02:00 "
+                + "123456789@en_US:GMT+01:00 123456789@en_US:GMT+01:00");
+    }
+    
+    @Test
+    public void testWrongFormatStrings() throws Exception {
+        setConfigurationWithDateTimeFormat("x1");
+        assertErrorContains("${.now}", "\"x1\"", "'x'");
+        assertErrorContains("${.now?string}", "\"x1\"", "'x'");
+        setConfigurationWithDateTimeFormat("short");
+        assertErrorContains("${.now?string('x2')}", "\"x2\"", "'x'");
+        assertErrorContains("${.now?string('[wrong]')}", "format string", "[wrong]");
+
+        setConfiguration(createConfigurationBuilder()
+                .dateFormat("[wrong d]")
+                .dateTimeFormat("[wrong dt]")
+                .timeFormat("[wrong t]")
+                .build());
+        assertErrorContains("${.now?date}", "\"date_format\"", "[wrong d]");
+        assertErrorContains("${.now?datetime}", "\"datetime_format\"", "[wrong dt]");
+        assertErrorContains("${.now?time}", "\"time_format\"", "[wrong t]");
+    }
+
+    @Test
+    public void testCustomParameterized() throws Exception {
+        Configuration cfg = getConfiguration();
+        addToDataModel("d", new SimpleDate(new Date(12345678L), TemplateDateModel.DATETIME));
+        setConfigurationWithDateTimeFormat("@div 1000");
+        assertOutput("${d}", "12345");
+        assertOutput("${d?string}", "12345");
+        assertOutput("${d?string.@div_100}", "123456");
+        
+        assertErrorContains("${d?string.@div_xyz}", "\"@div_xyz\"", "\"xyz\"");
+        setConfigurationWithDateTimeFormat("@div");
+        assertErrorContains("${d}", "\"datetime_format\"", "\"@div\"", "format parameter is required");
+    }
+    
+    @Test
+    public void testUnknownCustomFormat() throws Exception {
+        {
+            setConfigurationWithDateTimeFormat("@noSuchFormat");
+            Throwable exc = assertErrorContains(
+                    "${.now}",
+                    "\"@noSuchFormat\"", "\"noSuchFormat\"", "\"datetime_format\"");
+            assertThat(exc.getCause(), instanceOf(UndefinedCustomFormatException.class));
+            
+        }
+        {
+            setConfiguration(createConfigurationBuilder().dateFormat("@noSuchFormatD").build());
+            assertErrorContains(
+                    "${.now?date}",
+                    "\"@noSuchFormatD\"", "\"noSuchFormatD\"", "\"date_format\"");
+        }
+        {
+            setConfiguration(createConfigurationBuilder().timeFormat("@noSuchFormatT").build());
+            assertErrorContains(
+                    "${.now?time}",
+                    "\"@noSuchFormatT\"", "\"noSuchFormatT\"", "\"time_format\"");
+        }
+
+        {
+            setConfigurationWithDateTimeFormat("");
+            Throwable exc = assertErrorContains("${.now?string('@noSuchFormat2')}",
+                    "\"@noSuchFormat2\"", "\"noSuchFormat2\"");
+            assertThat(exc.getCause(), instanceOf(UndefinedCustomFormatException.class));
+        }
+    }
+
+    private void setConfigurationWithDateTimeFormat(String formatString) {
+        setConfiguration(createConfigurationBuilder().dateTimeFormat(formatString).build());
+    }
+
+    @Test
+    public void testNullInModel() throws Exception {
+        addToDataModel("d", new MutableTemplateDateModel());
+        assertErrorContains("${d}", "nothing inside it");
+        assertErrorContains("${d?string}", "nothing inside it");
+    }
+    
+    @Test
+    public void testIcIAndEscaping() throws Exception {
+        addToDataModel("d", new SimpleDate(new Date(12345678L), TemplateDateModel.DATETIME));
+        
+        setConfigurationWithDateTimeFormat("@epoch");
+        assertOutput("${d}", "12345678");
+        setConfigurationWithDateTimeFormat("'@'yyyy");
+        assertOutput("${d}", "@1970");
+        setConfigurationWithDateTimeFormat("@@yyyy");
+        assertOutput("${d}", "@@1970");
+
+        setConfiguration(createConfigurationBuilder()
+                .customDateFormats(Collections.<String, TemplateDateFormatFactory>emptyMap())
+                .dateTimeFormat("@epoch")
+                .build());
+        assertErrorContains("${d}", "custom", "\"epoch\"");
+    }
+
+    @Test
+    public void testEnvironmentGetters() throws Exception {
+        String dateFormatStr = "yyyy.MM.dd. (Z)";
+        String timeFormatStr = "HH:mm";
+        String dateTimeFormatStr = "yyyy.MM.dd. HH:mm";
+
+        setConfiguration(createConfigurationBuilder()
+                .dateFormat(dateFormatStr)
+                .timeFormat(timeFormatStr)
+                .dateTimeFormat(dateTimeFormatStr)
+                .build());
+
+        Configuration cfg = getConfiguration();
+
+        Template t = new Template(null, "", cfg);
+        Environment env = t.createProcessingEnvironment(null, null);
+        
+        // Test that values are coming from the cache if possible
+        for (Class dateClass : new Class[] { Date.class, Timestamp.class, java.sql.Date.class, Time.class } ) {
+            for (int dateType
+                    : new int[] { TemplateDateModel.DATE, TemplateDateModel.TIME, TemplateDateModel.DATETIME }) {
+                String formatString =
+                        dateType == TemplateDateModel.DATE ? cfg.getDateFormat() :
+                        (dateType == TemplateDateModel.TIME ? cfg.getTimeFormat()
+                        : cfg.getDateTimeFormat());
+                TemplateDateFormat expectedF = env.getTemplateDateFormat(formatString, dateType, dateClass);
+                assertSame(expectedF, env.getTemplateDateFormat(dateType, dateClass)); // Note: Only reads the cache
+                assertSame(expectedF, env.getTemplateDateFormat(formatString, dateType, dateClass));
+                assertSame(expectedF, env.getTemplateDateFormat(formatString, dateType, dateClass, cfg.getLocale()));
+                assertSame(expectedF, env.getTemplateDateFormat(formatString, dateType, dateClass, cfg.getLocale(),
+                        cfg.getTimeZone(), cfg.getSQLDateAndTimeTimeZone()));
+            }
+        }
+
+        String dateFormatStr2 = dateFormatStr + "'!'";
+        String timeFormatStr2 = timeFormatStr + "'!'";
+        String dateTimeFormatStr2 = dateTimeFormatStr + "'!'";
+        
+        assertEquals("2015.09.06. 13:00",
+                env.getTemplateDateFormat(TemplateDateModel.DATETIME, Date.class).formatToPlainText(TM));
+        assertEquals("2015.09.06. 13:00!",
+                env.getTemplateDateFormat(dateTimeFormatStr2, TemplateDateModel.DATETIME, Date.class).formatToPlainText(TM));
+        
+        assertEquals("2015.09.06. (+0100)",
+                env.getTemplateDateFormat(TemplateDateModel.DATE, Date.class).formatToPlainText(TM));
+        assertEquals("2015.09.06. (+0100)!",
+                env.getTemplateDateFormat(dateFormatStr2, TemplateDateModel.DATE, Date.class).formatToPlainText(TM));
+        
+        assertEquals("13:00",
+                env.getTemplateDateFormat(TemplateDateModel.TIME, Date.class).formatToPlainText(TM));
+        assertEquals("13:00!",
+                env.getTemplateDateFormat(timeFormatStr2, TemplateDateModel.TIME, Date.class).formatToPlainText(TM));
+        
+        assertEquals("2015.09.06. 13:00",
+                env.getTemplateDateFormat(TemplateDateModel.DATETIME, Timestamp.class).formatToPlainText(TM));
+        assertEquals("2015.09.06. 13:00!",
+                env.getTemplateDateFormat(dateTimeFormatStr2, TemplateDateModel.DATETIME, Timestamp.class).formatToPlainText(TM));
+
+        assertEquals("2015.09.06. (+0000)",
+                env.getTemplateDateFormat(TemplateDateModel.DATE, java.sql.Date.class).formatToPlainText(TM));
+        assertEquals("2015.09.06. (+0000)!",
+                env.getTemplateDateFormat(dateFormatStr2, TemplateDateModel.DATE, java.sql.Date.class).formatToPlainText(TM));
+
+        assertEquals("12:00",
+                env.getTemplateDateFormat(TemplateDateModel.TIME, Time.class).formatToPlainText(TM));
+        assertEquals("12:00!",
+                env.getTemplateDateFormat(timeFormatStr2, TemplateDateModel.TIME, Time.class).formatToPlainText(TM));
+
+        {
+            String dateTimeFormatStrLoc = dateTimeFormatStr + " EEEE";
+            // Gets into cache:
+            TemplateDateFormat format1
+                    = env.getTemplateDateFormat(dateTimeFormatStrLoc, TemplateDateModel.DATETIME, Date.class);
+            assertEquals("2015.09.06. 13:00 Sunday", format1.formatToPlainText(TM));
+            // Different locale (not cached):
+            assertEquals("2015.09.06. 13:00 Sonntag",
+                    env.getTemplateDateFormat(dateTimeFormatStrLoc, TemplateDateModel.DATETIME, Date.class,
+                            Locale.GERMANY).formatToPlainText(TM));
+            // Different locale and zone (not cached):
+            assertEquals("2015.09.06. 14:00 Sonntag",
+                    env.getTemplateDateFormat(dateTimeFormatStrLoc, TemplateDateModel.DATETIME, Date.class,
+                            Locale.GERMANY, TimeZone.getTimeZone("GMT+02"), TimeZone.getTimeZone("GMT+03")).formatToPlainText(TM));
+            // Different locale and zone (not cached):
+            assertEquals("2015.09.06. 15:00 Sonntag",
+                    env.getTemplateDateFormat(dateTimeFormatStrLoc, TemplateDateModel.DATETIME, java.sql.Date.class,
+                            Locale.GERMANY, TimeZone.getTimeZone("GMT+02"), TimeZone.getTimeZone("GMT+03")).formatToPlainText(TM));
+            // Check for corrupted cache:
+            TemplateDateFormat format2
+                    = env.getTemplateDateFormat(dateTimeFormatStrLoc, TemplateDateModel.DATETIME, Date.class);
+            assertEquals("2015.09.06. 13:00 Sunday", format2.formatToPlainText(TM));
+            assertSame(format1, format2);
+        }
+    }
+
+    @Test
+    public void testAliases() throws Exception {
+        setConfiguration(createConfigurationBuilder()
+                .customDateFormats(ImmutableMap.of(
+                        "d", new AliasTemplateDateFormatFactory("yyyy-MMM-dd"),
+                        "m", new AliasTemplateDateFormatFactory("yyyy-MMM"),
+                        "epoch", EpochMillisTemplateDateFormatFactory.INSTANCE))
+                .templateConfigurations(
+                        new ConditionalTemplateConfigurationFactory(
+                                new FileNameGlobMatcher("*2*"),
+                                new TemplateConfiguration.Builder()
+                                        .customDateFormats(ImmutableMap.<String, TemplateDateFormatFactory>of(
+                                                "m", new AliasTemplateDateFormatFactory("yyyy-MMMM"),
+                                                "i", new AliasTemplateDateFormatFactory("@epoch")))
+                                        .build()))
+                .build());
+
+        addToDataModel("d", TM);
+        String commonFtl = "${d?string.@d} ${d?string.@m} "
+                + "<#setting locale='fr_FR'>${d?string.@m} "
+                + "<#attempt>${d?string.@i}<#recover>E</#attempt>";
+        addTemplate("t1.ftl", commonFtl);
+        addTemplate("t2.ftl", commonFtl);
+        
+        // 2015-09-06T12:00:00Z
+        assertOutputForNamed("t1.ftl", "2015-Sep-06 2015-Sep 2015-sept. E");
+        assertOutputForNamed("t2.ftl", "2015-Sep-06 2015-September 2015-septembre " + T);
+    }
+    
+    @Test
+    public void testAliases2() throws Exception {
+        setConfiguration(
+                createConfigurationBuilder()
+                .customDateFormats(ImmutableMap.<String, TemplateDateFormatFactory>of(
+                        "d", new AliasTemplateDateFormatFactory("yyyy-MMM",
+                                ImmutableMap.of(
+                                        new Locale("en"), "yyyy-MMM'_en'",
+                                        Locale.UK, "yyyy-MMM'_en_GB'",
+                                        Locale.FRANCE, "yyyy-MMM'_fr_FR'"))))
+                .dateTimeFormat("@d")
+                .build());
+        addToDataModel("d", TM);
+        assertOutput(
+                "<#setting locale='en_US'>${d} "
+                + "<#setting locale='en_GB'>${d} "
+                + "<#setting locale='en_GB_Win'>${d} "
+                + "<#setting locale='fr_FR'>${d} "
+                + "<#setting locale='hu_HU'>${d}",
+                "2015-Sep_en 2015-Sep_en_GB 2015-Sep_en_GB 2015-sept._fr_FR 2015-szept.");
+    }
+    
+    /**
+     * ?date() and such are new in 2.3.24.
+     */
+    @Test
+    public void testZeroArgDateBI() throws IOException, TemplateException {
+        setConfiguration(
+                createConfigurationBuilder()
+                .dateFormat("@epoch")
+                .dateTimeFormat("@epoch")
+                .timeFormat("@epoch")
+                .build());
+
+        addToDataModel("t", String.valueOf(T));
+        
+        assertOutput(
+                "${t?date?string.xs_u} ${t?date()?string.xs_u}",
+                "2015-09-06Z 2015-09-06Z");
+        assertOutput(
+                "${t?time?string.xs_u} ${t?time()?string.xs_u}",
+                "12:00:00Z 12:00:00Z");
+        assertOutput(
+                "${t?datetime?string.xs_u} ${t?datetime()?string.xs_u}",
+                "2015-09-06T12:00:00Z 2015-09-06T12:00:00Z");
+    }
+
+    @Test
+    public void testAppMetaRoundtrip() throws IOException, TemplateException {
+        setConfiguration(
+                createConfigurationBuilder()
+                .dateFormat("@appMeta")
+                .dateTimeFormat("@appMeta")
+                .timeFormat("@appMeta")
+                .build());
+
+        addToDataModel("t", String.valueOf(T) + "/foo");
+        
+        assertOutput(
+                "${t?date} ${t?date()}",
+                T + " " + T + "/foo");
+        assertOutput(
+                "${t?time} ${t?time()}",
+                T + " " + T + "/foo");
+        assertOutput(
+                "${t?datetime} ${t?datetime()}",
+                T + " " + T + "/foo");
+    }
+    
+    @Test
+    public void testUnknownDateType() throws IOException, TemplateException {
+        addToDataModel("u", new Date(T));
+        assertErrorContains("${u?string}", "isn't known");
+        assertOutput("${u?string('yyyy')}", "2015");
+        assertOutput("<#assign s = u?string>${s('yyyy')}", "2015");
+    }
+    
+    private static class MutableTemplateDateModel implements TemplateDateModel {
+        
+        private Date date;
+
+        public void setDate(Date date) {
+            this.date = date;
+        }
+
+        @Override
+        public Date getAsDate() throws TemplateModelException {
+            return date;
+        }
+
+        @Override
+        public int getDateType() {
+            return DATETIME;
+        }
+        
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/test/java/org/apache/freemarker/core/DirectiveCallPlaceTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/test/java/org/apache/freemarker/core/DirectiveCallPlaceTest.java b/freemarker-core/src/test/java/org/apache/freemarker/core/DirectiveCallPlaceTest.java
new file mode 100644
index 0000000..29220c4
--- /dev/null
+++ b/freemarker-core/src/test/java/org/apache/freemarker/core/DirectiveCallPlaceTest.java
@@ -0,0 +1,249 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.freemarker.core;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.freemarker.core.model.TemplateDirectiveBody;
+import org.apache.freemarker.core.model.TemplateDirectiveModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.util.ObjectFactory;
+import org.apache.freemarker.test.TemplateTest;
+import org.junit.Test;
+
+public class DirectiveCallPlaceTest extends TemplateTest {
+
+    @Test
+    public void testCustomDataBasics() throws IOException, TemplateException {
+        addTemplate(
+                "customDataBasics.ftl",
+                "<@u...@uc> <@u...@uc> <@uc>Ab<#-- -->c</...@uc> <@l...@lc> <@l...@lc>");
+        
+        CachingTextConverterDirective.resetCacheRecreationCount();
+        for (int i = 0; i < 3; i++) {
+            assertOutputForNamed(
+                    "customDataBasics.ftl",
+                    "ABC[cached 1] X=123 ABC[cached 2]  abc[cached 3]");
+        }
+    }
+
+    @Test
+    public void testCustomDataProviderMismatch() throws IOException, TemplateException {
+        addTemplate(
+                "customDataProviderMismatch.ftl",
+                "<#list [uc, lc, uc] as d><#list 1..2 as _><@d...@d></#list></#list>");
+        
+        CachingTextConverterDirective.resetCacheRecreationCount();
+        assertOutputForNamed(
+                "customDataProviderMismatch.ftl",
+                "ABC[cached 1]ABC[cached 1]abc[cached 2]abc[cached 2]ABC[cached 3]ABC[cached 3]");
+        assertOutputForNamed(
+                "customDataProviderMismatch.ftl",
+                "ABC[cached 3]ABC[cached 3]abc[cached 4]abc[cached 4]ABC[cached 5]ABC[cached 5]");
+    }
+    
+    @Test
+    public void testPositions() throws IOException, TemplateException {
+        addTemplate(
+                "positions.ftl",
+                "<@pa />\n"
+                + "..<@pa\n"
+                + "/><@pa>xxx</@>\n"
+                + "<@pa>{<@pa/> <@pa/>}</@>\n"
+                + "${curDirLine}<@argP p=curDirLine?string>${curDirLine}</...@argP>${curDirLine}\n"
+                + "<#macro m p>(p=${p}){<#nested>}</#macro>\n"
+                + "${curDirLine}<@m p=curDirLine?string>${curDirLine}</...@m>${curDirLine}");
+        
+        assertOutputForNamed(
+                "positions.ftl",
+                "[positions.ftl:1:1-1:7]"
+                + "..[positions.ftl:2:3-3:2]"
+                + "[positions.ftl:3:3-3:14]xxx\n"
+                + "[positions.ftl:4:1-4:24]{[positions.ftl:4:7-4:12] [positions.ftl:4:14-4:19]}\n"
+                + "-(p=5){-}-\n"
+                + "-(p=7){-}-"
+                );
+    }
+    
+    @SuppressWarnings("boxing")
+    @Override
+    protected Object createDataModel() {
+        Map<String, Object> dm = new HashMap<>();
+        dm.put("uc", new CachingUpperCaseDirective());
+        dm.put("lc", new CachingLowerCaseDirective());
+        dm.put("pa", new PositionAwareDirective());
+        dm.put("argP", new ArgPrinterDirective());
+        dm.put("curDirLine", new CurDirLineScalar());
+        dm.put("x", 123);
+        return dm;
+    }
+
+    private abstract static class CachingTextConverterDirective implements TemplateDirectiveModel {
+
+        /** Only needed for testing. */
+        private static AtomicInteger cacheRecreationCount = new AtomicInteger();
+        
+        /** Only needed for testing. */
+        static void resetCacheRecreationCount() {
+            cacheRecreationCount.set(0);
+        }
+        
+        @Override
+        public void execute(Environment env, Map params, TemplateModel[] loopVars, final TemplateDirectiveBody body)
+                throws TemplateException, IOException {
+            if (body == null) {
+                return;
+            }
+            
+            final String convertedText;
+
+            final DirectiveCallPlace callPlace = env.getCurrentDirectiveCallPlace();
+            if (callPlace.isNestedOutputCacheable()) {
+                try {
+                    convertedText = (String) callPlace.getOrCreateCustomData(
+                            getTextConversionIdentity(), new ObjectFactory<String>() {
+
+                                @Override
+                                public String createObject() throws TemplateException, IOException {
+                                    return convertBodyText(body)
+                                            + "[cached " + cacheRecreationCount.incrementAndGet() + "]";
+                                }
+
+                            });
+                } catch (CallPlaceCustomDataInitializationException e) {
+                    throw new TemplateModelException("Failed to pre-render nested content", e);
+                }
+            } else {
+                convertedText = convertBodyText(body);
+            }
+
+            env.getOut().write(convertedText);
+        }
+
+        protected abstract Class getTextConversionIdentity();
+
+        private String convertBodyText(TemplateDirectiveBody body) throws TemplateException,
+                IOException {
+            StringWriter sw = new StringWriter();
+            body.render(sw);
+            return convertText(sw.toString());
+        }
+        
+        protected abstract String convertText(String s);
+
+    }
+    
+    private static class CachingUpperCaseDirective extends CachingTextConverterDirective {
+
+        @Override
+        protected String convertText(String s) {
+            return s.toUpperCase();
+        }
+        
+        @Override
+        protected Class getTextConversionIdentity() {
+            return CachingUpperCaseDirective.class;
+        }
+        
+    }
+
+    private static class CachingLowerCaseDirective extends CachingTextConverterDirective {
+
+        @Override
+        protected String convertText(String s) {
+            return s.toLowerCase();
+        }
+
+        @Override
+        protected Class getTextConversionIdentity() {
+            return CachingLowerCaseDirective.class;
+        }
+        
+    }
+    
+    private static class PositionAwareDirective implements TemplateDirectiveModel {
+
+        @Override
+        public void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body)
+                throws TemplateException, IOException {
+            Writer out = env.getOut();
+            DirectiveCallPlace callPlace = env.getCurrentDirectiveCallPlace();
+            out.write("[");
+            out.write(getTemplateSourceName(callPlace));
+            out.write(":");
+            out.write(Integer.toString(callPlace.getBeginLine()));
+            out.write(":");
+            out.write(Integer.toString(callPlace.getBeginColumn()));
+            out.write("-");
+            out.write(Integer.toString(callPlace.getEndLine()));
+            out.write(":");
+            out.write(Integer.toString(callPlace.getEndColumn()));
+            out.write("]");
+            if (body != null) {
+                body.render(out);
+            }
+        }
+
+        private String getTemplateSourceName(DirectiveCallPlace callPlace) {
+            return ((ASTDirUserDefined) callPlace).getTemplate().getSourceName();
+        }
+        
+    }
+
+    private static class ArgPrinterDirective implements TemplateDirectiveModel {
+
+        @Override
+        public void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body)
+                throws TemplateException, IOException {
+            final Writer out = env.getOut();
+            if (params.size() > 0) {
+                out.write("(p=");
+                out.write(((TemplateScalarModel) params.get("p")).getAsString());
+                out.write(")");
+            }
+            if (body != null) {
+                out.write("{");
+                body.render(out);
+                out.write("}");
+            }
+        }
+        
+    }
+    
+    private static class CurDirLineScalar implements TemplateScalarModel {
+
+        @Override
+        public String getAsString() throws TemplateModelException {
+            DirectiveCallPlace callPlace = Environment.getCurrentEnvironment().getCurrentDirectiveCallPlace();
+            return callPlace != null
+                    ? String.valueOf(Environment.getCurrentEnvironment().getCurrentDirectiveCallPlace().getBeginLine())
+                    : "-";
+        }
+        
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/test/java/org/apache/freemarker/core/EncodingOverrideTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/test/java/org/apache/freemarker/core/EncodingOverrideTest.java b/freemarker-core/src/test/java/org/apache/freemarker/core/EncodingOverrideTest.java
new file mode 100644
index 0000000..b1acead
--- /dev/null
+++ b/freemarker-core/src/test/java/org/apache/freemarker/core/EncodingOverrideTest.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.freemarker.core;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+
+import org.apache.freemarker.core.templateresolver.impl.ClassTemplateLoader;
+import org.junit.Test;
+
+public class EncodingOverrideTest {
+
+    @Test
+    public void testMarchingCharset() throws Exception {
+        Template t = createConfig(StandardCharsets.UTF_8).getTemplate("encodingOverride-UTF-8.ftl");
+        assertEquals(StandardCharsets.UTF_8, t.getActualSourceEncoding());
+        checkTemplateOutput(t);
+    }
+
+    @Test
+    public void testDifferentCharset() throws Exception {
+        Template t = createConfig(StandardCharsets.UTF_8).getTemplate("encodingOverride-ISO-8859-1.ftl");
+        assertEquals(StandardCharsets.ISO_8859_1, t.getActualSourceEncoding());
+        checkTemplateOutput(t);
+    }
+
+    private void checkTemplateOutput(Template t) throws TemplateException, IOException {
+        StringWriter out = new StringWriter(); 
+        t.process(Collections.emptyMap(), out);
+        assertEquals("Béka", out.toString());
+    }
+    
+    private Configuration createConfig(Charset charset) {
+       return new Configuration.Builder(Configuration.VERSION_3_0_0)
+               .templateLoader(new ClassTemplateLoader(EncodingOverrideTest.class, ""))
+               .sourceEncoding(charset)
+               .build();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/test/java/org/apache/freemarker/core/EnvironmentGetTemplateVariantsTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/test/java/org/apache/freemarker/core/EnvironmentGetTemplateVariantsTest.java b/freemarker-core/src/test/java/org/apache/freemarker/core/EnvironmentGetTemplateVariantsTest.java
new file mode 100644
index 0000000..b79559c
--- /dev/null
+++ b/freemarker-core/src/test/java/org/apache/freemarker/core/EnvironmentGetTemplateVariantsTest.java
@@ -0,0 +1,214 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.freemarker.core;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Collections;
+import java.util.Map;
+
+import org.apache.freemarker.core.model.TemplateDirectiveBody;
+import org.apache.freemarker.core.model.TemplateDirectiveModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+import org.apache.freemarker.core.templateresolver.impl.StringTemplateLoader;
+import org.apache.freemarker.test.TemplateTest;
+import org.apache.freemarker.test.TestConfigurationBuilder;
+import org.junit.Test;
+
+public class EnvironmentGetTemplateVariantsTest extends TemplateTest {
+
+    private static final StringTemplateLoader TEMPLATES = new StringTemplateLoader();
+    static {
+        TEMPLATES.putTemplate("main",
+                "<@tNames />\n"
+                + "---1---\n"
+                + "[imp: <#import 'imp' as i>${i.impIni}]\n"
+                + "---2---\n"
+                + "<@i.impM>"
+                    + "<@tNames />"
+                + "</@>\n"
+                + "---3---\n"
+                + "[inc: <#include 'inc'>]\n"
+                + "---4---\n"
+                + "<@incM>"
+                    + "<@tNames />"
+                + "</@>\n"
+                + "---5---\n"
+                + "[inc2: <#include 'inc2'>]\n"
+                + "---6---\n"
+                + "<#import 'imp2' as i2>"
+                + "<@i.impM2><@tNames /></@>\n"
+                + "---7---\n"
+                + "<#macro mainM>"
+                    + "[mainM: <@tNames /> {<#nested>} <@tNames />]"
+                + "</#macro>"
+                + "[inc3: <#include 'inc3'>]\n"
+                + "<@mainM><@tNames /> <#include 'inc4'> <@tNames /></@>\n"
+                + "<@tNames />\n"
+                + "---8---\n"
+                + "<#function mainF>"
+                    + "<@tNames />"
+                    + "<#return lastTNamesResult>"
+                + "</#function>"
+                + "mainF: ${mainF()}, impF: ${i.impF()}, incF: ${incF()}\n"
+                );
+        TEMPLATES.putTemplate("inc",
+                "<@tNames />\n"
+                + "<#macro incM>"
+                    + "[incM: <@tNames /> {<#nested>}]"
+                + "</#macro>"
+                + "<#function incF>"
+                    + "<@tNames />"
+                    + "<#return lastTNamesResult>"
+                + "</#function>"
+                + "<@incM><@tNames /></@>\n"
+                + "<#if !included!false>[incInc: <#assign included=true><#include 'inc'>]\n</#if>"
+                );
+        TEMPLATES.putTemplate("imp",
+                "<#assign impIni><@tNames /></#assign>\n"
+                + "<#macro impM>"
+                    + "[impM: <@tNames />\n"
+                        + "{<#nested>}\n"
+                        + "[inc: <#include 'inc'>]\n"
+                        + "<@incM><@tNames /></@>\n"
+                    + "]"
+                + "</#macro>"
+                + "<#macro impM2>"
+                    + "[impM2: <@tNames />\n"
+                    + "{<#nested>}\n"
+                    + "<@i2.imp2M><@tNames /></@>\n"
+                    + "]"
+                + "</#macro>"
+                + "<#function impF>"
+                    + "<@tNames />"
+                    + "<#return lastTNamesResult>"
+                + "</#function>"
+                );
+        TEMPLATES.putTemplate("inc2",
+                "<@tNames />\n"
+                + "<@i.impM><@tNames /></@>\n"
+                );
+        TEMPLATES.putTemplate("imp2",
+                "<#macro imp2M>"
+                    + "[imp2M: <@tNames /> {<#nested>}]"
+                + "</#macro>");
+        TEMPLATES.putTemplate("inc3",
+                "<@tNames />\n"
+                + "<@mainM><@tNames /></@>\n"
+                );
+        TEMPLATES.putTemplate("inc4",
+                "<@tNames />"
+                );
+    }
+    
+    @Test
+    public void test() throws IOException, TemplateException {
+        setConfiguration(createConfiguration(Configuration.VERSION_3_0_0));
+        assertOutputForNamed(
+                "main",
+                "<ct=main mt=main>\n"
+                + "---1---\n"
+                + "[imp: <ct=imp mt=main>]\n"
+                + "---2---\n"
+                + "[impM: <ct=imp mt=main>\n"
+                    + "{<ct=main mt=main>}\n"
+                    + "[inc: <ct=inc mt=main>\n"
+                        + "[incM: <ct=inc mt=main> {<ct=inc mt=main>}]\n"
+                        + "[incInc: <ct=inc mt=main>\n"
+                            + "[incM: <ct=inc mt=main> {<ct=inc mt=main>}]\n"
+                        + "]\n"
+                    + "]\n"
+                    + "[incM: <ct=inc mt=main> {<ct=imp mt=main>}]\n"
+                + "]\n"
+                + "---3---\n"
+                + "[inc: <ct=inc mt=main>\n"
+                    + "[incM: <ct=inc mt=main> {<ct=inc mt=main>}]\n"
+                    + "[incInc: <ct=inc mt=main>\n"
+                        + "[incM: <ct=inc mt=main> {<ct=inc mt=main>}]\n"
+                    + "]\n"
+                + "]\n"
+                + "---4---\n"
+                + "[incM: <ct=inc mt=main> {<ct=main mt=main>}]\n"
+                + "---5---\n"
+                + "[inc2: <ct=inc2 mt=main>\n"
+                    + "[impM: <ct=imp mt=main>\n"
+                        + "{<ct=inc2 mt=main>}\n"
+                        + "[inc: <ct=inc mt=main>\n"
+                            + "[incM: <ct=inc mt=main> {<ct=inc mt=main>}]\n"
+                        + "]\n"
+                        + "[incM: <ct=inc mt=main> {<ct=imp mt=main>}]\n"
+                    + "]\n"
+                + "]\n"
+                + "---6---\n"
+                + "[impM2: <ct=imp mt=main>\n"
+                    + "{<ct=main mt=main>}\n"
+                    + "[imp2M: <ct=imp2 mt=main> {<ct=imp mt=main>}]\n"
+                + "]\n"
+                + "---7---\n"
+                + "[inc3: <ct=inc3 mt=main>\n"
+                    + "[mainM: <ct=main mt=main> {<ct=inc3 mt=main>} <ct=main mt=main>]\n"
+                + "]\n"
+                + "[mainM: "
+                    + "<ct=main mt=main> "
+                    + "{<ct=main mt=main> <ct=inc4 mt=main> <ct=main mt=main>} "
+                    + "<ct=main mt=main>"
+                + "]\n"
+                + "<ct=main mt=main>\n"
+                + "---8---\n"
+                + "mainF: <ct=main mt=main>, impF: <ct=imp mt=main>, incF: <ct=inc mt=main>\n"
+                .replaceAll("<t=\\w+", "<t=main"));
+    }
+
+    @Test
+    public void testNotStarted() throws IOException, TemplateException {
+        Template t = new Template("foo", "", createConfiguration(Configuration.VERSION_3_0_0));
+        final Environment env = t.createProcessingEnvironment(null, null);
+        assertSame(t, env.getMainTemplate());
+        assertSame(t, env.getCurrentTemplate());
+    }
+    
+    private Configuration createConfiguration(Version iciVersion) {
+        return new TestConfigurationBuilder(iciVersion)
+                .templateLoader(TEMPLATES)
+                .whitespaceStripping(false)
+                .build();
+    }
+
+    @Override
+    protected Object createDataModel() {
+        return Collections.singletonMap("tNames", new TemplateDirectiveModel() {
+
+            @Override
+            public void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body)
+                    throws TemplateException, IOException {
+                Writer out = env.getOut();
+                final String r = "<ct=" + env.getCurrentTemplate().getLookupName() + " mt="
+                        + env.getMainTemplate().getLookupName() + ">";
+                out.write(r);
+                env.setGlobalVariable("lastTNamesResult", new SimpleScalar(r));
+            }
+            
+        });
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/test/java/org/apache/freemarker/core/ExceptionTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/test/java/org/apache/freemarker/core/ExceptionTest.java b/freemarker-core/src/test/java/org/apache/freemarker/core/ExceptionTest.java
new file mode 100644
index 0000000..19c3b6e
--- /dev/null
+++ b/freemarker-core/src/test/java/org/apache/freemarker/core/ExceptionTest.java
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.freemarker.core;
+
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.Collections;
+import java.util.Locale;
+
+import org.apache.freemarker.core.templateresolver.impl.StringTemplateLoader;
+import org.apache.freemarker.core.util._NullWriter;
+import org.apache.freemarker.test.TestConfigurationBuilder;
+
+import junit.framework.TestCase;
+public class ExceptionTest extends TestCase {
+    
+    public ExceptionTest(String name) {
+        super(name);
+    }
+
+    public void testParseExceptionSerializable() throws IOException, ClassNotFoundException {
+        try {
+            new Template(null, new StringReader("<@>"), new TestConfigurationBuilder().build());
+            fail();
+        } catch (ParseException e) {
+            ByteArrayOutputStream out = new ByteArrayOutputStream();
+            new ObjectOutputStream(out).writeObject(e);
+            new ObjectInputStream(new ByteArrayInputStream(out.toByteArray())).readObject();
+        }
+    }
+
+    public void testTemplateErrorSerializable() throws IOException, ClassNotFoundException {
+        Template tmp = new Template(null, new StringReader("${noSuchVar}"),
+                new TestConfigurationBuilder().build());
+        try {
+            tmp.process(Collections.EMPTY_MAP, new StringWriter());
+            fail();
+        } catch (TemplateException e) {
+            ByteArrayOutputStream out = new ByteArrayOutputStream();
+            new ObjectOutputStream(out).writeObject(e);
+            new ObjectInputStream(new ByteArrayInputStream(out.toByteArray())).readObject();
+        }
+    }
+    
+    @SuppressWarnings("boxing")
+    public void testTemplateExceptionLocationInformation() throws IOException {
+        StringTemplateLoader tl = new StringTemplateLoader();
+        tl.putTemplate("foo_en.ftl", "\n\nxxx${noSuchVariable}");
+
+        Template t = new TestConfigurationBuilder().templateLoader(tl).build()
+                .getTemplate("foo.ftl", Locale.US);
+        try {
+            t.process(null, _NullWriter.INSTANCE);
+            fail();
+        } catch (TemplateException e) {
+            assertEquals("foo.ftl", t.getLookupName());
+            assertEquals("foo.ftl", e.getTemplateLookupName());
+            assertEquals("foo_en.ftl", e.getTemplateSourceName());
+            assertEquals(3, (int) e.getLineNumber());
+            assertEquals(6, (int) e.getColumnNumber());
+            assertEquals(3, (int) e.getEndLineNumber());
+            assertEquals(19, (int) e.getEndColumnNumber());
+            assertThat(e.getMessage(), containsString("foo_en.ftl"));
+            assertThat(e.getMessage(), containsString("noSuchVariable"));
+        }
+    }
+
+    @SuppressWarnings("cast")
+    public void testParseExceptionLocationInformation() throws IOException {
+        StringTemplateLoader tl = new StringTemplateLoader();
+        tl.putTemplate("foo_en.ftl", "\n\nxxx<#noSuchDirective>");
+
+        try {
+            new TestConfigurationBuilder().templateLoader(tl).build()
+                    .getTemplate("foo.ftl", Locale.US);
+            fail();
+        } catch (ParseException e) {
+            System.out.println(e.getMessage());
+            assertEquals("foo_en.ftl", e.getTemplateSourceName());
+            assertEquals("foo.ftl", e.getTemplateLookupName());
+            assertEquals(3, e.getLineNumber());
+            assertEquals(5, e.getColumnNumber());
+            assertEquals(3, e.getEndLineNumber());
+            assertEquals(20, e.getEndColumnNumber());
+            assertThat(e.getMessage(), containsString("foo_en.ftl"));
+            assertThat(e.getMessage(), containsString("noSuchDirective"));
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/test/java/org/apache/freemarker/core/GetSourceTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/test/java/org/apache/freemarker/core/GetSourceTest.java b/freemarker-core/src/test/java/org/apache/freemarker/core/GetSourceTest.java
new file mode 100644
index 0000000..1462bfc
--- /dev/null
+++ b/freemarker-core/src/test/java/org/apache/freemarker/core/GetSourceTest.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.freemarker.core;
+
+import static org.junit.Assert.*;
+
+import org.apache.freemarker.test.TestConfigurationBuilder;
+import org.junit.Test;
+
+public class GetSourceTest {
+
+    @Test
+    public void testGetSource() throws Exception {
+        {
+            // Note: Default tab size is 8.
+            Template t = new Template(null, "a\n\tb\nc",
+                    new TestConfigurationBuilder().build());
+            // A historical quirk we keep for B.C.: it repaces tabs with spaces.
+            assertEquals("a\n        b\nc", t.getSource(1, 1, 1, 3));
+        }
+        
+        {
+            Template t = new Template(null, "a\n\tb\nc",
+                    new TestConfigurationBuilder().tabSize(4).build());
+            assertEquals("a\n    b\nc", t.getSource(1, 1, 1, 3));
+        }
+        
+        {
+            Template t = new Template(null, "a\n\tb\nc",
+                    new TestConfigurationBuilder().tabSize(1).build());
+            // If tab size is 1, it behaves as it always should have: it keeps the tab.
+            assertEquals("a\n\tb\nc", t.getSource(1, 1, 1, 3));
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/test/java/org/apache/freemarker/core/HeaderParsingTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/test/java/org/apache/freemarker/core/HeaderParsingTest.java b/freemarker-core/src/test/java/org/apache/freemarker/core/HeaderParsingTest.java
new file mode 100644
index 0000000..6f8851d
--- /dev/null
+++ b/freemarker-core/src/test/java/org/apache/freemarker/core/HeaderParsingTest.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.freemarker.core;
+
+import java.io.IOException;
+
+import org.apache.freemarker.test.TemplateTest;
+import org.apache.freemarker.test.TestConfigurationBuilder;
+import org.junit.Test;
+
+public class HeaderParsingTest extends TemplateTest {
+
+    private final Configuration cfgStripWS = new TestConfigurationBuilder().build();
+    private final Configuration cfgNoStripWS = new TestConfigurationBuilder().whitespaceStripping(false).build();
+
+    @Test
+    public void test() throws IOException, TemplateException {
+        assertOutput("<#ftl>text", "text", "text");
+        assertOutput(" <#ftl> text", " text", " text");
+        assertOutput("\n<#ftl>\ntext", "text", "text");
+        assertOutput("\n \n\n<#ftl> \ntext", "text", "text");
+        assertOutput("\n \n\n<#ftl>\n\ntext", "\ntext", "\ntext");
+    }
+    
+    private void assertOutput(final String ftl, String expectedOutStripped, String expectedOutNonStripped)
+            throws IOException, TemplateException {
+        for (int i = 0; i < 4; i++) {
+            String ftlPermutation = ftl;
+            if ((i & 1) == 1) {
+                ftlPermutation = ftlPermutation.replace("<#ftl>", "<#ftl encoding='utf-8'>");
+            }
+            if ((i & 2) == 2) {
+                ftlPermutation = ftlPermutation.replace('<', '[').replace('>', ']');
+            }
+            
+            setConfiguration(cfgStripWS);
+            assertOutput(ftlPermutation, expectedOutStripped);
+            setConfiguration(cfgNoStripWS);
+            assertOutput(ftlPermutation, expectedOutNonStripped);
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/test/java/org/apache/freemarker/core/IncludeAndImportConfigurableLayersTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/test/java/org/apache/freemarker/core/IncludeAndImportConfigurableLayersTest.java b/freemarker-core/src/test/java/org/apache/freemarker/core/IncludeAndImportConfigurableLayersTest.java
new file mode 100644
index 0000000..738b4de
--- /dev/null
+++ b/freemarker-core/src/test/java/org/apache/freemarker/core/IncludeAndImportConfigurableLayersTest.java
@@ -0,0 +1,354 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.freemarker.core;
+
+import static org.junit.Assert.*;
+
+import java.io.StringWriter;
+
+import org.apache.freemarker.core.templateresolver.ConditionalTemplateConfigurationFactory;
+import org.apache.freemarker.core.templateresolver.FileNameGlobMatcher;
+import org.apache.freemarker.core.templateresolver.impl.StringTemplateLoader;
+import org.apache.freemarker.test.TestConfigurationBuilder;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+public class IncludeAndImportConfigurableLayersTest {
+
+    @Test
+    public void test3LayerImportNoClashes() throws Exception {
+        TestConfigurationBuilder cfgB = createConfigurationBuilder()
+                .autoImports(ImmutableMap.of("t1", "t1.ftl"))
+                .templateConfigurations(
+                        new ConditionalTemplateConfigurationFactory(
+                                new FileNameGlobMatcher("main.ftl"),
+                                new TemplateConfiguration.Builder()
+                                        .autoImports(ImmutableMap.of("t2", "t2.ftl"))
+                                        .build()));
+        Configuration cfg = cfgB.build();
+
+        {
+            Template t = cfg.getTemplate("main.ftl");
+            StringWriter sw = new StringWriter();
+            Environment env = t.createProcessingEnvironment(null, sw);
+            env.addAutoImport("t3", "t3.ftl");
+    
+            env.process();
+            assertEquals("In main: t1;t2;t3;", sw.toString());
+        }
+
+        {
+            Template t = cfg.getTemplate("main.ftl");
+            StringWriter sw = new StringWriter();
+            Environment env = t.createProcessingEnvironment(null, sw);
+    
+            env.process();
+            assertEquals("In main: t1;t2;", sw.toString());
+        }
+        
+        {
+            Template t = cfg.getTemplate("main2.ftl");
+            StringWriter sw = new StringWriter();
+            Environment env = t.createProcessingEnvironment(null, sw);
+            env.addAutoImport("t3", "t3.ftl");
+    
+            env.process();
+            assertEquals("In main2: t1;t3;", sw.toString());
+        }
+        
+        cfgB.removeAutoImport("t1");
+        cfg = cfgB.build();
+
+        {
+            Template t = cfg.getTemplate("main.ftl");
+            StringWriter sw = new StringWriter();
+            Environment env = t.createProcessingEnvironment(null, sw);
+            env.addAutoImport("t3", "t3.ftl");
+    
+            env.process();
+            assertEquals("In main: t2;t3;", sw.toString());
+        }
+    }
+    
+    @Test
+    public void test3LayerImportClashes() throws Exception {
+        Configuration cfg = createConfigurationBuilder()
+                .autoImports(ImmutableMap.of(
+                        "t1", "t1.ftl",
+                        "t2", "t2.ftl",
+                        "t3", "t3.ftl"))
+                .templateConfigurations(
+                        new ConditionalTemplateConfigurationFactory(
+                                new FileNameGlobMatcher("main.ftl"),
+                                new TemplateConfiguration.Builder()
+                                        .autoImports(ImmutableMap.of("t2", "t2b.ftl"))
+                                        .build()))
+                .build();
+
+        {
+            Template t = cfg.getTemplate("main.ftl");
+            StringWriter sw = new StringWriter();
+            Environment env = t.createProcessingEnvironment(null, sw);
+            env.addAutoImport("t3", "t3b.ftl");
+    
+            env.process();
+            assertEquals("In main: t1;t2b;t3b;", sw.toString());
+        }
+        
+        {
+            Template t = cfg.getTemplate("main2.ftl");
+            StringWriter sw = new StringWriter();
+            Environment env = t.createProcessingEnvironment(null, sw);
+            env.addAutoImport("t3", "t3b.ftl");
+    
+            env.process();
+            assertEquals("In main2: t1;t2;t3b;", sw.toString());
+        }
+        
+        {
+            Template t = cfg.getTemplate("main.ftl");
+            StringWriter sw = new StringWriter();
+            Environment env = t.createProcessingEnvironment(null, sw);
+    
+            env.process();
+            assertEquals("In main: t1;t3;t2b;", sw.toString());
+        }
+    }
+
+    @Test
+    public void test3LayerIncludesNoClashes() throws Exception {
+        TestConfigurationBuilder cfgB = createConfigurationBuilder()
+                .autoIncludes(ImmutableList.of("t1.ftl"))
+                .templateConfigurations(
+                        new ConditionalTemplateConfigurationFactory(
+                                new FileNameGlobMatcher("main.ftl"),
+                                new TemplateConfiguration.Builder()
+                                        .autoIncludes(ImmutableList.of("t2.ftl"))
+                                        .build()));
+
+        Configuration cfg = cfgB.build();
+
+        {
+            Template t = cfg.getTemplate("main.ftl");
+            StringWriter sw = new StringWriter();
+            Environment env = t.createProcessingEnvironment(null, sw);
+            env.addAutoInclude("t3.ftl");
+    
+            env.process();
+            assertEquals("T1;T2;T3;In main: t1;t2;t3;", sw.toString());
+        }
+
+        {
+            Template t = cfg.getTemplate("main.ftl");
+            StringWriter sw = new StringWriter();
+            Environment env = t.createProcessingEnvironment(null, sw);
+    
+            env.process();
+            assertEquals("T1;T2;In main: t1;t2;", sw.toString());
+        }
+        
+        {
+            Template t = cfg.getTemplate("main2.ftl");
+            StringWriter sw = new StringWriter();
+            Environment env = t.createProcessingEnvironment(null, sw);
+            env.addAutoInclude("t3.ftl");
+    
+            env.process();
+            assertEquals("T1;T3;In main2: t1;t3;", sw.toString());
+        }
+        
+        cfgB.removeAutoInclude("t1.ftl");
+        cfg = cfgB.build();
+
+        {
+            Template t = cfg.getTemplate("main.ftl");
+            StringWriter sw = new StringWriter();
+            Environment env = t.createProcessingEnvironment(null, sw);
+            env.addAutoInclude("t3.ftl");
+    
+            env.process();
+            assertEquals("T2;T3;In main: t2;t3;", sw.toString());
+        }
+    }
+
+    @Test
+    public void test3LayerIncludeClashes() throws Exception {
+        Configuration cfg = createConfigurationBuilder()
+                .autoIncludes(ImmutableList.of(
+                        "t1.ftl",
+                        "t2.ftl",
+                        "t3.ftl"))
+                .templateConfigurations(new ConditionalTemplateConfigurationFactory(
+                        new FileNameGlobMatcher("main.ftl"),
+                        new TemplateConfiguration.Builder()
+                                .autoIncludes(ImmutableList.of("t2.ftl"))
+                                .build()))
+                .build();
+
+        {
+            Template t = cfg.getTemplate("main.ftl");
+            StringWriter sw = new StringWriter();
+            Environment env = t.createProcessingEnvironment(null, sw);
+            env.addAutoInclude("t3.ftl");
+    
+            env.process();
+            assertEquals("T1;T2;T3;In main: t1;t2;t3;", sw.toString());
+        }
+        
+        {
+            Template t = cfg.getTemplate("main2.ftl");
+            StringWriter sw = new StringWriter();
+            Environment env = t.createProcessingEnvironment(null, sw);
+            env.addAutoInclude("t3.ftl");
+    
+            env.process();
+            assertEquals("T1;T2;T3;In main2: t1;t2;t3;", sw.toString());
+        }
+        
+        {
+            Template t = cfg.getTemplate("main.ftl");
+            StringWriter sw = new StringWriter();
+            Environment env = t.createProcessingEnvironment(null, sw);
+    
+            env.process();
+            assertEquals("T1;T3;T2;In main: t1;t3;t2;", sw.toString());
+        }
+        
+        {
+            Template t = cfg.getTemplate("main.ftl");
+            StringWriter sw = new StringWriter();
+            Environment env = t.createProcessingEnvironment(null, sw);
+            env.addAutoInclude("t1.ftl");
+    
+            env.process();
+            assertEquals("T3;T2;T1;In main: t3;t2;t1;", sw.toString());
+        }
+    }
+    
+    @Test
+    public void test3LayerIncludesClashes2() throws Exception {
+        Configuration cfg = createConfigurationBuilder()
+                .autoIncludes(ImmutableList.of("t1.ftl", "t1.ftl"))
+                .templateConfigurations(
+                        new ConditionalTemplateConfigurationFactory(
+                                new FileNameGlobMatcher("main.ftl"),
+                                new TemplateConfiguration.Builder()
+                                        .autoIncludes(ImmutableList.of("t2.ftl", "t2.ftl"))
+                                        .build()))
+                .build();
+
+        {
+            Template t = cfg.getTemplate("main.ftl");
+            StringWriter sw = new StringWriter();
+            Environment env = t.createProcessingEnvironment(null, sw);
+            env.addAutoInclude("t3.ftl");
+            env.addAutoInclude("t3.ftl");
+            env.addAutoInclude("t1.ftl");
+            env.addAutoInclude("t1.ftl");
+    
+            env.process();
+            assertEquals("T2;T3;T1;In main: t2;t3;t1;", sw.toString());
+        }
+    }
+    
+    @Test
+    public void test3LayerLaziness() throws Exception {
+        for (Class<?> layer : new Class<?>[] { Configuration.class, Template.class, Environment.class }) {
+            test3LayerLaziness(layer, null, null, false, "t1;t2;");
+            test3LayerLaziness(layer, null, null, true, "t1;t2;");
+            test3LayerLaziness(layer, null, false, true, "t1;t2;");
+            test3LayerLaziness(layer, null, true, true, "t2;");
+            
+            test3LayerLaziness(layer, false, null, false, "t1;t2;");
+            test3LayerLaziness(layer, false, null, true, "t1;t2;");
+            test3LayerLaziness(layer, false, false, true, "t1;t2;");
+            test3LayerLaziness(layer, false, true, true, "t2;");
+
+            test3LayerLaziness(layer, true, null, false, "");
+            test3LayerLaziness(layer, true, null, true, "");
+            test3LayerLaziness(layer, true, false, true, "t1;");
+            test3LayerLaziness(layer, true, true, true, "");
+        }
+    }
+    
+    private void test3LayerLaziness(
+            Class<?> layer,
+            Boolean lazyImports,
+            Boolean lazyAutoImports, boolean setLazyAutoImports,
+            String expectedOutput)
+            throws Exception {
+        Configuration cfg;
+        {
+            TestConfigurationBuilder cfgB = createConfigurationBuilder()
+                    .autoImports(ImmutableMap.of("t1", "t1.ftl"));
+            if (layer == Configuration.class) {
+                setLazinessOfConfigurable(cfgB, lazyImports, lazyAutoImports, setLazyAutoImports);
+            }
+            cfg = cfgB.build();
+        }
+
+        TemplateConfiguration tc;
+        if (layer == Template.class) {
+            TemplateConfiguration.Builder tcb = new TemplateConfiguration.Builder();
+            setLazinessOfConfigurable(tcb, lazyImports, lazyAutoImports, setLazyAutoImports);
+            tc = tcb.build();
+        } else {
+            tc = null;
+        }
+
+        Template t = new Template(null, "<#import 't2.ftl' as t2>${loaded!}", cfg, tc);
+        StringWriter sw = new StringWriter();
+
+        Environment env = t.createProcessingEnvironment(null, sw);
+        if (layer == Environment.class) {
+            setLazinessOfConfigurable(env, lazyImports, lazyAutoImports, setLazyAutoImports);
+        }
+
+        env.process();
+        assertEquals(expectedOutput, sw.toString());
+    }
+
+    private void setLazinessOfConfigurable(
+            MutableProcessingConfiguration<?> cfg,
+            Boolean lazyImports, Boolean lazyAutoImports, boolean setLazyAutoImports) {
+        if (lazyImports != null) {
+            cfg.setLazyImports(lazyImports);
+        }
+        if (setLazyAutoImports) {
+            cfg.setLazyAutoImports(lazyAutoImports);
+        }
+    }
+    
+    private TestConfigurationBuilder createConfigurationBuilder() {
+        StringTemplateLoader loader = new StringTemplateLoader();
+        loader.putTemplate("main.ftl", "In main: ${loaded}");
+        loader.putTemplate("main2.ftl", "In main2: ${loaded}");
+        loader.putTemplate("t1.ftl", "<#global loaded = (loaded!) + 't1;'>T1;");
+        loader.putTemplate("t2.ftl", "<#global loaded = (loaded!) + 't2;'>T2;");
+        loader.putTemplate("t3.ftl", "<#global loaded = (loaded!) + 't3;'>T3;");
+        loader.putTemplate("t1b.ftl", "<#global loaded = (loaded!) + 't1b;'>T1b;");
+        loader.putTemplate("t2b.ftl", "<#global loaded = (loaded!) + 't2b;'>T2b;");
+        loader.putTemplate("t3b.ftl", "<#global loaded = (loaded!) + 't3b;'>T3b;");
+
+        return new TestConfigurationBuilder().templateLoader(loader);
+    }
+
+}