Back to Repositories

Testing Terminal Emulator Implementation in termux-app

A comprehensive JUnit test suite for the Termux terminal emulator that validates terminal functionality, cursor behavior, color handling, and screen management. This test file ensures proper implementation of terminal features like mouse tracking, cursor positioning, and text display.

Test Coverage Overview

The test suite provides extensive coverage of terminal emulator functionality including:
  • Cursor positioning and movement
  • Screen buffer management and text display
  • Color handling and text styling
  • Mouse event tracking and reporting
  • Terminal size reporting and cursor shape changes

Implementation Analysis

The testing approach uses JUnit framework with a custom TerminalTestCase base class. Tests are structured to validate both basic terminal operations and complex ANSI escape sequences. The implementation employs methodical test patterns with setup/assertion helpers for terminal sizing and state verification.

Technical Details

Key technical components include:
  • JUnit test framework integration
  • Custom terminal emulator test harness
  • ANSI escape sequence testing utilities
  • Screen buffer state verification methods
  • Color parsing and handling validation

Best Practices Demonstrated

The test suite exemplifies testing best practices through:
  • Comprehensive edge case coverage
  • Isolated test methods for specific features
  • Clear test naming and organization
  • Thorough validation of state changes
  • Proper setup and teardown patterns

termux/termux-app

terminal-emulator/src/test/java/com/termux/terminal/TerminalTest.java

            
package com.termux.terminal;

import java.io.UnsupportedEncodingException;

public class TerminalTest extends TerminalTestCase {

	public void testCursorPositioning() throws Exception {
		withTerminalSized(10, 10).placeCursorAndAssert(1, 2).placeCursorAndAssert(3, 5).placeCursorAndAssert(2, 2).enterString("A")
				.assertCursorAt(2, 3);
	}

	public void testScreen() throws UnsupportedEncodingException {
		withTerminalSized(3, 3);
		assertLinesAre("   ", "   ", "   ");

		assertEquals("", mTerminal.getScreen().getTranscriptText());
		enterString("hi").assertLinesAre("hi ", "   ", "   ");
		assertEquals("hi", mTerminal.getScreen().getTranscriptText());
		enterString("\r\nu");
		assertEquals("hi\nu", mTerminal.getScreen().getTranscriptText());
		mTerminal.reset();
		assertEquals("hi\nu", mTerminal.getScreen().getTranscriptText());

		withTerminalSized(3, 3).enterString("hello");
		assertEquals("hello", mTerminal.getScreen().getTranscriptText());
		enterString("\r\nworld");
		assertEquals("hello\nworld", mTerminal.getScreen().getTranscriptText());
	}

	public void testScrollDownInAltBuffer() {
		withTerminalSized(3, 3).enterString("\033[?1049h");
		enterString("\033[38;5;111m1\r\n");
		enterString("\033[38;5;112m2\r\n");
		enterString("\033[38;5;113m3\r\n");
		enterString("\033[38;5;114m4\r\n");
		enterString("\033[38;5;115m5");
		assertLinesAre("3  ", "4  ", "5  ");
		assertForegroundColorAt(0, 0, 113);
		assertForegroundColorAt(1, 0, 114);
		assertForegroundColorAt(2, 0, 115);
	}

	public void testMouseClick() throws Exception {
		withTerminalSized(10, 10);
		assertFalse(mTerminal.isMouseTrackingActive());
		enterString("\033[?1000h");
		assertTrue(mTerminal.isMouseTrackingActive());
		enterString("\033[?1000l");
		assertFalse(mTerminal.isMouseTrackingActive());
		enterString("\033[?1000h");
		assertTrue(mTerminal.isMouseTrackingActive());

		enterString("\033[?1006h");
		mTerminal.sendMouseEvent(TerminalEmulator.MOUSE_LEFT_BUTTON, 3, 4, true);
		assertEquals("\033[<0;3;4M", mOutput.getOutputAndClear());
		mTerminal.sendMouseEvent(TerminalEmulator.MOUSE_LEFT_BUTTON, 3, 4, false);
		assertEquals("\033[<0;3;4m", mOutput.getOutputAndClear());

		// When the client says that a click is outside (which could happen when pixels are outside
		// the terminal area, see https://github.com/termux/termux-app/issues/501) the terminal
		// sends a click at the edge.
		mTerminal.sendMouseEvent(TerminalEmulator.MOUSE_LEFT_BUTTON, 0, 0, true);
		assertEquals("\033[<0;1;1M", mOutput.getOutputAndClear());
		mTerminal.sendMouseEvent(TerminalEmulator.MOUSE_LEFT_BUTTON, 11, 11, false);
		assertEquals("\033[<0;10;10m", mOutput.getOutputAndClear());
	}

