unobtainium 0.8.1 → 0.9.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 32ec07edb9ef9b7a1794e3660a6dbaedec57054f
4
- data.tar.gz: 36ec4332fb3921acb4e3b723b4fe0f208bc839e6
3
+ metadata.gz: c48a4bbdaf69ce31be6eb42f8a10bd4af462fafb
4
+ data.tar.gz: f4f811e06460e1a124769e97dadb0d79de82ba93
5
5
  SHA512:
6
- metadata.gz: 140e9dbdc8f5f83d4ff607ffd9fe5381652f710e3a9cf6036ef278daa3da8c792c450883b701395680094997983bd880ee5dc5eb5d0403fd2f690da05b311d13
7
- data.tar.gz: 5f6dcef3056e6110c49a6b3fba2913641f43a1c4a15895c5802edf3af8569e6be0ec0f2a66b01a88694937d450b6650683981f456f39d400447ac77e914fdf8e
6
+ metadata.gz: 2582d432e5fc1c6d35cacee66c8eb71ec87437313f1ab3f8706dd457d4925ccb07cac952e535bba1898eb8e1e6b9cb1becaf3e90f2ab36d78c1a2d33e97ab575
7
+ data.tar.gz: 9e610ebc5b157cb7828a4be8f7208687b8df5acb2ebd7ee9f023173132dd3a0c4d285d1eaf725ea8f2b13ca6c7c2b0c41c71aec11fca49a41082303081ba8e0b
data/.travis.yml CHANGED
@@ -5,7 +5,7 @@ rvm:
5
5
  - 2.2
6
6
  script:
7
7
  - bundle exec rake
8
- - bundle exec codeclimate-test-reporter
8
+ - bundle exec codeclimate-test-reporter || true
9
9
  addons:
10
10
  code_climate:
11
11
  repo_token: 5dcf014e3a26ded8ffe8ff8bc70f98d49523e4851613123d8359035d44937953
data/Gemfile.lock CHANGED
@@ -1,8 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- unobtainium (0.8.1)
5
- collapsium (~> 0.6)
4
+ unobtainium (0.9.0)
5
+ collapsium (~> 0.8)
6
6
  collapsium-config (~> 0.4)
7
7
  ptools (~> 1.3)
8
8
  sys-proctable (~> 1.1)
@@ -10,11 +10,11 @@ PATH
10
10
  GEM
11
11
  remote: https://rubygems.org/
12
12
  specs:
13
- appium_lib (8.0.2)
13
+ appium_lib (8.1.0)
14
14
  awesome_print (~> 1.6)
15
15
  json (~> 1.8)
16
16
  nokogiri (~> 1.6.6)
17
- selenium-webdriver (~> 2.49)
17
+ selenium-webdriver (~> 2.50)
18
18
  tomlrb (~> 1.1)
19
19
  archive-zip (0.7.0)
20
20
  io-like (~> 0.3.0)
@@ -26,10 +26,11 @@ GEM
26
26
  chromedriver-helper (1.0.0)
27
27
  archive-zip (~> 0.7.0)
28
28
  nokogiri (~> 1.6)
29
- codeclimate-test-reporter (1.0.1)
30
- collapsium (0.6.1)
31
- collapsium-config (0.4.0)
32
- collapsium (~> 0.6)
29
+ codeclimate-test-reporter (1.0.3)
30
+ simplecov
31
+ collapsium (0.8.0)
32
+ collapsium-config (0.4.3)
33
+ collapsium (~> 0.7)
33
34
  cucumber (2.4.0)
34
35
  builder (>= 2.1.2)
35
36
  cucumber-core (~> 1.5.0)
@@ -52,7 +53,7 @@ GEM
52
53
  multi_test (0.1.2)
53
54
  nokogiri (1.6.8.1)
54
55
  mini_portile2 (~> 2.1.0)
55
- parser (2.3.1.4)
56
+ parser (2.3.2.0)
56
57
  ast (~> 2.2)
57
58
  phantomjs (2.1.1.0)
58
59
  powerpack (0.1.1)
@@ -243,7 +243,12 @@ module Unobtainium
243
243
  driver_klass.ensure_preconditions(@label, @options)
244
244
 
245
245
  # Great, instanciate!
246
- @impl = driver_klass.create(@label, @options)
246
+ opts = nil
247
+ if not @options.nil?
248
+ opts = @options.dup
249
+ opts.delete('unobtainium_instance_id')
250
+ end
251
+ @impl = driver_klass.create(@label, opts)
247
252
 
248
253
  # Now also extend this implementation with all the modues that match
