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 13 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)
36 changes: 36 additions & 0 deletions tests/libyui/menu_hotkeys_1177760_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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"
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")


yast_ncurses = "#{__dir__}/yast_ncurses"
example_dir = "/usr/share/doc/packages/yast2-ycp-ui-bindings/examples"
@tui = TmuxTui.new_session "#{yast_ncurses} #{example_dir}/MenuBar1.rb"
Copy link
Member

Choose a reason for hiding this comment

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

I expect this whole creation need to be redone in each test. So why not simply have helper in TmuxTui and have something like @tui = TmuxTui.testing_client("MenuBar1.rb") instead of those three lines?


ex.run

if @tui.has_session?
@tui.kill_session
Copy link
Member

Choose a reason for hiding this comment

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

why two methods? I would have just one like kill_session that internally check if session exists.

Copy link
Member

Choose a reason for hiding this comment

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

NP: Maybe we should print a warning on STDERR in this case, each test tries to finish cleanly (by pressing "Quit" or "Close" button), if the test is still running then something went wrong.

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
76 changes: 76 additions & 0 deletions tests/libyui/rspec_tmux_tui.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
require "shellwords"

class TmuxTui
class Error < RuntimeError
end

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

attr_reader :session_name

def initialize(shell_command, x: 80, y: 24, detach: true, session_name: nil)
@shell_command = shell_command
@x = x
@y = y
@detach = detach
@session_name = session_name || new_session_name

system "tmux", "new-session",
"-s", @session_name,
"-x", @x.to_s,
"-y", @y.to_s,
*(@detach ? ["-d"] : [] ),
"sh", "-c", "#{@shell_command}; sleep 9999"
Copy link
Member

Choose a reason for hiding this comment

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

is that long sleep still needed? I think you can do in rspec specific expect name like expect(@tui).to include_in_pane("Last Event") and have own specific error handler there if you need to somehow capture it.

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
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?
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
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
47 changes: 47 additions & 0 deletions tests/libyui/table_sort_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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_session "#{yast_ncurses} #{__dir__}/#{@base}.rb change-current-item"
ex.run
@tui.kill_session if @tui.has_session?
end

bug = "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"
it "ChangeWidget(_, :CurrentItem) activates the correct line, boo##{bug}" do
skip "not fixed yet"
Copy link
Member

Choose a reason for hiding this comment

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

Maybe marking as pending would be better than skipping completely?

https://relishapp.com/rspec/rspec-core/v/3-8/docs/pending-and-skipped-examples


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]