golly-utils 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +19 -0
- data/.rspec +8 -0
- data/.simplecov +14 -0
- data/.travis.yml +5 -0
- data/.yardopts +6 -0
- data/CHANGELOG.md +10 -0
- data/Gemfile +12 -0
- data/Gemfile.corvid +27 -0
- data/Gemfile.lock +86 -0
- data/Guardfile +44 -0
- data/README.md +3 -0
- data/RELEASE.md +13 -0
- data/Rakefile +6 -0
- data/bin/guard +16 -0
- data/bin/rake +16 -0
- data/bin/rspec +16 -0
- data/bin/yard +16 -0
- data/bin/yardoc +16 -0
- data/bin/yri +16 -0
- data/golly-utils.gemspec +17 -0
- data/lib/golly-utils/attr_declarative.rb +74 -0
- data/lib/golly-utils/callbacks.rb +92 -0
- data/lib/golly-utils/child_process.rb +124 -0
- data/lib/golly-utils/colourer.rb +50 -0
- data/lib/golly-utils/delegator.rb +81 -0
- data/lib/golly-utils/multi_io.rb +13 -0
- data/lib/golly-utils/ruby_ext.rb +2 -0
- data/lib/golly-utils/ruby_ext/array_to_hash.rb +29 -0
- data/lib/golly-utils/ruby_ext/deep_dup.rb +33 -0
- data/lib/golly-utils/ruby_ext/env_helpers.rb +27 -0
- data/lib/golly-utils/ruby_ext/hash_combinations.rb +49 -0
- data/lib/golly-utils/ruby_ext/pretty_error_messages.rb +9 -0
- data/lib/golly-utils/ruby_ext/subclasses.rb +17 -0
- data/lib/golly-utils/test/spec/deferrable_specs.rb +85 -0
- data/lib/golly-utils/test/spec/within_time.rb +56 -0
- data/lib/golly-utils/version.rb +3 -0
- data/test/bootstrap/all.rb +4 -0
- data/test/bootstrap/spec.rb +5 -0
- data/test/bootstrap/unit.rb +4 -0
- data/test/spec/child_process_mock_target.rb +28 -0
- data/test/spec/child_process_spec.rb +41 -0
- data/test/unit/attr_declarative_test.rb +54 -0
- data/test/unit/callbacks_test.rb +76 -0
- data/test/unit/delegator_test.rb +99 -0
- data/test/unit/multi_io_test.rb +18 -0
- data/test/unit/ruby_ext/env_helpers_test.rb +48 -0
- data/test/unit/ruby_ext/hash_combinations_test.rb +31 -0
- data/test/unit/ruby_ext/subclasses_test.rb +24 -0
- metadata +107 -0
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'golly-utils/ruby_ext/deep_dup'
|
2
|
+
|
3
|
+
module GollyUtils
|
4
|
+
module Callbacks
|
5
|
+
|
6
|
+
def self.included(base)
|
7
|
+
base.send :include, InstanceMethods
|
8
|
+
base.send :include, InstanceAndClassMethods
|
9
|
+
base.extend InstanceAndClassMethods
|
10
|
+
base.extend ClassMethods
|
11
|
+
end
|
12
|
+
|
13
|
+
#-------------------------------------------------------------------------------------------------------------------
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
|
17
|
+
def define_callbacks(*callbacks)
|
18
|
+
callbacks.each do |name|
|
19
|
+
name= _norm_callback_key(name)
|
20
|
+
|
21
|
+
if self.methods.include?(name.to_sym)
|
22
|
+
raise "Can't create callback with name '#{name}'. A method with that name already exists."
|
23
|
+
end
|
24
|
+
|
25
|
+
_callbacks[name] ||= {}
|
26
|
+
class_eval <<-EOB
|
27
|
+
def self.#{name}(&block)
|
28
|
+
v= (_callbacks[#{name.inspect}] ||= {})
|
29
|
+
(v[:procs] ||= [])<< block
|
30
|
+
end
|
31
|
+
EOB
|
32
|
+
end
|
33
|
+
end
|
34
|
+
alias :define_callback :define_callbacks
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def _callbacks
|
39
|
+
@callbacks ||= {}
|
40
|
+
end
|
41
|
+
|
42
|
+
def _get_callback_procs(name)
|
43
|
+
name_verified= false
|
44
|
+
results= []
|
45
|
+
|
46
|
+
# Get local
|
47
|
+
if local= _callbacks[name]
|
48
|
+
name_verified= true
|
49
|
+
results.concat local[:procs] if local[:procs]
|
50
|
+
end
|
51
|
+
|
52
|
+
# Get inherited
|
53
|
+
if superclass.private_methods.include?(:_get_callback_procs)
|
54
|
+
n,r = superclass.send(:_get_callback_procs,name)
|
55
|
+
name_verified ||= n
|
56
|
+
results.concat r
|
57
|
+
end
|
58
|
+
|
59
|
+
[name_verified,results]
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
#-------------------------------------------------------------------------------------------------------------------
|
65
|
+
|
66
|
+
module InstanceAndClassMethods
|
67
|
+
|
68
|
+
private
|
69
|
+
def _norm_callback_key(key)
|
70
|
+
key.to_sym
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
#-------------------------------------------------------------------------------------------------------------------
|
76
|
+
|
77
|
+
module InstanceMethods
|
78
|
+
|
79
|
+
def run_callbacks(*callbacks)
|
80
|
+
callbacks.each do |name|
|
81
|
+
name= _norm_callback_key(name)
|
82
|
+
name_verified,results = self.class.send :_get_callback_procs, name
|
83
|
+
raise "There is no callback defined with name #{name}." unless name_verified
|
84
|
+
results.each{|cb| cb.call }
|
85
|
+
end
|
86
|
+
true
|
87
|
+
end
|
88
|
+
alias :run_callback :run_callbacks
|
89
|
+
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
module GollyUtils
|
2
|
+
|
3
|
+
# Start, manage, and stop a child process.
|
4
|
+
class ChildProcess
|
5
|
+
attr_accessor :start_command, :quiet, :spawn_options, :env
|
6
|
+
attr_reader :pid
|
7
|
+
alias :quiet? :quiet
|
8
|
+
|
9
|
+
# @option options [String] :start_command The shell command to start the child process.
|
10
|
+
# @option options [Hash] :env Environment variables to set in the child process.
|
11
|
+
# @option options [Boolean] :quiet (false) Whether to print startup/shutdown info to stdout, and whether or not to
|
12
|
+
# the stdout and stderr streams of the child process (unless explictly redirected via :spawn_options)
|
13
|
+
# @option options [Hash] :spawn_options Options to pass to Process#spawn.
|
14
|
+
def initialize(options={})
|
15
|
+
options= {env: {}, quiet: false, spawn_options: {}}.merge(options)
|
16
|
+
options[:spawn_options][:in] ||= '/dev/null'
|
17
|
+
options.each {|k,v| send "#{k}=", v}
|
18
|
+
end
|
19
|
+
|
20
|
+
# Starts the child process.
|
21
|
+
#
|
22
|
+
# If it is already running, then this will do nothing.
|
23
|
+
#
|
24
|
+
# @return @self@
|
25
|
+
def startup
|
26
|
+
unless alive?
|
27
|
+
opt= self.spawn_options
|
28
|
+
if quiet?
|
29
|
+
opt= opt.dup
|
30
|
+
mute= [:out,:err] - opt.keys.flatten
|
31
|
+
mute.each {|fd| opt[fd] ||= '/dev/null'}
|
32
|
+
end
|
33
|
+
unless quiet?
|
34
|
+
e= ''
|
35
|
+
env.each{|k,v| e+="#{k}=#{v} "} if env
|
36
|
+
puts "> #{e}#{start_command}"
|
37
|
+
end
|
38
|
+
@pid= spawn env, start_command, opt
|
39
|
+
Process.detach @pid
|
40
|
+
puts "< Spawned process #@pid" unless quiet?
|
41
|
+
end
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
# Stops the child process.
|
46
|
+
#
|
47
|
+
# If it is already running, then this will do nothing.
|
48
|
+
#
|
49
|
+
# @return [Boolean] Boolean indicating whether shutdown was successful and process is down.
|
50
|
+
def shutdown
|
51
|
+
if alive?
|
52
|
+
t= Thread.new{ attempt_kill }
|
53
|
+
if quiet?
|
54
|
+
t.join
|
55
|
+
else
|
56
|
+
puts "Stopping process #@pid..."
|
57
|
+
t.join
|
58
|
+
end
|
59
|
+
end
|
60
|
+
!alive?
|
61
|
+
end
|
62
|
+
|
63
|
+
# Checks if the process [*previously started by this class*] is still alive.
|
64
|
+
#
|
65
|
+
# If it is determined that the process is no longer alive then the internal {#pid PID} is cleared.
|
66
|
+
#
|
67
|
+
# @return [Boolean]
|
68
|
+
def alive?
|
69
|
+
return false if @pid.nil?
|
70
|
+
alive= begin
|
71
|
+
Process.getpgid(@pid) != -1
|
72
|
+
rescue Errno::ESRCH
|
73
|
+
false
|
74
|
+
end
|
75
|
+
@pid= nil unless alive
|
76
|
+
alive
|
77
|
+
end
|
78
|
+
|
79
|
+
protected
|
80
|
+
|
81
|
+
def kill_signals
|
82
|
+
[
|
83
|
+
['TERM', 6],
|
84
|
+
['QUIT', 6],
|
85
|
+
['INT' , 6],
|
86
|
+
['KILL', 1],
|
87
|
+
]
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def attempt_kill
|
93
|
+
kill_signals.each do |signal, wait_time|
|
94
|
+
Process.kill(signal, @pid)
|
95
|
+
start_time= Time.now
|
96
|
+
sleep 0.1 while alive? and (Time.now - start_time) < wait_time
|
97
|
+
break unless alive?
|
98
|
+
end
|
99
|
+
if alive?
|
100
|
+
STDERR.puts "Failed to kill process, PID #@pid"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# --------------------------------------------------------------------------------------------------------------------
|
105
|
+
class << self
|
106
|
+
OPTION_TRANSLATION= {
|
107
|
+
stdout: :out,
|
108
|
+
stderr: :err,
|
109
|
+
stdin: :in,
|
110
|
+
}.freeze
|
111
|
+
|
112
|
+
def translate_options(prefix='')
|
113
|
+
spawn_opts= {}
|
114
|
+
OPTION_TRANSLATION.each do |from,to|
|
115
|
+
if v= ENV["#{prefix}#{from}"] and !v.empty?
|
116
|
+
spawn_opts[to]= v
|
117
|
+
end
|
118
|
+
end
|
119
|
+
spawn_opts
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module GollyUtils
|
2
|
+
|
3
|
+
# Helps decorate text with ANSI colour codes.
|
4
|
+
class Colourer
|
5
|
+
attr_reader :output
|
6
|
+
|
7
|
+
# @param output The target IO object that text will be written to.
|
8
|
+
def initialize(output)
|
9
|
+
@output= output
|
10
|
+
end
|
11
|
+
|
12
|
+
def color_enabled?
|
13
|
+
@color ||= output_to_tty?
|
14
|
+
end
|
15
|
+
|
16
|
+
def color_enabled=(bool)
|
17
|
+
# Ungracefully ripped out of RSpec.
|
18
|
+
return unless bool
|
19
|
+
@color = true
|
20
|
+
if bool && ::RbConfig::CONFIG['host_os'] =~ /mswin|mingw/
|
21
|
+
unless ENV['ANSICON']
|
22
|
+
warn "You must use ANSICON 1.31 or later (http://adoxa.110mb.com/ansicon/) to use colour on Windows"
|
23
|
+
@color = false
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Wraps text in a given colour code (with a colour-clear code on the end) if colours are enabled.
|
29
|
+
# @return [String]
|
30
|
+
def add_color(text, color_code)
|
31
|
+
color_enabled? ? "#{color_code}#{text}\e[0m" : text
|
32
|
+
end
|
33
|
+
|
34
|
+
# Calls `puts` on the output stream with optionally coloured text.
|
35
|
+
def puts(text, color_code)
|
36
|
+
output.puts add_color(text, color_code)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def output_to_tty?
|
42
|
+
begin
|
43
|
+
output.tty?
|
44
|
+
rescue NoMethodError
|
45
|
+
false
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module GollyUtils
|
2
|
+
|
3
|
+
# An object that delegates method calls to eligible delegate objects.
|
4
|
+
class Delegator
|
5
|
+
attr_reader :delegate_to
|
6
|
+
|
7
|
+
# @overload initialize(*delegates, options={})
|
8
|
+
# @param [Object] delegates Objects that method calls may be delegated to.
|
9
|
+
# @param [Hash] options
|
10
|
+
# @option options [true,false] :cache (true) Whether or not to maintain a cache of which delegate objects can
|
11
|
+
# respond to each method call.
|
12
|
+
# @option options [:first,:all] :delegate_to (:first) When multiple delegates can respond to a method call, this
|
13
|
+
# setting determines which object(s) are delegated to.
|
14
|
+
# @option options [String, Symbol, Regexp, Array] :method_whitelist Method name matcher(s) that specify which
|
15
|
+
# methods are allowed to be delegated.
|
16
|
+
# @option options [String, Symbol, Regexp, Array] :method_blacklist Method name matcher(s) that specify methods
|
17
|
+
# that are not allowed to be delegated.
|
18
|
+
def initialize(*args)
|
19
|
+
options= args.last.kind_of?(Hash) ? args.pop.clone : {}
|
20
|
+
@original_options= options
|
21
|
+
|
22
|
+
@delegates= args
|
23
|
+
@delegate_to= options[:delegate_to] || :first
|
24
|
+
@cache= {} unless options.has_key?(:cache) && !options[:cache]
|
25
|
+
parse_method_delegation_option options, :method_whitelist
|
26
|
+
parse_method_delegation_option options, :method_blacklist
|
27
|
+
end
|
28
|
+
|
29
|
+
def dup; Delegator.new @delegates.map(&:dup), @original_options end
|
30
|
+
def clone; Delegator.new @delegates.map(&:clone), @original_options end
|
31
|
+
|
32
|
+
def method_missing(method, *args)
|
33
|
+
matches= delegates_that_respond_to(method)
|
34
|
+
return super(method,*args) if matches.empty?
|
35
|
+
|
36
|
+
case delegate_to
|
37
|
+
when :first
|
38
|
+
matches[0].public_send(method,*args)
|
39
|
+
when :all
|
40
|
+
matches.map{|m| m.public_send(method,*args)}
|
41
|
+
else
|
42
|
+
raise "Don't know how to respond to :delegate_to value of #{delegate_to.inspect}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def respond_to?(method)
|
47
|
+
!delegates_that_respond_to(method).empty?
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def parse_method_delegation_option(options, name)
|
53
|
+
if values= options[name]
|
54
|
+
methods= [values].flatten.compact.map{|m| m.is_a?(String) ? m.to_sym : m}.uniq
|
55
|
+
instance_variable_set :"@#{name}", methods
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def delegates_that_respond_to(method)
|
60
|
+
delegation_cache(method){ uncached_delegates_that_respond_to(method) }
|
61
|
+
end
|
62
|
+
|
63
|
+
NO_MATCHES= [].freeze
|
64
|
+
def uncached_delegates_that_respond_to(method)
|
65
|
+
return NO_MATCHES unless @delegates # i.e. not initialised yet, e.g. missing method before super() called in subclass
|
66
|
+
return NO_MATCHES if @method_whitelist && !@method_whitelist.any?{|m| m === method}
|
67
|
+
return NO_MATCHES if @method_blacklist && @method_blacklist.any?{|m| m === method}
|
68
|
+
r= @delegates.select{|d| d.respond_to?(method)}
|
69
|
+
r.empty? ? NO_MATCHES : r
|
70
|
+
end
|
71
|
+
|
72
|
+
# Don't allow nil values
|
73
|
+
def delegation_cache(key)
|
74
|
+
if @cache
|
75
|
+
@cache[key] ||= yield
|
76
|
+
else
|
77
|
+
yield
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'golly-utils/delegator'
|
2
|
+
|
3
|
+
module GollyUtils
|
4
|
+
|
5
|
+
# A fake IO implmentation that writes data to multiple underlying IO objects.
|
6
|
+
class MultiIO < Delegator
|
7
|
+
|
8
|
+
# @param targets Real IO objects.
|
9
|
+
def initialize(*targets)
|
10
|
+
super *targets, delegate_to: :all
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class Array
|
2
|
+
|
3
|
+
# Converts the array to a hash where the values are the array elements and the keys are provided by calling a given
|
4
|
+
# block.
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
# ['m','abc'].to_hash_keyed_by{ |v| v.length } # => {1 => 'm', 3 => 'abc}
|
8
|
+
def to_hash_keyed_by(raise_on_duplicate_keys=true, &key_provider)
|
9
|
+
h= {}
|
10
|
+
each {|e|
|
11
|
+
k= key_provider.call(e)
|
12
|
+
raise "Duplicate key: #{k.inspect}" if raise_on_duplicate_keys and h.has_key?(k)
|
13
|
+
h[k]= e
|
14
|
+
}
|
15
|
+
h
|
16
|
+
end
|
17
|
+
|
18
|
+
# Converts the array to a hash where the keys are the array elements and the values are provided by either calling a
|
19
|
+
# given block, or using a fixed, provided argument.
|
20
|
+
#
|
21
|
+
# @example
|
22
|
+
# [2,5].to_hash_with_values('x') # => {2 => 'x', 5 => 'x'}
|
23
|
+
# [2,5].to_hash_with_values{ |k| 'xo' * k } # => {2 => 'xoxo', 5 => 'xoxoxoxoxo'}
|
24
|
+
def to_hash_with_values(value=nil)
|
25
|
+
h= {}
|
26
|
+
each {|e| h[e]= block_given? ? yield(e) : value}
|
27
|
+
h
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
class Object
|
2
|
+
# Creates a deep copy of the object. Where supported (arrays and hashes by default), object state will be duplicated
|
3
|
+
# and used, rather than the original and duplicate objects sharing the same state.
|
4
|
+
def deep_dup
|
5
|
+
dup
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class Array
|
10
|
+
# Creates a copy of the array with deep copies of each element.
|
11
|
+
#
|
12
|
+
# @see Object#deep_dup
|
13
|
+
def deep_dup
|
14
|
+
map(&:deep_dup)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class Hash
|
19
|
+
# Creates a copy of the hash with deep copies of each key and value.
|
20
|
+
#
|
21
|
+
# @see Object#deep_dup
|
22
|
+
def deep_dup
|
23
|
+
duplicate = {}
|
24
|
+
each_pair do |k,v|
|
25
|
+
duplicate[k.deep_dup]= v.deep_dup
|
26
|
+
end
|
27
|
+
duplicate
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
[TrueClass, FalseClass, NilClass, Symbol, Numeric].each do |klass|
|
32
|
+
klass.class_eval "def deep_dup; self end"
|
33
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module GollyUtils
|
2
|
+
module EnvHelpers
|
3
|
+
|
4
|
+
def boolean(key, default=nil)
|
5
|
+
return default unless self.has_key?(key)
|
6
|
+
v= self[key]
|
7
|
+
return true if v =~ /^\s*(?:[1yt]|yes|true|on|enabled?)\s*$/i
|
8
|
+
return false if v =~ /^\s*(?:[0nf]|no|false|off|disabled?)\s*$/i
|
9
|
+
STDERR.puts "Unable to parse boolean value #{v.inspect} for key #{key.inspect}."
|
10
|
+
default
|
11
|
+
end
|
12
|
+
|
13
|
+
alias yes? boolean
|
14
|
+
alias on? boolean
|
15
|
+
alias enabled? boolean
|
16
|
+
|
17
|
+
def no?(key, default=nil)
|
18
|
+
!boolean(key, default)
|
19
|
+
end
|
20
|
+
alias off? no?
|
21
|
+
alias disabled? no?
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Add helpers to ENV
|
27
|
+
ENV.send :extend, GollyUtils::EnvHelpers
|