golly-utils 0.0.1
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.
- 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
|