unobtainium-cucumber 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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