Updated unit test for StaticLayout line breaking

Now correctly handles space consumption at the end of lines.

Also added tests for the maximum number of lines constraint.

Change-Id: I9b354bf3cc8789b9ccc489099e19e6e4019e4744
diff --git a/tests/tests/text/src/android/text/cts/StaticLayoutLineBreakingTest.java b/tests/tests/text/src/android/text/cts/StaticLayoutLineBreakingTest.java
index aa8874c..7e06697 100644
--- a/tests/tests/text/src/android/text/cts/StaticLayoutLineBreakingTest.java
+++ b/tests/tests/text/src/android/text/cts/StaticLayoutLineBreakingTest.java
@@ -20,14 +20,17 @@
 import android.text.SpannableStringBuilder;
 import android.text.Spanned;
 import android.text.StaticLayout;
+import android.text.TextDirectionHeuristics;
 import android.text.TextPaint;
 import android.text.Layout.Alignment;
 import android.text.style.MetricAffectingSpan;
+import android.util.Log;
 
 public class StaticLayoutLineBreakingTest extends AndroidTestCase {
     // Span test are currently not supported because text measurement uses the MeasuredText
     // internal mWorkPaint instead of the provided MockTestPaint.
     private static final boolean SPAN_TESTS_SUPPORTED = false;
+    private static final boolean DEBUG = false;
 
     private static final float SPACE_MULTI = 1.0f;
     private static final float SPACE_ADD = 0.0f;
@@ -89,7 +92,7 @@
             case 'L': return 50.0f;
             case 'C': return 100.0f; // equals to WIDTH
             case ' ': return 10.0f;
-            case '.': return 0.0f; // 0-width character
+            case '_': return 0.0f; // 0-width character
             case SURR_FIRST: return 7.0f;
             case SURR_SECOND: return 3.0f; // Sum of SURR_FIRST-SURR_SECOND is 10
             default: return 10.0f;
@@ -100,11 +103,13 @@
         return new StaticLayout(source, mTextPaint, width, ALIGN, SPACE_MULTI, SPACE_ADD, false);
     }
 
