unobtainium 0.2.1 → 0.3.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.
- 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
|