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.
File without changes
File without changes
@@ -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 '-local' appended before the
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: if your configuration file's top-level structure is an array, it will
36
- # be returned as a hash with a 'config' key that maps to your file's contents.
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 :load_config calls this function, so normally you don't need to call
188
- # it yourself. You can switch this behaviour off in :load_config.
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
- # 1) If a hash can't be extended because the base cannot be found, an error
209
+ #
210
+ # 1. If a hash can't be extended because the base cannot be found, an error
192
211
  # is raised.
193
- # 2) If a hash got successfully extended, the :extends keyword itself is
212
+ # 1. If a hash got successfully extended, the `extends` keyword itself is
194
213
  # removed from the hash.
195
- # 3) In a successfully extended hash, an :base keyword, which contains
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
@@ -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
- def create(*args)
26
- new(*args)
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
- # Ensures arguments are according to expectations.
67
- def sanitize_options(*args)
68
- if args.empty?
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 = args[0].to_sym
90
+ label = label.to_sym
73
91
 
74
- options = nil
75
- if args.length > 1
76
- if not args[1].nil? and not args[1].is_a? Hash
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
- # Determine the driver class, if any
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 #{@label} found, "\
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
- if driver_klass.respond_to?(:sanitize_options)
94
- label, options = driver_klass.sanitize_options(label, options)
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
- # Out of the loaded drivers, returns the one matching the label (if any)
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
- attr_reader :label, :options, :impl
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(*args)
168
- # Load drivers
169
- ::Unobtainium::Driver.load_drivers
170
-
195
+ def initialize(label, opts = nil)
171
196
  # Sanitize options
172
- @label, @options = ::Unobtainium::Driver.sanitize_options(*args)
173
-
174
- # Get the driver class. We kind of know this works because
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 './support/util'
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
- appPackage: 'com.android.chrome',
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::Drivers::Utility
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 sanitize_options(label, options)
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(label, options)
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.keys.each do |key|
111
+ data.each do |key, value|
116
112
  key_s = key.to_s
117
- if not options['caps'].key?(key) and not options['caps'].key?(key_s)
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