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.
@@ -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
  ##
@@ -20,13 +22,12 @@ module Unobtainium
20
22
  firefox: [:ff,],
21
23
  internet_explorer: [:internetexplorer, :explorer, :ie,],
22
24
  safari: [],
23
- chrome: [],
24
- chromium: [],
25
+ chrome: [:chromium],
25
26
  remote: [],
26
27
  }.freeze
27
28
 
28
29
  class << self
29
- include ::Unobtainium::Drivers::Utility
30
+ include ::Unobtainium::Support::Utility
30
31
 
31
32
  ##
32
33
  # Return true if the given label matches this driver implementation,
@@ -47,7 +48,7 @@ module Unobtainium
47
48
 
48
49
  ##
49
50
  # Selenium really wants symbol keys for the options
50
- def sanitize_options(label, options)
51
+ def resolve_options(label, options)
51
52
  new_opts = {}
52
53
 
53
54
  if not options.nil?
@@ -13,14 +13,25 @@ module Unobtainium
13
13
 
14
14
  ##
15
15
  # The PathedHash class wraps Hash by offering pathed access on top of
16
- # regular access, i.e. instead of h["first"]["second"] you can write
17
- # h["first.second"]
16
+ # regular access, i.e. instead of `h["first"]["second"]` you can write
17
+ # `h["first.second"]`.
18
+ #
19
+ # The main benefit is much simpler code for accessing nested structured.
20
+ # For any given path, PathedHash will return nil from `[]` if *any* of
21
+ # the path components do not exist.
22
+ #
23
+ # Similarly, intermediate nodes will be created when you write a value
24
+ # for a path.
25
+ #
26
+ # PathedHash also includes RecursiveMerge.
18
27
  class PathedHash
19
28
  include RecursiveMerge
20
29
 
21
30
  ##
22
- # Initializer
23
- def initialize(init = {})
31
+ # Initializer. Accepts `nil`, hashes or pathed hashes.
32
+ #
33
+ # @param init [NilClass, Hash] initial values.
34
+ def initialize(init = nil)
24
35
  if init.nil?
25
36
  @data = {}
26
37
  else
@@ -29,18 +40,23 @@ module Unobtainium
29
40
  @separator = '.'
30
41
  end
31
42
 
32
- # The separator is the character or pattern splitting paths
43
+ # @return [String] the separator is the character or pattern splitting paths.
33
44
  attr_accessor :separator
34
45
 
46
+ # @api private
47
+ # Methods redefined to support pathed read access.
35
48
  READ_METHODS = [
36
49
  :[], :default, :delete, :fetch, :has_key?, :include?, :key?, :member?,
37
50
  ].freeze
51
+
52
+ # @api private
53
+ # Methods redefined to support pathed write access.
38
54
  WRITE_METHODS = [
39
55
  :[]=, :store,
40
56
  ].freeze
41
57
 
42
58
  ##
43
- # Returns the pattern to split paths at
59
+ # @return [RegExp] the pattern to split paths at; based on `separator`
44
60
  def split_pattern
45
61
  /(?<!\\)#{Regexp.escape(@separator)}/
46
62
  end
@@ -99,14 +115,18 @@ module Unobtainium
99
115
  end
100
116
  end
101
117
 
118
+ # @return [String] string representation
102
119
  def to_s
103
120
  @data.to_s
104
121
  end
105
122
 
123
+ # @return [PathedHash] duplicate, as `.dup` usually works
106
124
  def dup
107
125
  PathedHash.new(@data.dup)
108
126
  end
109
127
 
128
+ # In place merge, as it usually works for hashes.
129
+ # @return [PathedHash] self
110
130
  def merge!(*args, &block)
111
131
  # FIXME: we may need other methods like this. This is used by
112
132
  # RecursiveMerge, so we know it's required.
@@ -114,7 +134,7 @@ module Unobtainium
114
134
  end
115
135
 
116
136
  ##
117
- # Map any missing method to the driver implementation
137
+ # Map any missing method to the Hash implementation
118
138
  def respond_to?(meth)
