From f3147ea2d1598914c2db53e8cfb34c8ff81e2ff4 Mon Sep 17 00:00:00 2001 From: Daniel Lange Date: Thu, 27 Aug 2020 07:48:10 +0200 Subject: New upstream version 3.0.0 --- test_spec.lua | 687 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 687 insertions(+) create mode 100755 test_spec.lua (limited to 'test_spec.lua') diff --git a/test_spec.lua b/test_spec.lua new file mode 100755 index 0000000..2ae8015 --- /dev/null +++ b/test_spec.lua @@ -0,0 +1,687 @@ +#!/usr/bin/env lua + +local VISUALDELAY = os.getenv("VISUALDELAY") + +local visual = VISUALDELAY or false +local visual_delay = VISUALDELAY and (tonumber(VISUALDELAY)) or 0.1 +local short_delay = 0.3 +local long_delay = 1 + +local unistd = require("posix.unistd") +local time = require("posix.time") +local curses = require("posix.curses") +local rote = require("rote") + +local rt = rote.RoteTerm(24, 80) + +--[[ +local function os_execread(cmd) + local fd = io.popen(cmd, "r") + local out = fd:read("*a") + fd:close() + return (out:gsub("\n$", "")) +end +]] +--local branch = os_execread("git branch | grep '*'"):sub(3) +--print("Running in branch "..branch) + +os.execute("make coverage") +os.execute("rm -f *.gcda */*.gcda") +os.execute("rm -f coverage.info test.htoprc") +os.execute("rm -rf lcov") +os.execute("killall htop") +os.execute("ps aux | grep '[s]leep 12345' | awk '{print $2}' | xargs kill 2> /dev/null") + +os.execute("cp ./default.htoprc ./test.htoprc") +rt:forkPty("LC_ALL=C HTOPRC=./test.htoprc ./htop 2> htop-valgrind.txt") + +local stdscr, term_win +-- Curses initialization needed even when not in visual mode +-- because luaposix only initializes KEY_* constants after initscr(). +stdscr = curses.initscr() +if visual then + curses.echo(false) + curses.start_color() + curses.raw(true) + curses.halfdelay(1) + stdscr:keypad(true) + term_win = curses.newwin(24, 80, 0, 0) + local function makePair(foreground, background) + return background * 8 + 7 - foreground + end + -- initialize the color pairs the way rt:draw() expects it + for foreground = 0, 7 do + for background = 0, 7 do + if foreground ~= 7 or background ~= 0 then + local pair = makePair(foreground, background) + curses.init_pair(pair, foreground, background) + end + end + end +else + curses.endwin() +end + +local function show(key) + rt:update() + if visual then + rt:draw(term_win, 0, 0) + if key then + term_win:mvaddstr(0, 0, tostring(key)) + end + term_win:refresh() + + delay(visual_delay) + end +end + +local function send(key, times, quick) + if times == 0 then return end + for _ = 1, times or 1 do + delay(0.003) -- 30ms delay to avoid clobbering Esc sequences + if type(key) == "string" then + for c in key:gmatch('.') do + rt:keyPress(string.byte(c)) + end + else + rt:keyPress(key) + end + if not quick then + show(key) + end + end + if quick then + show(key) + end +end + +local function string_at(x, y, len) + rt:update() + local out = {} + for i = 1, len do + out[#out+1] = rt:cellChar(y-1, x+i-2) + end + return table.concat(out) +end + +local function is_string_at(x, y, str) + return string_at(x, y, #str) == str +end + +local function check_string_at(x, y, str) + return { str, string_at(x, y, #str) } +end + +local ESC = "\27\27" + +function delay(t) + time.nanosleep({ tv_sec = math.floor(t), tv_nsec = (t - math.floor(t)) * 1000000000 }) +end + +delay(2) -- give some time for htop to initialize. +rt:update() + +local y_panelhdr = (function() + for y = 1, 24 do + if is_string_at(3, y, "PID") then + return y + end + end +end)() or 1 + +assert.not_equal(y_panelhdr, 1) + +local x_metercol2 = 41 + +show() + +os.execute("sleep 12345 &") + +local function terminated() + return not os.execute("ps aux | grep -q '\\./[h]top'") +end + +local function running_it(desc, fn) + it(desc, function() + assert(not terminated()) + show() + fn() + assert(not terminated()) + end) +end + +local function check(t) + return t[1], t[2] +end + +local attrs = { + black_on_cyan = 6, + red_on_cyan = 22, + white_on_black = 176, + yellow_on_black = 112, +} + +local function find_selected_y(from) + rt:update() + for y = from or (y_panelhdr + 1), rt:rows() - 1 do + local attr = rt:cellAttr(y-1, 1) + if attr == attrs.black_on_cyan then + return y + end + end + return y_panelhdr + 1 +end + +local function find_command_x() + for x = 1, 80 do + if is_string_at(x, y_panelhdr, "Command") then + return x + end + end + return 64 +end + +local function set_display_option(n) + send("S") + send(curses.KEY_DOWN) + send(curses.KEY_RIGHT) + send(curses.KEY_DOWN, n, "quick") + send("\n") + send(curses.KEY_F10) +end + +describe("htop test suite", function() + + running_it("performs incremental filter", function() + send("\\") + send("x\127bux\127sted") -- test backspace + send("\n") + delay(short_delay) + rt:update() + local pid = (" "..tostring(unistd.getpid())):sub(-5) + local ourpid = check_string_at(1, y_panelhdr + 1, pid) + send("\\") + send(ESC) + send(curses.KEY_F5) + send(curses.KEY_HOME) + delay(short_delay) + rt:update() + local initpid = check_string_at(1, y_panelhdr + 1, " 1") + delay(short_delay) + rt:update() + send(curses.KEY_F5) + assert.equal(check(ourpid)) + assert.equal(check(initpid)) + end) + + running_it("performs incremental search", function() + send(curses.KEY_HOME) + send("/") + send("busted") + local attr = rt:cellAttr(rt:rows() - 1, 30) + delay(short_delay) + local line = find_selected_y() + local pid = (" "..tostring(unistd.getpid())):sub(-5) + assert.equal(attr, attrs.black_on_cyan) + local ourpid = check_string_at(1, line, pid) + send("\n") + send(curses.KEY_HOME) + assert.equal(check(ourpid)) + end) + + running_it("performs pid search", function() + send(curses.KEY_F5) + send(curses.KEY_END) + send("1") + delay(short_delay) + local line = find_selected_y() + local initpid = check_string_at(1, line, " 1") + send(curses.KEY_F5) + assert.equal(check(initpid)) + end) + + + running_it("horizontal scroll", function() + local h_scroll = 20 + send(curses.KEY_F5) + delay(short_delay) + local str1 = string_at(1+h_scroll, y_panelhdr+1, 5) + send(curses.KEY_RIGHT) + delay(short_delay) + local str2 = string_at(1, y_panelhdr+1, 5) + send(curses.KEY_LEFT) + delay(short_delay) + local str3 = string_at(1+h_scroll, y_panelhdr+1, 5) + send(curses.KEY_LEFT) + delay(short_delay) + local str4 = string_at(1+h_scroll, y_panelhdr+1, 5) + send(curses.KEY_F5) + assert.equal(str1, str2) + assert.equal(str2, str3) + assert.equal(str3, str4) + end) + + running_it("kills a process", function() + send(curses.KEY_HOME) + send("\\") + send("sleep 12345") + local attr = rt:cellAttr(rt:rows() - 1, 30) + assert.equal(attr, attrs.black_on_cyan) + send("\n") + delay(short_delay) + rt:update() + local col = find_command_x() + local procname = check_string_at(col, y_panelhdr + 1, "sleep 12345") + send("k") + send("\n") + send("\\") + send(ESC) + delay(short_delay) + assert.equal(check(procname)) + assert.not_equal((os.execute("ps aux | grep -q '[s]leep 12345'")), true) + end) + + running_it("runs strace", function() + send(curses.KEY_HOME) + send("/") + send("busted") + send("\n") + send("s") + delay(long_delay) + send(ESC) + end) + + running_it("runs lsof", function() + send(curses.KEY_HOME) + send("/") + send("busted") + send("\n") + send("l") + delay(long_delay) + send(ESC) + end) + + running_it("performs filtering in lsof", function() + send(curses.KEY_HOME) + send("/") + send("htop") + send("\n") + send("l") + send(curses.KEY_F4) + send("pipe") + delay(long_delay) + local pipefd = check_string_at(1, 3, " 3") + send(ESC) + assert.equal(check(pipefd)) + end) + + running_it("performs search in lsof", function() + send(curses.KEY_HOME) + send("/") + send("htop") + send("\n") + send("l") + send(curses.KEY_F3) + send("pipe") + delay(long_delay) + local line = find_selected_y(3) + local pipefd = check_string_at(1, line, " 3") + send(ESC) + assert.equal(check(pipefd)) + end) + + + running_it("cycles through meter modes in the default meters", function() + send("S") + for _ = 1, 2 do + send(curses.KEY_RIGHT) + for _ = 1, 3 do + send("\n", 4) + send(curses.KEY_DOWN) + end + end + send(ESC) + end) + + running_it("show process of a user", function() + send(curses.KEY_F5) + send("u") + send(curses.KEY_DOWN) + delay(short_delay) + rt:update() + local chosen = string_at(1, y_panelhdr + 2, 9) + send("\n") + send(curses.KEY_HOME) + delay(short_delay) + rt:update() + local shown = string_at(7, y_panelhdr + 1, 9) + send("u") + send("\n") + send(curses.KEY_HOME) + delay(short_delay) + rt:update() + local inituser = string_at(7, y_panelhdr + 1, 9) + send(curses.KEY_F5) + assert.equal(shown, chosen) + assert.equal(inituser, "root ") + end) + + running_it("performs failing search", function() + send(curses.KEY_HOME) + send("/") + send("xxxxxxxxxx") + delay(short_delay) + rt:update() + local attr = rt:cellAttr(rt:rows() - 1, 30) + assert.equal(attr, attrs.red_on_cyan) + send("\n") + end) + + running_it("cycles through search", function() + send(curses.KEY_HOME) + send("/") + send("sh") + local lastpid + local pidpairs = {} + for _ = 1, 3 do + send(curses.KEY_F3) + local line = find_selected_y() + local pid = string_at(1, line, 5) + if lastpid then + pidpairs[#pidpairs + 1] = { lastpid, pid } + lastpid = pid + end + end + send(curses.KEY_HOME) + for _, pair in pairs(pidpairs) do + assert.not_equal(pair[1], pair[2]) + end + end) + + running_it("visits each setup screen", function() + send("S") + send(curses.KEY_DOWN, 3) + send(curses.KEY_F10) + end) + + running_it("adds and removes PPID column", function() + send("S") + send(curses.KEY_DOWN, 3) + send(curses.KEY_RIGHT, 2) + send(curses.KEY_DOWN, 2) + send("\n") + send(curses.KEY_F10) + delay(short_delay) + local ppid = check_string_at(2, y_panelhdr, "PPID") + send("S") + send(curses.KEY_DOWN, 3) + send(curses.KEY_RIGHT, 1) + send(curses.KEY_DC) + send(curses.KEY_F10) + delay(short_delay) + local not_ppid = check_string_at(2, y_panelhdr, "PPID") + assert.equal(check(ppid)) + assert.not_equal(check(not_ppid)) + end) + + running_it("changes CPU affinity for a process", function() + send("a") + send(" \n") + send(ESC) + end) + + running_it("renices for a process", function() + send("/") + send("busted") + send("\n") + local line = find_selected_y() + local before = check_string_at(22, line, " 0") + send(curses.KEY_F8) + delay(short_delay) + local after = check_string_at(22, line, " 1") + assert.equal(check(before)) + assert.equal(check(after)) + end) + + running_it("tries to lower nice for a process", function() + send("/") + send("busted") + send("\n") + local line = find_selected_y() + local before = string_at(22, line, 2) + send(curses.KEY_F7) + delay(short_delay) + local after = string_at(22, line, 2) + assert.equal(before, after) -- no permissions + end) + + running_it("invert sort order", function() + local cpu_col = 45 + send("P") + send("I") + send(curses.KEY_HOME) + delay(short_delay) + local zerocpu = check_string_at(cpu_col, y_panelhdr + 1, " 0.0") + send("I") + delay(short_delay) + local nonzerocpu = check_string_at(cpu_col, y_panelhdr + 1, " 0.0") + assert.equal(check(zerocpu)) + assert.not_equal(check(nonzerocpu)) + end) + + running_it("changes IO priority for a process", function() + send("/") + send("htop") + send("\n") + send("i") + send(curses.KEY_END) + send("\n") + send(ESC) + end) + + running_it("shows help", function() + send(curses.KEY_F1) + send("\n") + set_display_option(9) + send(curses.KEY_F1) + send("\n") + set_display_option(9) + end) + + running_it("moves meters around", function() + send("S") + send(curses.KEY_RIGHT) + send(curses.KEY_UP) + send("\n") + send(curses.KEY_DOWN) + send(curses.KEY_UP) + send(curses.KEY_RIGHT) + send(curses.KEY_RIGHT) + send(curses.KEY_LEFT) + send(curses.KEY_LEFT) + send("\n") + send(curses.KEY_F10) + end) + + local meters = { + { name = "clock", down = 0, string = "Time" }, + { name = "load", down = 2, string = "Load" }, + { name = "battery", down = 7, string = "Battery" }, + { name = "hostname", down = 8, string = "Hostname" }, + { name = "memory", down = 3, string = "Mem" }, + { name = "CPU average", down = 16, string = "Avg" }, + } + + running_it("checks various CPU meters", function() + send("S") + send(curses.KEY_RIGHT, 3) + send(curses.KEY_DOWN, 9, "quick") + for _ = 9, 14 do + send("\n") + send("\n") + send(curses.KEY_DC) + send(curses.KEY_RIGHT) + send(curses.KEY_DOWN) + end + end) + + for _, item in ipairs(meters) do + running_it("adds and removes a "..item.name.." widget", function() + send("S") + send(curses.KEY_RIGHT, 3) + send(curses.KEY_DOWN, item.down) + send("\n") + send(curses.KEY_UP, 4) + send("\n") + send(curses.KEY_F4, 4) -- cycle through meter modes + delay(short_delay) + rt:update() + local with = check_string_at(x_metercol2, 2, item.string) + send(curses.KEY_DC) + delay(short_delay) + local without = check_string_at(x_metercol2, 2, item.string) + send(curses.KEY_F10) + assert.equal(check(with)) + assert.not_equal(check(without)) + end) + end + + running_it("goes through themes", function() + send(curses.KEY_F2) + send(curses.KEY_DOWN, 2) + send(curses.KEY_RIGHT) + for _ = 1, 6 do + send("\n") + send(curses.KEY_DOWN) + end + send(curses.KEY_UP, 6) + send("\n") + send(curses.KEY_F10) + end) + + local display_options = { + { name = "tree view", down = 0 }, + { name = "shadow other user's process", down = 1 }, + { name = "hide kernel threads", down = 2 }, + { name = "hide userland threads", down = 3 }, + { name = "display threads in different color", down = 4 }, + { name = "show custom thread names", down = 5 }, + { name = "highlight basename", down = 6 }, + { name = "highlight large numbers", down = 7 }, + { name = "leave margin around header", down = 8 }, + { name = "use detailed CPU time", down = 9 }, + { name = "count from zero", down = 10 }, + { name = "update process names", down = 11 }, + { name = "guest time in CPU%", down = 12 }, + } + + for _, item in ipairs(display_options) do + running_it("checks display option to "..item.name, function() + for _ = 1, 2 do + set_display_option(item.down) + delay(short_delay) + end + end) + end + + running_it("shows detailed CPU with guest time", function() + for _ = 1, 2 do + send("S") + send(curses.KEY_DOWN) + send(curses.KEY_RIGHT) + send(curses.KEY_DOWN, 9) + send("\n") + send(curses.KEY_DOWN, 3) + send("\n") + send(curses.KEY_LEFT) + send(curses.KEY_UP) + send(curses.KEY_RIGHT) + send(curses.KEY_F4, 4) -- cycle through CPU meter modes + send(curses.KEY_F10) + delay(short_delay) + end + end) + + running_it("expands and collapses tree", function() + send(curses.KEY_F5) -- tree view + send(curses.KEY_HOME) + send(curses.KEY_DOWN) -- second process in the tree + send("-") + send("+") + send(curses.KEY_F5) + end) + + running_it("sets sort key", function() + send(".") + send("\n") + end) + + running_it("tags all children", function() + send(curses.KEY_F5) -- tree view + send(curses.KEY_HOME) -- ensure we're at init + send("c") + local taggedattrs = {} + rt:update() + for y = y_panelhdr + 2, 23 do + table.insert(taggedattrs, rt:cellAttr(y-1, 4)) + end + delay(short_delay) + send("U") + local untaggedattrs = {} + rt:update() + for y = y_panelhdr + 2, 23 do + table.insert(untaggedattrs, rt:cellAttr(y-1, 4)) + end + send(curses.KEY_F5) + + for _, taggedattr in ipairs(taggedattrs) do + assert.equal(attrs.yellow_on_black, taggedattr) + end + for _, untaggedattr in ipairs(untaggedattrs) do + assert.equal(attrs.white_on_black, untaggedattr) + end + end) + + for i = 1, 62 do + running_it("show column "..i, function() + send("S") + send(curses.KEY_END) + send(curses.KEY_RIGHT, 1) + if i > 1 then + send(curses.KEY_DC) + end + send(curses.KEY_RIGHT, 1) + local down = i + while down > 13 do + send(curses.KEY_NPAGE) + down = down - 13 + end + send(curses.KEY_DOWN, down, "quick") + send("\n") + send(curses.KEY_F10) + if i == 62 then + send("S") + send(curses.KEY_END) + send(curses.KEY_RIGHT, 1) + if i > 1 then + send(curses.KEY_DC) + end + send(curses.KEY_F10) + end + end) + end + + it("finally quits", function() + assert(not terminated()) + send("q") + while not terminated() do + unistd.sleep(1) + send("q") + end + assert(terminated()) + if visual then + curses.endwin() + end + os.execute("make lcov && xdg-open lcov/index.html") + end) +end) -- cgit v1.2.3