249
254
  @@modules.each do |klass, _|
@@ -68,9 +68,7 @@ module Unobtainium
68
68
  return driver.send(meth.to_s, *args, &block)
69
69
  end
70
70
  end
71
- # :nocov:
72
71
  return super
73
- # :nocov:
74
72
  end
75
73
  end
76
74
 
@@ -119,13 +117,14 @@ module Unobtainium
119
117
 
120
118
  # Merge 'caps' and 'desired_capabilities', letting the former win
121
119
  options[:caps] =
122
- ::Collapsium::UberHash.new(options[:desired_capabilities])
120
+ ::Collapsium::UberHash.new(options['desired_capabilities'])
121
+ .recursive_merge(options[:desired_capabilities])
123
122
  .recursive_merge(options[:caps])
124
123
  options.delete(:desired_capabilities)
125
124
  options.delete('desired_capabilities')
126
125
 
127
126
  # The label specifies the platform, if no other platform is given.
128
- if options['caps.platformName']
127
+ if not options['caps.platformName']
129
128
  options['caps.platformName'] = normalized.to_s
130
129
  end
131
130
 
@@ -144,12 +143,14 @@ module Unobtainium
144
143
  # some information
145
144
  options = supplement_browser(options)
146
145
 
147
- return label, options
146
+ return normalized, options
148
147
  end
149
148
 
150
149
  ##
151
150
  # Create and return a driver instance
152
151
  def create(_, options)
152
+ # :nocov:
153
+
153
154
  # Determine compatibility option
154
155
  compat = options.fetch(:webdriver_compatibility, true)
155
156
  options.delete(:webdriver_compatibility)
@@ -158,6 +159,7 @@ module Unobtainium
158
159
  driver = ::Appium::Driver.new(options)
159
160
  result = DriverProxy.new(driver, compat)
160
161
  return result
162
+ # :nocov:
161
163
  end
162
164
 
163
165
  private
@@ -176,7 +178,7 @@ module Unobtainium
176
178
  platform = options['caps.platformName'].to_s.downcase.to_sym
177
179
 
178
180
  # If we have supplement data matching the platform and browser, great!
179
- data = BROWSER_MATCHES[platform][browser]
181
+ data = (BROWSER_MATCHES[platform] || {})[browser]
180
182
  if data.nil?
181
183
  return options
182
184
  end
@@ -7,10 +7,13 @@
7
7
  # All rights reserved.
8
8
  #
9
9
 
10
+ require 'collapsium'
11
+
10
12
  require_relative './selenium'
11
13
  require_relative '../support/util'
12
14
  require_relative '../support/port_scanner'
13
15
  require_relative '../support/runner'
16
+ require_relative '../support/identifiers'
14
17
  require_relative '../runtime'
15
18
 
16
19
  module Unobtainium
@@ -24,7 +27,7 @@ module Unobtainium
24
27
  class Phantom < Selenium
25
28
  # Recognized labels for matching the driver
26
29
  LABELS = {
27
- phantomjs: [:headless,],
30
+ phantomjs: [:headless, :phantom],
28
31
  }.freeze
29
32
 
30
33
  # Port scanning ranges (can also be arrays or single port numbers.
@@ -43,6 +46,7 @@ module Unobtainium
43
46
  class << self
44
47
  include ::Unobtainium::Support::Utility
45
48
  include ::Unobtainium::Support::PortScanner
49
+ include ::Unobtainium::Support::Identifiers
46
50
 
47
51
  ##
48
52
  # Ensure that the driver's preconditions are fulfilled.
@@ -62,40 +66,56 @@ module Unobtainium
62
66
  def resolve_options(label, options)
63
67
  label, options = super
64
68
 
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
69
+ options = ::Collapsium::UberHash.new(options)
73
70
 
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)
71
+ # If a URL is already provided, we should respect this.
72
+ merge_url(options)
73
+
74
+ # Provide defaults for webdriver host and port.
75
+ merge_defaults(options)
76
+
77
+ # At this point, the :phantomjs field is canonical in that it will
78
+ # be used to generate a :port and :url if necessary. That means we
79
+ # can use it to create a stable ID, too.
80
+ # This also implies that the :url field is pointless and should not
81
+ # be part of the ID; it will be generated again later on.
82
+ options.delete(:url)
83
+
84
+ # We need to figure out what we have to do based on detecting whether
85
+ # a port or some other option changed (or nothing did!)
86
+ fix_ports(label, options)
86
87
 
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!"
88
+ # We find a free port here, so there's a possibility it'll get used
89
+ # before we run the server in #create. However, for the purpose of
90
+ # resolving options that's necessary. So we'll just live with this
91
+ # until it becomes a problem.
92
+ if options['phantomjs.generated_port'].nil?
93
+ if options['phantomjs.port'].nil?
94
+ ports = scan(options['phantomjs.host'], *PORT_RANGES,
95
+ for: :available, amount: :first)
96
+ if ports.empty?
97
+ raise "Could not find an available port for the PhantomJS server!"
98
+ end
99
+ options['phantomjs.generated_port'] = ports[0]
100
+ else
101
+ options['phantomjs.generated_port'] = options['phantomjs.port']
92
102
  end
