unobtainium 0.2.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +4 -0
- data/.rubocop.yml +6 -0
- data/Gemfile.lock +44 -0
- data/README.md +17 -14
- data/Rakefile +20 -4
- data/config/config.yml +14 -0
- data/docs/CONFIGURATION.md +250 -0
- data/{cuke/features → features}/step_definitions/steps.rb +0 -0
- data/{cuke/features → features}/support/env.rb +0 -0
- data/{cuke/features → features}/world.feature +0 -0
- data/lib/unobtainium/config.rb +31 -10
- data/lib/unobtainium/driver.rb +55 -36
- data/lib/unobtainium/drivers/appium.rb +16 -13
- data/lib/unobtainium/drivers/phantom.rb +138 -0
- data/lib/unobtainium/drivers/selenium.rb +6 -5
- data/lib/unobtainium/pathed_hash.rb +29 -7
- data/lib/unobtainium/recursive_merge.rb +15 -0
- data/lib/unobtainium/runtime.rb +33 -8
- data/lib/unobtainium/support/port_scanner.rb +160 -0
- data/lib/unobtainium/support/runner.rb +180 -0
- data/lib/unobtainium/{drivers/support → support}/util.rb +10 -3
- data/lib/unobtainium/version.rb +2 -1
- data/lib/unobtainium/world.rb +6 -3
- data/spec/port_scanner_spec.rb +131 -0
- data/spec/runner_spec.rb +66 -0
- data/spec/utility_spec.rb +31 -0
- data/unobtainium.gemspec +9 -1
- metadata +105 -12
- data/cuke/.gitignore +0 -2
- data/cuke/Gemfile +0 -13
- data/cuke/Gemfile.lock +0 -59
- data/cuke/README.md +0 -2
- data/cuke/config/config.yml +0 -7
File without changes
|
File without changes
|
File without changes
|
data/lib/unobtainium/config.rb
CHANGED
@@ -14,46 +14,54 @@ require 'unobtainium/pathed_hash'
|
|
14
14
|
module Unobtainium
|
15
15
|
##
|
16
16
|
# The Config class extends PathedHash by two main pieces of functionality:
|
17
|
+
#
|
17
18
|
# - it loads configuration files and turns them into pathed hashes, and
|
18
19
|
# - it treats environment variables as overriding anything contained in
|
19
20
|
# the configuration file.
|
20
21
|
#
|
21
22
|
# For configuration file loading, a named configuration file will be laoaded
|
22
|
-
# if present. A file with the same name but
|
23
|
+
# if present. A file with the same name but `-local` appended before the
|
23
24
|
# extension will be loaded as well, overriding any values in the original
|
24
25
|
# configuration file.
|
25
26
|
#
|
26
27
|
# For environment variable support, any environment variable named like a
|
27
28
|
# path into the configuration hash, but with separators transformed to
|
28
29
|
# underscore and all letters capitalized will override values from the
|
29
|
-
# configuration files under that path, i.e. FOO_BAR will override 'foo.bar'
|
30
|
+
# configuration files under that path, i.e. `FOO_BAR` will override `'foo.bar'`.
|
30
31
|
#
|
31
32
|
# Environment variables can contain JSON *only*; if the value can be parsed
|
32
33
|
# as JSON, it becomes a Hash in the configuration tree. If it cannot be parsed
|
33
34
|
# as JSON, it remains a string.
|
34
35
|
#
|
35
|
-
# Note
|
36
|
-
# be returned as a hash with a 'config' key that maps to your file's
|
36
|
+
# **Note:** if your configuration file's top-level structure is an array, it
|
37
|
+
# will be returned as a hash with a 'config' key that maps to your file's
|
38
|
+
# contents.
|
37
39
|
# That means that if you are trying to merge a hash with an array config, the
|
38
40
|
# result may be unexpected.
|
39
41
|
class Config < PathedHash
|
42
|
+
# @api private
|
40
43
|
# Very simple YAML parser
|
41
44
|
class YAMLParser
|
42
45
|
require 'yaml'
|
43
46
|
|
47
|
+
# @return parsed string
|
44
48
|
def self.parse(string)
|
45
49
|
YAML.load(string)
|
46
50
|
end
|
47
51
|
end
|
52
|
+
private_constant :YAMLParser
|
48
53
|
|
54
|
+
# @api private
|
49
55
|
# Very simple JSON parser
|
50
56
|
class JSONParser
|
51
57
|
require 'json'
|
52
58
|
|
59
|
+
# @return parsed string
|
53
60
|
def self.parse(string)
|
54
61
|
JSON.parse(string)
|
55
62
|
end
|
56
63
|
end
|
64
|
+
private_constant :JSONParser
|
57
65
|
|
58
66
|
PathedHash::READ_METHODS.each do |method|
|
59
67
|
# Wrap all read functions into something that checks for environment
|
@@ -89,20 +97,28 @@ module Unobtainium
|
|
89
97
|
end
|
90
98
|
|
91
99
|
class << self
|
100
|
+
# @api private
|
92
101
|
# Mapping of file name extensions to parser types.
|
93
102
|
FILE_TO_PARSER = {
|
94
103
|
'.yml' => YAMLParser,
|
95
104
|
'.yaml' => YAMLParser,
|
96
105
|
'.json' => JSONParser,
|
97
106
|
}.freeze
|
107
|
+
private_constant :FILE_TO_PARSER
|
98
108
|
|
109
|
+
# @api private
|
99
110
|
# If the config file contains an Array, this is what they key of the
|
100
111
|
# returned Hash will be.
|
101
112
|
ARRAY_KEY = 'config'.freeze
|
113
|
+
private_constant :ARRAY_KEY
|
102
114
|
|
103
115
|
##
|
104
116
|
# Loads a configuration file with the given file name. The format is
|
105
117
|
# detected based on one of the extensions in FILE_TO_PARSER.
|
118
|
+
#
|
119
|
+
# @param path [String] the path of the configuration file to load.
|
120
|
+
# @param resolve_extensions [Boolean] flag whether to resolve configuration
|
121
|
+
# hash extensions. (see `#resolve_extensions`)
|
106
122
|
def load_config(path, resolve_extensions = true)
|
107
123
|
# Load base and local configuration files
|
108
124
|
base, config = load_base_config(path)
|
@@ -177,22 +193,25 @@ module Unobtainium
|
|
177
193
|
##
|
178
194
|
# Resolve extensions in configuration hashes. If your hash contains e.g.:
|
179
195
|
#
|
196
|
+
# ```yaml
|
180
197
|
# foo:
|
181
198
|
# bar:
|
182
199
|
# some: value
|
183
200
|
# baz:
|
184
201
|
# extends: bar
|
202
|
+
# ```
|
185
203
|
#
|
186
|
-
# Then 'foo.baz.some' will equal 'value' after resolving extensions. Note
|
187
|
-
# that
|
188
|
-
# it yourself. You can switch this behaviour off in
|
204
|
+
# Then `'foo.baz.some'` will equal `'value'` after resolving extensions. Note
|
205
|
+
# that `:load_config` calls this function, so normally you don't need to call
|
206
|
+
# it yourself. You can switch this behaviour off in `:load_config`.
|
189
207
|
#
|
190
208
|
# Note that this process has some intended side-effects:
|
191
|
-
#
|
209
|
+
#
|
210
|
+
# 1. If a hash can't be extended because the base cannot be found, an error
|
192
211
|
# is raised.
|
193
|
-
#
|
212
|
+
# 1. If a hash got successfully extended, the `extends` keyword itself is
|
194
213
|
# removed from the hash.
|
195
|
-
#
|
214
|
+
# 1. In a successfully extended hash, an `base` keyword, which contains
|
196
215
|
# the name of the base. In case of multiple recursive extensions, the
|
197
216
|
# final base is stored here.
|
198
217
|
#
|
@@ -203,6 +222,8 @@ module Unobtainium
|
|
203
222
|
recursive_merge("", "")
|
204
223
|
end
|
205
224
|
|
225
|
+
##
|
226
|
+
# Same as `dup.resolve_extensions!`
|
206
227
|
def resolve_extensions
|
207
228
|
dup.resolve_extensions!
|
208
229
|
end
|
data/lib/unobtainium/driver.rb
CHANGED
@@ -21,19 +21,29 @@ module Unobtainium
|
|
21
21
|
# Class methods
|
22
22
|
class << self
|
23
23
|
##
|
24
|
-
# Create a driver instance with the given arguments
|
25
|
-
|
26
|
-
|
24
|
+
# Create a driver instance with the given arguments.
|
25
|
+
#
|
26
|
+
# @param label [String, Symbol] Label for the driver to create. Driver
|
27
|
+
# implementations may accept normalized and alias labels, e.g.
|
28
|
+
# `:firefox, `:ff`, `:remote`, etc.
|
29
|
+
# @param opts [Hash] Options passed to the driver implementation.
|
30
|
+
def create(label, opts = nil)
|
31
|
+
new(label, opts)
|
27
32
|
end
|
28
33
|
|
29
34
|
##
|
30
35
|
# Add a new driver implementation. The first parameter is the class
|
31
36
|
# itself, the second should be a file path pointing to the file where
|
32
|
-
# the class is defined. You would typically pass __FILE__ for the second
|
37
|
+
# the class is defined. You would typically pass `__FILE__` for the second
|
33
38
|
# parameter.
|
34
39
|
#
|
35
40
|
# Using file names lets us figure out whether the class is a duplicate,
|
36
41
|
# or merely a second registration of the same class.
|
42
|
+
#
|
43
|
+
# Driver classes must implement the class methods listed in `DRIVER_METHODS`.
|
44
|
+
#
|
45
|
+
# @param klass (Class) Driver implementation class to register.
|
46
|
+
# @param path (String) Implementation path of the driver class.
|
37
47
|
def register_implementation(klass, path)
|
38
48
|
# We need to deal with absolute paths only
|
39
49
|
fpath = File.absolute_path(path)
|
@@ -63,41 +73,46 @@ module Unobtainium
|
|
63
73
|
private :new
|
64
74
|
|
65
75
|
##
|
66
|
-
#
|
67
|
-
|
68
|
-
|
76
|
+
# @api private
|
77
|
+
# Resolves everything to do with driver options:
|
78
|
+
#
|
79
|
+
# - Normalizes the label
|
80
|
+
# - Loads the driver class
|
81
|
+
# - Normalizes and extends options from the driver implementation
|
82
|
+
#
|
83
|
+
# @param label [Symbol, String] the driver label
|
84
|
+
# @param opts [Hash] driver options
|
85
|
+
def resolve_options(label, opts = nil)
|
86
|
+
if label.nil? or label.empty?
|
69
87
|
raise ArgumentError, "Need at least one argument specifying the driver!"
|
70
88
|
end
|
71
89
|
|
72
|
-
label =
|
90
|
+
label = label.to_sym
|
73
91
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
raise ArgumentError, "The second argument is expected to be an "\
|
78
|
-
"options hash!"
|
79
|
-
end
|
80
|
-
options = args[1]
|
92
|
+
if not opts.nil? and not opts.is_a? Hash
|
93
|
+
raise ArgumentError, "The second argument is expected to be an "\
|
94
|
+
"options Hash!"
|
81
95
|
end
|
82
96
|
|
83
|
-
#
|
97
|
+
# Get the driver class.
|
84
98
|
load_drivers
|
85
|
-
|
86
99
|
driver_klass = get_driver(label)
|
87
100
|
if not driver_klass
|
88
|
-
raise LoadError, "No driver implementation matching #{
|
101
|
+
raise LoadError, "No driver implementation matching #{label} found, "\
|
89
102
|
"aborting!"
|
90
103
|
end
|
91
104
|
|
92
105
|
# Sanitize options according to the driver's idea
|
93
|
-
|
94
|
-
|
106
|
+
options = opts
|
107
|
+
if driver_klass.respond_to?(:resolve_options)
|
108
|
+
label, options = driver_klass.resolve_options(label, opts)
|
95
109
|
end
|
96
110
|
|
97
|
-
return label, options
|
111
|
+
return label, options, driver_klass
|
98
112
|
end
|
99
113
|
|
100
114
|
##
|
115
|
+
# @api private
|
101
116
|
# Load drivers; this loads all driver implementations included in this gem.
|
102
117
|
# You can register external implementations with the :register_implementation
|
103
118
|
# method.
|
@@ -124,7 +139,9 @@ module Unobtainium
|
|
124
139
|
end
|
125
140
|
|
126
141
|
##
|
127
|
-
#
|
142
|
+
# @api private
|
143
|
+
# Out of the loaded drivers, returns the one matching the label (if any).
|
144
|
+
# @param label [Symbol] The label matching a driver.
|
128
145
|
def get_driver(label)
|
129
146
|
# Of all the loaded classes, choose the first (unsorted) to match the
|
130
147
|
# requested driver label
|
@@ -142,7 +159,16 @@ module Unobtainium
|
|
142
159
|
|
143
160
|
############################################################################
|
144
161
|
# Public methods
|
145
|
-
|
162
|
+
|
163
|
+
# @return [Symbol] the normalized label for the driver implementation
|
164
|
+
attr_reader :label
|
165
|
+
|
166
|
+
# @return [Hash] the options hash the driver implementation is using.
|
167
|
+
attr_reader :options
|
168
|
+
|
169
|
+
# @return [Object] the driver implementation itself; do not use this unless
|
170
|
+
# you have to.
|
171
|
+
attr_reader :impl
|
146
172
|
|
147
173
|
##
|
148
174
|
# Map any missing method to the driver implementation
|
@@ -153,6 +179,8 @@ module Unobtainium
|
|
153
179
|
return super
|
154
180
|
end
|
155
181
|
|
182
|
+
##
|
183
|
+
# Map any missing method to the driver implementation
|
156
184
|
def method_missing(meth, *args, &block)
|
157
185
|
if not @impl.nil? and @impl.respond_to?(meth)
|
158
186
|
return @impl.send(meth.to_s, *args, &block)
|
@@ -164,20 +192,11 @@ module Unobtainium
|
|
164
192
|
|
165
193
|
##
|
166
194
|
# Initializer
|
167
|
-
def initialize(
|
168
|
-
# Load drivers
|
169
|
-
::Unobtainium::Driver.load_drivers
|
170
|
-
|
195
|
+
def initialize(label, opts = nil)
|
171
196
|
# Sanitize options
|
172
|
-
@label, @options = ::Unobtainium::Driver.
|
173
|
-
|
174
|
-
|
175
|
-
# sanitize_options does the same, but let's be strict.
|
176
|
-
driver_klass = ::Unobtainium::Driver.get_driver(label)
|
177
|
-
if not driver_klass
|
178
|
-
raise LoadError, "No driver implementation matching #{@label} found, "\
|
179
|
-
"aborting!"
|
180
|
-
end
|
197
|
+
@label, @options, driver_klass = ::Unobtainium::Driver.resolve_options(
|
198
|
+
label,
|
199
|
+
opts)
|
181
200
|
|
182
201
|
# Perform precondition checks of the driver class
|
183
202
|
driver_klass.ensure_preconditions(@label, @options)
|
@@ -7,9 +7,11 @@
|
|
7
7
|
# All rights reserved.
|
8
8
|
#
|
9
9
|
|
10
|
-
require_relative '
|
10
|
+
require_relative '../support/util'
|
11
11
|
|
12
12
|
module Unobtainium
|
13
|
+
# @api private
|
14
|
+
# Contains driver implementations
|
13
15
|
module Drivers
|
14
16
|
|
15
17
|
##
|
@@ -27,14 +29,13 @@ module Unobtainium
|
|
27
29
|
BROWSER_MATCHES = {
|
28
30
|
android: {
|
29
31
|
chrome: {
|
30
|
-
|
31
|
-
appActivity: 'com.google.android.apps.chrome.Main',
|
32
|
+
browserName: 'Chrome',
|
32
33
|
},
|
33
34
|
},
|
34
35
|
}.freeze
|
35
36
|
|
36
37
|
class << self
|
37
|
-
include ::Unobtainium::
|
38
|
+
include ::Unobtainium::Support::Utility
|
38
39
|
|
39
40
|
##
|
40
41
|
# Return true if the given label matches this driver implementation,
|
@@ -55,7 +56,7 @@ module Unobtainium
|
|
55
56
|
|
56
57
|
##
|
57
58
|
# Sanitize options, and expand the :browser key, if present.
|
58
|
-
def
|
59
|
+
def resolve_options(label, options)
|
59
60
|
# The label specifies the platform, if no other platform is given.
|
60
61
|
normalized = normalize_label(label)
|
61
62
|
|
@@ -79,9 +80,7 @@ module Unobtainium
|
|
79
80
|
|
80
81
|
##
|
81
82
|
# Create and return a driver instance
|
82
|
-
def create(
|
83
|
-
_, options = sanitize_options(label, options)
|
84
|
-
|
83
|
+
def create(_, options)
|
85
84
|
# Create the driver
|
86
85
|
driver = ::Appium::Driver.new(options).start_driver
|
87
86
|
return driver
|
@@ -108,13 +107,17 @@ module Unobtainium
|
|
108
107
|
return options
|
109
108
|
end
|
110
109
|
|
111
|
-
# We have data, so we can remove the browser key itself
|
112
|
-
options.delete('browser')
|
113
|
-
|
114
110
|
# We do have to check that we're not overwriting any of the keys.
|
115
|
-
data.
|
111
|
+
data.each do |key, value|
|
116
112
|
key_s = key.to_s
|
117
|
-
|
113
|
+
option_value = nil
|
114
|
+
if options['caps'].key?(key)
|
115
|
+
option_value = options['caps'][key]
|
116
|
+
elsif options['caps'].key?(key_s)
|
117
|
+
option_value = options['caps'][key_s]
|
118
|
+
end
|
119
|
+
|
120
|
+
if option_value.nil? or option_value == value
|
118
121
|
next
|
119
122
|
end
|
120
123
|
raise ArgumentError, "You specified the browser option as, "\
|
@@ -0,0 +1,138 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
#
|
3
|
+
# unobtainium
|
4
|
+
# https://github.com/jfinkhaeuser/unobtainium
|
5
|
+
#
|
6
|
+
# Copyright (c) 2016 Jens Finkhaeuser and other unobtainium contributors.
|
7
|
+
# All rights reserved.
|
8
|
+
#
|
9
|
+
|
10
|
+
require_relative './selenium'
|
11
|
+
require_relative '../support/util'
|
12
|
+
require_relative '../support/port_scanner'
|
13
|
+
require_relative '../support/runner'
|
14
|
+
require_relative '../runtime'
|
15
|
+
|
16
|
+
module Unobtainium
|
17
|
+
# @api private
|
18
|
+
# Contains driver implementations
|
19
|
+
module Drivers
|
20
|
+
|
21
|
+
##
|
22
|
+
# Driver implementation using the selenium-webdriver gem to connect to
|
23
|
+
# PhantomJS.
|
24
|
+
class Phantom < Selenium
|
25
|
+
# Recognized labels for matching the driver
|
26
|
+
LABELS = {
|
27
|
+
phantomjs: [:headless,],
|
28
|
+
}.freeze
|
29
|
+
|
30
|
+
# Port scanning ranges (can also be arrays or single port numbers.
|
31
|
+
PORT_RANGES = [
|
32
|
+
9134,
|
33
|
+
8080,
|
34
|
+
8000..8079,
|
35
|
+
8081..10000,
|
36
|
+
1025..7999,
|
37
|
+
10001..65535,
|
38
|
+
].freeze
|
39
|
+
|
40
|
+
# Timeout for waiting for connecting to PhantomJS server, in seconds
|
41
|
+
CONNECT_TIMEOUT = 60
|
42
|
+
|
43
|
+
class << self
|
44
|
+
include ::Unobtainium::Support::Utility
|
45
|
+
include ::Unobtainium::Support::PortScanner
|
46
|
+
|
47
|
+
##
|
48
|
+
# Ensure that the driver's preconditions are fulfilled.
|
49
|
+
def ensure_preconditions(_, _)
|
50
|
+
super
|
51
|
+
begin
|
52
|
+
require 'phantomjs'
|
53
|
+
rescue LoadError => err
|
54
|
+
raise LoadError, "#{err.message}: you need to add "\
|
55
|
+
"'phantomjs' to your Gemfile to use this driver!",
|
56
|
+
err.backtrace
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
##
|
61
|
+
# Mostly provides webdriver-specific options for PhantomJS
|
62
|
+
def resolve_options(label, options)
|
63
|
+
label, options = super
|
64
|
+
|
65
|
+
if not options[:phantomjs].nil? and not options['phantomjs'].nil?
|
66
|
+
raise ArgumentError, "Use either of 'phantomjs' or :phantomjs as "\
|
67
|
+
"option keys, not both!"
|
68
|
+
end
|
69
|
+
if not options[:phantomjs].nil?
|
70
|
+
options['phantomjs'] = options[:phantomjs]
|
71
|
+
options.delete(:phantomjs)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Provide defaults for webdriver host and port. We find a free port
|
75
|
+
# here, so there's a possibility it'll get used before we run the
|
76
|
+
# server in #create. However, for the purpose of resolving options
|
77
|
+
# that's necessary. So we'll just live with this until it becomes a
|
78
|
+
# problem.
|
79
|
+
defaults = {
|
80
|
+
"phantomjs" => {
|
81
|
+
"host" => "localhost",
|
82
|
+
"port" => nil,
|
83
|
+
},
|
84
|
+
}
|
85
|
+
options = defaults.merge(options)
|
86
|
+
|
87
|
+
if options['phantomjs']['port'].nil?
|
88
|
+
ports = scan(options['phantomjs']['host'], *PORT_RANGES,
|
89
|
+
for: :available, amount: :first)
|
90
|
+
if ports.empty?
|
91
|
+
raise "Could not find an available port for the PhantomJS server!"
|
92
|
+
end
|
93
|
+
options['phantomjs']['port'] = ports[0]
|
94
|
+
end
|
95
|
+
|
96
|
+
# Now override connection options for Selenium
|
97
|
+
options[:url] = "http://#{options['phantomjs']['host']}:"\
|
98
|
+
"#{options['phantomjs']['port']}"
|
99
|
+
|
100
|
+
return label, options
|
101
|
+
end
|
102
|
+
|
103
|
+
##
|
104
|
+
# Create and return a driver instance
|
105
|
+
def create(_, options)
|
106
|
+
# Extract PhantomJS options
|
107
|
+
host = options['phantomjs']['host']
|
108
|
+
port = options['phantomjs']['port']
|
109
|
+
opts = options.dup
|
110
|
+
opts.delete('phantomjs')
|
111
|
+
|
112
|
+
# Start PhantomJS server, if it isn't already running
|
113
|
+
conn_str = "#{host}:#{port}"
|
114
|
+
runner = ::Unobtainium::Runtime.instance.store_with_if(conn_str) do
|
115
|
+
::Unobtainium::Support::Runner.new(conn_str,
|
116
|
+
Phantomjs.path,
|
117
|
+
"--webdriver=#{conn_str}")
|
118
|
+
end
|
119
|
+
runner.start
|
120
|
+
|
121
|
+
# Wait for the server to open a port.
|
122
|
+
timeout = CONNECT_TIMEOUT
|
123
|
+
while timeout > 0 and not port_open?(host, port)
|
124
|
+
sleep 1
|
125
|
+
timeout -= 1
|
126
|
+
end
|
127
|
+
if timeout <= 0
|
128
|
+
raise "Timeout waiting to connect to PhantomJS!"
|
129
|
+
end
|
130
|
+
|
131
|
+
# Run Selenium against server
|
132
|
+
driver = ::Selenium::WebDriver.for(:remote, opts)
|
133
|
+
return driver
|
134
|
+
end
|
135
|
+
end # class << self
|
136
|
+
end # class PhantomJS
|
137
|
+
end # module Drivers
|
138
|
+
end # module Unobtainium
|