	public void testNormalization() throws UnsupportedEncodingException {
		// int lowerCaseN = 0x006E;
		// int combiningTilde = 0x0303;
		// int combined = 0x00F1;
		withTerminalSized(3, 3).assertLinesAre("   ", "   ", "   ");
		enterString("\u006E\u0303");
		assertEquals(1, WcWidth.width("\u006E\u0303".toCharArray(), 0));
		// assertEquals("\u00F1  ", new String(mTerminal.getScreen().getLine(0)));
		assertLinesAre("\u006E\u0303  ", "   ", "   ");
	}

	/** On "\e[18t" xterm replies with "\e[8;${HEIGHT};${WIDTH}t" */
	public void testReportTerminalSize() throws Exception {
		withTerminalSized(5, 5);
		assertEnteringStringGivesResponse("\033[18t", "\033[8;5;5t");
		for (int width = 3; width < 12; width++) {
			for (int height = 3; height < 12; height++) {
				resize(width, height);
				assertEnteringStringGivesResponse("\033[18t", "\033[8;" + height + ";" + width + "t");
			}
		}
	}

	/** Device Status Report (DSR) and Report Cursor Position (CPR). */
	public void testDeviceStatusReport() throws Exception {
		withTerminalSized(5, 5);
		assertEnteringStringGivesResponse("\033[5n", "\033[0n");

		assertEnteringStringGivesResponse("\033[6n", "\033[1;1R");
		enterString("AB");
		assertEnteringStringGivesResponse("\033[6n", "\033[1;3R");
		enterString("\r\n");
		assertEnteringStringGivesResponse("\033[6n", "\033[2;1R");
	}

	/** Test the cursor shape changes using DECSCUSR. */
	public void testSetCursorStyle() throws Exception {
		withTerminalSized(5, 5);
		assertEquals(TerminalEmulator.TERMINAL_CURSOR_STYLE_BLOCK, mTerminal.getCursorStyle());
		enterString("\033[3 q");
		assertEquals(TerminalEmulator.TERMINAL_CURSOR_STYLE_UNDERLINE, mTerminal.getCursorStyle());
		enterString("\033[5 q");
		assertEquals(TerminalEmulator.TERMINAL_CURSOR_STYLE_BAR, mTerminal.getCursorStyle());
		enterString("\033[0 q");
		assertEquals(TerminalEmulator.TERMINAL_CURSOR_STYLE_BLOCK, mTerminal.getCursorStyle());
		enterString("\033[6 q");
		assertEquals(TerminalEmulator.TERMINAL_CURSOR_STYLE_BAR, mTerminal.getCursorStyle());
		enterString("\033[4 q");
		assertEquals(TerminalEmulator.TERMINAL_CURSOR_STYLE_UNDERLINE, mTerminal.getCursorStyle());
		enterString("\033[1 q");
		assertEquals(TerminalEmulator.TERMINAL_CURSOR_STYLE_BLOCK, mTerminal.getCursorStyle());
		enterString("\033[4 q");
		assertEquals(TerminalEmulator.TERMINAL_CURSOR_STYLE_UNDERLINE, mTerminal.getCursorStyle());
		enterString("\033[2 q");
		assertEquals(TerminalEmulator.TERMINAL_CURSOR_STYLE_BLOCK, mTerminal.getCursorStyle());
	}

	public void testPaste() {
		withTerminalSized(5, 5);
		mTerminal.paste("hi");
		assertEquals("hi", mOutput.getOutputAndClear());

		enterString("\033[?2004h");
		mTerminal.paste("hi");
		assertEquals("\033[200~" + "hi" + "\033[201~", mOutput.getOutputAndClear());

		enterString("\033[?2004l");
		mTerminal.paste("hi");
		assertEquals("hi", mOutput.getOutputAndClear());
	}

	public void testSelectGraphics() {
		selectGraphicsTestRun(';');
		selectGraphicsTestRun(':');
	}

	public void selectGraphicsTestRun(char separator) {
		withTerminalSized(5, 5);
		enterString("\033[31m");
		assertEquals(mTerminal.mForeColor, 1);
		enterString("\033[32m");
		assertEquals(mTerminal.mForeColor, 2);
		enterString("\033[43m");
		assertEquals(2, mTerminal.mForeColor);
		assertEquals(3, mTerminal.mBackColor);

		// SGR 0 should reset both foreground and background color.
		enterString("\033[0m");
		assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
		assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);