93
- options['phantomjs']['port'] = ports[0]
94
103
  end
95
104
 
96
- # Now override connection options for Selenium
97
- options[:url] = "http://#{options['phantomjs']['host']}:"\
98
- "#{options['phantomjs']['port']}"
105
+ # Now we can't just use new_id because we might have found a new
106
+ # port in the meantime. We'll have to generate yet another ID, and
107
+ # use that.
108
+ # Now before calculating this new ID, we'll run the options through
109
+ # the super method again. This is to ensure that all keys have the
110
+ # expected class *before* we perform this calculation.
111
+ new_id = identifier('driver', label, options)
112
+ options['unobtainium_instance_id'] = new_id
113
+
114
+ # Now we can generate the :url field for Selenium's benefit; it's
115
+ # just a copy of the canonical options.
116
+ options[:url] = "#{options['phantomjs.scheme']}://"\
117
+ "#{options['phantomjs.host']}:"\
118
+ "#{options['phantomjs.generated_port']}"
99
119
 
100
120
  return label, options
101
121
  end
@@ -103,9 +123,11 @@ module Unobtainium
103
123
  ##
104
124
  # Create and return a driver instance
105
125
  def create(_, options)
126
+ # :nocov:
127
+
106
128
  # Extract PhantomJS options
107
- host = options['phantomjs']['host']
108
- port = options['phantomjs']['port']
129
+ host = options['phantomjs.host']
130
+ port = options['phantomjs.port'] || options['phantomjs.generated_port']
109
131
  opts = options.dup
110
132
  opts.delete('phantomjs')
111
133
 
@@ -138,6 +160,100 @@ module Unobtainium
138
160
  # Run Selenium against server
139
161
  driver = ::Selenium::WebDriver.for(:remote, opts)
140
162
  return driver
163
+
164
+ # :nocov:
165
+ end
166
+
167
+ private
168
+
169
+ def merge_url(options)
170
+ if not options[:url]
171
+ return
172
+ end
173
+
174
+ require 'uri'
175
+ parsed = URI.parse(options[:url])
176
+ parsed_port = parsed.port.to_i
177
+ from_parsed = {
178
+ phantomjs: {
179
+ scheme: parsed.scheme,
180
+ host: parsed.host,
181
+ port: nil,
182
+ },
183
+ }
184
+
185
+ # Very special case: if the parsed port matches the generated port,
186
+ # and the port is nil, we want to keep it that way for deduplication
187
+ # purposes. See `#fix_ports` for how this interacts.
188
+ port = options['phantomjs.port']
189
+ generated_port = options['phantomjs.generated_port']
190
+ if not (parsed_port == generated_port and port.nil?)
191
+ from_parsed[:phantomjs][:port] = parsed_port
192
+ end
193
+
194
+ options.recursive_merge!(from_parsed, false)
195
+ end
196
+
197
+ def merge_defaults(options)
198
+ defaults = {
199
+ phantomjs: {
200
+ scheme: 'http',
201
+ host: 'localhost',
202
+ port: nil,
203
+ generated_port: nil,
204
+ },
205
+ }
206
+ options.recursive_merge!(defaults, false)
207
+ end
208
+
209
+ def fix_ports(label, options)
210
+ # Let's keep the old ID around and generate a new one, largely as
211
+ # a checksum of the options.
212
+ old_id = options.delete('unobtainium_instance_id')
213
+ new_id = identifier('driver', label, options)
214
+
215
+ # If the IDs don't match, it means some options changed. This may be
216
+ # the port or another option:
217
+ # #1 If IDs are the same and port is the same as generated port, we
218
+ # need not do anything.
219
+ # #2 If IDs are the same and the ports differ, we have reached an
220
+ # undefined state. This should be impossible.
221
+ # #3 If IDs differ and the ports are the same, some other option was
222
+ # changed. We need to generate a new port and new ID (and warn about
223
+ # this).
224
+ # #4 If IDs differ and ports differ, a new port was set. We need to
225
+ # proceed with the new port and generate a new ID.
226
+ port = options['phantomjs.port']
227
+ generated_port = options['phantomjs.generated_port']
228
+
229
+ if old_id == new_id and port == generated_port
230
+ # #1 above, nothing to do.
231
+ elsif old_id == new_id and port != generated_port
232
+ # :nocov:
233
+ # #2 above, raise hell
234
+ if not port.nil?
235
+ raise "This can't happen; the instance ID (checksum) is the same, "\
236
+ "but the input differed: #{port} <-> #{generated_port}"
237
+ end
238
+ # :nocov:
239
+ elsif old_id != new_id and port == generated_port
240
+ # #3 above; nuke the ports and warn.
241
+ if not port.nil? and not generated_port.nil?
242
+ warn "Rejecting port #{port} and generating new port because "\
243
+ "options changed."
244
+ end
245
+ options['phantomjs.port'] = nil
246
+ options['phantomjs.generated_port'] = nil
247
+ elsif old_id != new_id and port != generated_port
248
+ # #4 above
249
+ options['phantomjs.generated_port'] = nil
250
+ else
251
+ # :nocov:
252
+ # Unreachable
253
+ raise "This can't happen; we have four cases and handle each of "\
254
+ "them. This line is unreachable. Please check the logic."
255
+ # :nocov:
256
+ end
141
257
  end
