unobtainium 0.2.1 → 0.3.0

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