Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

automatic tests for TUI (ncurses) #251

Merged
merged 16 commits into from
Oct 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@
/.yardoc
/package/*.tar.*
*.pot
# screen captures from the libyui tests
*.out.txt
*.out.esc
5 changes: 5 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,9 @@ FROM registry.opensuse.org/yast/head/containers/yast-cpp:latest
# Install tmux to make sure the libyui+YaST integration tests are run
RUN zypper --non-interactive in tmux

# Enable installing docs...
RUN sed -i 's/\(rpm\.install\.excludedocs =\).*/\1 no/' /etc/zypp/zypp.conf
# ... and reinstall the RPM containing the examples we use for tests
RUN zypper --non-interactive in --force yast2-ycp-ui-bindings-devel

COPY . /usr/src/app
6 changes: 6 additions & 0 deletions package/yast2-ruby-bindings.changes
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
-------------------------------------------------------------------
Tue Oct 13 14:42:52 UTC 2020 - Martin Vidner <[email protected]>

- Add automatic TUI (ncurses) tests using tmux (bsc#1165388).
- 4.3.5

-------------------------------------------------------------------
Thu Sep 24 19:46:00 UTC 2020 - [email protected]

Expand Down
8 changes: 4 additions & 4 deletions package/yast2-ruby-bindings.spec
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@


Name: yast2-ruby-bindings
Version: 4.3.4
Version: 4.3.5
Release: 0
URL: https://github.com/yast/yast-ruby-bindings
BuildRoot: %{_tmppath}/%{name}-%{version}-build
Expand Down Expand Up @@ -46,9 +46,9 @@ BuildRequires: yast2-ycp-ui-bindings-devel >= 4.3.1
# The test suite includes a regression test (std_streams_spec.rb) for a
# libyui-ncurses bug fixed in 2.47.3
BuildRequires: libyui-ncurses >= 2.47.3
# The mentioned test requires to check if tmux is there, because tmux is
# needed to execute the test in headless systems
BuildRequires: which
# The mentioned test requires tmux in order to be executed in headless systems
# Also many other libyui tests to come
BuildRequires: tmux

# only a soft dependency, the Ruby debugger is optional
Suggests: rubygem(%{rb_default_ruby_abi}:byebug)
Expand Down
16 changes: 13 additions & 3 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
#
# CMakeLists.txt for yast2/ruby-bindings/tests/ruby
#
# CMakeLists.txt for yast-ruby-bindings/tests

# use
# make test
# or, for verbose output,
# make test ARGS=-V
# ARGS is passed to ctest; see also
# man ctest

ENABLE_TESTING()

Expand All @@ -12,3 +17,8 @@ endforeach(test)

ADD_TEST("integration" ruby ${CMAKE_CURRENT_SOURCE_DIR}/integration/run.rb)
ADD_TEST("translations" rspec --format doc ${CMAKE_CURRENT_SOURCE_DIR}/integration/translations_spec.rb)

file(GLOB libyui_specs "libyui/*_spec.rb")
foreach(test ${libyui_specs})
ADD_TEST(${test} rspec --format doc ${test})
endforeach(test)
30 changes: 30 additions & 0 deletions tests/libyui/menu_hotkeys_1177760_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
require_relative "rspec_tmux_tui"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NP: It would be nice to have a full bug URL here, just having a bug number might not be enough for people not familiar with libyui or SUSE.

describe "Menu Item" do
bug = "1177760" # https://bugzilla.suse.com/show_bug.cgi?id=1177760
around(:each) do |ex|
@base = "menu_hotkeys_#{bug}"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where is it used?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in the screenshot filenames: @tui.capture_pane_to("#{@base}-1-initial")

@tui = YastTui.new
@tui.example("MenuBar1") do
ex.run
end
end

it "has hotkeys in menu items, boo##{bug}" do
@tui.await(/File.*Edit.*View/)
@tui.capture_pane_to("#{@base}-1-initial")

@tui.send_keys "M-V" # &View
@tui.capture_pane_to("#{@base}-2-view-menu-activated")

@tui.send_keys "M-N" # &Normal
@tui.capture_pane_to("#{@base}-3-normal-menu-item-activated")

# the label
expect(@tui.capture_pane).to include("Last Event")
# the output
expect(@tui.capture_pane).to include("view_normal")

@tui.send_keys "M-Q" # &Quit
end
end
118 changes: 118 additions & 0 deletions tests/libyui/rspec_tmux_tui.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
require "shellwords"

# Drive interactive TUI (textual user interface) with tmux.
# https://github.com/tmux/tmux
class TmuxTui
class Error < RuntimeError
end

def self.new_session(*args)
new(*args)
end

attr_reader :session_name

# @param session_name [String]
def initialize(session_name: nil)
@session_name = session_name || new_session_name
end

# @param shell_command [String]
# @param xy [(Integer, Integer)]
# @param detach [Boolean]
# @param remain_on_exit [Boolean] useful if shell_command may unexpectedly
# fail quickly. In that case we can still capture the pane
# and read the error messages.
def new_session(shell_command,
xy: [80, 24], detach: true, remain_on_exit: true)

@shell_command = shell_command
@x, @y = xy
@detach = detach

detach_args = @detach ? ["-d"] : []
remain_on_exit_args = if remain_on_exit
["set-hook", "-g", "session-created", "set remain-on-exit on", ";"]
else
[]
end

tmux_ret = system "tmux",
* remain_on_exit_args,
"new-session",
"-s", @session_name,
"-x", @x.to_s,
"-y", @y.to_s,
* detach_args,
"sh", "-c", shell_command

return tmux_ret unless block_given?

yield
ensure_no_session
end

def new_session_name
"tmux-tui-#{rand 10000}"
end

# @return [String]
def capture_pane(color: false)
esc = color ? "-e" : ""
# FIXME: failure of the command?
`tmux capture-pane -t #{session_name.shellescape} -p #{esc}`
end

def capture_pane_to(filename)
txt = capture_pane(color: false)
esc = capture_pane(color: true)
File.write("#{filename}.out.txt", txt)
File.write("#{filename}.out.esc", esc)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer .raw suffix so it's clear that it's the original unprocessed screen dump.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, as I understand it, there is no such thing as a raw terminal dump, unless we're talking about the linux console devices (/dev/vcsa7 having colors for /dev/vcs7). I feel .esc is more specific.

end

def await(pattern)
sleeps = [0.1, 0.2, 0.2, 0.5, 1, 2, 2, 5]
txt = ""
sleeps.each do |sl|
txt = capture_pane
case txt
when pattern
sleep 0.1 # draw the rest of the screen
return nil
else
sleep sl
end
end
raise Error, "Timed out waiting for #{pattern.inspect}. Seen:\n#{txt}"
end

# @param keys [String] "C-X" for Ctrl-X, "M-X" for Alt-X, think "Meta";
# for details see:
# man tmux | less +/"^KEY BINDINGS"
def send_keys(keys)
system "tmux", "send-keys", "-t", session_name, keys
end

def has_session? # rubocop:disable Style/PredicateName
# the method name mimics the tmux command
system "tmux", "has-session", "-t", session_name
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Um, because of that sleep 9999 the session will be always present, or am I wrong ❓

end

def kill_session
system "tmux", "kill-session", "-t", session_name
end

def ensure_no_session
kill_session if has_session?
end
end

class YastTui < TmuxTui
def example(basename, &block)
basename += ".rb" unless basename.end_with? ".rb"
yast_ncurses = "#{__dir__}/yast_ncurses"
example_dir = "/usr/share/doc/packages/yast2-ycp-ui-bindings/examples"

new_session("#{yast_ncurses} #{example_dir}/#{basename}", &block)
end
end
62 changes: 62 additions & 0 deletions tests/libyui/table_sort.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#! /usr/bin/env ruby

require_relative "../test_helper"
require "yast"

if Yast.ui_component == ""
Yast.ui_component = ARGV[0] || "ncurses"
end

module Yast
class TableCellClient < Client
def main
Yast.import "UI"

# notice that neither the ids nor the values are sorted here
contents = [
Item(Id("id-zzz-1-bbb"), "name-bbb", "value-bbb"),
Item(Id("id-yyy-2-ccc"), "name-ccc", "value-ccc"),
Item(Id("id-xxx-3-aaa"), "name-aaa", "value-aaa"),
]
keep_sorting = WFM.Args()[0] == "no-sort"
opts = keep_sorting ? Opt(:keepSorting, :notify) : Opt(:notify)
UI.OpenDialog(
VBox(
Label("Table sorting test"),
MinSize(
25, 8,
Table(Id(:table), opts, Header("Name", "Value"), contents)
),
Label("Enter/Double-click any item to uppercase the value"),
HBox(
HSquash(Label("Current Item: ")),
Label(Id(:current_item), Opt(:outputField, :hstretch), "...")
),
PushButton(Id(:cancel), "&Close")
)
)

if WFM.Args()[0] == "change-current-item"
# test boo#1177145, wrong item is selected
UI.ChangeWidget(Id(:table), :CurrentItem, "id-yyy-2-ccc")
current_item_id = UI.QueryWidget(Id(:table), :CurrentItem)
UI.ChangeWidget(Id(:current_item), :Value, current_item_id.inspect)
end

while UI.UserInput != :cancel
current_item_id = UI.QueryWidget(Id(:table), :CurrentItem)
UI.ChangeWidget(Id(:current_item), :Value, current_item_id.inspect)

value = UI.QueryWidget(:table, Cell(current_item_id, 1))
UI.ChangeWidget(Id(:table), Cell(current_item_id, 1), value.upcase)
end
items = UI.QueryWidget(:table, :Items)
Builtins.y2milestone("Items: %1", items)

UI.CloseDialog
nil
end
end
end

Yast::TableCellClient.new.main
48 changes: 48 additions & 0 deletions tests/libyui/table_sort_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
require_relative "rspec_tmux_tui"

describe "Table" do
context "when it sorts the items," do
around(:each) do |ex|
yast_ncurses = "#{__dir__}/yast_ncurses"
@base = "table_sort"
@tui = TmuxTui.new
@tui.new_session "#{yast_ncurses} #{__dir__}/#{@base}.rb change-current-item" do
ex.run
end
end

bug = "1165388" # https://bugzilla.suse.com/show_bug.cgi?id=1165388
it "ChangeWidget(_, Cell(row, col)) changes the correct cell, boo##{bug}" do
base = @base + "_cell"
@tui.await(/Table sorting test/)
@tui.capture_pane_to("#{base}-1-initial")

@tui.send_keys "Home" # go to first table row
@tui.capture_pane_to("#{base}-2-first-row-selected")

@tui.send_keys "Enter" # activate first table row
@tui.capture_pane_to("#{base}-3-first-row-activated")

expect(@tui.capture_pane).to match(/name-aaa.VALUE-AAA/)

@tui.send_keys "M-C" # &Close
end

bug = "1177145" # https://bugzilla.suse.com/show_bug.cgi?id=1177145
it "ChangeWidget(_, :CurrentItem) activates the correct line, boo##{bug}" do
pending "not fixed yet"

base = @base + "_current_item"
@tui.await(/Table sorting test/)
@tui.capture_pane_to("#{base}-1-ccc-selected")
# the UI code performs a
# UI.ChangeWidget(Id(:table), :CurrentItem, "id-yyy-2-ccc")
# then
# UI.QueryWidget(Id(:table), :CurrentItem)
@tui.send_keys "Enter" # activate the current item to produce an event
expect(@tui.capture_pane).to match(/Current Item: "id-yyy-2-ccc"/)

@tui.send_keys "M-C" # &Close
end
end
end
5 changes: 5 additions & 0 deletions tests/libyui/yast_ncurses
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#! /usr/bin/env ruby
require_relative "../test_helper"
require "yast"
Yast.ui_component = "ncurses"
load ARGV[0]