142
258
  end # class << self
143
259
  end # class PhantomJS
@@ -64,7 +64,8 @@ module Unobtainium
64
64
 
65
65
  # Merge 'caps' and 'desired_capabilities', letting the latter win
66
66
  options[:desired_capabilities] =
67
- ::Collapsium::UberHash.new(options[:caps])
67
+ ::Collapsium::UberHash.new(options['caps'])
68
+ .recursive_merge(options[:caps])
68
69
  .recursive_merge(options[:desired_capabilities])
69
70
  options.delete(:caps)
70
71
  options.delete('caps')
@@ -82,8 +83,10 @@ module Unobtainium
82
83
  ##
83
84
  # Create and return a driver instance
84
85
  def create(label, options)
86
+ # :nocov:
85
87
  driver = ::Selenium::WebDriver.for(normalize_label(label), options)
86
88
  return driver
89
+ # :nocov:
87
90
  end
88
91
 
89
92
  private
@@ -0,0 +1,25 @@
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
+ module Unobtainium
10
+ # @api private
11
+ # Contains support code
12
+ module Support
13
+ # Contains code for dealing with instance identifiers.
14
+ module Identifiers
15
+ # Given a label and a set of options, generate a unique identifier
16
+ # string.
17
+ def identifier(scope, label, options = nil)
18
+ digest = { label: label, options: options }
19
+ require 'digest/sha1'
20
+ digest = Digest::SHA1.hexdigest(digest.to_s)
21
+ return "#{scope}-#{digest}"
22
+ end
23
+ end # module Identifiers
24
+ end # module Support
25
+ end # module Unobtainium
@@ -30,6 +30,12 @@ module Unobtainium
30
30
  # A port scanner for finding a free port for running e.g. a selenium
31
31
  # or appium server.
32
32
  module PortScanner
33
+ # Retry a port this many times before failing
34
+ MAX_RETRIES = 5
35
+
36
+ # Delay each retry by this many seconds before trying again
37
+ RETRY_DELAY = 0.5
38
+
33
39
  ##
34
40
  # Returns true if the port is open on the host, false otherwise.
35
41
  # @param host [String] host name or IP address
@@ -175,14 +181,20 @@ module Unobtainium
175
181
  sock = Socket.new(domain, :STREAM)
176
182
 
177
183
  connected = false
184
+ tries = MAX_RETRIES
178
185
  loop do
179
186
  begin
180
187
  sock.connect_nonblock(addr)
181
188
  rescue Errno::EINPROGRESS
182
- if not IO.select(nil, [sock], nil, 1)
183
- # Timed out, retry?
184
- next
189
+ tries -= 1
190
+ if tries <= 0
191
+ # That's it, we've got enough.
192
+ break
185
193
  end
194
+
195
+ # The result of select doesn't matter. What matters is that we wait
196
+ # for sock to become usable, or for the timeout to occur.
197
+ IO.select([sock], [sock], nil, RETRY_DELAY)
186
198
  rescue Errno::EISCONN
187
199
  connected = true
188
200
  break
@@ -8,5 +8,5 @@
8
8
  #
9
9
  module Unobtainium