-    private static StaticLayout getStaticLayout(CharSequence source) {
-        return getStaticLayout(source, WIDTH);
+    private static int[] getBreaks(CharSequence source) {
+        return getBreaks(source, WIDTH);
     }
 
-    private static int[] getBreaks(StaticLayout staticLayout) {
+    private static int[] getBreaks(CharSequence source, int width) {
+        StaticLayout staticLayout = getStaticLayout(source, width);
+
         int[] breaks = new int[staticLayout.getLineCount() - 1];
         for (int line = 0; line < breaks.length; line++) {
             breaks[line] = staticLayout.getLineEnd(line);
@@ -112,14 +117,28 @@
         return breaks;
     }
 
-    private static void layout(CharSequence source, int[] breaks) {
-        StaticLayout staticLayout = getStaticLayout(source);
-        layout(staticLayout, source, breaks);
+    private static void debugLayout(CharSequence source, StaticLayout staticLayout) {
+        if (DEBUG) {
+            int count = staticLayout.getLineCount();
+            Log.i("StaticLayoutLineBreakingTest", "\"" + source.toString() + "\": " +
+                    count + " lines");
+            for (int line = 0; line < count; line++) {
+                int lineStart = staticLayout.getLineStart(line);
+                int lineEnd = staticLayout.getLineEnd(line);
+                Log.i("StaticLayoutLineBreakingTest", "Line " + line + " [" + lineStart + ".." +
+                        lineEnd + "]\t" + source.subSequence(lineStart, lineEnd));
+            }
+        }
     }
 
-    private static void layout(StaticLayout staticLayout, CharSequence source, int[] breaks) {
-        //Log.i("StaticLayoutLineWrappingTest", "String " + source.toString() + "; " +
-        //        staticLayout.getLineCount() + " lines");
+    private static void layout(CharSequence source, int[] breaks) {
+        layout(source, breaks, WIDTH);
+    }
+
+    private static void layout(CharSequence source, int[] breaks, int width) {
+        StaticLayout staticLayout = getStaticLayout(source, width);
+
+        debugLayout(source, staticLayout);
 
         int lineCount = breaks.length + 1;
         assertEquals("Number of lines", lineCount, staticLayout.getLineCount());
@@ -128,9 +147,6 @@
             int lineStart = staticLayout.getLineStart(line);
             int lineEnd = staticLayout.getLineEnd(line);
 
-            //Log.i("StaticLayoutLineWrappingTest", "Line " + line + " [" + lineStart + ".." +
-            //        lineEnd + "]\t" + source.subSequence(lineStart, lineEnd));
-
             if (line == 0) {
                 assertEquals("Line start for first line", 0, lineStart);
             } else {
@@ -145,6 +161,34 @@
         }
     }
 
+    private static void layoutMaxLines(CharSequence source, int[] breaks, int maxLines) {
+        StaticLayout staticLayout = new StaticLayout(source, 0, source.length(), mTextPaint, WIDTH,
+                ALIGN, TextDirectionHeuristics.LTR, SPACE_MULTI, SPACE_ADD, false /* includePad */,
+                null, WIDTH, maxLines);
+
+        debugLayout(source, staticLayout);
+
+        int lineCount = staticLayout.getLineCount();
+        assertTrue("Number of lines", lineCount <= maxLines);
+
+        for (int line = 0; line < lineCount; line++) {
+            int lineStart = staticLayout.getLineStart(line);
+            int lineEnd = staticLayout.getLineEnd(line);
+
+            if (line == 0) {
+                assertEquals("Line start for first line", 0, lineStart);
+            } else {
+                assertEquals("Line start for line " + line, breaks[line - 1], lineStart);
+            }
+
+            if (line == lineCount - 1 && line != breaks.length - 1) {
+                assertEquals("Line end for last line", source.length(), lineEnd);
+            } else {
+                assertEquals("Line end for line " + line, breaks[line], lineEnd);
+            }
+        }
+    }
+
     final static int MAX_SPAN_COUNT = 10;
     final static int[] spanStarts = new int[MAX_SPAN_COUNT];
     final static int[] spanEnds = new int[MAX_SPAN_COUNT];
@@ -218,7 +262,7 @@
         layout("  XX  XXX  ", NO_BREAK);
         layout("XX XXX XXX ", NO_BREAK);
         layout("XX XXX XXX     ", NO_BREAK);
-        layout("XXXXXXXXXX     ", new int[] {10}); // Bug, should be NO_BREAK as above
+        layout("XXXXXXXXXX     ", NO_BREAK);
         //      01234567890
     }
 
@@ -235,18 +279,18 @@
         layout("XXXXXXX XXX", new int[] {8});
         layout("XXXXXX XXXX", new int[] {7});
         //      01234567890
-        layout("LL LL", new int[] {2, 3}); // Bug: should be {3}
+        layout("LL LL", new int[] {3});
         layout("LLLL", new int[] {2});
-        layout("C C", new int[] {1, 2}); // Bug: should be {2}
+        layout("C C", new int[] {2});
         layout("CC", new int[] {1});
     }
 
     public void testSpaceAtBreak() {
         //      0123456789012
         layout("XXXX XXXXX X", new int[] {11});
-        layout("XXXXXXXXXX X", new int[] {10}); // Bug: should be {11}. Consume spaces in the non ok case too
-        layout("XXXXXXXXXV X", new int[] {10}); // Bug: should be {11}
-        layout("C X", new int[] {1}); // Should be 2
+        layout("XXXXXXXXXX X", new int[] {11});
+        layout("XXXXXXXXXV X", new int[] {11});
+        layout("C X", new int[] {2});
     }
 
     public void testMultipleSpacesAtBreak() {
@@ -260,10 +304,10 @@
 
     public void testZeroWidthCharacters() {
         //      0123456789012345678901234
-        layout("X.X.X.X.X.X.X.X.X.X", NO_BREAK);
-        layout("...X.X.X.X.X.X.X.X.X.X...", NO_BREAK);
-        layout("C.X", new int[] {2});
-        layout("C..X", new int[] {3});
+        layout("X_X_X_X_X_X_X_X_X_X", NO_BREAK);
+        layout("___X_X_X_X_X_X_X_X_X_X___", NO_BREAK);
+        layout("C_X", new int[] {2});
+        layout("C__X", new int[] {3});
     }
 
     /**
@@ -289,11 +333,6 @@
         layout(spanify("012 456 89 <LXX> XX XX"), new int[] {11, 18});
     }
 
-    public void testWithOverlappingSpans() {
-        // TODO Also try overlapping spans. The current implementation does not care, but would be
-        // good to have before any serious refactoring.
-    }
-
     /*
      * Adding a span to the string should not change the layout, since the metrics are unchanged.
      */
@@ -307,8 +346,7 @@
 
         for (String text : texts) {
             // Get the line breaks without any span
-            StaticLayout sl = getStaticLayout(text);
-            int[] breaks = getBreaks(sl);
+            int[] breaks = getBreaks(text);
 
             // Add spans on all possible offsets
             for (int spanStart = 0; spanStart < text.length(); spanStart++) {
@@ -333,8 +371,7 @@
 
         for (String text : texts) {
             // Get the line breaks without any span
-            StaticLayout sl = getStaticLayout(text);
-            int[] breaks = getBreaks(sl);
+            int[] breaks = getBreaks(text);
 
             // Add spans on all possible offsets
             for (int spanStart1 = 0; spanStart1 < text.length(); spanStart1++) {
@@ -355,8 +392,82 @@
         }
     }
 
-    public void testBreakCondition() {
-        // Try all the different line break characters, space, tab, ','...
+    public static String replace(String string, char c, char r) {
+        return string.replaceAll(String.valueOf(c), String.valueOf(r));
+    }
+
+    public void testClassIS() {
+        char[] classISCharacters = new char[] {'.', ',', ':', ';'};
+        char[] digitCharacters = new char[] {'0', '\u0660', '\u06F0', '\u0966', '\uFF10'};
+
+        for (char c : classISCharacters) {
+            // .,:; are class IS breakpoints...
+            //              01234567
+            layout(replace("L XXX#X", '#', c), new int[] {6});
+            layout(replace("L XXXX#X", '#', c), new int[] {2});
+
+            // ...except when adjacent to digits
+            for (char d : digitCharacters) {
+                //                      01234567
+                layout(replace(replace("L XX0#X", '#', c), '0', d), new int[] {2});
+                layout(replace(replace("L XXX#0", '#', c), '0', d), new int[] {2});
+                layout(replace(replace("L XXX0#X", '#', c), '0', d), new int[] {2});
+                layout(replace(replace("L XXXX#0", '#', c), '0', d), new int[] {2});
+            }
+        }
+    }
+
+    public void testClassSYandHY() {
+        char[] classSYorHYCharacters = new char[] {'/', '-'};
+        char[] digitCharacters = new char[] {'0', '\u0660', '\u06F0', '\u0966', '\uFF10'};
+
+        for (char c : classSYorHYCharacters) {
+            // / is a class SY breakpoint, - a class HY...
+            //              01234567
+            layout(replace("L XXX#X", '#', c), new int[] {6});
+            layout(replace("L XXXX#X", '#', c), new int[] {2});
+
+            // ...except when followed by a digits
+            for (char d : digitCharacters) {
+                //                      01234567
+                layout(replace(replace("L XX0#X", '#', c), '0', d), new int[] {6});
+                layout(replace(replace("L XXX#0", '#', c), '0', d), new int[] {2});
+                layout(replace(replace("L XXX0#X", '#', c), '0', d), new int[] {2});
+                layout(replace(replace("L XXXX#0", '#', c), '0', d), new int[] {2});
+            }
+        }
+    }
+
+    public void testClassID() {
+        char ideographic = '\u8a9e'; // regular ideographic character
+        char hyphen = '\u30A0'; // KATAKANA-HIRAGANA DOUBLE HYPHEN, ideographic but non starter
+
+        // Single ideographs are normal characters
+        layout("L XXX" + ideographic, NO_BREAK);
+        layout("L XXX" + ideographic + "X", new int[] {2});
+        layout("L XXXX" + ideographic, new int[] {2});
+        layout("L XXXX" + ideographic + "X", new int[] {2});
+
+        // Two adjacent ideographs create a possible breakpoint
+        layout("L X" + ideographic + ideographic + "X", NO_BREAK);
+        layout("L X" + ideographic + ideographic + "XX", new int[] {4});
+        layout("L XX" + ideographic + ideographic + "XX", new int[] {5});
+        layout("L XXX" + ideographic + ideographic + "X", new int[] {6});
+        layout("L XXXX" + ideographic + ideographic + "X", new int[] {2});
+
+        // Except when the second one is a non starter
+        layout("L X" + ideographic + hyphen + "X", NO_BREAK);
+        layout("L X" + ideographic + hyphen + "XX", new int[] {2});
+        layout("L XX" + ideographic + hyphen + "XX", new int[] {2});
+        layout("L XXX" + ideographic + hyphen + "X", new int[] {2});
+        layout("L XXXX" + ideographic + hyphen + "X", new int[] {2});
+
+        // When the non-starter is first, a pair of ideographic characters is a line break
+        layout("L X" + hyphen + ideographic + "X", NO_BREAK);
+        layout("L X" + hyphen + ideographic + "XX", new int[] {4});
+        layout("L XX" + hyphen + ideographic + "XX", new int[] {5});
+        layout("L XXX" + hyphen + ideographic + "X", new int[] {6});
+        layout("L XXXX" + hyphen + ideographic + "X", new int[] {2});
     }
 
     public void testReplacementSpan() {
@@ -387,19 +498,58 @@
 
     public void testNarrowWidth() {
         int[] widths = new int[] { 0, 4, 10 };
-        String[] texts = new String[] { "", "X", " ", "XX", "XX ", "X ", "XXX", "XXX ", "X X",
-            " X X " /* Bug "X  X", ".X..", "  ", "XX  ", "X." should work too */ };
+        String[] texts = new String[] { "", "X", " ", "XX", " X", "XXX" };
 
         for (String text: texts) {
             // 15 is such that only one character will fit
-            StaticLayout reference = getStaticLayout(text, 15);
-            int[] breaks = getBreaks(reference);
+            int[] breaks = getBreaks(text, 15);
 
             // Width under 15 should all lead to the same line break
             for (int width: widths) {
-                StaticLayout sl = getStaticLayout(text, width);
-                layout(sl, text, breaks);
+                layout(text, breaks, width);
             }
         }
     }
+
+    public void testNarrowWidthWithSpace() {
+        int[] widths = new int[] { 0, 4 };
+        for (int width: widths) {
+            layout("X ", new int[] {1}, width);
+            layout("X  ", new int[] {1}, width);
+            layout("XX ", new int[] {1, 2}, width);
+            layout("XX  ", new int[] {1, 2}, width);
+            layout("X  X", new int[] {1, 3}, width);
+            layout("X X", new int[] {1, 2}, width);
+
+            layout(" ", NO_BREAK, width);
+            layout(" X", new int[] {1}, width);
+            layout("  ", NO_BREAK, width);
+            layout(" X X", new int[] {1, 2, 3}, width);
+            layout("  X", new int[] {2}, width);
+        }
+    }
+
+    public void testNarrowWidthZeroWidth() {
+        int[] widths = new int[] { 0, 4 };
+        for (int width: widths) {
+            layout("X.", new int[] {1}, width);
+            layout("X__", new int[] {1}, width);
+            layout("X__X", new int[] {1, 3}, width); // Could be {1}
+            layout("X__X_", new int[] {1, 3, 4}, width); // Could be {1, 4}
+
+            layout("_", NO_BREAK, width);
+            layout("__", NO_BREAK, width);
+            layout("_X", new int[] {1}, width); // Could be NO_BREAK
+            layout("_X_", new int[] {1, 2}, width); // Could be {2}
+            layout("_X__", new int[] {1, 2}, width); // Could be {2}
+        }
+    }
+
+    public void testMaxLines() {
+        layoutMaxLines("C", NO_BREAK, 1);
+        layoutMaxLines("C C", new int[] {2}, 1);
+        layoutMaxLines("C C", new int[] {2}, 2);
+        layoutMaxLines("CC", new int[] {1}, 1);
+        layoutMaxLines("CC", new int[] {1}, 2);
+    }
 }