#include #include #include "ssh_hosts.h" #include "terminal.h" using namespace roro::term; void setUp() {} void tearDown() {} namespace { void feed(Terminal& t, const std::string& s) { t.feed(reinterpret_cast(s.data()), s.size()); } #define ROW(t, r, text) TEST_ASSERT_EQUAL_STRING(text, (t).rowText(r).c_str()) } // namespace void test_text_lines_and_the_cursor() { Terminal t(20, 5, 10); feed(t, "hello\r\nworld"); ROW(t, 0, "hello"); ROW(t, 1, "world"); TEST_ASSERT_EQUAL_INT(1, t.cursorRow()); TEST_ASSERT_EQUAL_INT(5, t.cursorCol()); feed(t, "\b\b!"); // backspace moves, it doesn't erase ROW(t, 1, "wor!d"); feed(t, "\rW\tx"); // a tab goes to the next eighth column ROW(t, 1, "Wor!d x"); feed(t, "\a"); TEST_ASSERT_TRUE(t.takeBell()); TEST_ASSERT_FALSE(t.takeBell()); } void test_wrapping_and_scrolling_into_history() { Terminal t(10, 3, 5); feed(t, "0123456789"); // exactly a line: the cursor waits at the edge TEST_ASSERT_EQUAL_INT(0, t.cursorRow()); TEST_ASSERT_EQUAL_INT(9, t.cursorCol()); feed(t, "ab"); ROW(t, 0, "0123456789"); ROW(t, 1, "ab"); feed(t, "\r\nc\r\nd\r\ne"); // five lines through three rows ROW(t, 0, "c"); ROW(t, 2, "e"); TEST_ASSERT_EQUAL_INT(2, t.historyCount()); TEST_ASSERT_EQUAL_STRING("0123456789", t.historyLine(0).c_str()); TEST_ASSERT_EQUAL_STRING("ab", t.historyLine(1).c_str()); for (int i = 0; i < 20; i++) feed(t, "\r\nx"); TEST_ASSERT_EQUAL_INT(5, t.historyCount()); // no more than it was given room for } void test_moving_and_erasing() { Terminal t(20, 5, 0); feed(t, "aaaaaaaaaa\r\nbbbbbbbbbb\r\ncccccccccc"); feed(t, "\x1b[2;4H"); // row 2, column 4 TEST_ASSERT_EQUAL_INT(1, t.cursorRow()); TEST_ASSERT_EQUAL_INT(3, t.cursorCol()); feed(t, "\x1b[K"); // to the end of the line ROW(t, 1, "bbb"); feed(t, "\x1b[A\x1b[1K"); // up one, erase to the start (the cursor's place included) ROW(t, 0, " aaaaaa"); feed(t, "\x1b[3;1H\x1b[2K"); ROW(t, 2, ""); feed(t, "\x1b[1;1Hxyz\x1b[J"); // from the cursor to the end of the screen ROW(t, 0, "xyz"); ROW(t, 1, ""); feed(t, "\x1b[5;18Hend\x1b[99;99H"); // far outside: the last cell TEST_ASSERT_EQUAL_INT(4, t.cursorRow()); TEST_ASSERT_EQUAL_INT(19, t.cursorCol()); feed(t, "\x1b[2J"); for (int r = 0; r < 5; r++) ROW(t, r, ""); feed(t, "\x1b[Habcdef\x1b[1;3H\x1b[2P"); // two characters deleted in the middle ROW(t, 0, "abef"); feed(t, "\x1b[2@"); // and two blanks put in ROW(t, 0, "ab ef"); feed(t, "\x1b[1;1H\x1b[3X"); ROW(t, 0, " ef"); feed(t, "\x1b[4G!\x1b[2d?"); // column 4, then row 2 in the column after ROW(t, 0, " !ef"); ROW(t, 1, " ?"); } void test_colours() { Terminal t(20, 3, 0); feed(t, "a\x1b[31mb\x1b[1mc\x1b[0;44md\x1b[7me\x1b[0m\x1b[38;5;196mf\x1b[48;2;0;0;255mg\x1b[92mh"); TEST_ASSERT_EQUAL_HEX8(0x07, t.cell(0, 0).attr); TEST_ASSERT_EQUAL_HEX8(0x01, t.cell(0, 1).attr); // red TEST_ASSERT_EQUAL_HEX8(0x09, t.cell(0, 2).attr); // bold red is bright red TEST_ASSERT_EQUAL_HEX8(0x47, t.cell(0, 3).attr); // on blue TEST_ASSERT_EQUAL_HEX8(0x74, t.cell(0, 4).attr); // inverse: the two swapped TEST_ASSERT_EQUAL_HEX8(0x09, t.cell(0, 5).attr); // one of 256, as the nearest of sixteen TEST_ASSERT_EQUAL_HEX8(0xC9, t.cell(0, 6).attr); // a colour by its red, green and blue TEST_ASSERT_EQUAL_HEX8(0xCA, t.cell(0, 7).attr); // bright green, written directly feed(t, "\x1b[0m\x1b[42m\x1b[2;1H\x1b[K"); // erasing paints with the colour behind TEST_ASSERT_EQUAL_HEX8(0x27, t.cell(1, 5).attr); uint8_t r, g, b; colourRgb(15, r, g, b); TEST_ASSERT_EQUAL_UINT8(255, r); } void test_scroll_regions_as_less_and_vim_use_them() { Terminal t(10, 5, 5); feed(t, "1\r\n2\r\n3\r\n4\r\n5"); feed(t, "\x1b[2;4r"); // rows 2 to 4 scroll; the cursor goes home TEST_ASSERT_EQUAL_INT(0, t.cursorRow()); feed(t, "\x1b[4;1H\n"); // a line feed at the bottom of the region ROW(t, 0, "1"); ROW(t, 1, "3"); ROW(t, 2, "4"); ROW(t, 3, ""); ROW(t, 4, "5"); TEST_ASSERT_EQUAL_INT(0, t.historyCount()); // a region that isn't the whole screen keeps nothing feed(t, "\x1b[2;1H\x1bM"); // reverse index at its top: it scrolls down ROW(t, 1, ""); ROW(t, 2, "3"); ROW(t, 3, "4"); feed(t, "\x1b[3;1H\x1b[L"); // a line put in ROW(t, 2, ""); ROW(t, 3, "3"); feed(t, "\x1b[M"); // and taken out ROW(t, 2, "3"); feed(t, "\x1b[r\x1b[1;1H\x1b[M"); // the whole screen again; a deleted first line isn't history ROW(t, 0, ""); TEST_ASSERT_EQUAL_INT(0, t.historyCount()); } void test_the_alternate_screen() { Terminal t(10, 3, 5); feed(t, "shell$ vi"); feed(t, "\x1b[?1049h"); TEST_ASSERT_TRUE(t.altScreen()); ROW(t, 0, ""); feed(t, "~\r\n~\r\n~\r\n~"); // scrolling here is nobody's history TEST_ASSERT_EQUAL_INT(0, t.historyCount()); feed(t, "\x1b[?1049l"); TEST_ASSERT_FALSE(t.altScreen()); ROW(t, 0, "shell$ vi"); // as it was left TEST_ASSERT_EQUAL_INT(9, t.cursorCol()); } void test_modes_and_what_is_asked_back() { Terminal t(10, 3, 0); std::string said; t.reply = [&](const std::string& s) { said += s; }; feed(t, "\x1b[?25l"); TEST_ASSERT_FALSE(t.cursorVisible()); feed(t, "\x1b[?25h\x1b[?1h"); TEST_ASSERT_TRUE(t.cursorVisible()); TEST_ASSERT_TRUE(t.appCursorKeys()); feed(t, "\x1b[2;5H\x1b[6n"); TEST_ASSERT_EQUAL_STRING("\x1b[2;5R", said.c_str()); said.clear(); feed(t, "\x1b[c"); TEST_ASSERT_EQUAL_STRING("\x1b[?6c", said.c_str()); feed(t, "\x1b[?7l\x1b[1;1H0123456789abc"); // no wrapping: the last column is overwritten ROW(t, 0, "012345678c"); ROW(t, 1, ""); feed(t, "\x1b" "c"); // a full reset ROW(t, 0, ""); TEST_ASSERT_FALSE(t.appCursorKeys()); } void test_what_is_skipped_and_what_is_replaced() { Terminal t(30, 3, 0); feed(t, "\x1b]0;a window title\x07" "A"); // a title: not shown feed(t, "\x1b]2;another\x1b\\" "B"); // ended the other way feed(t, "\x1b[?2004h\x1b[>4;2m\x1b=" "C"); // modes this has no use for ROW(t, 0, "ABC"); feed(t, "\r\n\xC3\xA9t\xC3\xA9 \xE2\x82\xAC \xE2\x94\x8C\xE2\x94\x80\xE2\x94\x90 \xE2\x94\x82 \xF0\x9F\x98\x80"); // été € ┌─┐ │ 😀 ROW(t, 1, "\xE9t\xE9 ? +-+ | ?"); feed(t, "\r\n\x1b(0lqk\x1b(B lqk"); // the old way of drawing lines, then letters again ROW(t, 2, "+-+ lqk"); feed(t, "\x1b[2J\x1b[H\xC3("); // a character cut short doesn't swallow what follows ROW(t, 0, "("); // Nonsense must not get it stuck or out of its grid. std::string noise; for (int i = 0; i < 4000; i++) noise += static_cast((i * 73 + i / 7) & 0xFF); feed(t, noise); feed(t, "\x1b" "c" "ok"); ROW(t, 0, "ok"); } void test_a_new_size() { Terminal t(10, 4, 10); feed(t, "one\r\ntwo\r\nthree\r\nfour"); t.resize(20, 2); // fewer rows: the cursor's line stays, the top goes to the history ROW(t, 0, "three"); ROW(t, 1, "four"); TEST_ASSERT_EQUAL_INT(1, t.cursorRow()); TEST_ASSERT_EQUAL_INT(2, t.historyCount()); TEST_ASSERT_EQUAL_STRING("two", t.historyLine(1).c_str()); t.resize(4, 6); // narrower: what doesn't fit is cut ROW(t, 0, "thre"); feed(t, "\r\n123456"); ROW(t, 2, "1234"); ROW(t, 3, "56"); } void test_keys() { TEST_ASSERT_EQUAL_STRING("a", encodeKey(TermKey::Char, 'a', false, false, false).c_str()); TEST_ASSERT_EQUAL_STRING("\x03", encodeKey(TermKey::Char, 'c', true, false, false).c_str()); TEST_ASSERT_EQUAL_STRING("\x03", encodeKey(TermKey::Char, 'C', true, false, false).c_str()); TEST_ASSERT_EQUAL_STRING("\x1b", encodeKey(TermKey::Char, '[', true, false, false).c_str()); TEST_ASSERT_EQUAL_size_t(1, encodeKey(TermKey::Char, ' ', true, false, false).size()); // Ctrl+Space is a zero TEST_ASSERT_EQUAL_STRING("\x1b" "b", encodeKey(TermKey::Char, 'b', false, true, false).c_str()); // Alt is Esc first TEST_ASSERT_EQUAL_STRING("\xC3\xA9", encodeKey(TermKey::Char, 0xE9, false, false, false).c_str()); TEST_ASSERT_EQUAL_STRING("\x1b[A", encodeKey(TermKey::Up, 0, false, false, false).c_str()); TEST_ASSERT_EQUAL_STRING("\x1bOA", encodeKey(TermKey::Up, 0, false, false, true).c_str()); // as vim asks for them TEST_ASSERT_EQUAL_STRING("\x1b[D", encodeKey(TermKey::Left, 0, false, false, false).c_str()); TEST_ASSERT_EQUAL_STRING("\r", encodeKey(TermKey::Enter, 0, false, false, false).c_str()); TEST_ASSERT_EQUAL_STRING("\x7f", encodeKey(TermKey::Backspace, 0, false, false, false).c_str()); TEST_ASSERT_EQUAL_STRING("\x1b", encodeKey(TermKey::Escape, 0, false, false, false).c_str()); TEST_ASSERT_EQUAL_STRING("\x1b[5~", encodeKey(TermKey::PageUp, 0, false, false, false).c_str()); } void test_who_to_connect_to() { SshTarget t; TEST_ASSERT_EQUAL_STRING("", parseSshTarget("pi@raspberrypi.local", t).c_str()); TEST_ASSERT_EQUAL_STRING("pi", t.user.c_str()); TEST_ASSERT_EQUAL_STRING("raspberrypi.local", t.host.c_str()); TEST_ASSERT_EQUAL_UINT16(22, t.port); TEST_ASSERT_EQUAL_STRING("pi@raspberrypi.local", t.text().c_str()); TEST_ASSERT_EQUAL_STRING("raspberrypi.local:22", t.hostPort().c_str()); TEST_ASSERT_EQUAL_STRING("", parseSshTarget("root@10.9.0.1:2222", t).c_str()); TEST_ASSERT_EQUAL_UINT16(2222, t.port); TEST_ASSERT_EQUAL_STRING("root@10.9.0.1:2222", t.text().c_str()); TEST_ASSERT_EQUAL_STRING("user@host, please", parseSshTarget("raspberrypi.local", t).c_str()); TEST_ASSERT_EQUAL_STRING("user@host, please", parseSshTarget("@host", t).c_str()); TEST_ASSERT_EQUAL_STRING("a port from 1 to 65535", parseSshTarget("a@b:0", t).c_str()); TEST_ASSERT_EQUAL_STRING("a port from 1 to 65535", parseSshTarget("a@b:99999", t).c_str()); TEST_ASSERT_EQUAL_STRING("that isn't a host", parseSshTarget("a@b c", t).c_str()); TEST_ASSERT_EQUAL_STRING("that isn't a host", parseSshTarget("a@", t).c_str()); TEST_ASSERT_EQUAL_UINT16(2222, t.port); // a refused line changes nothing } void test_saved_hosts_and_the_servers_met() { SshHosts hosts; SshTarget a, b; parseSshTarget("pi@one", a); parseSshTarget("root@two:2222", b); hosts.used(a); hosts.used(b); hosts.used(a); // again: to the front, not twice TEST_ASSERT_EQUAL_size_t(2, hosts.list().size()); TEST_ASSERT_EQUAL_STRING("pi@one", hosts.list()[0].c_str()); SshHosts again(hosts.stored()); TEST_ASSERT_EQUAL_STRING("root@two:2222", again.list()[1].c_str()); for (int i = 0; i < 12; i++) { SshTarget t; parseSshTarget("u@h" + std::to_string(i), t); again.used(t); } TEST_ASSERT_EQUAL_size_t(8, again.list().size()); TEST_ASSERT_EQUAL_STRING("u@h11", again.list()[0].c_str()); again.remove(0); TEST_ASSERT_EQUAL_STRING("u@h10", again.list()[0].c_str()); TEST_ASSERT_EQUAL_size_t(0, SshHosts("garbage\n\n@@\n").list().size()); SshKnownHosts known; TEST_ASSERT_EQUAL_STRING("", known.fingerprintOf("one:22").c_str()); known.remember("one:22", "SHA256:aaaa"); known.remember("two:2222", "SHA256:bbbb"); known.remember("one:22", "SHA256:cccc"); // it changed, and the user said yes SshKnownHosts stored(known.stored()); TEST_ASSERT_EQUAL_STRING("SHA256:cccc", stored.fingerprintOf("one:22").c_str()); TEST_ASSERT_EQUAL_STRING("SHA256:bbbb", stored.fingerprintOf("two:2222").c_str()); TEST_ASSERT_EQUAL_STRING("", stored.fingerprintOf("one:2222").c_str()); // another port is another server for (int i = 0; i < 20; i++) stored.remember("h" + std::to_string(i) + ":22", "SHA256:x"); TEST_ASSERT_EQUAL_STRING("", stored.fingerprintOf("one:22").c_str()); // sixteen kept, the oldest gone TEST_ASSERT_EQUAL_STRING("SHA256:x", stored.fingerprintOf("h19:22").c_str()); stored.forget("h19:22"); // forgotten with its host: met again, it is asked about again TEST_ASSERT_EQUAL_STRING("", stored.fingerprintOf("h19:22").c_str()); TEST_ASSERT_EQUAL_STRING("SHA256:x", stored.fingerprintOf("h18:22").c_str()); } int main() { UNITY_BEGIN(); RUN_TEST(test_text_lines_and_the_cursor); RUN_TEST(test_wrapping_and_scrolling_into_history); RUN_TEST(test_moving_and_erasing); RUN_TEST(test_colours); RUN_TEST(test_scroll_regions_as_less_and_vim_use_them); RUN_TEST(test_the_alternate_screen); RUN_TEST(test_modes_and_what_is_asked_back); RUN_TEST(test_what_is_skipped_and_what_is_replaced); RUN_TEST(test_a_new_size); RUN_TEST(test_keys); RUN_TEST(test_who_to_connect_to); RUN_TEST(test_saved_hosts_and_the_servers_met); return UNITY_END(); }