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 2016/06/12 16:53:56 UTC

[15/50] incubator-freemarker git commit: Further tests for <#list xs as k , v> and <#items as k , v>.

Further tests for <#list xs as k ,v> and <#items as k ,v>.


Project: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/commit/d8d070a1
Tree: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/tree/d8d070a1
Diff: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/diff/d8d070a1

Branch: refs/heads/2.3
Commit: d8d070a1c24efc3c0573b7a51f3e45b8ec9112db
Parents: 1ecf10a
Author: ddekany <dd...@apache.org>
Authored: Mon May 30 23:53:32 2016 +0200
Committer: ddekany <dd...@apache.org>
Committed: Mon May 30 23:53:32 2016 +0200

----------------------------------------------------------------------
 .../java/freemarker/core/ListErrorsTest.java    | 137 ++++++++++++++
 .../freemarker/core/ListValidationsTest.java    | 125 -------------
 .../test/templatesuite/TemplateTestCase.java    |   5 +-
 .../test/templatesuite/models/Listables.java    | 104 +++++++++++
 .../test/templatesuite/expected/listhash.txt    | 183 +++++++++++++++++++
 .../test/templatesuite/templates/listhash.ftl   |  52 ++++++
 .../freemarker/test/templatesuite/testcases.xml |   1 +
 7 files changed, 480 insertions(+), 127 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/d8d070a1/src/test/java/freemarker/core/ListErrorsTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/freemarker/core/ListErrorsTest.java b/src/test/java/freemarker/core/ListErrorsTest.java
