unobtainium 0.2.1 → 0.3.0

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