yui_rest_client 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/publish.yml +33 -0
  3. data/.github/workflows/test.yml +26 -0
  4. data/.gitignore +15 -0
  5. data/.rspec +3 -0
  6. data/.rubocop.yml +41 -0
  7. data/Gemfile +8 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +216 -0
  10. data/Rakefile +20 -0
  11. data/bin/console +15 -0
  12. data/bin/setup +8 -0
  13. data/lib/yui_rest_client.rb +42 -0
  14. data/lib/yui_rest_client/actions.rb +12 -0
  15. data/lib/yui_rest_client/app.rb +230 -0
  16. data/lib/yui_rest_client/error.rb +13 -0
  17. data/lib/yui_rest_client/filter_extractor.rb +28 -0
  18. data/lib/yui_rest_client/http/http_client.rb +36 -0
  19. data/lib/yui_rest_client/http/response.rb +21 -0
  20. data/lib/yui_rest_client/http/version_controller.rb +26 -0
  21. data/lib/yui_rest_client/http/widget_controller.rb +54 -0
  22. data/lib/yui_rest_client/local_process.rb +73 -0
  23. data/lib/yui_rest_client/logger.rb +32 -0
  24. data/lib/yui_rest_client/timer.rb +20 -0
  25. data/lib/yui_rest_client/version.rb +6 -0
  26. data/lib/yui_rest_client/wait.rb +21 -0
  27. data/lib/yui_rest_client/waitable.rb +39 -0
  28. data/lib/yui_rest_client/widgets.rb +30 -0
  29. data/lib/yui_rest_client/widgets/bargraph.rb +62 -0
  30. data/lib/yui_rest_client/widgets/base.rb +114 -0
  31. data/lib/yui_rest_client/widgets/button.rb +33 -0
  32. data/lib/yui_rest_client/widgets/checkbox.rb +53 -0
  33. data/lib/yui_rest_client/widgets/combobox.rb +95 -0
  34. data/lib/yui_rest_client/widgets/datefield.rb +47 -0
  35. data/lib/yui_rest_client/widgets/label.rb +41 -0
  36. data/lib/yui_rest_client/widgets/menubutton.rb +48 -0
  37. data/lib/yui_rest_client/widgets/multilinebox.rb +84 -0
  38. data/lib/yui_rest_client/widgets/numberbox.rb +76 -0
  39. data/lib/yui_rest_client/widgets/progressbar.rb +45 -0
  40. data/lib/yui_rest_client/widgets/radiobutton.rb +35 -0
  41. data/lib/yui_rest_client/widgets/richtext.rb +36 -0
  42. data/lib/yui_rest_client/widgets/selectionbox.rb +87 -0
  43. data/lib/yui_rest_client/widgets/tab.rb +81 -0
  44. data/lib/yui_rest_client/widgets/table.rb +154 -0
  45. data/lib/yui_rest_client/widgets/textbox.rb +94 -0
  46. data/lib/yui_rest_client/widgets/timefield.rb +45 -0
  47. data/lib/yui_rest_client/widgets/tree.rb +149 -0
  48. data/yui_rest_client.gemspec +38 -0
  49. metadata +222 -0
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YuiRestClient
4
+ module Actions
5
+ PRESS = 'press'
6
+ TOGGLE = 'toggle'
7
+ CHECK = 'check'
8
+ UNCHECK = 'uncheck'
9
+ SELECT = 'select'
10
+ ENTER_TEXT = 'enter_text'
11
+ end
12
+ end
@@ -0,0 +1,230 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YuiRestClient
4
+ class App
5
+ # Used to initialize main entry point of YuiRestClient and set host and port
6
+ # for the application under control.
7
+ # @param host [String] host address (e.g. 'localhost', '192.168.0.1')
8
+ # @param port [String] port opened for communication (e.g. '9999')
9
+ def initialize(host:, port:)
10
+ @host = host
11
+ @port = port
12
+ @widget_controller = Http::WidgetController.new(host: host, port: port)
13
+ @version_controller = Http::VersionController.new(host: host, port: port)
14
+ end
15
+
16
+ # Initializes new instance of Bargraph with the filter provided.
17
+ # Does not make request to libyui-rest-api.
18
+ # @param filter [Hash] filter to find a widget
19
+ # @return [Widgets::Bargraph] new instance of Table
20
+ # @example
21
+ # app.bargraph(id: 'id', label: 'label', class: 'YBarGraph')
22
+ def bargraph(filter)
23
+ Widgets::Bargraph.new(@widget_controller, FilterExtractor.new(filter))
24
+ end
25
+
26
+ # Initializes new instance of Button with the filter provided.
27
+ # Does not make request to libyui-rest-api.
28
+ # @param filter [Hash] filter to find a widget
29
+ # @return [Widgets::Button] new instance of Button
30
+ # @example
31
+ # app.button(id: 'id', label: 'label', class: 'YPushButton')
32
+ def button(filter)
33
+ Widgets::Button.new(@widget_controller, FilterExtractor.new(filter))
34
+ end
35
+
36
+ # Initializes new instance of Checkbox with the filter provided.
37
+ # Does not make request to libyui-rest-api.
38
+ # @param filter [Hash] filter to find a widget
39
+ # @return [Widgets::Checkbox] new instance of Checkbox
40
+ # @example
41
+ # app.checkbox(id: 'id', label: 'label', class: 'YCheckBox')
42
+ def checkbox(filter)
43
+ Widgets::Checkbox.new(@widget_controller, FilterExtractor.new(filter))
44
+ end
45
+
46
+ # Initializes new instance of Combobox with the filter provided.
47
+ # Does not make request to libyui-rest-api.
48
+ # @param filter [Hash] filter to find a widget
49
+ # @return [Widgets::Combobox] new instance of Combobox
50
+ # @example
51
+ # app.combobox(id: 'id', label: 'label', class: 'YComboBox')
52
+ def combobox(filter)
53
+ Widgets::Combobox.new(@widget_controller, FilterExtractor.new(filter))
54
+ end
55
+
56
+ # Initializes new instance of Datefield with the filter provided.
57
+ # Does not make request to libyui-rest-api.
58
+ # @param filter [Hash] filter to find a widget
59
+ # @return [Widgets::Datefield] new instance of Datefield
60
+ # @example
61
+ # app.datefield(id: 'id', label: 'label', class: 'YDateField')
62
+ def datefield(filter)
63
+ Widgets::Datefield.new(@widget_controller, FilterExtractor.new(filter))
64
+ end
65
+
66
+ # Initializes new instance of Label with the filter provided.
67
+ # Does not make request to libyui-rest-api.
68
+ # @param filter [Hash] filter to find a widget
69
+ # @return [Widgets::Label] new instance of Label
70
+ # @example
71
+ # app.label(id: 'id', label: 'label', class: 'YLabel')
72
+ def label(filter)
73
+ Widgets::Label.new(@widget_controller, FilterExtractor.new(filter))
74
+ end
75
+
76
+ # Initializes new instance of Menubutton with the filter provided.
77
+ # Does not make request to libyui-rest-api.
78
+ # @param filter [Hash] filter to find a widget
79
+ # @return [Widgets::Menubutton] new instance of Menubutton
80
+ # @example
81
+ # app.menubutton(id: 'id', label: 'label', class: 'YMenuButton')
82
+ def menubutton(filter)
83
+ Widgets::Menubutton.new(@widget_controller, FilterExtractor.new(filter))
84
+ end
85
+
86
+ # Initializes new instance of Multilinebox with the filter provided.
87
+ # Does not make request to libyui-rest-api.
88
+ # @param filter [Hash] filter to find a widget
89
+ # @return [Widgets::Multilinebox] new instance of Multilinebox
90
+ # @example
91
+ # app.multilinebox(id: 'id', label: 'label', class: 'YMultiLineEdit')
92
+ def multilinebox(filter)
93
+ Widgets::Multilinebox.new(@widget_controller, FilterExtractor.new(filter))
94
+ end
95
+
96
+ # Initializes new instance of Numberbox with the filter provided.
97
+ # Does not make request to libyui-rest-api.
98
+ # @param filter [Hash] filter to find a widget
99
+ # @return [Widgets::Numberbox] new instance of Numberbox
100
+ # @example
101
+ # app.numberbox(id: 'id', label: 'label', class: 'YIntField')
102
+ def numberbox(filter)
103
+ Widgets::Numberbox.new(@widget_controller, FilterExtractor.new(filter))
104
+ end
105
+
106
+ # Initializes new instance of Progressbar with the filter provided.
107
+ # Does not make request to libyui-rest-api.
108
+ # @param filter [Hash] filter to find a widget
109
+ # @return [Widgets::Progressbar] new instance of Progressbar
110
+ # @example
111
+ # app.progressbar(id: 'id', label: 'label', class: 'YProgressBar')
112
+ def progressbar(filter)
113
+ Widgets::Progressbar.new(@widget_controller, FilterExtractor.new(filter))
114
+ end
115
+
116
+ # Initializes new instance of Radiobutton with the filter provided.
117
+ # Does not make request to libyui-rest-api.
118
+ # @param filter [Hash] filter to find a widget
119
+ # @return [Widgets::Radiobutton] new instance of Radiobutton
120
+ # @example
121
+ # app.radiobutton(id: 'id', label: 'label', class: 'YRadioButton')
122
+ def radiobutton(filter)
123
+ Widgets::Radiobutton.new(@widget_controller, FilterExtractor.new(filter))
124
+ end
125
+
126
+ # Initializes new instance of Richtext with the filter provided.
127
+ # Does not make request to libyui-rest-api.
128
+ # @param filter [Hash] filter to find a widget
129
+ # @return [Widgets::Richtext] new instance of Richtext
130
+ # @example
131
+ # app.richtext(id: 'id', label: 'label', class: 'YRichText')
132
+ def richtext(filter)
133
+ Widgets::Richtext.new(@widget_controller, FilterExtractor.new(filter))
134
+ end
135
+
136
+ # Initializes new instance of Selectionbox with the filter provided.
137
+ # Does not make request to libyui-rest-api.
138
+ # @param filter [Hash] filter to find a widget
139
+ # @return [Widgets::Selectionbox] new instance of Selectionbox
140
+ # @example
141
+ # app.selectionbox(id: 'id', label: 'label', class: 'YSelectionBox')
142
+ def selectionbox(filter)
143
+ Widgets::Selectionbox.new(@widget_controller, FilterExtractor.new(filter))
144
+ end
145
+
146
+ # Initializes new instance of Tab with the filter provided.
147
+ # Does not make request to libyui-rest-api.
148
+ # @param filter [Hash] filter to find a widget
149
+ # @return [Widgets::Tab] new instance of Tab
150
+ # @example
151
+ # app.tab(id: 'id', label: 'label', class: 'YDumbTab')
152
+ def tab(filter)
153
+ Widgets::Tab.new(@widget_controller, FilterExtractor.new(filter))
154
+ end
155
+
156
+ # Initializes new instance of Table with the filter provided.
157
+ # Does not make request to libyui-rest-api.
158
+ # @param filter [Hash] filter to find a widget
159
+ # @return [Widgets::Table] new instance of Table
160
+ # @example
161
+ # app.table(id: 'id', label: 'label', class: 'YTable')
162
+ def table(filter)
163
+ Widgets::Table.new(@widget_controller, FilterExtractor.new(filter))
164
+ end
165
+
166
+ # Initializes new instance of time field with the filter provided.
167
+ # Does not make request to libyui-rest-api.
168
+ # @param filter [Hash] filter to find a widget
169
+ # @return [Widgets::Timefield] new instance of Table
170
+ # @example
171
+ # app.timefield(id: 'id', label: 'label', class: 'YTimeField')
172
+ def timefield(filter)
173
+ Widgets::Timefield.new(@widget_controller, FilterExtractor.new(filter))
174
+ end
175
+
176
+ # Initializes new instance of Textbox with the filter provided.
177
+ # Does not make request to libyui-rest-api.
178
+ # @param filter [Hash] filter to find a widget
179
+ # @return [Widgets::Textbox] new instance of Textbox
180
+ # @example
181
+ # app.textbox(id: 'id', label: 'label', class: 'YInputField')
182
+ def textbox(filter)
183
+ Widgets::Textbox.new(@widget_controller, FilterExtractor.new(filter))
184
+ end
185
+
186
+ # Initializes new instance of Tree with the filter provided.
187
+ # Does not make request to libyui-rest-api.
188
+ # @param filter [Hash] filter to find a widget
189
+ # @return [Widgets::Tree] new instance of Tree
190
+ # @example
191
+ # app.tree(id: 'id', label: 'label', class: 'YTree')
192
+ def tree(filter)
193
+ Widgets::Tree.new(@widget_controller, FilterExtractor.new(filter))
194
+ end
195
+
196
+ # Initializes new instance of Wizard with the filter provided.
197
+ # Does not make request to libyui-rest-api.
198
+ # @param filter [Hash] filter to find a widget
199
+ # @return [Widgets::Wizard] new instance of Wizard
200
+ # @example
201
+ # app.wizard(id: 'id', label: 'label', class: 'YWizard')
202
+ def wizard(filter)
203
+ Widgets::Wizard.new(@widget_controller, FilterExtractor.new(filter))
204
+ end
205
+
206
+ # Returns client side libyui REST API version
207
+ # @return libyui client REST API version
208
+ def client_api_version
209
+ API_VERSION
210
+ end
211
+
212
+ # Returns server side libyui REST API version
213
+ # @return libyui server REST API version
214
+ def server_api_version
215
+ @version_controller.api_version
216
+ end
217
+
218
+ # Validates if server side REST API is compatible with client inside
219
+ # @return true if version is compatible, false if not or any error while
220
+ # receiving version from the server
221
+ def check_api_version
222
+ YuiRestClient.logger.info("Client API version: #{API_VERSION}")
223
+ server_api_v = server_api_version
224
+ raise Error::YuiRestClientError if server_api_v.nil?
225
+
226
+ YuiRestClient.logger.info("Server API version: #{server_api_v}")
227
+ server_api_v <= client_api_version
228
+ end
229
+ end
230
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YuiRestClient
4
+ module Error
5
+ class YuiRestClientError < StandardError; end
6
+
7
+ class TimeoutError < YuiRestClientError; end
8
+
9
+ class WidgetNotFoundError < YuiRestClientError; end
10
+
11
+ class ItemNotFoundInWidgetError < YuiRestClientError; end
12
+ end
13
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YuiRestClient
4
+ class FilterExtractor
5
+ attr_reader :full, :plain, :regex
6
+
7
+ def initialize(filter)
8
+ @full = build_filters(filter)
9
+ @plain = @full.reject { |_, v| v.is_a? Regexp }
10
+ @regex = @full.select { |_, v| v.is_a? Regexp }
11
+ end
12
+
13
+ def to_s
14
+ full.to_s
15
+ end
16
+
17
+ private
18
+
19
+ def build_filters(hash)
20
+ filter = {}
21
+ filter[:id] = hash[:id]
22
+ # Replace '&' in label filter as search is not possible when it contains the character
23
+ filter[:label] = hash[:label].is_a?(String) ? hash[:label].tr('&', '') : hash[:label]
24
+ filter[:type] = hash[:class] if hash.key?(:class)
25
+ filter.compact
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YuiRestClient
4
+ module Http
5
+ module HttpClient
6
+ module_function
7
+
8
+ def http_get(uri)
9
+ YuiRestClient.logger.debug("Request: [GET] #{uri}")
10
+ res = Net::HTTP.get_response(uri)
11
+ YuiRestClient.logger.debug("Response: [#{res.code}]\n#{res.body}")
12
+ res
13
+ end
14
+
15
+ def http_post(uri)
16
+ YuiRestClient.logger.debug("Request: [POST] #{uri}")
17
+ # a trick how to add query parameters to a POST request,
18
+ # the usual Net::HTTP.post(uri, data) does not allow using a query
19
+ req = Net::HTTP::Post.new("#{uri.path}?#{uri.query}")
20
+ http = Net::HTTP.new(uri.host, uri.port)
21
+ res = http.request(req)
22
+ YuiRestClient.logger.debug("Response: [#{res.code}]\n#{res.body}")
23
+ res
24
+ end
25
+
26
+ def compose_uri(host, port, path, params = {})
27
+ URI::HTTP.build(
28
+ host: host,
29
+ port: port,
30
+ path: path,
31
+ query: URI.encode_www_form(params)
32
+ )
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YuiRestClient
4
+ module Http
5
+ class Response
6
+ def initialize(res)
7
+ @res = res
8
+ end
9
+
10
+ def body(regex_filter: {})
11
+ result = JSON.parse(@res.body, symbolize_names: true)
12
+ result.select do |widget|
13
+ regex_filter.all? { |key, value| value.match(widget[key.to_sym]) }
14
+ end
15
+ rescue JSON::ParserError => e
16
+ YuiRestClient.logger.error("Error while parsing JSON from response:\n"\
17
+ "#{e.message}\n#{e.backtrace.inspect}")
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YuiRestClient
4
+ module Http
5
+ class VersionController
6
+ def initialize(host:, port:)
7
+ @host = host
8
+ @port = port
9
+ @timeout = YuiRestClient.timeout
10
+ @interval = YuiRestClient.interval
11
+ end
12
+
13
+ # Gets server api version, so one could compare compatibility and detect
14
+ # if newer version was deployed
15
+ # @return server side REST API version
16
+ def api_version
17
+ Wait.until(timeout: @timeout, interval: @interval) do
18
+ res = HttpClient.http_get(HttpClient.compose_uri(@host, @port, '/version'))
19
+ raise Error::YuiRestClientError unless res.is_a?(Net::HTTPOK)
20
+
21
+ JSON.parse(res.body)['api_version']
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YuiRestClient
4
+ module Http
5
+ class WidgetController
6
+ def initialize(host:, port:)
7
+ @host = host
8
+ @port = port
9
+ @timeout = YuiRestClient.timeout
10
+ @interval = YuiRestClient.interval
11
+ end
12
+
13
+ # Find a widget using the filter.
14
+ # @param filter [Hash] identifiers to find a widget
15
+ # @return [Response]
16
+ def find(filter)
17
+ res = nil
18
+ Wait.until(timeout: @timeout, interval: @interval) do
19
+ uri = HttpClient.compose_uri(@host, @port, "/#{API_VERSION}/widgets", filter)
20
+ res = HttpClient.http_get(uri)
21
+ Response.new(res) if res.is_a?(Net::HTTPOK)
22
+ end
23
+ rescue Error::TimeoutError
24
+ rescue_errors(res)
25
+ end
26
+
27
+ # Perform an action on the widget.
28
+ # @param filter [Hash] identifiers to find a widget
29
+ # @param action [Hash] what to do with the widget
30
+ # @return [Response]
31
+ def send_action(filter, action)
32
+ res = nil
33
+ Wait.until(timeout: @timeout, interval: @interval) do
34
+ uri = HttpClient.compose_uri(@host, @port, "/#{API_VERSION}/widgets",
35
+ filter.merge(action))
36
+ res = HttpClient.http_post(uri)
37
+ Response.new(res) if res.is_a?(Net::HTTPOK)
38
+ end
39
+ rescue Error::TimeoutError
40
+ rescue_errors(res)
41
+ end
42
+
43
+ private
44
+
45
+ def rescue_errors(response)
46
+ raise Error::WidgetNotFoundError if response.is_a?(Net::HTTPNotFound)
47
+
48
+ raise Error::ItemNotFoundInWidgetError if response.is_a?(Net::HTTPUnprocessableEntity)
49
+
50
+ raise Error::YuiRestClientError
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Client to interact with YAST UI rest api framework for integration testing
4
+ module YuiRestClient
5
+ class LocalProcess
6
+ # default timeout for process
7
+ DEFAULT_TIMEOUT_PROCESS = 2
8
+
9
+ # start the application in background
10
+ # @param application [String] the command to start
11
+ def start_app(application)
12
+ @app_host = 'localhost'
13
+ @app_port = port
14
+
15
+ # another app already running?
16
+ raise "The port #{@app_host}:#{@app_port} is already open!" if port_open?(@app_host, @app_port)
17
+
18
+ YuiRestClient.logger.debug("Starting #{application}...")
19
+ # create a new process group so easily we will be able
20
+ # to kill all its sub-processes
21
+ @app_pid = spawn(application, pgroup: true)
22
+ wait_for_port(@app_host, @app_port)
23
+ YuiRestClient.logger.debug("App started: '#{application}'")
24
+ end
25
+
26
+ # kill the process if it is still running after finishing a scenario
27
+ def kill_app
28
+ return unless @app_pid
29
+
30
+ Process.waitpid(@app_pid, Process::WNOHANG)
31
+ YuiRestClient.logger.debug("Sending KILL signal for PID #{@app_pid}")
32
+ Process.kill('-KILL', @app_pid)
33
+ rescue Errno::ECHILD, Errno::ESRCH
34
+ # the process has already exited
35
+ @app_pid = nil
36
+ end
37
+
38
+ private
39
+
40
+ # set the application introspection port for communication
41
+ def port
42
+ ENV['YUI_HTTP_PORT'] ||= '9999'
43
+ end
44
+
45
+ # is the target port open?
46
+ # @param host [String] the host to connect to
47
+ # @param port [Integer] the port number
48
+ # @return [Boolean] true if the port is open, false otherwise
49
+ def port_open?(host, port)
50
+ TCPSocket.new(host, port).close
51
+ true
52
+ rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
53
+ false
54
+ end
55
+
56
+ # wait until the specified port is open or until the timeout is reached
57
+ # @param host [String] the host to connect to
58
+ # @param port [Integer] the port number
59
+ # @raise YuiRestClient::Error::TimeoutError if the port is not opened in time
60
+ def wait_for_port(host, port)
61
+ Wait.until(timeout: YuiRestClient.timeout, interval: YuiRestClient.interval) do
62
+ YuiRestClient.logger.debug("Waiting for #{host}:#{port}...")
63
+ port_open?(host, port)
64
+ end
65
+ end
66
+
67
+ # optionally allow a short delay between the steps to watch the UI changes
68
+ def add_step_delay
69
+ delay = ENV['STEP_DELAY'].to_f
70
+ sleep(delay) if delay.positive?
71
+ end
72
+ end
73
+ end