package com.termux.terminal; import junit.framework.TestCase; import java.util.Arrays; import java.util.Random; public class TerminalRowTest extends TestCase { /** The properties of these code points are validated in {@link #testStaticConstants()}. */ private static final int ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1 = 0x679C; private static final int ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2 = 0x679D; private static final int TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_1 = 0x2070E; private static final int TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_2 = 0x20731; /** Unicode Character 'MUSICAL SYMBOL G CLEF' (U+1D11E). Two java chars required for this. */ static final int TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_1 = 0x1D11E; /** Unicode Character 'MUSICAL SYMBOL G CLEF OTTAVA ALTA' (U+1D11F). Two java chars required for this. */ private static final int TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_2 = 0x1D11F; private final int COLUMNS = 80; /** A combining character. */ private static final int DIARESIS_CODEPOINT = 0x0308; private TerminalRow row; @Override protected void setUp() throws Exception { row = new TerminalRow(COLUMNS, TextStyle.NORMAL); } private void assertLineStartsWith(int... codePoints) { char[] chars = row.mText; int charIndex = 0; for (int i = 0; i < codePoints.length; i++) { int lineCodePoint = chars[charIndex++]; if (Character.isHighSurrogate((char) lineCodePoint)) { lineCodePoint = Character.toCodePoint((char) lineCodePoint, chars[charIndex++]); } assertEquals("Differing a code point index=" + i, codePoints[i], lineCodePoint); } } private void assertColumnCharIndicesStartsWith(int... indices) { for (int i = 0; i < indices.length; i++) { int expected = indices[i]; int actual = row.findStartOfColumn(i); assertEquals("At index=" + i, expected, actual); } } public void testSimpleDiaresis() { row.setChar(0, DIARESIS_CODEPOINT, 0); assertEquals(81, row.getSpaceUsed()); row.setChar(0, DIARESIS_CODEPOINT, 0); assertEquals(82, row.getSpaceUsed()); assertLineStartsWith(' ', DIARESIS_CODEPOINT, DIARESIS_CODEPOINT, ' '); } public void testStaticConstants() { assertEquals(1, Character.charCount(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1)); assertEquals(1, Character.charCount(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2)); assertEquals(2, WcWidth.width(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1)); assertEquals(2, WcWidth.width(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2)); assertEquals(2, Character.charCount(TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_1)); assertEquals(2, Character.charCount(TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_2)); assertEquals(1, WcWidth.width(TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_1)); assertEquals(1, WcWidth.width(TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_2)); assertEquals(2, Character.charCount(TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_1)); assertEquals(2, Character.charCount(TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_2)); assertEquals(2, WcWidth.width(TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_1)); assertEquals(2, WcWidth.width(TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_2)); assertEquals(1, Character.charCount(DIARESIS_CODEPOINT)); assertEquals(0, WcWidth.width(DIARESIS_CODEPOINT)); } public void testOneColumn() { assertEquals(0, row.findStartOfColumn(0)); row.setChar(0, 'a', 0); assertEquals(0, row.findStartOfColumn(0)); } public void testAscii() { assertEquals(0, row.findStartOfColumn(0)); row.setChar(0, 'a', 0); assertLineStartsWith('a', ' ', ' '); assertEquals(1, row.findStartOfColumn(1)); assertEquals(80, row.getSpaceUsed()); row.setChar(0, 'b', 0); assertEquals(1, row.findStartOfColumn(1)); assertEquals(2, row.findStartOfColumn(2)); assertEquals(80, row.getSpaceUsed()); assertColumnCharIndicesStartsWith(0, 1, 2, 3); char[] someChars = new char[]{'a', 'c', 'e', '4', '5', '6', '7', '8'}; char[] rawLine = new char[80]; Arrays.fill(rawLine, ' '); Random random = new Random(); for (int i = 0; i < 1000; i++) { int lineIndex = random.nextInt(rawLine.length); int charIndex = random.nextInt(someChars.length); rawLine[lineIndex] = someChars[charIndex]; row.setChar(lineIndex, someChars[charIndex], 0); } char[] lineChars = row.mText; for (int i = 0; i < rawLine.length; i++) { assertEquals(rawLine[i], lineChars[i]); } } public void testUnicode() { assertEquals(0, row.findStartOfColumn(0)); assertEquals(80, row.getSpaceUsed()); row.setChar(0, TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_1, 0); assertEquals(81, row.getSpaceUsed()); assertEquals(0, row.findStartOfColumn(0)); assertEquals(2, row.findStartOfColumn(1)); assertLineStartsWith(TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_1, ' ', ' '); assertColumnCharIndicesStartsWith(0, 2, 3, 4); row.setChar(0, 'a', 0); assertEquals(80, row.getSpaceUsed()); assertEquals(0, row.findStartOfColumn(0)); assertEquals(1, row.findStartOfColumn(1)); assertLineStartsWith('a', ' ', ' '); assertColumnCharIndicesStartsWith(0, 1, 2, 3); row.setChar(0, TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_1, 0); row.setChar(1, 'a', 0); assertLineStartsWith(TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_1, 'a', ' '); row.setChar(0, TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_1, 0); row.setChar(1, TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_2, 0); assertLineStartsWith(TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_1, TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_2, ' '); assertColumnCharIndicesStartsWith(0, 2, 4, 5); assertEquals(82, row.getSpaceUsed()); } public void testDoubleWidth() { row.setChar(0, 'a', 0); row.setChar(1, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2, 0); assertLineStartsWith('a', ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2, ' '); assertColumnCharIndicesStartsWith(0, 1, 1, 2); row.setChar(0, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, 0); assertLineStartsWith(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, ' ', ' '); assertColumnCharIndicesStartsWith(0, 0, 1, 2); row.setChar(0, ' ', 0); assertLineStartsWith(' ', ' ', ' ', ' '); assertColumnCharIndicesStartsWith(0, 1, 2, 3, 4); row.setChar(0, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, 0); row.setChar(2, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2, 0); assertLineStartsWith(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2); assertColumnCharIndicesStartsWith(0, 0, 1, 1, 2); row.setChar(0, 'a', 0); assertLineStartsWith('a', ' ', ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2, ' '); } /** Just as {@link #testDoubleWidth()} but requires a surrogate pair. */ public void testDoubleWidthSurrogage() { row.setChar(0, 'a', 0); assertColumnCharIndicesStartsWith(0, 1, 2, 3, 4); row.setChar(1, TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_2, 0); assertColumnCharIndicesStartsWith(0, 1, 1, 3, 4); assertLineStartsWith('a', TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_2, ' '); row.setChar(0, TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_1, 0); assertColumnCharIndicesStartsWith(0, 0, 2, 3, 4); assertLineStartsWith(TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_1, ' ', ' ', ' '); row.setChar(0, ' ', 0); assertLineStartsWith(' ', ' ', ' ', ' '); row.setChar(0, TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_1, 0); row.setChar(1, TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_2, 0); assertLineStartsWith(' ', TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_2, ' '); row.setChar(0, 'a', 0); assertLineStartsWith('a', TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_2, ' '); } public void testReplacementChar() { row.setChar(0, TerminalEmulator.UNICODE_REPLACEMENT_CHAR, 0); row.setChar(1, 'Y', 0); assertLineStartsWith(TerminalEmulator.UNICODE_REPLACEMENT_CHAR, 'Y', ' ', ' '); } public void testSurrogateCharsWithNormalDisplayWidth() { // These requires a UTF-16 surrogate pair, and has a display width of one. int first = 0x1D306; int second = 0x1D307; // Assert the above statement: assertEquals(2, Character.toChars(first).length); assertEquals(2, Character.toChars(second).length); row.setChar(0, second, 0); assertEquals(second, Character.toCodePoint(row.mText[0], row.mText[1])); assertEquals(' ', row.mText[2]); assertEquals(2, row.findStartOfColumn(1)); row.setChar(0, first, 0); assertEquals(first, Character.toCodePoint(row.mText[0], row.mText[1])); assertEquals(' ', row.mText[2]); assertEquals(2, row.findStartOfColumn(1)); row.setChar(1, second, 0); row.setChar(2, 'a', 0); assertEquals(first, Character.toCodePoint(row.mText[0], row.mText[1])); assertEquals(second, Character.toCodePoint(row.mText[2], row.mText[3])); assertEquals('a', row.mText[4]); assertEquals(' ', row.mText[5]); assertEquals(0, row.findStartOfColumn(0)); assertEquals(2, row.findStartOfColumn(1)); assertEquals(4, row.findStartOfColumn(2)); assertEquals(5, row.findStartOfColumn(3)); assertEquals(6, row.findStartOfColumn(4)); row.setChar(0, ' ', 0); assertEquals(' ', row.mText[0]); assertEquals(second, Character.toCodePoint(row.mText[1], row.mText[2])); assertEquals('a', row.mText[3]); assertEquals(' ', row.mText[4]); assertEquals(0, row.findStartOfColumn(0)); assertEquals(1, row.findStartOfColumn(1)); assertEquals(3, row.findStartOfColumn(2)); assertEquals(4, row.findStartOfColumn(3)); assertEquals(5, row.findStartOfColumn(4)); for (int i = 0; i < 80; i++) { row.setChar(i, i % 2 == 0 ? first : second, 0); } for (int i = 0; i < 80; i++) { int idx = row.findStartOfColumn(i); assertEquals(i % 2 == 0 ? first : second, Character.toCodePoint(row.mText[idx], row.mText[idx + 1])); } for (int i = 0; i < 80; i++) { row.setChar(i, i % 2 == 0 ? 'a' : 'b', 0); } for (int i = 0; i < 80; i++) { int idx = row.findStartOfColumn(i); assertEquals(i, idx); assertEquals(i % 2 == 0 ? 'a' : 'b', row.mText[i]); } } public void testOverwritingDoubleDisplayWidthWithNormalDisplayWidth() { // Initial "OO " row.setChar(0, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, 0); assertEquals(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, row.mText[0]); assertEquals(' ', row.mText[1]); assertEquals(0, row.findStartOfColumn(0)); assertEquals(0, row.findStartOfColumn(1)); assertEquals(1, row.findStartOfColumn(2)); // Setting first column to a clears second: "a " row.setChar(0, 'a', 0); assertEquals('a', row.mText[0]); assertEquals(' ', row.mText[1]); assertEquals(0, row.findStartOfColumn(0)); assertEquals(1, row.findStartOfColumn(1)); assertEquals(2, row.findStartOfColumn(2)); // Back to initial "OO " row.setChar(0, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, 0); assertEquals(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, row.mText[0]); assertEquals(' ', row.mText[1]); assertEquals(0, row.findStartOfColumn(0)); assertEquals(0, row.findStartOfColumn(1)); assertEquals(1, row.findStartOfColumn(2)); // Setting first column to a clears first: " a " row.setChar(1, 'a', 0); assertEquals(' ', row.mText[0]); assertEquals('a', row.mText[1]); assertEquals(' ', row.mText[2]); assertEquals(0, row.findStartOfColumn(0)); assertEquals(1, row.findStartOfColumn(1)); assertEquals(2, row.findStartOfColumn(2)); } public void testOverwritingDoubleDisplayWidthWithSelf() { row.setChar(0, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, 0); row.setChar(0, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, 0); assertEquals(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, row.mText[0]); assertEquals(' ', row.mText[1]); assertEquals(0, row.findStartOfColumn(0)); assertEquals(0, row.findStartOfColumn(1)); assertEquals(1, row.findStartOfColumn(2)); } public void testNormalCharsWithDoubleDisplayWidth() { // These fit in one java char, and has a display width of two. assertTrue(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1 != ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2); assertEquals(1, Character.charCount(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1)); assertEquals(1, Character.charCount(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2)); assertEquals(2, WcWidth.width(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1)); assertEquals(2, WcWidth.width(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2)); row.setChar(0, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, 0); assertEquals(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, row.mText[0]); assertEquals(0, row.findStartOfColumn(1)); assertEquals(' ', row.mText[1]); row.setChar(0, 'a', 0); assertEquals('a', row.mText[0]); assertEquals(' ', row.mText[1]); assertEquals(1, row.findStartOfColumn(1)); row.setChar(0, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, 0); assertEquals(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, row.mText[0]); // The first character fills both first columns. assertEquals(0, row.findStartOfColumn(1)); row.setChar(2, 'a', 0); assertEquals(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, row.mText[0]); assertEquals('a', row.mText[1]); assertEquals(1, row.findStartOfColumn(2)); row.setChar(0, 'c', 0); assertEquals('c', row.mText[0]); assertEquals(' ', row.mText[1]); assertEquals('a', row.mText[2]); assertEquals(' ', row.mText[3]); assertEquals(0, row.findStartOfColumn(0)); assertEquals(1, row.findStartOfColumn(1)); assertEquals(2, row.findStartOfColumn(2)); } public void testNormalCharsWithDoubleDisplayWidthOverlapping() { // These fit in one java char, and has a display width of two. row.setChar(0, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, 0); row.setChar(2, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2, 0); row.setChar(4, 'a', 0); // O = ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO // A = ANOTHER_JAVA_CHAR_DISPLAY_WIDTH_TWO // "OOAAa " assertEquals(0, row.findStartOfColumn(0)); assertEquals(0, row.findStartOfColumn(1)); assertEquals(1, row.findStartOfColumn(2)); assertEquals(1, row.findStartOfColumn(3)); assertEquals(2, row.findStartOfColumn(4)); assertEquals(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, row.mText[0]); assertEquals(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2, row.mText[1]); assertEquals('a', row.mText[2]); assertEquals(' ', row.mText[3]); row.setChar(1, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2, 0); // " AA a " assertEquals(' ', row.mText[0]); assertEquals(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2, row.mText[1]); assertEquals(' ', row.mText[2]); assertEquals('a', row.mText[3]); assertEquals(' ', row.mText[4]); assertEquals(0, row.findStartOfColumn(0)); assertEquals(1, row.findStartOfColumn(1)); assertEquals(1, row.findStartOfColumn(2)); assertEquals(2, row.findStartOfColumn(3)); assertEquals(3, row.findStartOfColumn(4)); } // https://github.com/jackpal/Android-Terminal-Emulator/issues/145 public void testCrashATE145() { // 0xC2541 is unassigned, use display width 1 for UNICODE_REPLACEMENT_CHAR. // assertEquals(1, WcWidth.width(0xC2541)); assertEquals(2, Character.charCount(0xC2541)); assertEquals(2, WcWidth.width(0x73EE)); assertEquals(1, Character.charCount(0x73EE)); assertEquals(0, WcWidth.width(0x009F)); assertEquals(1, Character.charCount(0x009F)); int[] points = new int[]{0xC2541, 'a', '8', 0x73EE, 0x009F, 0x881F, 0x8324, 0xD4C9, 0xFFFD, 'B', 0x009B, 0x61C9, 'Z'}; // int[] expected = new int[] { TerminalEmulator.UNICODE_REPLACEMENT_CHAR, 'a', '8', 0x73EE, 0x009F, 0x881F, 0x8324, 0xD4C9, 0xFFFD, // 'B', 0x009B, 0x61C9, 'Z' }; int currentColumn = 0; for (int point : points) { row.setChar(currentColumn, point, 0); currentColumn += WcWidth.width(point); } // assertLineStartsWith(points); // assertEquals(Character.highSurrogate(0xC2541), line.mText[0]); // assertEquals(Character.lowSurrogate(0xC2541), line.mText[1]); // assertEquals('a', line.mText[2]); // assertEquals('8', line.mText[3]); // assertEquals(Character.highSurrogate(0x73EE), line.mText[4]); // assertEquals(Character.lowSurrogate(0x73EE), line.mText[5]); // // char[] chars = line.mText; // int charIndex = 0; // for (int i = 0; i < points.length; i++) { // char c = chars[charIndex]; // charIndex++; // int thisPoint = (int) c; // if (Character.isHighSurrogate(c)) { // thisPoint = Character.toCodePoint(c, chars[charIndex]); // charIndex++; // } // assertEquals("At index=" + i + ", charIndex=" + charIndex + ", char=" + (char) thisPoint, points[i], thisPoint); // } } public void testNormalization() { // int lowerCaseN = 0x006E; // int combiningTilde = 0x0303; // int combined = 0x00F1; row.setChar(0, 0x006E, 0); assertEquals(80, row.getSpaceUsed()); row.setChar(0, 0x0303, 0); assertEquals(81, row.getSpaceUsed()); // assertEquals("\u00F1 ", new String(term.getScreen().getLine(0))); assertLineStartsWith(0x006E, 0x0303, ' '); } public void testInsertWideAtLastColumn() { row.setChar(COLUMNS - 2, 'Z', 0); row.setChar(COLUMNS - 1, 'a', 0); assertEquals('Z', row.mText[row.findStartOfColumn(COLUMNS - 2)]); assertEquals('a', row.mText[row.findStartOfColumn(COLUMNS - 1)]); row.setChar(COLUMNS - 1, 'ö', 0); assertEquals('Z', row.mText[row.findStartOfColumn(COLUMNS - 2)]); assertEquals('ö', row.mText[row.findStartOfColumn(COLUMNS - 1)]); // line.setChar(COLUMNS - 1, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1); // assertEquals('Z', line.mText[line.findStartOfColumn(COLUMNS - 2)]); // assertEquals(' ', line.mText[line.findStartOfColumn(COLUMNS - 1)]); } }