new file mode 100644
index 0000000..0c322c6
--- /dev/null
+++ b/src/test/java/freemarker/core/ListErrorsTest.java
@@ -0,0 +1,137 @@
+/*
+ * 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 freemarker.core;
+
+import java.io.IOException;
+
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableMap;
+
+import freemarker.template.DefaultObjectWrapper;
+import freemarker.template.TemplateException;
+import freemarker.test.TemplateTest;
+import freemarker.test.templatesuite.models.Listables;
+
+public class ListErrorsTest extends TemplateTest {
+    
+    @Test
+    public void testValid() throws IOException, TemplateException {
+        assertOutput("<#list 1..2 as x><#list 3..4>${x}:<#items as x>${x}</#items></#list>;</#list>", "1:34;2:34;");
+        assertOutput("<#list [] as x>${x}<#else><#list 1..2 as x>${x}<#sep>, </#list></#list>", "1, 2");
+        assertOutput("<#macro m>[<#nested 3>]</#macro>"
+                + "<#list 1..2 as x>"
+                + "${x}@${x?index}"
+                + "<@m ; x>"
+                + "${x},"
+                + "<#list 4..4 as x>${x}@${x?index}</#list>"
+                + "</@>"
+                + "${x}@${x?index}; "
+                + "</#list>",
+                "1@0[3,4@0]1@0; 2@1[3,4@0]2@1; ");
+    }
+
+    @Test
+    public void testInvalidItemsParseTime() throws IOException, TemplateException {
+        assertErrorContains("<#items as x>${x}</#items>",
+                "#items", "must be inside", "#list");
+        assertErrorContains("<#list xs><#macro m><#items as x></#items></#macro></#list>",
+                "#items", "must be inside", "#list");
+        assertErrorContains("<#list xs><#forEach x in xs><#items as x></#items></#forEach></#list>",
+                "#forEach", "doesn't support", "#items");
+        assertErrorContains("<#list xs as x><#items as x>${x}</#items></#list>",
+                "#list", "must not have", "#items", "as loopVar");
+        assertErrorContains("<#list xs><#list xs as x><#items as x>${x}</#items></#list></#list>",
+                "#list", "must not have", "#items", "as loopVar");
+        assertErrorContains("<#list xs></#list>",
+                "#list", "must have", "#items", "as loopVar");
+        assertErrorContains("<#forEach x in xs><#items as x></#items></#forEach>",
+                "#forEach", "doesn't support", "#items");
+        assertErrorContains("<#list xs><#forEach x in xs><#items as x></#items></#forEach></#list>",
+                "#forEach", "doesn't support", "#items");
+    }
+
+    @Test
+    public void testInvalidSepParseTime() throws IOException, TemplateException {
+        assertErrorContains("<#sep>, </#sep>",
+                "#sep", "must be inside", "#list", "#foreach");
+        assertErrorContains("<#sep>, ",
+                "#sep", "must be inside", "#list", "#foreach");
+        assertErrorContains("<#list xs as x><#else><#sep>, </#list>",
+                "#sep", "must be inside", "#list", "#foreach");
+        assertErrorContains("<#list xs as x><#macro m><#sep>, </#macro></#list>",
+                "#sep", "must be inside", "#list", "#foreach");
+    }
+
+    @Test
+    public void testInvalidItemsRuntime() throws IOException, TemplateException {
+        assertErrorContains("<#list 1..1><#items as x></#items><#items as x></#items></#list>",
+                "#items", "already entered earlier");
+        assertErrorContains("<#list 1..1><#items as x><#items as y>${x}/${y}</#items></#items></#list>",
+                "#items", "Can't nest #items into each other");
+    }
+    
+    @Test
+    public void testInvalidLoopVarBuiltinLHO() {
+        assertErrorContains("<#list foos>${foo?index}</#list>",
+                "?index", "foo", "no loop variable");
+        assertErrorContains("<#list foos as foo></#list>${foo?index}",
+                "?index", "foo" , "no loop variable");
+        assertErrorContains("<#list foos as foo><#macro m>${foo?index}</#macro></#list>",
+                "?index", "foo" , "no loop variable");
+        assertErrorContains("<#list foos as foo><#function f>${foo?index}</#function></#list>",
+                "?index", "foo" , "no loop variable");
+        assertErrorContains("<#list xs as x>${foo?index}</#list>",
+                "?index", "foo" , "no loop variable");
+        assertErrorContains("<#list foos as foo><@m; foo>${foo?index}</@></#list>",
+                "?index", "foo" , "user defined directive");
+        assertErrorContains(
+                "<#list foos as foo><@m; foo><@m; foo>${foo?index}</@></@></#list>",
+                "?index", "foo" , "user defined directive");
+        assertErrorContains(
+                "<#list foos as foo><@m; foo>"
+                + "<#list foos as foo><@m; foo>${foo?index}</@></#list>"
+                + "</@></#list>",
+                "?index", "foo" , "user defined directive");
+    }
+
+    @Test
+    public void testKeyValueSameName() {
+        assertErrorContains("<#list {} as foo, foo></#list>",
+                "key", "value", "both" , "foo");
+    }
+
+    @Test
+    public void testCollectionVersusHash() {
+        assertErrorContains("<#list {} as i></#list>",
+                "as k, v");
+        assertErrorContains("<#list [] as k, v></#list>",
+                "only one loop variable");
+    }
+
+    @Test
+    public void testNonEx2NonStringKey() throws IOException, TemplateException {
+        addToDataModel("m", new Listables.NonEx2MapAdapter(ImmutableMap.of("k1", "v1", 2, "v2"), new DefaultObjectWrapper()));
+        assertOutput("<#list m?keys as k>${k};</#list>", "k1;2;");
+        assertErrorContains("<#list m as k, v></#list>",
+                "string", "number", ".TemplateHashModelEx2");
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/d8d070a1/src/test/java/freemarker/core/ListValidationsTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/freemarker/core/ListValidationsTest.java b/src/test/java/freemarker/core/ListValidationsTest.java
deleted file mode 100644
index eed74ce..0000000
--- a/src/test/java/freemarker/core/ListValidationsTest.java
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- * 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 freemarker.core;
-
-import java.io.IOException;
-
-import org.junit.Test;
-
-import freemarker.template.TemplateException;
-import freemarker.test.TemplateTest;
-
-public class ListValidationsTest extends TemplateTest {
-    
-    @Test
-    public void testValid() throws IOException, TemplateException {
-        assertOutput("<#list 1..2 as x><#list 3..4>${x}:<#items as x>${x}</#items></#list>;</#list>", "1:34;2:34;");
-        assertOutput("<#list [] as x>${x}<#else><#list 1..2 as x>${x}<#sep>, </#list></#list>", "1, 2");
-        assertOutput("<#macro m>[<#nested 3>]</#macro>"
-                + "<#list 1..2 as x>"
-                + "${x}@${x?index}"
-                + "<@m ; x>"
-                + "${x},"
-                + "<#list 4..4 as x>${x}@${x?index}</#list>"
-                + "</@>"
-                + "${x}@${x?index}; "
-                + "</#list>",
-                "1@0[3,4@0]1@0; 2@1[3,4@0]2@1; ");
-    }
-
-    @Test
-    public void testInvalidItemsParseTime() throws IOException, TemplateException {
-        assertErrorContains("<#items as x>${x}</#items>",
-                "#items", "must be inside", "#list");
-        assertErrorContains("<#list xs><#macro m><#items as x></#items></#macro></#list>",
-                "#items", "must be inside", "#list");
-        assertErrorContains("<#list xs><#forEach x in xs><#items as x></#items></#forEach></#list>",
-                "#forEach", "doesn't support", "#items");
-        assertErrorContains("<#list xs as x><#items as x>${x}</#items></#list>",
-                "#list", "must not have", "#items", "as loopVar");
-        assertErrorContains("<#list xs><#list xs as x><#items as x>${x}</#items></#list></#list>",
-                "#list", "must not have", "#items", "as loopVar");
-        assertErrorContains("<#list xs></#list>",
-                "#list", "must have", "#items", "as loopVar");
-        assertErrorContains("<#forEach x in xs><#items as x></#items></#forEach>",
-                "#forEach", "doesn't support", "#items");
-        assertErrorContains("<#list xs><#forEach x in xs><#items as x></#items></#forEach></#list>",
-                "#forEach", "doesn't support", "#items");
-    }
-
-    @Test
-    public void testInvalidSepParseTime() throws IOException, TemplateException {
-        assertErrorContains("<#sep>, </#sep>",
-                "#sep", "must be inside", "#list", "#foreach");
-        assertErrorContains("<#sep>, ",
-                "#sep", "must be inside", "#list", "#foreach");
-        assertErrorContains("<#list xs as x><#else><#sep>, </#list>",
-                "#sep", "must be inside", "#list", "#foreach");
-        assertErrorContains("<#list xs as x><#macro m><#sep>, </#macro></#list>",
-                "#sep", "must be inside", "#list", "#foreach");
-    }
-
-    @Test
-    public void testInvalidItemsRuntime() throws IOException, TemplateException {
-        assertErrorContains("<#list 1..1><#items as x></#items><#items as x></#items></#list>",
-                "#items", "already entered earlier");
-        assertErrorContains("<#list 1..1><#items as x><#items as y>${x}/${y}</#items></#items></#list>",
-                "#items", "Can't nest #items into each other");
-    }
-    
-    @Test
-    public void testInvalidLoopVarBuiltinLHO() {
-        assertErrorContains("<#list foos>${foo?index}</#list>",
-                "?index", "foo", "no loop variable");
-        assertErrorContains("<#list foos as foo></#list>${foo?index}",
-                "?index", "foo" , "no loop variable");
-        assertErrorContains("<#list foos as foo><#macro m>${foo?index}</#macro></#list>",
-                "?index", "foo" , "no loop variable");
-        assertErrorContains("<#list foos as foo><#function f>${foo?index}</#function></#list>",
-                "?index", "foo" , "no loop variable");
-        assertErrorContains("<#list xs as x>${foo?index}</#list>",
-                "?index", "foo" , "no loop variable");
-        assertErrorContains("<#list foos as foo><@m; foo>${foo?index}</@></#list>",
-                "?index", "foo" , "user defined directive");
-        assertErrorContains(
-                "<#list foos as foo><@m; foo><@m; foo>${foo?index}</@></@></#list>",
-                "?index", "foo" , "user defined directive");
-        assertErrorContains(
-                "<#list foos as foo><@m; foo>"
-                + "<#list foos as foo><@m; foo>${foo?index}</@></#list>"
-                + "</@></#list>",
-                "?index", "foo" , "user defined directive");
-    }
-
-    @Test
-    public void testKeyValueSameName() {
-        assertErrorContains("<#list {} as foo, foo></#list>",
-                "key", "value", "both" , "foo");
-    }
-
-    @Test
-    public void testCollectionVersusHash() {
-        assertErrorContains("<#list {} as i></#list>",
-                "as k, v");
-        assertErrorContains("<#list [] as k, v></#list>",
-                "only one loop variable");
-    }
-    
-}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/d8d070a1/src/test/java/freemarker/test/templatesuite/TemplateTestCase.java
----------------------------------------------------------------------
diff --git a/src/test/java/freemarker/test/templatesuite/TemplateTestCase.java b/src/test/java/freemarker/test/templatesuite/TemplateTestCase.java
index 9ccf93f..9e9b4ff 100644
--- a/src/test/java/freemarker/test/templatesuite/TemplateTestCase.java
+++ b/src/test/java/freemarker/test/templatesuite/TemplateTestCase.java
@@ -277,8 +277,9 @@ public class TemplateTestCase extends FileTestCase {
             });
             dataModel.put("sqlDate", new java.sql.Date(1273955885023L));
             dataModel.put("sqlTime", new java.sql.Time(74285023L));
-        } else if (templateName.equals("list.ftl")
-                || templateName.equals("list2.ftl") || templateName.equals("list3.ftl")) {
+        } else if (
+                templateName.equals("list.ftl") || templateName.equals("list2.ftl") || templateName.equals("list3.ftl")
+                || simpleTestName.equals("listhash")) {
             dataModel.put("listables", new Listables());
         } else if (simpleTestName.startsWith("number-format")) {
             dataModel.put("int", new SimpleNumber(Integer.valueOf(1)));

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/d8d070a1/src/test/java/freemarker/test/templatesuite/models/Listables.java
----------------------------------------------------------------------
diff --git a/src/test/java/freemarker/test/templatesuite/models/Listables.java b/src/test/java/freemarker/test/templatesuite/models/Listables.java
index 42d43dc..d607a7e 100644
--- a/src/test/java/freemarker/test/templatesuite/models/Listables.java
+++ b/src/test/java/freemarker/test/templatesuite/models/Listables.java
@@ -18,14 +18,44 @@
  */
 package freemarker.test.templatesuite.models;
 
