unobtainium 0.8.1 → 0.9.0

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