test_spec.lua 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687
  1. #!/usr/bin/env lua
  2. local VISUALDELAY = os.getenv("VISUALDELAY")
  3. local visual = VISUALDELAY or false
  4. local visual_delay = VISUALDELAY and (tonumber(VISUALDELAY)) or 0.1
  5. local short_delay = 0.3
  6. local long_delay = 1
  7. local unistd = require("posix.unistd")
  8. local time = require("posix.time")
  9. local curses = require("posix.curses")
  10. local rote = require("rote")
  11. local rt = rote.RoteTerm(24, 80)
  12. --[[
  13. local function os_execread(cmd)
  14. local fd = io.popen(cmd, "r")
  15. local out = fd:read("*a")
  16. fd:close()
  17. return (out:gsub("\n$", ""))
  18. end
  19. ]]
  20. --local branch = os_execread("git branch | grep '*'"):sub(3)
  21. --print("Running in branch "..branch)
  22. os.execute("make coverage")
  23. os.execute("rm -f *.gcda */*.gcda")
  24. os.execute("rm -f coverage.info test.htoprc")
  25. os.execute("rm -rf lcov")
  26. os.execute("killall htop")
  27. os.execute("ps aux | grep '[s]leep 12345' | awk '{print $2}' | xargs kill 2> /dev/null")
  28. os.execute("cp ./default.htoprc ./test.htoprc")
  29. rt:forkPty("LC_ALL=C HTOPRC=./test.htoprc ./htop 2> htop-valgrind.txt")
  30. local stdscr, term_win
  31. -- Curses initialization needed even when not in visual mode
  32. -- because luaposix only initializes KEY_* constants after initscr().
  33. stdscr = curses.initscr()
  34. if visual then
  35. curses.echo(false)
  36. curses.start_color()
  37. curses.raw(true)
  38. curses.halfdelay(1)
  39. stdscr:keypad(true)
  40. term_win = curses.newwin(24, 80, 0, 0)
  41. local function makePair(foreground, background)
  42. return background * 8 + 7 - foreground
  43. end
  44. -- initialize the color pairs the way rt:draw() expects it
  45. for foreground = 0, 7 do
  46. for background = 0, 7 do
  47. if foreground ~= 7 or background ~= 0 then
  48. local pair = makePair(foreground, background)
  49. curses.init_pair(pair, foreground, background)
  50. end
  51. end
  52. end
  53. else
  54. curses.endwin()
  55. end
  56. local function show(key)
  57. rt:update()
  58. if visual then
  59. rt:draw(term_win, 0, 0)
  60. if key then
  61. term_win:mvaddstr(0, 0, tostring(key))
  62. end
  63. term_win:refresh()
  64. delay(visual_delay)
  65. end
  66. end
  67. local function send(key, times, quick)
  68. if times == 0 then return end
  69. for _ = 1, times or 1 do
  70. delay(0.003) -- 30ms delay to avoid clobbering Esc sequences
  71. if type(key) == "string" then
  72. for c in key:gmatch('.') do
  73. rt:keyPress(string.byte(c))
  74. end
  75. else
  76. rt:keyPress(key)
  77. end
  78. if not quick then
  79. show(key)
  80. end
  81. end
  82. if quick then
  83. show(key)
  84. end
  85. end
  86. local function string_at(x, y, len)
  87. rt:update()
  88. local out = {}
  89. for i = 1, len do
  90. out[#out+1] = rt:cellChar(y-1, x+i-2)
  91. end
  92. return table.concat(out)
  93. end
  94. local function is_string_at(x, y, str)
  95. return string_at(x, y, #str) == str
  96. end
  97. local function check_string_at(x, y, str)
  98. return { str, string_at(x, y, #str) }
  99. end
  100. local ESC = "\27\27"
  101. function delay(t)
  102. time.nanosleep({ tv_sec = math.floor(t), tv_nsec = (t - math.floor(t)) * 1000000000 })
  103. end
  104. delay(2) -- give some time for htop to initialize.
  105. rt:update()
  106. local y_panelhdr = (function()
  107. for y = 1, 24 do
  108. if is_string_at(3, y, "PID") then
  109. return y
  110. end
  111. end
  112. end)() or 1
  113. assert.not_equal(y_panelhdr, 1)
  114. local x_metercol2 = 41
  115. show()
  116. os.execute("sleep 12345 &")
  117. local function terminated()
  118. return not os.execute("ps aux | grep -q '\\./[h]top'")
  119. end
  120. local function running_it(desc, fn)
  121. it(desc, function()
  122. assert(not terminated())
  123. show()
  124. fn()
  125. assert(not terminated())
  126. end)
  127. end
  128. local function check(t)
  129. return t[1], t[2]
  130. end
  131. local attrs = {
  132. black_on_cyan = 6,
  133. red_on_cyan = 22,
  134. white_on_black = 176,
  135. yellow_on_black = 112,
  136. }
  137. local function find_selected_y(from)
  138. rt:update()
  139. for y = from or (y_panelhdr + 1), rt:rows() - 1 do
  140. local attr = rt:cellAttr(y-1, 1)
  141. if attr == attrs.black_on_cyan then
  142. return y
  143. end
  144. end
  145. return y_panelhdr + 1
  146. end
  147. local function find_command_x()
  148. for x = 1, 80 do
  149. if is_string_at(x, y_panelhdr, "Command") then
  150. return x
  151. end
  152. end
  153. return 64
  154. end
  155. local function set_display_option(n)
  156. send("S")
  157. send(curses.KEY_DOWN)
  158. send(curses.KEY_RIGHT)
  159. send(curses.KEY_DOWN, n, "quick")
  160. send("\n")
  161. send(curses.KEY_F10)
  162. end
  163. describe("htop test suite", function()
  164. running_it("performs incremental filter", function()
  165. send("\\")
  166. send("x\127bux\127sted") -- test backspace
  167. send("\n")
  168. delay(short_delay)
  169. rt:update()
  170. local pid = (" "..tostring(unistd.getpid())):sub(-5)
  171. local ourpid = check_string_at(1, y_panelhdr + 1, pid)
  172. send("\\")
  173. send(ESC)
  174. send(curses.KEY_F5)
  175. send(curses.KEY_HOME)
  176. delay(short_delay)
  177. rt:update()
  178. local initpid = check_string_at(1, y_panelhdr + 1, " 1")
  179. delay(short_delay)
  180. rt:update()
  181. send(curses.KEY_F5)
  182. assert.equal(check(ourpid))
  183. assert.equal(check(initpid))
  184. end)
  185. running_it("performs incremental search", function()
  186. send(curses.KEY_HOME)
  187. send("/")
  188. send("busted")
  189. local attr = rt:cellAttr(rt:rows() - 1, 30)
  190. delay(short_delay)
  191. local line = find_selected_y()
  192. local pid = (" "..tostring(unistd.getpid())):sub(-5)
  193. assert.equal(attr, attrs.black_on_cyan)
  194. local ourpid = check_string_at(1, line, pid)
  195. send("\n")
  196. send(curses.KEY_HOME)
  197. assert.equal(check(ourpid))
  198. end)
  199. running_it("performs pid search", function()
  200. send(curses.KEY_F5)
  201. send(curses.KEY_END)
  202. send("1")
  203. delay(short_delay)
  204. local line = find_selected_y()
  205. local initpid = check_string_at(1, line, " 1")
  206. send(curses.KEY_F5)
  207. assert.equal(check(initpid))
  208. end)
  209. running_it("horizontal scroll", function()
  210. local h_scroll = 20
  211. send(curses.KEY_F5)
  212. delay(short_delay)
  213. local str1 = string_at(1+h_scroll, y_panelhdr+1, 5)
  214. send(curses.KEY_RIGHT)
  215. delay(short_delay)
  216. local str2 = string_at(1, y_panelhdr+1, 5)
  217. send(curses.KEY_LEFT)
  218. delay(short_delay)
  219. local str3 = string_at(1+h_scroll, y_panelhdr+1, 5)
  220. send(curses.KEY_LEFT)
  221. delay(short_delay)
  222. local str4 = string_at(1+h_scroll, y_panelhdr+1, 5)
  223. send(curses.KEY_F5)
  224. assert.equal(str1, str2)
  225. assert.equal(str2, str3)
  226. assert.equal(str3, str4)
  227. end)
  228. running_it("kills a process", function()
  229. send(curses.KEY_HOME)
  230. send("\\")
  231. send("sleep 12345")
  232. local attr = rt:cellAttr(rt:rows() - 1, 30)
  233. assert.equal(attr, attrs.black_on_cyan)
  234. send("\n")
  235. delay(short_delay)
  236. rt:update()
  237. local col = find_command_x()
  238. local procname = check_string_at(col, y_panelhdr + 1, "sleep 12345")
  239. send("k")
  240. send("\n")
  241. send("\\")
  242. send(ESC)
  243. delay(short_delay)
  244. assert.equal(check(procname))
  245. assert.not_equal((os.execute("ps aux | grep -q '[s]leep 12345'")), true)
  246. end)
  247. running_it("runs strace", function()
  248. send(curses.KEY_HOME)
  249. send("/")
  250. send("busted")
  251. send("\n")
  252. send("s")
  253. delay(long_delay)
  254. send(ESC)
  255. end)
  256. running_it("runs lsof", function()
  257. send(curses.KEY_HOME)
  258. send("/")
  259. send("busted")
  260. send("\n")
  261. send("l")
  262. delay(long_delay)
  263. send(ESC)
  264. end)
  265. running_it("performs filtering in lsof", function()
  266. send(curses.KEY_HOME)
  267. send("/")
  268. send("htop")
  269. send("\n")
  270. send("l")
  271. send(curses.KEY_F4)
  272. send("pipe")
  273. delay(long_delay)
  274. local pipefd = check_string_at(1, 3, " 3")
  275. send(ESC)
  276. assert.equal(check(pipefd))
  277. end)
  278. running_it("performs search in lsof", function()
  279. send(curses.KEY_HOME)
  280. send("/")
  281. send("htop")
  282. send("\n")
  283. send("l")
  284. send(curses.KEY_F3)
  285. send("pipe")
  286. delay(long_delay)
  287. local line = find_selected_y(3)
  288. local pipefd = check_string_at(1, line, " 3")
  289. send(ESC)
  290. assert.equal(check(pipefd))
  291. end)
  292. running_it("cycles through meter modes in the default meters", function()
  293. send("S")
  294. for _ = 1, 2 do
  295. send(curses.KEY_RIGHT)
  296. for _ = 1, 3 do
  297. send("\n", 4)
  298. send(curses.KEY_DOWN)
  299. end
  300. end
  301. send(ESC)
  302. end)
  303. running_it("show process of a user", function()
  304. send(curses.KEY_F5)
  305. send("u")
  306. send(curses.KEY_DOWN)
  307. delay(short_delay)
  308. rt:update()
  309. local chosen = string_at(1, y_panelhdr + 2, 9)
  310. send("\n")
  311. send(curses.KEY_HOME)
  312. delay(short_delay)
  313. rt:update()
  314. local shown = string_at(7, y_panelhdr + 1, 9)
  315. send("u")
  316. send("\n")
  317. send(curses.KEY_HOME)
  318. delay(short_delay)
  319. rt:update()
  320. local inituser = string_at(7, y_panelhdr + 1, 9)
  321. send(curses.KEY_F5)
  322. assert.equal(shown, chosen)
  323. assert.equal(inituser, "root ")
  324. end)
  325. running_it("performs failing search", function()
  326. send(curses.KEY_HOME)
  327. send("/")
  328. send("xxxxxxxxxx")
  329. delay(short_delay)
  330. rt:update()
  331. local attr = rt:cellAttr(rt:rows() - 1, 30)
  332. assert.equal(attr, attrs.red_on_cyan)
  333. send("\n")
  334. end)
  335. running_it("cycles through search", function()
  336. send(curses.KEY_HOME)
  337. send("/")
  338. send("sh")
  339. local lastpid
  340. local pidpairs = {}
  341. for _ = 1, 3 do
  342. send(curses.KEY_F3)
  343. local line = find_selected_y()
  344. local pid = string_at(1, line, 5)
  345. if lastpid then
  346. pidpairs[#pidpairs + 1] = { lastpid, pid }
  347. lastpid = pid
  348. end
  349. end
  350. send(curses.KEY_HOME)
  351. for _, pair in pairs(pidpairs) do
  352. assert.not_equal(pair[1], pair[2])
  353. end
  354. end)
  355. running_it("visits each setup screen", function()
  356. send("S")
  357. send(curses.KEY_DOWN, 3)
  358. send(curses.KEY_F10)
  359. end)
  360. running_it("adds and removes PPID column", function()
  361. send("S")
  362. send(curses.KEY_DOWN, 3)
  363. send(curses.KEY_RIGHT, 2)
  364. send(curses.KEY_DOWN, 2)
  365. send("\n")
  366. send(curses.KEY_F10)
  367. delay(short_delay)
  368. local ppid = check_string_at(2, y_panelhdr, "PPID")
  369. send("S")
  370. send(curses.KEY_DOWN, 3)
  371. send(curses.KEY_RIGHT, 1)
  372. send(curses.KEY_DC)
  373. send(curses.KEY_F10)
  374. delay(short_delay)
  375. local not_ppid = check_string_at(2, y_panelhdr, "PPID")
  376. assert.equal(check(ppid))
  377. assert.not_equal(check(not_ppid))
  378. end)
  379. running_it("changes CPU affinity for a process", function()
  380. send("a")
  381. send(" \n")
  382. send(ESC)
  383. end)
  384. running_it("renices for a process", function()
  385. send("/")
  386. send("busted")
  387. send("\n")
  388. local line = find_selected_y()
  389. local before = check_string_at(22, line, " 0")
  390. send(curses.KEY_F8)
  391. delay(short_delay)
  392. local after = check_string_at(22, line, " 1")
  393. assert.equal(check(before))
  394. assert.equal(check(after))
  395. end)
  396. running_it("tries to lower nice for a process", function()
  397. send("/")
  398. send("busted")
  399. send("\n")
  400. local line = find_selected_y()
  401. local before = string_at(22, line, 2)
  402. send(curses.KEY_F7)
  403. delay(short_delay)
  404. local after = string_at(22, line, 2)
  405. assert.equal(before, after) -- no permissions
  406. end)
  407. running_it("invert sort order", function()
  408. local cpu_col = 45
  409. send("P")
  410. send("I")
  411. send(curses.KEY_HOME)
  412. delay(short_delay)
  413. local zerocpu = check_string_at(cpu_col, y_panelhdr + 1, " 0.0")
  414. send("I")
  415. delay(short_delay)
  416. local nonzerocpu = check_string_at(cpu_col, y_panelhdr + 1, " 0.0")
  417. assert.equal(check(zerocpu))
  418. assert.not_equal(check(nonzerocpu))
  419. end)
  420. running_it("changes IO priority for a process", function()
  421. send("/")
  422. send("htop")
  423. send("\n")
  424. send("i")
  425. send(curses.KEY_END)
  426. send("\n")
  427. send(ESC)
  428. end)
  429. running_it("shows help", function()
  430. send(curses.KEY_F1)
  431. send("\n")
  432. set_display_option(9)
  433. send(curses.KEY_F1)
  434. send("\n")
  435. set_display_option(9)
  436. end)
  437. running_it("moves meters around", function()
  438. send("S")
  439. send(curses.KEY_RIGHT)
  440. send(curses.KEY_UP)
  441. send("\n")
  442. send(curses.KEY_DOWN)
  443. send(curses.KEY_UP)
  444. send(curses.KEY_RIGHT)
  445. send(curses.KEY_RIGHT)
  446. send(curses.KEY_LEFT)
  447. send(curses.KEY_LEFT)
  448. send("\n")
  449. send(curses.KEY_F10)
  450. end)
  451. local meters = {
  452. { name = "clock", down = 0, string = "Time" },
  453. { name = "load", down = 2, string = "Load" },
  454. { name = "battery", down = 7, string = "Battery" },
  455. { name = "hostname", down = 8, string = "Hostname" },
  456. { name = "memory", down = 3, string = "Mem" },
  457. { name = "CPU average", down = 16, string = "Avg" },
  458. }
  459. running_it("checks various CPU meters", function()
  460. send("S")
  461. send(curses.KEY_RIGHT, 3)
  462. send(curses.KEY_DOWN, 9, "quick")
  463. for _ = 9, 14 do
  464. send("\n")
  465. send("\n")
  466. send(curses.KEY_DC)
  467. send(curses.KEY_RIGHT)
  468. send(curses.KEY_DOWN)
  469. end
  470. end)
  471. for _, item in ipairs(meters) do
  472. running_it("adds and removes a "..item.name.." widget", function()
  473. send("S")
  474. send(curses.KEY_RIGHT, 3)
  475. send(curses.KEY_DOWN, item.down)
  476. send("\n")
  477. send(curses.KEY_UP, 4)
  478. send("\n")
  479. send(curses.KEY_F4, 4) -- cycle through meter modes
  480. delay(short_delay)
  481. rt:update()
  482. local with = check_string_at(x_metercol2, 2, item.string)
  483. send(curses.KEY_DC)
  484. delay(short_delay)
  485. local without = check_string_at(x_metercol2, 2, item.string)
  486. send(curses.KEY_F10)
  487. assert.equal(check(with))
  488. assert.not_equal(check(without))
  489. end)
  490. end
  491. running_it("goes through themes", function()
  492. send(curses.KEY_F2)
  493. send(curses.KEY_DOWN, 2)
  494. send(curses.KEY_RIGHT)
  495. for _ = 1, 6 do
  496. send("\n")
  497. send(curses.KEY_DOWN)
  498. end
  499. send(curses.KEY_UP, 6)
  500. send("\n")
  501. send(curses.KEY_F10)
  502. end)
  503. local display_options = {
  504. { name = "tree view", down = 0 },
  505. { name = "shadow other user's process", down = 1 },
  506. { name = "hide kernel threads", down = 2 },
  507. { name = "hide userland threads", down = 3 },
  508. { name = "display threads in different color", down = 4 },
  509. { name = "show custom thread names", down = 5 },
  510. { name = "highlight basename", down = 6 },
  511. { name = "highlight large numbers", down = 7 },
  512. { name = "leave margin around header", down = 8 },
  513. { name = "use detailed CPU time", down = 9 },
  514. { name = "count from zero", down = 10 },
  515. { name = "update process names", down = 11 },
  516. { name = "guest time in CPU%", down = 12 },
  517. }
  518. for _, item in ipairs(display_options) do
  519. running_it("checks display option to "..item.name, function()
  520. for _ = 1, 2 do
  521. set_display_option(item.down)
  522. delay(short_delay)
  523. end
  524. end)
  525. end
  526. running_it("shows detailed CPU with guest time", function()
  527. for _ = 1, 2 do
  528. send("S")
  529. send(curses.KEY_DOWN)
  530. send(curses.KEY_RIGHT)
  531. send(curses.KEY_DOWN, 9)
  532. send("\n")
  533. send(curses.KEY_DOWN, 3)
  534. send("\n")
  535. send(curses.KEY_LEFT)
  536. send(curses.KEY_UP)
  537. send(curses.KEY_RIGHT)
  538. send(curses.KEY_F4, 4) -- cycle through CPU meter modes
  539. send(curses.KEY_F10)
  540. delay(short_delay)
  541. end
  542. end)
  543. running_it("expands and collapses tree", function()
  544. send(curses.KEY_F5) -- tree view
  545. send(curses.KEY_HOME)
  546. send(curses.KEY_DOWN) -- second process in the tree
  547. send("-")
  548. send("+")
  549. send(curses.KEY_F5)
  550. end)
  551. running_it("sets sort key", function()
  552. send(".")
  553. send("\n")
  554. end)
  555. running_it("tags all children", function()
  556. send(curses.KEY_F5) -- tree view
  557. send(curses.KEY_HOME) -- ensure we're at init
  558. send("c")
  559. local taggedattrs = {}
  560. rt:update()
  561. for y = y_panelhdr + 2, 23 do
  562. table.insert(taggedattrs, rt:cellAttr(y-1, 4))
  563. end
  564. delay(short_delay)
  565. send("U")
  566. local untaggedattrs = {}
  567. rt:update()
  568. for y = y_panelhdr + 2, 23 do
  569. table.insert(untaggedattrs, rt:cellAttr(y-1, 4))
  570. end
  571. send(curses.KEY_F5)
  572. for _, taggedattr in ipairs(taggedattrs) do
  573. assert.equal(attrs.yellow_on_black, taggedattr)
  574. end
  575. for _, untaggedattr in ipairs(untaggedattrs) do
  576. assert.equal(attrs.white_on_black, untaggedattr)
  577. end
  578. end)
  579. for i = 1, 62 do
  580. running_it("show column "..i, function()
  581. send("S")
  582. send(curses.KEY_END)
  583. send(curses.KEY_RIGHT, 1)
  584. if i > 1 then
  585. send(curses.KEY_DC)
  586. end
  587. send(curses.KEY_RIGHT, 1)
  588. local down = i
  589. while down > 13 do
  590. send(curses.KEY_NPAGE)
  591. down = down - 13
  592. end
  593. send(curses.KEY_DOWN, down, "quick")
  594. send("\n")
  595. send(curses.KEY_F10)
  596. if i == 62 then
  597. send("S")
  598. send(curses.KEY_END)
  599. send(curses.KEY_RIGHT, 1)
  600. if i > 1 then
  601. send(curses.KEY_DC)
  602. end
  603. send(curses.KEY_F10)
  604. end
  605. end)
  606. end
  607. it("finally quits", function()
  608. assert(not terminated())
  609. send("q")
  610. while not terminated() do
  611. unistd.sleep(1)
  612. send("q")
  613. end
  614. assert(terminated())
  615. if visual then
  616. curses.endwin()
  617. end
  618. os.execute("make lcov && xdg-open lcov/index.html")
  619. end)
  620. end)