+import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.Iterator;
+import java.util.LinkedHashMap;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
+import java.util.SortedMap;
 import java.util.TreeSet;
 
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import freemarker.core._DelayedJQuote;
+import freemarker.core._TemplateModelException;
+import freemarker.ext.beans.BeansWrapperBuilder;
+import freemarker.ext.util.WrapperTemplateModel;
+import freemarker.template.AdapterTemplateModel;
+import freemarker.template.Configuration;
+import freemarker.template.DefaultMapAdapter;
+import freemarker.template.DefaultObjectWrapperBuilder;
+import freemarker.template.MapKeyValuePairIterator;
+import freemarker.template.ObjectWrapper;
+import freemarker.template.SimpleCollection;
+import freemarker.template.SimpleHash;
+import freemarker.template.TemplateCollectionModel;
+import freemarker.template.TemplateHashModelEx;
+import freemarker.template.TemplateHashModelEx2;
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+import freemarker.template.TemplateModelWithAPISupport;
+import freemarker.template.WrappingTemplateModel;
+import freemarker.template.TemplateHashModelEx2.KeyValuePairIterator;
+import freemarker.template.utility.ObjectWrapperWithAPISupport;
+
 @SuppressWarnings("boxing")
 public class Listables {
     
@@ -94,4 +124,78 @@ public class Listables {
         return Collections.<Integer>emptySet().iterator();
     }
     
+    public List<TemplateHashModelEx2> getHashEx2s() throws TemplateModelException {
+        Map<Object, Object> map;
+        map = new LinkedHashMap<Object, Object>();
+        map.put("k1", "v1");
+        map.put(2, "v2");
+        map.put("k3", "v3");
+        map.put(null, "v4");
+        map.put(true, "v5");
+        map.put(false, null);
+        
+        return getMapsWrappedAsEx2(map);
+    }
+
+    public List<? extends TemplateHashModelEx> getEmptyHashes() throws TemplateModelException {
+        List<TemplateHashModelEx> emptyMaps = new ArrayList<TemplateHashModelEx>();
+        emptyMaps.addAll(getMapsWrappedAsEx2(Collections.emptyMap()));
+        emptyMaps.add((TemplateHashModelEx) new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_24).build()
+                .wrap(Collections.emptyMap()));
+        return emptyMaps;
+    }
+    
+    /**
+     * Returns the map wrapped on various ways.
+     */
+    private List<TemplateHashModelEx2> getMapsWrappedAsEx2(Map<?, ?> map) throws TemplateModelException {
+        List<TemplateHashModelEx2> maps = new ArrayList<TemplateHashModelEx2>();
+        
+        maps.add((SimpleHash) new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_0).build().wrap(map));
+        
+        maps.add((DefaultMapAdapter) new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_24).build().wrap(map));
+        
+        BeansWrapperBuilder bwb = new BeansWrapperBuilder(Configuration.VERSION_2_3_24);
+        bwb.setSimpleMapWrapper(true);
+        maps.add((TemplateHashModelEx2) bwb.build().wrap(map));
+
+        return maps;
+    }
+    
+    public TemplateHashModelEx getHashNonEx2() {
+        return new NonEx2MapAdapter(ImmutableMap.of("k1", 11, "k2", 22),
+                new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_24).build());
+    }
+    
+    public static class NonEx2MapAdapter extends WrappingTemplateModel implements TemplateHashModelEx {
+
+        private final Map<?, ?> map;
+        
+        public NonEx2MapAdapter(Map<?, ?> map, ObjectWrapper wrapper) {
+            super(wrapper);
+            this.map = map;
+        }
+        
+        public TemplateModel get(String key) throws TemplateModelException {
+            return wrap(map.get(key));
+        }
+        
+        public boolean isEmpty() {
+            return map.isEmpty();
+        }
+        
+        public int size() {
+            return map.size();
+        }
+        
+        public TemplateCollectionModel keys() {
+            return new SimpleCollection(map.keySet(), getObjectWrapper());
+        }
+        
+        public TemplateCollectionModel values() {
+            return new SimpleCollection(map.values(), getObjectWrapper());
+        }
+        
+    }
+    
 }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/d8d070a1/src/test/resources/freemarker/test/templatesuite/expected/listhash.txt