		// Test CSI resetting to default if sequence starts with ; or has sequential ;;
        // Check TerminalEmulator.parseArg()
        enterString("\033[31m\033[m");
        assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
        enterString("\033[31m\033[;m".replace(';', separator));
        assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
        enterString("\033[31m\033[0m");
        assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
        enterString("\033[31m\033[0;m".replace(';', separator));
        assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
        enterString("\033[31;;m");
        assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
        enterString("\033[31::m");
        assertEquals(1, mTerminal.mForeColor);
        enterString("\033[31;m");
        assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
        enterString("\033[31:m");
        assertEquals(1, mTerminal.mForeColor);
        enterString("\033[31;;41m");
        assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
        assertEquals(1, mTerminal.mBackColor);
        enterString("\033[0m");
        assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);

		// 256 colors:
		enterString("\033[38;5;119m".replace(';', separator));
		assertEquals(119, mTerminal.mForeColor);
		assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);
		enterString("\033[48;5;129m".replace(';', separator));
		assertEquals(119, mTerminal.mForeColor);
		assertEquals(129, mTerminal.mBackColor);

		// Invalid parameter:
		enterString("\033[48;8;129m".replace(';', separator));
		assertEquals(119, mTerminal.mForeColor);
		assertEquals(129, mTerminal.mBackColor);

		// Multiple parameters at once:
		enterString("\033[38;5;178".replace(';', separator) + ";" + "48;5;179m".replace(';', separator));
		assertEquals(178, mTerminal.mForeColor);
		assertEquals(179, mTerminal.mBackColor);

		// Omitted parameter means zero:
		enterString("\033[38;5;m".replace(';', separator));
		assertEquals(0, mTerminal.mForeColor);
		assertEquals(179, mTerminal.mBackColor);
		enterString("\033[48;5;m".replace(';', separator));
		assertEquals(0, mTerminal.mForeColor);
		assertEquals(0, mTerminal.mBackColor);

		// 24 bit colors:
		enterString(("\033[0m")); // Reset fg and bg colors.
		enterString("\033[38;2;255;127;2m".replace(';', separator));
		int expectedForeground = 0xff000000 | (255 << 16) | (127 << 8) | 2;
		assertEquals(expectedForeground, mTerminal.mForeColor);
		assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);
		enterString("\033[48;2;1;2;254m".replace(';', separator));
		int expectedBackground = 0xff000000 | (1 << 16) | (2 << 8) | 254;
		assertEquals(expectedForeground, mTerminal.mForeColor);
		assertEquals(expectedBackground, mTerminal.mBackColor);

		// 24 bit colors, set fg and bg at once:
		enterString(("\033[0m")); // Reset fg and bg colors.
		assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
		assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);
		enterString("\033[38;2;255;127;2".replace(';', separator) + ";" + "48;2;1;2;254m".replace(';', separator));
		assertEquals(expectedForeground, mTerminal.mForeColor);
		assertEquals(expectedBackground, mTerminal.mBackColor);

		// 24 bit colors, invalid input:
		enterString("\033[38;2;300;127;2;48;2;1;300;254m".replace(';', separator));
		assertEquals(expectedForeground, mTerminal.mForeColor);
		assertEquals(expectedBackground, mTerminal.mBackColor);

		// 24 bit colors, omitted parameter means zero:
		enterString("\033[38;2;255;127;m".replace(';', separator));
		expectedForeground = 0xff000000 | (255 << 16) | (127 << 8);
		assertEquals(expectedForeground, mTerminal.mForeColor);
		assertEquals(expectedBackground, mTerminal.mBackColor);
		enterString("\033[38;2;123;;77m".replace(';', separator));
		expectedForeground = 0xff000000 | (123 << 16) | 77;
		assertEquals(expectedForeground, mTerminal.mForeColor);
		assertEquals(expectedBackground, mTerminal.mBackColor);

		// 24 bit colors, extra sub-parameters are skipped:
		expectedForeground = 0xff000000 | (255 << 16) | (127 << 8) | 2;
		enterString("\033[0;38:2:255:127:2:48:2:1:2:254m");
		assertEquals(expectedForeground, mTerminal.mForeColor);
		assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);
	}

	public void testBackgroundColorErase() {
		final int rows = 3;
		final int cols = 3;
		withTerminalSized(cols, rows);
		for (int r = 0; r < rows; r++) {
			for (int c = 0; c < cols; c++) {
				long style = getStyleAt(r, c);
				assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, TextStyle.decodeForeColor(style));
				assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, TextStyle.decodeBackColor(style));
			}
		}
		// Foreground color to 119:
		enterString("\033[38;5;119m");
		// Background color to 129:
		enterString("\033[48;5;129m");
		// Clear with ED, Erase in Display:
		enterString("\033[2J");
		for (int r = 0; r < rows; r++) {
			for (int c = 0; c < cols; c++) {
				long style = getStyleAt(r, c);
				assertEquals(119, TextStyle.decodeForeColor(style));
				assertEquals(129, TextStyle.decodeBackColor(style));
			}
		}
		// Background color to 139:
		enterString("\033[48;5;139m");
		// Insert two blank lines.
		enterString("\033[2L");
		for (int r = 0; r < rows; r++) {
			for (int c = 0; c < cols; c++) {
				long style = getStyleAt(r, c);
				assertEquals((r == 0 || r == 1) ? 139 : 129, TextStyle.decodeBackColor(style));
			}
		}

		withTerminalSized(cols, rows);
		// Background color to 129:
		enterString("\033[48;5;129m");
		// Erase two characters, filling them with background color:
		enterString("\033[2X");
		assertEquals(129, TextStyle.decodeBackColor(getStyleAt(0, 0)));
		assertEquals(129, TextStyle.decodeBackColor(getStyleAt(0, 1)));
		assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, TextStyle.decodeBackColor(getStyleAt(0, 2)));
	}

	public void testParseColor() {
		assertEquals(0xFF0000FA, TerminalColors.parse("#0000FA"));
		assertEquals(0xFF000000, TerminalColors.parse("#000000"));
		assertEquals(0xFF000000, TerminalColors.parse("#000"));
		assertEquals(0xFF000000, TerminalColors.parse("#000000000"));
		assertEquals(0xFF53186f, TerminalColors.parse("#53186f"));

		assertEquals(0xFFFF00FF, TerminalColors.parse("rgb:F/0/F"));
		assertEquals(0xFF0000FA, TerminalColors.parse("rgb:00/00/FA"));
		assertEquals(0xFF53186f, TerminalColors.parse("rgb:53/18/6f"));

		assertEquals(0, TerminalColors.parse("invalid_0000FA"));
		assertEquals(0, TerminalColors.parse("#3456"));
	}

	/** The ncurses library still uses this. */
	public void testLineDrawing() {
		// 016 - shift out / G1. 017 - shift in / G0. "ESC ) 0" - use line drawing for G1
		withTerminalSized(4, 2).enterString("q\033)0q\016q\017q").assertLinesAre("qq─q", "    ");
		// "\0337", saving cursor should save G0, G1 and invoked charset and "ESC 8" should restore.
		withTerminalSized(4, 2).enterString("\033)0\016qqq\0337\017\0338q").assertLinesAre("────", "    ");
	}

	public void testSoftTerminalReset() {
		// See http://vt100.net/docs/vt510-rm/DECSTR and https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=650304
		// "\033[?7l" is DECRST to disable wrap-around, and DECSTR ("\033[!p") should reset it.
		withTerminalSized(3, 3).enterString("\033[?7lABCD").assertLinesAre("ABD", "   ", "   ");
		enterString("\033[!pEF").assertLinesAre("ABE", "F  ", "   ");
	}

	public void testBel() {
		withTerminalSized(3, 3);
		assertEquals(0, mOutput.bellsRung);
		enterString("\07");
		assertEquals(1, mOutput.bellsRung);
		enterString("hello\07");
		assertEquals(2, mOutput.bellsRung);
		enterString("\07hello");
		assertEquals(3, mOutput.bellsRung);
		enterString("hello\07world");
		assertEquals(4, mOutput.bellsRung);
	}

	public void testAutomargins() throws UnsupportedEncodingException {
		withTerminalSized(3, 3).enterString("abc").assertLinesAre("abc", "   ", "   ").assertCursorAt(0, 2);
		enterString("d").assertLinesAre("abc", "d  ", "   ").assertCursorAt(1, 1);

		withTerminalSized(3, 3).enterString("abc\r ").assertLinesAre(" bc", "   ", "   ").assertCursorAt(0, 1);
	}

	public void testTab() {
		withTerminalSized(11, 2).enterString("01234567890\r\tXX").assertLinesAre("01234567XX0", "           ");
		withTerminalSized(11, 2).enterString("01234567890\033[44m\r\tXX").assertLinesAre("01234567XX0", "           ");
	}

}