119
139
  if not @data.nil? and @data.respond_to?(meth)
120
140
  return true
@@ -122,6 +142,8 @@ module Unobtainium
122
142
  return super
123
143
  end
124
144
 
145
+ ##
146
+ # Map any missing method to the Hash implementation
125
147
  def method_missing(meth, *args, &block)
126
148
  if not @data.nil? and @data.respond_to?(meth)
127
149
  return @data.send(meth.to_s, *args, &block)
@@ -11,6 +11,18 @@ module Unobtainium
11
11
  ##
12
12
  # Provides recursive merge functions for hashes. Used in PathedHash.
13
13
  module RecursiveMerge
14
+ ##
15
+ # Recursively merge `:other` into this Hash.
16
+ #
17
+ # This starts by merging the leaf-most Hash entries. Arrays are merged
18
+ # by addition.
19
+ #
20
+ # For everything that's neither Hash or Array, if the `:overwrite`
21
+ # parameter is true, the entry from `:other` is used. Otherwise the entry
22
+ # from `:self` is used.
23
+ #
24
+ # @param other [Hash] the hash to merge into `:self`
25
+ # @param overwrite [Boolean] see method description.
14
26
  def recursive_merge!(other, overwrite = true)
15
27
  if other.nil?
16
28
  return self
@@ -33,6 +45,9 @@ module Unobtainium
33
45
  merge!(other, &merger)
34
46
  end
35
47
 
48
+ ##
49
+ # Same as `dup.recursive_merge!`
50
+ # @param (see #recursive_merge!)
36
51
  def recursive_merge(other, overwrite = true)
37
52
  dup.recursive_merge!(other, overwrite)
38
53
  end
@@ -34,13 +34,14 @@ module Unobtainium
34
34
  end
35
35
 
36
36
  ##
37
- # Number of objects stored in the object map
37
+ # @return [Integer] number of objects stored in the object map
38
38
  def length
39
39
  return @objects.length
40
40
  end
41
41
 
42
42
  ##
43
- # Does an object with the given name exist?
43
+ # @param name [String, Symbol] name or label for an object
44
+ # @return [Boolean] does an object with the given name exist?
44
45
  def has?(name)
45
46
  return @objects.key?(name)
46
47
  end
@@ -51,8 +52,14 @@ module Unobtainium
51
52
  # is stored.
52
53
  #
53
54
  # If a destructor is passed, it is used to destroy the *new* object only.
54
- # If no destructor is passed and the object responds to a :destroy method, that
55
- # method is called.
55
+ # If no destructor is passed and the object responds to a `#destroy` method,
56
+ # that method is called.
57
+ #
58
+ # @param name [String, Symbol] name or label for the object to store
59
+ # @param object [Object] the object to store
60
+ # @param destructor [Func] a custom destructor accepting the object as its
61
+ # parameter.
62
+ # @return [Object] the stored object
56
63
  def store(name, object, destructor = nil)
57
64
  delete(name)
58
65
 
@@ -65,7 +72,13 @@ module Unobtainium
65
72
  # Store the object returned by the block, if any. If no object is returned
66
73
  # or no block is given, this function does nothing.
67
74
  #
68
- # Otherwise it works much like :store above.
75
+ # Otherwise it works much like `#store`.
76
+ #
77
+ # @param name [String, Symbol] name or label for the object to store
78
+ # @param destructor [Func] a custom destructor accepting the object as its
79
+ # parameter.
80
+ # @param block [Func] a block returning the created object.
81
+ # @return [Object] the stored object
69
82
  def store_with(name, destructor = nil, &block)
70
83
  object = nil
71
84
  if not block.nil?
@@ -80,7 +93,8 @@ module Unobtainium
80
93
  end
81
94
 
82
95
  ##
83
- # Like :store, but only stores the object if none exists for that key yet.
96
+ # (see #store)
97
+ # Like `#store`, but only stores the object if none exists for that key yet.
84
98
  def store_if(name, object, destructor = nil)
85
99
  if has?(name)