----------------------------------------------------------------------
diff --git a/src/test/resources/freemarker/test/templatesuite/expected/listhash.txt b/src/test/resources/freemarker/test/templatesuite/expected/listhash.txt
new file mode 100644
index 0000000..c603890
--- /dev/null
+++ b/src/test/resources/freemarker/test/templatesuite/expected/listhash.txt
@@ -0,0 +1,183 @@
+
+Non-empty maps:
+
+    Map:
+    
+    [
+      k1 = v1
+      2 = v2
+      k3 = v3
+      null = v4
+      Y = v5
+      N = null
+    ]
+  
+    [
+      k1 = v1; // @0=@0; odd=odd; Y=Y
+      2 = v2; // @1=@1; even=even; Y=Y
+      k3 = v3; // @2=@2; odd=odd; Y=Y
+      null = v4; // @3=@3; even=even; Y=Y
+      Y = v5; // @4=@4; odd=odd; Y=Y
+      N = null // @5=@5; even=even; N=N
+    ]
+  
+    {
+      [
+        k1 = v1; // @0=@0; odd=odd; Y=Y
+        2 = v2; // @1=@1; even=even; Y=Y
+        k3 = v3; // @2=@2; odd=odd; Y=Y
+        null = v4; // @3=@3; even=even; Y=Y
+        Y = v5; // @4=@4; odd=odd; Y=Y
+        N = null // @5=@5; even=even; N=N
+      ]
+    }
+
+    Map:
+    
+    [
+      k1 = v1
+      2 = v2
+      k3 = v3
+      null = v4
+      Y = v5
+      N = null
+    ]
+  
+    [
+      k1 = v1; // @0=@0; odd=odd; Y=Y
+      2 = v2; // @1=@1; even=even; Y=Y
+      k3 = v3; // @2=@2; odd=odd; Y=Y
+      null = v4; // @3=@3; even=even; Y=Y
+      Y = v5; // @4=@4; odd=odd; Y=Y
+      N = null // @5=@5; even=even; N=N
+    ]
+  
+    {
+      [
+        k1 = v1; // @0=@0; odd=odd; Y=Y
+        2 = v2; // @1=@1; even=even; Y=Y
+        k3 = v3; // @2=@2; odd=odd; Y=Y
+        null = v4; // @3=@3; even=even; Y=Y
+        Y = v5; // @4=@4; odd=odd; Y=Y
+        N = null // @5=@5; even=even; N=N
+      ]
+    }
+
+    Map:
+    
+    [
+      k1 = v1
+      2 = v2
+      k3 = v3
+      null = v4
+      Y = v5
+      N = null
+    ]
+  
+    [
+      k1 = v1; // @0=@0; odd=odd; Y=Y
+      2 = v2; // @1=@1; even=even; Y=Y
+      k3 = v3; // @2=@2; odd=odd; Y=Y
+      null = v4; // @3=@3; even=even; Y=Y
+      Y = v5; // @4=@4; odd=odd; Y=Y
+      N = null // @5=@5; even=even; N=N
+    ]
+  
+    {
+      [
+        k1 = v1; // @0=@0; odd=odd; Y=Y
+        2 = v2; // @1=@1; even=even; Y=Y
+        k3 = v3; // @2=@2; odd=odd; Y=Y
+        null = v4; // @3=@3; even=even; Y=Y
+        Y = v5; // @4=@4; odd=odd; Y=Y
+        N = null // @5=@5; even=even; N=N
+      ]
+    }
+
+    Map:
+    
+    [
+      k1 = 11
+      k2 = 22
+    ]
+  
+    [
+      k1 = 11; // @0=@0; odd=odd; Y=Y
+      k2 = 22 // @1=@1; even=even; N=N
+    ]
+  
+    {
+      [
+        k1 = 11; // @0=@0; odd=odd; Y=Y
+        k2 = 22 // @1=@1; even=even; N=N
+      ]
+    }
+
+
+Empty maps:
+
+    Map:
+    
+    [
+    ]
+  
+    [
+      Empty
+    ]
+  
+    {
+      Empty
+    }
+
+    Map:
+    
+    [
+    ]
+  
+    [
+      Empty
+    ]
+  
+    {
+      Empty
+    }
+
+    Map:
+    
+    [
+    ]
+  
+    [
+      Empty
+    ]
+  
+    {
+      Empty
+    }
+
+    Map:
+    
+    [
+    ]
+  
+    [
+      Empty
+    ]
+  
+    {
+      Empty
+    }
+
+
+  a @ 0, 1
+    aa = 11  @ 0 // inside a @ 0, 1
+  a @ 0, 1
+  --
+  b @ 1, 2
+    ba = 21  @ 0 // inside b @ 1, 2
+    bb = 22  @ 1 // inside b @ 1, 2
+  b @ 1, 2
+  --
+  c @ 2, 0
+  c @ 2, 0
+  --

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/d8d070a1/src/test/resources/freemarker/test/templatesuite/templates/listhash.ftl
----------------------------------------------------------------------
diff --git a/src/test/resources/freemarker/test/templatesuite/templates/listhash.ftl b/src/test/resources/freemarker/test/templatesuite/templates/listhash.ftl
new file mode 100644
index 0000000..3668f89
--- /dev/null
+++ b/src/test/resources/freemarker/test/templatesuite/templates/listhash.ftl
@@ -0,0 +1,52 @@
+<#setting booleanFormat='Y,N'>
+
+<#macro listings maps>
+  <#list maps as m>
+    Map:
+    
+    [
+    <#list m as k, v>
+      ${k!'null'} = ${v!'null'}
+    </#list>
+    ]
+  
+    [
+    <#list m as k, v>
+      ${k!'null'} = ${v!'null'}<#sep>;</#sep> // @${k?index}=@${v?index}; ${k?itemParity}=${v?itemParity}; ${k?hasNext}=${v?hasNext}
+    <#else>
+      Empty
+    </#list>
+    ]
+  
+    {
+    <#list m>
+      [
+      <#items as k, v>
+        ${k!'null'} = ${v!'null'}<#sep>;</#sep> // @${k?index}=@${v?index}; ${k?itemParity}=${v?itemParity}; ${k?hasNext}=${v?hasNext}
+      </#items>
+      ]
+    <#else>
+      Empty
+    </#list>
+    }
+
+  </#list>
+</#macro>
+
+Non-empty maps:
+
+<@listings listables.hashEx2s />
+<@listings [ listables.hashNonEx2 ] />
+
+Empty maps:
+
+<@listings listables.emptyHashes />
+
+<#list { 'a': { 'aa': 11 }, 'b': { 'ba': 21, 'bb': 22 }, 'c': {} } as k1, v1>
+  ${k1} @ ${k1?index}, ${v1?size}
+  <#list v1 as k2, v2>
+    ${k2} = ${v2}  @ ${k2?index} // inside ${k1} @ ${k1?index}, ${v1?size}
+  </#list>
+  ${k1} @ ${k1?index}, ${v1?size}
+  --
+</#list>

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/d8d070a1/src/test/resources/freemarker/test/templatesuite/testcases.xml
----------------------------------------------------------------------
diff --git a/src/test/resources/freemarker/test/templatesuite/testcases.xml b/src/test/resources/freemarker/test/templatesuite/testcases.xml
index cc42796..8df64bd 100644
--- a/src/test/resources/freemarker/test/templatesuite/testcases.xml
+++ b/src/test/resources/freemarker/test/templatesuite/testcases.xml
@@ -133,6 +133,7 @@
    <testCase name="list-bis[#endTN]-collectionAdapter" expected="list-bis.txt">
       <setting object_wrapper="DefaultObjectWrapper(2.3.22, forceLegacyNonListCollections=false)" />
    </testCase>
+   <testCase name="listhash" />
    <testCase name="listliteral" />
    <testCase name="localization" >
       <setting locale="en_AU"/>