unobtainium-cucumber 0.1.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.
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ #
3
+ # unobtainium-cucumber
4
+ # https://github.com/jfinkhaeuser/unobtainium-cucumber
5
+ #
6
+ # Copyright (c) 2016 Jens Finkhaeuser and other unobtainium-cucumber
7
+ # contributors. All rights reserved.
8
+ #
9
+
10
+ # Code coverage first
11
+ require 'simplecov'
12
+ SimpleCov.start do
13
+ add_filter 'features'
14
+ end
15
+
16
+ # Other requires after SimpleCov
17
+ require "unobtainium-cucumber"
18
+
19
+ # Extensions used for testing
20
+ module MyExtensions
21
+ def method_from_own_extension(*args, &block); end
22
+ end # module MyExtensions
23
+
24
+ World(MyExtensions)
25
+
26
+ # Utility code for testing
27
+ require_relative './utils'
@@ -0,0 +1,47 @@
1
+ # coding: utf-8
2
+ #
3
+ # unobtainium-cucumber
4
+ # https://github.com/jfinkhaeuser/unobtainium-cucumber
5
+ #
6
+ # Copyright (c) 2016 Jens Finkhaeuser and other unobtainium-cucumber
7
+ # contributors. All rights reserved.
8
+ #
9
+
10
+ TIMESTAMP_REGEX ||= /\d{4}_\d{2}_\d{2}T\d{2}_\d{2}_\d{2}Z/
11
+
12
+ ##
13
+ # Given a file name, extrat and convert timestamps
14
+ def timestamp_from_filename(filename)
15
+ # First, check if we can find the file name matching a timestamp
16
+ parts = File.split(filename)
17
+ parts = parts[-1].split('-')
18
+ timestamp = parts[0]
19
+ if not TIMESTAMP_REGEX.match(timestamp)
20
+ return
21
+ end
22
+
23
+ # Great, then fix the formatting...
24
+ timestamp[4] = '-'
25
+ timestamp[7] = '-'
26
+ timestamp[13] = ':'
27
+ timestamp[16] = ':'
28
+
29
+ return Time.parse(timestamp)
30
+ end
31
+
32
+ ##
33
+ # Given a match pattern and optional timeout, finds files matching the pattern
34
+ # that have been created within the given timeout from now.
35
+ def timeout_match_files(pattern, timeout = 5)
36
+ now = Time.now.utc
37
+
38
+ Dir[pattern].each do |fname|
39
+ ts = timestamp_from_filename(fname)
40
+ if (now - ts).abs <= timeout
41
+ return fname
42
+ end
43
+ end
44
+
45
+ raise "No file matching '#{pattern}' with a timestamp in the last #{timeout} "\
46
+ "seconds was found!"
47
+ end
@@ -0,0 +1,40 @@
1
+ # coding: utf-8
2
+ #
3
+ # unobtainium-cucumber
4
+ # https://github.com/jfinkhaeuser/unobtainium-cucumber
5
+ #
6
+ # Copyright (c) 2016 Jens Finkhaeuser and other unobtainium-cucumber
7
+ # contributors. All rights reserved.
8
+ #
9
+
10
+ require 'unobtainium-cucumber/action/support/naming'
11
+
12
+ module Unobtainium
13
+ module Cucumber
14
+
15
+ ##
16
+ # Namespace for built-in status actions
17
+ module Action
18
+
19
+ class << self
20
+ include Support
21
+
22
+ ##
23
+ # Status action function that stores the page content (main page only)
24
+ def store_content(world, scenario)
25
+ # Make sure the content directory exists.
26
+ basedir = File.join(Dir.pwd, 'content')
27
+ FileUtils.mkdir_p(basedir)
28
+
29
+ # Store content. Note that not all drivers may support this.
30
+ filename = File.join(basedir, base_filename(scenario))
31
+ filename += '.txt'
32
+
33
+ File.open(filename, 'w') do |file|
34
+ file.write(world.driver.page_source)
35
+ end
36
+ end
37
+ end # class << self
38
+ end # module Action
39
+ end # module Cucumber
40
+ end # module Unobtainium
@@ -0,0 +1,37 @@
1
+ # coding: utf-8
2
+ #
3
+ # unobtainium-cucumber
4
+ # https://github.com/jfinkhaeuser/unobtainium-cucumber
5
+ #
6
+ # Copyright (c) 2016 Jens Finkhaeuser and other unobtainium-cucumber
7
+ # contributors. All rights reserved.
8
+ #
9
+
10
+ require 'unobtainium-cucumber/action/support/naming'
11
+
12
+ module Unobtainium
13
+ module Cucumber
14
+
15
+ ##
16
+ # Namespace for built-in status actions
17
+ module Action
18
+
19
+ class << self
20
+ include Support
21
+
22
+ ##
23
+ # Status action function that takes a screenshot.
24
+ def store_screenshot(world, scenario)
25
+ # Make sure the screenshots directory exists.
26
+ basedir = File.join(Dir.pwd, 'screenshots')
27
+ FileUtils.mkdir_p(basedir)
28
+
29
+ # Take screenshot.
30
+ filename = File.join(basedir, base_filename(scenario))
31
+ filename += '.png'
32
+ world.driver.save_screenshot(filename)
33
+ end
34
+ end # class << self
35
+ end # module Action
36
+ end # module Cucumber
37
+ end # module Unobtainium
@@ -0,0 +1,44 @@
1
+ # coding: utf-8
2
+ #
3
+ # unobtainium-cucumber
4
+ # https://github.com/jfinkhaeuser/unobtainium-cucumber
5
+ #
6
+ # Copyright (c) 2016 Jens Finkhaeuser and other unobtainium-cucumber
7
+ # contributors. All rights reserved.
8
+ #
9
+ module Unobtainium
10
+ module Cucumber
11
+ module Action
12
+
13
+ ##
14
+ # Support functions for actions
15
+ module Support
16
+
17
+ ##
18
+ # Given a cucumber scenario, this function returns a timestamped
19
+ # base filename (without extension) that reflects parts of the scenario
20
+ # name.
21
+ # Note that the optional tag is not related to cucumber tags. It's just
22
+ # a way to distinguish two filenames for the same scenario at the same
23
+ # timestamp.
24
+ # We use '_' as a replacement for unrenderable characters, and
25
+ # '-' as a separator between file name components.
26
+ def base_filename(scenario, tag = nil, timestamp = nil)
27
+ # Build base name from parameters
28
+ require 'time'
29
+ timestamp ||= Time.now.utc.iso8601
30
+ timestamp.tr!('-', '_')
31
+ scenario_name = scenario.name
32
+ base_name = [timestamp, tag, scenario_name].reject(&:nil?).join('-')
33
+
34
+ # Make base name filename safe
35
+ base_name.gsub!(/^.*(\\|\/)/, '')
36
+ base_name.gsub!(/[^0-9A-Za-z.\-]+/, '_')
37
+
38
+ return base_name
39
+ end
40
+
41
+ end # module Support
42
+ end # module Action
43
+ end # module Cucumber
44
+ end # module Unobtainium
@@ -0,0 +1,34 @@
1
+ # coding: utf-8
2
+ #
3
+ # unobtainium-cucumber
4
+ # https://github.com/jfinkhaeuser/unobtainium-cucumber
5
+ #
6
+ # Copyright (c) 2016 Jens Finkhaeuser and other unobtainium-cucumber
7
+ # contributors. All rights reserved.
8
+ #
9
+
10
+ After do |_|
11
+ if not driver.respond_to? :reset
12
+ next
13
+ end
14
+
15
+ # Covering this would require two separate test suites. I'm not sure I'm
16
+ # ready for that.
17
+ # :nocov:
18
+ if config['cucumber.driver_reset'] == false
19
+ next
20
+ end
21
+ # :nocov:
22
+
23
+ begin
24
+ driver.reset
25
+ rescue => error
26
+ # :nocov:
27
+ # If driver reset isn't possible, it doesn't make sense to continue
28
+ puts "DRIVER RESET WASN'T POSSIBLE DUE TO #{error} - BUT IS REQUIRED "\
29
+ "FOR BEFORE NEXT SCENARIO. WILL EXIT THE PROGRAM"
30
+ driver.quit
31
+ Kernel.exit(2)
32
+ # :nocov:
33
+ end
34
+ end
@@ -0,0 +1,272 @@
1
+ # coding: utf-8
2
+ #
3
+ # unobtainium-cucumber
4
+ # https://github.com/jfinkhaeuser/unobtainium-cucumber
5
+ #
6
+ # Copyright (c) 2016 Jens Finkhaeuser and other unobtainium-cucumber
7
+ # contributors. All rights reserved.
8
+ #
9
+
10
+ require 'unobtainium-cucumber/action/screenshot'
11
+ require 'unobtainium-cucumber/action/content'
12
+
13
+ module Unobtainium
14
+ module Cucumber
15
+ ##
16
+ # The StatusActions module contains all functionality for registering
17
+ # actions to be run when a scenario or outline ended with a particular
18
+ # status.
19
+ module StatusActions
20
+ # Key for storing actions in unobtainium's runtime instance.
21
+ RUNTIME_KEY = 'unobtainium-cucumber-status-actions'.freeze
22
+
23
+ # Default status actions
24
+ DEFAULTS = {
25
+ failed?: [
26
+ '::Unobtainium::Cucumber::Action.store_screenshot',
27
+ ]
28
+ }.freeze
29
+
30
+ ##
31
+ # Register a action for a :passed? or :failed? scenario.
32
+ # The :type parameter may either be :scenario or :outline.
33
+ # The action will be passed the matching scenario. If no
34
+ # explicit action is given, a block is also acceptable.
35
+ def register_action(status, action = nil, options = nil, &block)
36
+ # If the action is a Hash, then the options should be nil. If that's
37
+ # the case, we have no action and instead got passed options.
38
+ if action.is_a? Hash
39
+ if not options.nil?
40
+ raise "Can't pass a Hash as an action!"
41
+ end
42
+ options = action
43
+ action = nil
44
+ end
45
+
46
+ # Parameter checks!
47
+ if not [:passed?, :failed?].include?(status)
48
+ raise "Status may be one of :passed? or :failed? only!"
49
+ end
50
+
51
+ options ||= {}
52
+ type = options[:type] || :scenario
53
+ if not [:scenario, :outline].include?(type)
54
+ raise "The :type option may be one of :scenario or :outline only!"
55
+ end
56
+
57
+ if action.nil? and block.nil?
58
+ raise "Must provide either an action method or a block!"
59
+ end
60
+ if not action.nil? and not block.nil?
61
+ raise "Cannot provide both an action method and a block!"
62
+ end
63
+
64
+ callback = action || block
65
+
66
+ # The key to store callbacks under is comprised of the status
67
+ # and the type. That way we can match precisely when actions are to
68
+ # be executed.
69
+ key = [status, type]
70
+
71
+ # Retrieve existing actions
72
+ actions = {}
73
+ if ::Unobtainium::Runtime.instance.has?(RUNTIME_KEY)
74
+ actions = ::Unobtainium::Runtime.instance.fetch(RUNTIME_KEY)
75
+ end
76
+
77
+ # Add the callback
78
+ actions[key] ||= []
79
+ actions[key] |= [callback]
80
+
81
+ # And store the callback actions again.
82
+ ::Unobtainium::Runtime.instance.store(RUNTIME_KEY, actions)
83
+ end
84
+
85
+ ##
86
+ # For a given scenario, return the matching key for the actions.
87
+ def action_key(scenario)
88
+ return [
89
+ (scenario.passed? ? :passed? : :failed?),
90
+ (scenario.outline? ? :outline : :scenario)
91
+ ]
92
+ end
93
+
94
+ ##
95
+ # Automatically register all configured status actions.
96
+ # This is largely an internal function, run after the first scenario
97
+ # and before status actions are executed.
98
+ def register_config_actions(world)
99
+ to_register = world.config['cucumber.status_actions'] || DEFAULTS
100
+
101
+ [:passed?, :failed?].each do |status|
102
+ for_status = to_register[status]
103
+ if for_status.nil?
104
+ # :nocov:
105
+ next
106
+ # :nocov:
107
+ end
108
+
109
+ # If the entry for the status is an Array, it applies to scenarios
110
+ # and outlines equally. Otherwise we'll have to find a Hash with
111
+ # entries for either or both.
112
+ actions = {}
113
+ if for_status.is_a?(Array)
114
+ actions[:scenario] = for_status
115
+ actions[:outline] = for_status
116
+ elsif for_status.is_a?(Hash)
117
+ actions[:scenario] = for_status[:scenario] || []
118
+ actions[:outline] = for_status[:outline] || []
119
+ else
120
+ # :nocov:
121
+ raise "Cannot interpret status action configuration for status "\
122
+ "#{status}; it should be an Array or Hash, but instead it was"\
123
+ " this: #{for_status}"
124
+ # :nocov:
125
+ end
126
+
127
+ # Now we have actions for the statuses, we can register them.
128
+ [:scenario, :outline].each do |type|
129
+ actions[type].each do |action|
130
+ register_action(status, action, type: type)
131
+ end
132
+ end
133
+ end
134
+ end
135
+
136
+ ##
137
+ # Given an action and a scenario, execute the action. This includes
138
+ # late/lazy resolution of String or Symbol actions.
139
+ def execute_action(world, action, scenario)
140
+ # Simplest case first: the action is already callable.
141
+ if action.respond_to?(:call)
142
+ return action.call(world, scenario)
143
+ end
144
+
145
+ # Symbols are almost as easy to handle: they must resolve to a
146
+ # method, either globally or on the world object.
147
+ if action.is_a? Symbol
148
+ meth = resolve_action(Object, world, action)
149
+ if meth.nil?
150
+ raise NoMethodError, "Symbol :#{action} could not be resolved "\
151
+ "either globally or as part of the cucumber World object, "\
152
+ "aborting!"
153
+ end
154
+ return meth.call(world, scenario)
155
+ end
156
+
157
+ # At this point, the action better be a String.
158
+ if not action.is_a? String
159
+ raise "Action '#{action}' is not callable, and not a method name. "\
160
+ "Aborting!"
161
+ end
162
+
163
+ # Split module and method name
164
+ module_name, method_name = split_string_action(action)
165
+
166
+ # If we have no discernable module, use Object instead.
167
+ the_module = Object
168
+ if not module_name.nil? and not module_name.empty?
169
+ the_module = Object.const_get(module_name)
170
+ end
171
+
172
+ # Try the module we found (i.e. possibly Object) and world for
173
+ # resolving the method name.
174
+ meth = resolve_action(the_module, world, method_name)
175
+ if meth.nil?
176
+ raise NoMethodError, "Action '#{action}' could not be resolved!"
177
+ end
178
+
179
+ return meth.call(world, scenario)
180
+ end
181
+
182
+ ##
183
+ # For a given status and type, return the registered actions.
184
+ def registered_actions(status, type)
185
+ if not ::Unobtainium::Runtime.instance.has?(RUNTIME_KEY)
186
+ return []
187
+ end
188
+
189
+ actions = ::Unobtainium::Runtime.instance.fetch(RUNTIME_KEY)
190
+ return actions[[status, type]] || []
191
+ end
192
+
193
+ ##
194
+ # Partially for testing purposes, clears the action registry.
195
+ def clear_actions
196
+ if not ::Unobtainium::Runtime.instance.has?(RUNTIME_KEY)
197
+ # :nocov:
198
+ return
199
+ # :nocov:
200
+ end
201
+
202
+ ::Unobtainium::Runtime.instance.delete(RUNTIME_KEY)
203
+ end
204
+
205
+ ##
206
+ # Resolves the given symbol in either of the given namespace or the
207
+ # world object. Returns nil if neither contained the symbol.
208
+ def resolve_action(namespace, world, method_name)
209
+ method_sym = method_name.to_sym
210
+
211
+ [namespace, world].each do |receiver|
212
+ begin
213
+ return receiver.method(method_sym)
214
+ rescue NameError
215
+ next
216
+ end
217
+ end
218
+ return nil
219
+ end
220
+
221
+ ##
222
+ # Splits a string action into a module prefix and a method name
223
+ def split_string_action(action)
224
+ # Try to see whether we have a fully qualified name that requires us
225
+ # to query a particular module for the method.
226
+ split_action = action.split(/::/)
227
+ method_name = split_action.pop
228
+
229
+ # If the method name contains a dot, it uses 'Module.method' notation.
230
+ # rubocop:disable Lint/EmptyWhen
231
+ split_method = method_name.split(/\./)
232
+ case split_method.length
233
+ when 1
234
+ # Do nothing; the method name did not contain a dot
235
+ when 2
236
+ # We have a method name and a module part
237
+ split_action << split_method[0]
238
+ method_name = split_method[1]
239
+ else
240
+ raise "Too many dots in method name: #{method_name}"
241
+ end
242
+ # rubocop:enable Lint/EmptyWhen
243
+
244
+ # Re-join the module name
245
+ module_name = split_action.join('::')
246
+
247
+ return module_name, method_name
248
+ end
249
+ end # module StatusActions
250
+ end # module Cucumber
251
+ end # module Unobtainium
252
+
253
+ ##
254
+ # In the Before hook, we register configured actions.
255
+ Before do |_|
256
+ # Register all configured actions.
257
+ register_config_actions(self)
258
+ end
259
+
260
+ ##
261
+ # The After hook with cucumber defined here will execute all matching,
262
+ # registered actions.
263
+ After do |scenario|
264
+ # Fetch actions applying to this scenario
265
+ key = action_key(scenario)
266
+ applicable_actions = registered_actions(key[0], key[1])
267
+
268
+ # Execute all actions applying to this scenario
269
+ applicable_actions.each do |action|
270
+ execute_action(self, action, scenario)
271
+ end
272
+ end
@@ -0,0 +1,14 @@
1
+ # coding: utf-8
2
+ #
3
+ # unobtainium-cucumber
4
+ # https://github.com/jfinkhaeuser/unobtainium-cucumber
5
+ #
6
+ # Copyright (c) 2016 Jens Finkhaeuser and other unobtainium-cucumber
7
+ # contributors. All rights reserved.
8
+ #
9
+ module Unobtainium
10
+ module Cucumber
11
+ # The current release version
12
+ VERSION = "0.1.0".freeze
13
+ end # module Cucumber
14
+ end # module Unobtainium
@@ -0,0 +1,20 @@
1
+ # coding: utf-8
2
+ #
3
+ # unobtainium-cucumber
4
+ # https://github.com/jfinkhaeuser/unobtainium-cucumber
5
+ #
6
+ # Copyright (c) 2016 Jens Finkhaeuser and other unobtainium-cucumber
7
+ # contributors. All rights reserved.
8
+ #
9
+
10
+ require 'unobtainium'
11
+ require 'unobtainium-cucumber/version'
12
+
13
+ # First things first: extend the World. Otherwise nothing in this file will
14
+ # work.
15
+ World(Unobtainium::World)
16
+
17
+ require 'unobtainium-cucumber/driver_reset.rb'
18
+ require 'unobtainium-cucumber/status_actions.rb'
19
+
20
+ World(Unobtainium::Cucumber::StatusActions)
@@ -0,0 +1,54 @@
1
+ # coding: utf-8
2
+ #
3
+ # unobtainium-cucumber
4
+ # https://github.com/jfinkhaeuser/unobtainium-cucumber
5
+ #
6
+ # Copyright (c) 2016 Jens Finkhaeuser and other unobtainium-cucumber
7
+ # contributors. All rights reserved.
8
+ #
9
+
10
+ lib = File.expand_path('../lib', __FILE__)
11
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
12
+ require 'unobtainium-cucumber/version'
13
+
14
+ # rubocop:disable Style/UnneededPercentQ, Style/ExtraSpacing
15
+ # rubocop:disable Style/SpaceAroundOperators
16
+ # rubocop:disable Metrics/BlockLength
17
+ Gem::Specification.new do |spec|
18
+ spec.name = "unobtainium-cucumber"
19
+ spec.version = Unobtainium::Cucumber::VERSION
20
+ spec.authors = ["Jens Finkhaeuser"]
21
+ spec.email = ["jens@finkhaeuser.de"]
22
+ spec.description = %q(
23
+ The unobtainium-cucucmber gem adds some convenient cucumber specific hooks
24
+ for use with unobtainium.
25
+ )
26
+ spec.summary = %q(
27
+ Cucumber hooks for unobtainium.
28
+ )
29
+ spec.homepage = "https://github.com/jfinkhaeuser/unobtainium-cucumber"
30
+ spec.license = "MITNFA"
31
+
32
+ spec.files = `git ls-files -z`.split("\x0")
33
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
34
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
35
+ spec.require_paths = ["lib"]
36
+
37
+ spec.required_ruby_version = '>= 2.0'
38
+
39
+ spec.add_development_dependency "bundler", "~> 1.12"
40
+ spec.add_development_dependency "rubocop", "~> 0.46"
41
+ spec.add_development_dependency "rake", "~> 11.2"
42
+ spec.add_development_dependency "simplecov", "~> 0.12"
43
+ spec.add_development_dependency "yard", "~> 0.9"
44
+ spec.add_development_dependency "appium_lib"
45
+ spec.add_development_dependency "selenium-webdriver"
46
+ spec.add_development_dependency "chromedriver-helper"
47
+ spec.add_development_dependency "phantomjs"
48
+
49
+ spec.add_dependency "unobtainium", "~> 0.10"
50
+ spec.add_dependency "cucumber", "~> 2.0"
51
+ end
52
+ # rubocop:enable Metrics/BlockLength
53
+ # rubocop:enable Style/SpaceAroundOperators
54
+ # rubocop:enable Style/UnneededPercentQ, Style/ExtraSpacing