86
100
  return self[name]
@@ -89,7 +103,8 @@ module Unobtainium
89
103
  end
90
104
 
91
105
  ##
92
- # Like :store_if, but as a block version similar to :store_with.
106
+ # (see #store_with)
107
+ # Like `#store_if`, but as a block version similar to `#store_with`.
93
108
  def store_with_if(name, destructor = nil, &block)
94
109
  if has?(name)
95
110
  return self[name]
@@ -99,6 +114,8 @@ module Unobtainium
99
114
 
100
115
  ##
101
116
  # Deletes (and destroys) any object found under the given name.
117
+ #
118
+ # @param name [String, Symbol] name or label for the object to store
102
119
  def delete(name)
103
120
  if not @objects.key?(name)
104
121
  return
@@ -112,6 +129,11 @@ module Unobtainium
112
129
  ##
113
130
  # Returns the object with the given name, or the default value if no such
114
131
  # object exists.
132
+ #
133
+ # @param name [String, Symbol] name or label for the object to retrieve
134
+ # @param default [Object] default value to return if no object is found for
135
+ # name or label.
136
+ # @return [Object] the object matching the name/label, or the default value.
115
137
  def fetch(name, default = nil)
116
138
  return @objects.fetch(name)[0]
117
139
  rescue KeyError
@@ -122,8 +144,11 @@ module Unobtainium
122
144
  end
123
145
 
124
146
  ##
125
- # Similar to :fetch, but always returns nil for an object that could not
147
+ # Similar to `#fetch`, but always returns nil for an object that could not
126
148
  # be found.
149
+ #
150
+ # @param name [String, Symbol] name or label for the object to retrieve
151
+ # @return [Object] the object matching the name/label, or the default value.
127
152
  def [](name)
128
153
  val = @objects[name]
129
154
  if val.nil?