10
10
  # The current release version
11
- VERSION = "0.8.1".freeze
11
+ VERSION = "0.9.0".freeze
12
12
  end
@@ -12,6 +12,7 @@ require 'collapsium-config'
12
12
 
13
13
  require 'unobtainium/driver'
14
14
  require 'unobtainium/runtime'
15
+ require 'unobtainium/support/identifiers'
15
16
 
16
17
  module Unobtainium
17
18
  ##
@@ -63,6 +64,8 @@ module Unobtainium
63
64
  end # module ClassMethods
64
65
  extend ClassMethods
65
66
 
67
+ include ::Unobtainium::Support::Identifiers
68
+
66
69
  ##
67
70
  # (see Driver#create)
68
71
  #
@@ -93,6 +96,7 @@ module Unobtainium
93
96
  next
94
97
  end
95
98
  label = base.gsub(/^\.drivers\./, '')
99
+ break
96
100
  end
97
101
 
98
102
  # Unfortunately, the "base" key may not be recognized by the drivers,
@@ -108,10 +112,10 @@ module Unobtainium
108
112
 
109
113
  # Create a key for the label and options. This should always
110
114
  # return the same key for the same label and options.
111
- key = { label: label, options: options }
112
- require 'digest/sha1'
113
- key = Digest::SHA1.hexdigest(key.to_s)
114
- key = "driver-#{key}"
115
+ key = options['unobtainum_instance_id']
116
+ if key.nil?
117
+ key = identifier('driver', label, options)
118
+ end
115
119
 
116
120
  # Only create a driver with this exact configuration once. Unfortunately
117
121
  # We'll have to bind the destructor to whatever configuration exists at
@@ -7,6 +7,11 @@
7
7
  #
8
8
  # so the order in this file has to be mock -> branch2 -> branch1 -> leaf
9
9
  ---
10
+ global:
11
+ global_opt: 'set'
12
+
13
+ driver: leaf
14
+
10
15
  drivers:
11
16
  mock:
12
17
  mockoption: 42
@@ -16,11 +21,13 @@ drivers:
16
21
  branch1:
17
22
  extends: mock
18
23
  branch1option: foo
24
+ branch3:
25
+ extends: .global
26
+ branch3option: baz
19
27
  leaf:
20
- extends: branch2
28
+ extends: .global, branch2
21
29
  leafoption: baz
22
30
  branch1option: override
23
31
  base_does_not_exist:
24
32
  extends: nonexistent_base
25
33
  some: value
26
- driver: leaf
data/spec/driver_spec.rb CHANGED
@@ -34,7 +34,10 @@ end # module FakeModule
34
34
 
35
35
  describe ::Unobtainium::Driver do
36
36
  before :each do
37
- ::Unobtainium::Driver.register_implementation(MockDriver, "mock_driver.rb")
37
+ ::Unobtainium::Driver.register_implementation(MockDriver,
38
+ "mock_driver.rb")
39
+ ::Unobtainium::Driver.register_implementation(OptionResolvingMockDriver,
40
+ "mock_driver.rb")
38
41
  end
39
42
 
40
43
  describe "driver registration" do
@@ -101,6 +104,32 @@ describe ::Unobtainium::Driver do
101
104
  drv = ::Unobtainium::Driver.create(:mock, foo: 42)
102
105
  expect(drv.passed_options).to eql foo: 42
103
106
  end
107
+
108
+ context "option resolution" do
109
+ it "provides defaults" do
110
+ drv = ::Unobtainium::Driver.create(:option_resolving)
111
+ expect(drv.options).to include(:foo)
112
+ expect(drv.options[:foo]).to eql 42
113
+ end
114
+
115
+ it "overrides when appropriate" do
116
+ drv = ::Unobtainium::Driver.create(:option_resolving, foo: 123)
117
+ expect(drv.options).to include(:foo)
118
+ expect(drv.options[:foo]).to eql 42
119
+ end
120
+
121
+ it "does not override always" do
122
+ drv = ::Unobtainium::Driver.create(:option_resolving, foo: 456)
123
+ expect(drv.options).to include(:foo)
124
+ expect(drv.options[:foo]).to eql 456
125
+ end
126
+
127
+ it "sets an instance ID" do
128
+ drv = ::Unobtainium::Driver.create(:option_resolving)
129
+ expect(drv.options).to include('unobtainium_instance_id')
130
+ expect(drv.options['unobtainium_instance_id']).to eql 'FIXED'
131
+ end
132
+ end
104
133
  end
105
134
 
106
135
  describe 'modules' do