@@ -0,0 +1,160 @@
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 'socket'
11
+
12
+ module Unobtainium
13
+ # @api private
14
+ # Contains support code
15
+ module Support
16
+ ##
17
+ # A bit of metaprogramming hackery to make a constant with possible domains
18
+ # from Socket::Constants.
19
+ if not constants.include?('DOMAINS')
20
+ domains = []
21
+ Socket::Constants.constants.each do |name|
22
+ if name.to_s.start_with?("AF_")
23
+ domains << name.to_s.gsub(/^AF_/, '').to_sym
24
+ end
25
+ end
26
+ const_set('DOMAINS', domains.freeze)
27
+ end
28
+
29
+ ##
30
+ # A port scanner for finding a free port for running e.g. a selenium
31
+ # or appium server.
32
+ module PortScanner
33
+ ##
34
+ # Returns true if the port is open on the host, false otherwise.
35
+ # @param host [String] host name or IP address
36
+ # @param port [Integer] port number (1..65535)
37
+ # @param domains [Array/Symbol] :INET, :INET6, etc. or an Array of
38
+ # these. Any from Socket::Constants::AF_* work. Defaults to
39
+ # [:INET, :INET6].
40
+ def port_open?(host, port, domains = [:INET, :INET6])
41
+ if port < 1 or port > 65535
42
+ raise ArgumentError, "Port must be in range 1..65535!"
43
+ end
44
+
45
+ test_domains = nil
46
+ if domains.is_a? Array
47
+ test_domains = domains.dup
48
+ else
49
+ test_domains = [domains]
50
+ end
51
+
52
+ test_domains.each do |domain|
53
+ if not DOMAINS.include?(domain)
54
+ raise ArgumentError, "Domains must be one of #{DOMAINS}, or an Array "\
55
+ "of them, but #{domain} isn't!"
56
+ end
57
+ end
58
+
59
+ # Test a socket for each domain
60
+ addr = Socket.sockaddr_in(port, host)
61
+
62
+ test_domains.each do |domain|
63
+ begin
64
+ sock = Socket.new(domain, :STREAM)
65
+ return 0 == sock.connect(addr)
66
+ rescue Errno::EAFNOSUPPORT
67
+ next # try next domain
68
+ rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT
69
+ return false
70
+ ensure
71
+ if not sock.nil?
72
+ sock.close
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ ##
79
+ # Scan a mixture of ranges and arrays of ports for a given host.
80
+ # Return those that are open or closed, depending on the options
81
+ # given.
82
+ def scan(host, *args)
83
+ # Argument checks
84
+ if args.empty?
85
+ raise ArgumentError, "Need at least one port to scan!"
86
+ end
87
+
88
+ args.each do |item|
89
+ if not item.respond_to?(:each) and not item.respond_to?(:to_i)
90
+ raise ArgumentError, "The argument '#{item}' to #scan is not a "\
91
+ "Range, Array or convertible to Integer, aborting!"
92
+ end
93
+ end
94
+
95
+ # If the last argument is a Hash, treat it as options.
96
+ opts = {}
97
+ if args.last.is_a? Hash
98
+ opts = args.pop
99
+ end
100
+ opts = { for: :open, amount: :all }.merge(opts)
101
+
102
+ if not [:all, :first].include?(opts[:amount])
103
+ raise ArgumentError, ":amount must be one of :all, :first!"
104
+ end
105
+ if not [:open, :closed, :available].include?(opts[:for])
106
+ raise ArgumentError, ":for must beone of :open, :closed, :available!"
107
+ end
108
+
109
+ return run_scan(host, opts, *args)
110
+ end
111
+
112
+ private
113
+
114
+ def run_scan(host, opts, *args)
115
+ results = []
116
+
117
+ # Iteratively scan all arguments
118
+ args.each do |item|
119
+ if item.respond_to?(:to_i)
120
+ item_i = item.to_i
121
+ if test_port(host, item_i, opts[:for])
122
+ results << item_i
123
+ if opts[:amount] == :first
124
+ return results
125
+ end
126
+ end
127
+ next
128
+ end
129
+
130
+ item.each do |port|
131
+ if not test_port(host, port, opts[:for])
132
+ next
133
+ end
134
+
135
+ results << port
136
+ if opts[:amount] == :first
137
+ return results
138
+ end
139
+ end
140
+ end
141
+
142
+ return results
143
+ end
144
+
145
+ def test_port(host, port, test_for)
146
+ open = port_open?(host, port)
147
+
148
+ if open and :open == test_for
149
+ return true
150
+ end
151
+
152
+ if not open and [:closed, :available].include?(test_for)
153
+ return true
154
+ end
155
+
156
+ return false
157
+ end
158
+ end # module PortScanner
159
+ end # module Support
160
+ end # module Unobtainium
@@ -0,0 +1,180 @@
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
+ require 'sys-proctable'
10
+
11
+ module Unobtainium
12
+ # @api private
13
+ # Contains support code
14
+ module Support
15
+ ##
16
+ # Runs a shell command and detaches. Offers methods for managing the process
17
+ # lifetime. Implements #destroy, so you can use it with the Runtime class to
18
+ # kill the shell command upon script end.
19
+ class Runner
20
+ # @return [String] the ID passed to the constructor
21
+ attr_reader :id
22
+
23
+ # @return [Array] the command passed to the constructor
24
+ attr_reader :command
25
+
26
+ # @return [Integer] if the command is started, the pid of the process,
27
+ # or nil otherwise.
28
+ attr_reader :pid
29
+
30
+ # @return [IO] if the command is started, an IO object to read the
31
+ # commands output from, or nil otherwise.
32
+ attr_reader :stdout
33
+
34
+ # @return [IO] if the command is started, an IO object to read the
35
+ # commands error output from, or nil otherwise.
36
+ attr_reader :stderr
37
+
38
+ ##
39
+ # Initialize with a shell command, but do not run yet.
40
+ #
41
+ # @param id [String] a unique ID for the command. This is to differentiate
42
+ # multiple similar commands from each other.
43
+ # @param command [Array] the remaining parameters are the command and its
44
+ # arguments.
45
+ def initialize(id, *command)
46
+ if command.empty?
47
+ raise ArgumentError, "Command may not be empty!"
48
+ end
49
+
50
+ @id = id
51
+ @command = command
52
+ @pid = nil
53
+ @stdout = nil
54
+ @stderr = nil
55
+ @wout = nil
56
+ @werr = nil
57
+ end
58
+
59
+ ##
60
+ # Start the command. Afterwards, #pid, #stdout, and #stderr should be
61
+ # non-nil.
62
+ # @return [Integer] the pid of the command process
63
+ def start
64
+ if not @pid.nil?
65
+ raise "Command already running!"
66
+ end
67
+
68
+ # Capture options; pipes for stdout and stderr
69
+ @stdout, @wout = IO.pipe
70
+ @stderr, @werr = IO.pipe
71
+ opts = {
72
+ out: @wout,
73
+ err: @werr,
74
+ }
75
+
76
+ @pid = spawn({}, *@command, opts)
77
+ return @pid
78
+ end
79
+
80
+ ##
81
+ # Wait for the command to exit.
82
+ # @return [Process::Status] exit status of the command.
83
+ def wait
84
+ _, status = Process.wait2(@pid)
85
+ cleanup
86
+ return status
87
+ end
88
+
89
+ ##
90
+ # Send the "KILL" signal to the command process and all its children
91
+ def kill
92
+ signal("KILL", scope: :all)
93
+ cleanup
94
+ end
95
+
96
+ ##
97
+ # Send the given signal to the process, and/or it's children.
98
+ # @param signal [String] the signal to send
99
+ # @param scope [Symbol] one of :self (the command process),
100
+ # :children (it's children *only) or :all (the process and its
101
+ # children.
102
+ def signal(signal, scope: :self)
103
+ if @pid.nil?
104
+ raise "No command is running!"
105
+ end
106
+
107
+ if not [:self, :children, :all].include?(scope)
108
+ raise ArgumentError, "The :scope argument must be one of :self, "\
109
+ ":children or :all!"
110
+ end
111
+
112
+ # Figure out which pids to send the signal to. That is usually @pid,
113
+ # but possibly its children.
114
+ to_send = []
115
+ if [:self, :all].include?(scope)
116
+ to_send << @pid
117
+ end
118
+
119
+ if [:children, :all].include?(scope)
120
+ children = ::Sys::ProcTable.ps.select { |p| p.ppid == @pid }
121
+ to_send += children.collect(&:pid)
122
+ end
123
+
124
+ if to_send.empty?
125
+ raise "This should not happen. I have no pids to send a signal to!"
126
+ end
127
+
128
+ # Alright, send the signal!
129
+ to_send.each do |pid|
130
+ # rubocop:disable Lint/HandleExceptions
131
+ begin
132
+ Process.kill(signal, pid)
133
+ rescue
134
+ # If the kill didn't work, we don't really care.
135
+ end
136
+ # rubocop:enable Lint/HandleExceptions
137
+ end
138
+
139
+ # Clean up everything
140
+ cleanup(true)
141
+ end
142
+
143
+ ##
144
+ # (see #kill)
145
+ # Use together with Runtime class to clean up any commands at exit.
146
+ def destroy
147
+ kill
148
+ end
149
+
150
+ private
151
+
152
+ def cleanup(all = false)
153
+ @pid = nil
154
+ # rubocop:disable Style/GuardClause
155
+ if not @wout.nil?
156
+ @wout.close
157
+ @wout = nil
158
+ end
159
+ if not @werr.nil?
160
+ @werr.close
161
+ @werr = nil
162
+ end
163
+
164
+ if not all
165
+ return
166
+ end
167
+
168
+ if not @stdout.nil?
169
+ @stdout.close
170
+ @stdout = nil
171
+ end
172
+ if not @stderr.nil?
173
+ @stderr.close
174
+ @stderr = nil
175
+ end
176
+ # rubocop:enable Style/GuardClause
177
+ end
178
+ end # class Runner
179
+ end # module Support
180
+ end # module Unobtainium