golly-utils 0.0.1 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/.corvid/Gemfile +28 -0
  2. data/.corvid/features.yml +4 -0
  3. data/.corvid/plugins.yml +4 -0
  4. data/.corvid/stats_cfg.rb +13 -0
  5. data/.corvid/todo_cfg.rb +15 -0
  6. data/.corvid/versions.yml +2 -0
  7. data/.simplecov +6 -3
  8. data/CHANGELOG.md +45 -2
  9. data/Gemfile +6 -3
  10. data/Gemfile.lock +34 -37
  11. data/Guardfile +10 -4
  12. data/RELEASE.md +2 -0
  13. data/Rakefile +1 -1
  14. data/golly-utils.gemspec +19 -10
  15. data/lib/golly-utils/attr_declarative.rb +120 -49
  16. data/lib/golly-utils/callbacks.rb +211 -19
  17. data/lib/golly-utils/child_process.rb +28 -8
  18. data/lib/golly-utils/delegator.rb +14 -3
  19. data/lib/golly-utils/ruby_ext/classes_and_types.rb +120 -0
  20. data/lib/golly-utils/ruby_ext/enumerable.rb +16 -0
  21. data/lib/golly-utils/ruby_ext/env_helpers.rb +17 -0
  22. data/lib/golly-utils/ruby_ext/kernel.rb +18 -0
  23. data/lib/golly-utils/ruby_ext/options.rb +28 -0
  24. data/lib/golly-utils/ruby_ext/pretty_error_messages.rb +1 -1
  25. data/lib/golly-utils/singleton.rb +130 -0
  26. data/lib/golly-utils/testing/dynamic_fixtures.rb +268 -0
  27. data/lib/golly-utils/testing/file_helpers.rb +117 -0
  28. data/lib/golly-utils/testing/helpers_base.rb +20 -0
  29. data/lib/golly-utils/testing/rspec/arrays.rb +85 -0
  30. data/lib/golly-utils/testing/rspec/base.rb +9 -0
  31. data/lib/golly-utils/testing/rspec/deferrable_specs.rb +111 -0
  32. data/lib/golly-utils/testing/rspec/files.rb +262 -0
  33. data/lib/golly-utils/{test/spec → testing/rspec}/within_time.rb +17 -7
  34. data/lib/golly-utils/version.rb +2 -1
  35. data/test/bootstrap/all.rb +8 -1
  36. data/test/bootstrap/spec.rb +1 -1
  37. data/test/bootstrap/unit.rb +1 -1
  38. data/test/spec/child_process_spec.rb +1 -1
  39. data/test/spec/testing/dynamic_fixtures_spec.rb +131 -0
  40. data/test/spec/testing/rspec/arrays_spec.rb +33 -0
  41. data/test/spec/testing/rspec/files_spec.rb +300 -0
  42. data/test/unit/attr_declarative_test.rb +79 -13
  43. data/test/unit/callbacks_test.rb +103 -5
  44. data/test/unit/delegator_test.rb +25 -1
  45. data/test/unit/ruby_ext/classes_and_types_test.rb +103 -0
  46. data/test/unit/ruby_ext/enumerable_test.rb +12 -0
  47. data/test/unit/ruby_ext/options_test.rb +29 -0
  48. data/test/unit/singleton_test.rb +59 -0
  49. metadata +100 -10
  50. data/Gemfile.corvid +0 -27
  51. data/lib/golly-utils/ruby_ext.rb +0 -2
  52. data/lib/golly-utils/ruby_ext/subclasses.rb +0 -17
  53. data/lib/golly-utils/test/spec/deferrable_specs.rb +0 -85
  54. data/test/unit/ruby_ext/subclasses_test.rb +0 -24
@@ -0,0 +1,16 @@
1
+ module Enumerable
2
+
3
+ # Creates a map of all elements by the frequency that they occur.
4
+ #
5
+ # @example
6
+ # %w[big house big car].frequency_map # => {"big"=>2, "car"=>1, "house"=>1}
7
+ #
8
+ # @return [Hash<Object,Fixnum>] Map of element to frequency.
9
+ def frequency_map
10
+ inject Hash.new do |h,e|
11
+ h[e]= (h[e] || 0) + 1
12
+ h
13
+ end
14
+ end
15
+
16
+ end
@@ -1,6 +1,19 @@
1
1
  module GollyUtils
2
+ # Mixin that enriches Ruby's `ENV`.
3
+ #
4
+ # @note This is mixed-in to `ENV` automatically.
2
5
  module EnvHelpers
3
6
 
7
+ # Parses an environment variable that is expected to be a boolean.
8
+ #
9
+ # Regardless of case,
10
+ #
11
+ # * the following values are interpretted as positive: `1, y, yes, t, true, on, enabled`
12
+ # * the following values are interpretted as negative: `0, n, no, f, false, off, disabled`
13
+ #
14
+ # @param [String] key The `ENV` key. The environment variable name.
15
+ # @param [Boolean, nil] default The value to return if there is no environment variable of given key.
16
+ # @return [Boolean,nil] The result of parsing the env var value, else `default`.
4
17
  def boolean(key, default=nil)
5
18
  return default unless self.has_key?(key)
6
19
  v= self[key]
@@ -14,6 +27,10 @@ module GollyUtils
14
27
  alias on? boolean
15
28
  alias enabled? boolean
16
29
 
30
+ # Parses an environment variable and checks if it indicates a negative boolean value.
31
+ #
32
+ # @param (see #boolean)
33
+ # @return [Boolean,nil] The result of parsing the env var value and it indicating the negative, else `default`.
17
34
  def no?(key, default=nil)
18
35
  !boolean(key, default)
19
36
  end
@@ -0,0 +1,18 @@
1
+ module Kernel
2
+
3
+ # Alternate implementation of `at_exit` that preserves the exit status (unless you call `exit` yourself and an error
4
+ # is raised).
5
+ #
6
+ # The initial driver for this was that using `at_exit` to clean up global resources in RSpec tests, RSpec's exit
7
+ # status would be lost which means CI processes and such were unable to tell whether there were test failures.
8
+ #
9
+ # @return [Proc] Whatever `at_exit` returns.
10
+ def at_exit_preserving_exit_status(&block)
11
+ at_exit {
12
+ status= $!.is_a?(::SystemExit) ? $!.status : nil
13
+ block.()
14
+ exit status if status
15
+ }
16
+ end
17
+ end
18
+
@@ -0,0 +1,28 @@
1
+ class Hash
2
+
3
+ # Checks that all keys in this hash are part of a specific set; fails if not.
4
+ #
5
+ # @param [Array] valid_keys A set of valid keys.
6
+ # @param [String] error_msg A message to include in the error when raised.
7
+ # @return [self]
8
+ # @raise Invalid keys are present.
9
+ def validate_keys(valid_keys, error_msg = "Invalid hash keys")
10
+ invalid= self.keys - [valid_keys].flatten
11
+ unless invalid.empty?
12
+ raise "#{error_msg}: #{invalid.sort_by(&:to_s).map(&:inspect).join ', '}"
13
+ end
14
+ self
15
+ end
16
+
17
+ # Checks that all keys in this hash are part of a specific set; fails if not.
18
+ #
19
+ # Differs from {#validate_keys} by providing an option-related error message.
20
+ #
21
+ # @param valid_keys A set of valid keys.
22
+ # @return [self]
23
+ # @raise Invalid keys are present.
24
+ def validate_option_keys(*valid_keys)
25
+ validate_keys valid_keys, "Invalid options provided"
26
+ end
27
+
28
+ end
@@ -2,7 +2,7 @@ class StandardError
2
2
 
3
3
  # Creates a string that provides more information (a backtrace) than the default `to_s` method.
4
4
  #
5
- # @return String
5
+ # @return [String]
6
6
  def to_pretty_s
7
7
  "#{self.inspect}\n#{self.backtrace.map{|b| " #{b}"}.join "\n"}"
8
8
  end
@@ -0,0 +1,130 @@
1
+ require 'singleton'
2
+
3
+ module GollyUtils
4
+ # Makes the including class a singleton.
5
+ # Much like Ruby's `Singleton` module, with extra features.
6
+ #
7
+ # Features:
8
+ #
9
+ # * Target class includes Ruby's `Singleton` module too.
10
+ # * Class methods are added to the target class that delegate to the singleton instance.
11
+ # * A convenience method {ClassMethods#def_accessor def_accessor} is provided to create an accessor in the calling
12
+ # class, as per `attr_accessor`, except that the value defaults to the singleton instance.
13
+ #
14
+ # @example
15
+ # class Stam1na
16
+ # include GollyUtils::Singleton
17
+ #
18
+ # def band_rating
19
+ # puts 'AWESOME!!'
20
+ # end
21
+ # end
22
+ #
23
+ # Stam1na.instance.band_rating #=> AWESOME!!
24
+ # Stam1na.band_rating #=> AWESOME!!
25
+ module Singleton
26
+
27
+ # @!visibility private
28
+ def self.included(base)
29
+ base.send :include, ::Singleton
30
+ base.extend ClassMethods
31
+ base.class_eval <<-EOB
32
+ class << self
33
+
34
+ alias :method_missing_before_gu_singleton :method_missing
35
+
36
+ def method_missing(method, *args, &block)
37
+ if method != :__gu_singleton_method_missing
38
+
39
+ r= __gu_singleton_method_missing(self, method, *args, &block)
40
+ unless ::GollyUtils::Singleton::NO_MATCH == r
41
+ return r
42
+ end
43
+
44
+ end
45
+ method_missing_before_gu_singleton method, *args, &block
46
+ end
47
+
48
+ def __default_singleton_attr_name
49
+ '#{base.to_s.sub(/^.*::/,'').gsub(/(?<=[a-z])(?=[A-Z])/,'_').downcase}'
50
+ end
51
+
52
+ end
53
+ EOB
54
+ end
55
+
56
+ module ClassMethods
57
+
58
+ # Creates an instance accessor as `attr_accessor` does, execpt that the default value will be the singleton
59
+ # instance.
60
+ #
61
+ # @example
62
+ # class HappyDays
63
+ # include GollyUtils::Singleton
64
+ # end
65
+ #
66
+ # class A
67
+ # # @!attribute [rw] happy_days
68
+ # # @return [HappyDays]
69
+ # HappyDays.def_accessor self
70
+ # end
71
+ #
72
+ # A.new.happy_days == HappyDays.instance #=> true
73
+ #
74
+ # @param [Class|Module] target The object definition to add the attribute methods to.
75
+ # @param [String|Symbol] name The attribute name. Defaults to the singleton class name, with underscores and in
76
+ # lowercase.
77
+ # @return [nil]
78
+ def def_accessor(target, name=nil)
79
+ name ||= __default_singleton_attr_name
80
+ target.class_eval <<-EOB
81
+ def #{name}
82
+ @#{name} ||= ::#{self}.instance
83
+ end
84
+ def #{name}=(v)
85
+ @#{name}= v
86
+ end
87
+ EOB
88
+ nil
89
+ end
90
+
91
+ # Prevents class-level delegate methods from being created for certain instance methods.
92
+ #
93
+ # @param [Array<Regexp|String>] matchers Either the name of the method, or a regex that matches methods.
94
+ # @return [nil]
95
+ def hide_singleton_methods(*matchers)
96
+ r= __gu_singleton_rejects
97
+ r.concat matchers
98
+ r.flatten!
99
+ r.uniq!
100
+ nil
101
+ end
102
+
103
+ private
104
+
105
+ def __gu_singleton_rejects
106
+ @__gu_singleton_rejects ||= []
107
+ end
108
+
109
+ # Called on `method_missing`. Rather than simply delegating the method call, it will instead see if there are any
110
+ # delegate methods that haven't been created yet and if so, creates them. This results in improved performance and
111
+ # less hits to `method_missing`.
112
+ def __gu_singleton_method_missing(singleton_class ,method, *args, &block)
113
+ methods= (singleton_class.instance.public_methods - singleton_class.methods)
114
+ if rej= __gu_singleton_rejects
115
+ methods.reject!{|m| m= m.to_s; rej.any?{|r| r === m }}
116
+ end
117
+ unless methods.empty?
118
+ code= methods.map {|m| "def self.#{m}(*a,&b); self.instance.send :#{m},*a,&b; end" }
119
+ singleton_class.class_eval(code.join "\n")
120
+ return singleton_class.send(method,*args,&block) if methods.include?(method)
121
+ end
122
+ ::GollyUtils::Singleton::NO_MATCH
123
+ end
124
+
125
+ end
126
+
127
+ # @!visibility private
128
+ NO_MATCH= Object.new
129
+ end
130
+ end
@@ -0,0 +1,268 @@
1
+ require 'golly-utils/ruby_ext/kernel'
2
+ require 'golly-utils/ruby_ext/options'
3
+ require 'fileutils'
4
+ require 'monitor'
5
+ require 'thread'
6
+ require 'tmpdir'
7
+
8
+ module GollyUtils
9
+ module Testing
10
+
11
+ # Provides globally-shared, cached, lazily-loaded fixtures that are generated once on-demand, and copied when access
12
+ # is required.
13
+ #
14
+ # @example
15
+ # describe 'My Git Utility' do
16
+ # include GollyUtils::Testing::DynamicFixtures
17
+ #
18
+ # def_fixture :git do # Dynamic fixture definition.
19
+ # system 'git init' # Expensive to create.
20
+ # File.write 'test.txt', 'hello' # Only runs once.
21
+ # system 'git add -A && git commit -m x'
22
+ # end
23
+ #
24
+ # run_each_in_dynamic_fixture :git # RSpec helper for fixture usage.
25
+ #
26
+ # it("detects deleted files") { # Runs in a copy of the fixture.
27
+ # File.delete 'test.txt' # Free to modify its fixture copy.
28
+ # subject.deleted_files.should == %w[test.txt] # Other tests isolated from these these changes.
29
+ # }
30
+ #
31
+ # it("detects new files") { # Runs in a clean copy of the fixture.
32
+ # File.create 'new.txt' # Generated quickly by copying cache.
33
+ # subject.new_files.should == %w[new.txt] # Unaffected by other tests' fixture modification.
34
+ # }
35
+ #
36
+ # end
37
+ module DynamicFixtures
38
+
39
+ # @!visibility private
40
+ def self.included(base)
41
+ base.extend ClassMethods
42
+ end
43
+ module ClassMethods
44
+
45
+ # Defines a dynamic fixture.
46
+ #
47
+ # @param [Symbol|String] name The dynamic fixture name.
48
+ # @param [Hash] options
49
+ # @option options [nil|String] :cd_into (nil) A fixture subdirectory to change directory into by default when
50
+ # using the fixture.
51
+ # @option options [nil|String] :dir_name (nil) A specific name to call the temporary directory used when first
52
+ # creating the fixture. If `nil` then the name will non-deterministic.
53
+ # @param [Proc] block Code that when run in an empty, temporary directory, will create the fixture contents.
54
+ # @yield Yields control in an empty directory at a later point in time.
55
+ # @return [void]
56
+ def def_fixture(name, options={}, &block)
57
+ raise "Block not provided." unless block
58
+ options.validate_option_keys :cd_into, :dir_name
59
+
60
+ name= DynamicFixtures.normalise_dynfix_name(name)
61
+ STDERR.warn "Dyanmic fixture being redefined: #{name}." if $gu_dynamic_fixtures[name]
62
+ $gu_dynamic_fixtures[name]= options.merge(block: block)
63
+
64
+ nil
65
+ end
66
+
67
+ # RSpec helper that directs that each example be run in it's own clean copy of a given dynamic fixture.
68
+ #
69
+ # @param [Symbol|String] name The dynamic fixture name.
70
+ # @param [Hash] options Options to pass to {DynamicFixtures#inside_dynamic_fixture}. See that method for option
71
+ # details.
72
+ # @return [void]
73
+ # @see DynamicFixtures#inside_dynamic_fixture
74
+ def run_each_in_dynamic_fixture(name, options={})
75
+ raise "Block not supported." if block_given?
76
+ options.validate_option_keys DynamicFixtures::INSIDE_DYNAMIC_FIXTURE_OPTIONS
77
+
78
+ class_eval <<-EOB
79
+ around :each do |ex|
80
+ inside_dynamic_fixture(#{name.inspect}, #{options.inspect}){ ex.run }
81
+ end
82
+ EOB
83
+
84
+ nil
85
+ end
86
+
87
+ # RSpec helper that directs that all examples be run in the same (initially-clean) copy of a given dynamic
88
+ # fixture.
89
+ #
90
+ # @param [Symbol|String] name The dynamic fixture name.
91
+ # @param [Hash] options
92
+ # @option options [nil|String] :dir_name (nil) A specific name to call the empty directory basename.
93
+ # @option options [nil|String] :cd_into (nil) A fixture subdirectory to change directory into when running
94
+ # examples.
95
+ # @yield Invokes the given block (if one given) once before any examples run, inside the empty dir, to perform
96
+ # any additional initialisation required.
97
+ # @return [void]
98
+ # @see GollyUtils::Testing::Helpers::ClassMethods#run_all_in_empty_dir
99
+ def run_all_in_dynamic_fixture(name, options={}, &block)
100
+ options.validate_option_keys :cd_into, :dir_name
101
+
102
+ require 'golly-utils/testing/rspec/files'
103
+ name= DynamicFixtures.normalise_dynfix_name(name) # actually just wanted dup
104
+ run_all_in_empty_dir(options[:dir_name]) {
105
+ copy_dynamic_fixture name
106
+ block.() if block
107
+ }
108
+
109
+ if cd_into= options[:cd_into]
110
+ class_eval <<-EOB
111
+ around(:each){|ex|
112
+ Dir.chdir(#{cd_into.inspect}){ ex.run }
113
+ }
114
+ EOB
115
+ end
116
+ end
117
+
118
+ end
119
+
120
+ #------------------------------------------------------------------------------------------------------------------
121
+
122
+ # Callback invoked just before creating a dynamic fixture for the first time.
123
+ #
124
+ # Override to customise.
125
+ #
126
+ # @param [Symbol] name The name of the dynamic fixture being created.
127
+ # @return [void]
128
+ def before_dynamic_fixture_creation(name)
129
+ end
130
+
131
+ # Callback invoked just after creating a dynamic fixture for the first time.
132
+ #
133
+ # Override to customise.
134
+ #
135
+ # @param [Symbol] name The name of the dynamic fixture being created.
136
+ # @param [Float] creation_time_in_sec The number of seconds taken to create the fixture.
137
+ # @return [void]
138
+ def after_dynamic_fixture_creation(name, creation_time_in_sec)
139
+ end
140
+
141
+ # Copies the contents of a dynamic fixture to a given directory.
142
+ #
143
+ # @param [Symbol|String] name The name of the dynamic fixture to copy.
144
+ # @param [String] target_dir The (existing) directory to copy the fixture to.
145
+ # @return [void]
146
+ def copy_dynamic_fixture(name, target_dir = '.')
147
+ FileUtils.cp_r "#{dynamic_fixture_dir name}/.", target_dir
148
+ end
149
+
150
+ # Creates a clean copy of a predefined dynamic fixture, changes directory into it and yields. The fixture copy
151
+ # is removed from the file system after the yield block returns.
152
+ #
153
+ # @param [Symbol|String] name The name of the dynamic fixture.
154
+ # @param [Hash] options
155
+ # @option options [nil|String] :cd_into (nil) A fixture subdirectory to change directory into before yielding.
156
+ # @yield Yields control in the directory of a fixture copy.
157
+ # @return The value of the given block.
158
+ def inside_dynamic_fixture(name, options={}, &block)
159
+ options.validate_option_keys INSIDE_DYNAMIC_FIXTURE_OPTIONS
160
+
161
+ Dir.mktmpdir {|dir|
162
+ copy_dynamic_fixture name, dir
163
+ df= get_dynamic_fixture_data(name)
164
+
165
+ if cd_into= options[:cd_into] || df[:cd_into]
166
+ dir= File.join dir, cd_into
167
+ end
168
+
169
+ $gu_dynamic_fixture_chdir_lock.synchronize {
170
+ return Dir.chdir dir, &block
171
+ }
172
+ }
173
+ end
174
+
175
+ # @!visibility private
176
+ INSIDE_DYNAMIC_FIXTURE_OPTIONS= [:cd_into].freeze
177
+
178
+ private
179
+
180
+ def get_dynamic_fixture_data(name)
181
+ name= DynamicFixtures.normalise_dynfix_name(name)
182
+ $gu_dynamic_fixtures[name]
183
+ end
184
+
185
+ # Creates and provides the global, temporary directory that will serve as the base directory for generating and
186
+ # storing all dynamic fixtures.
187
+ #
188
+ # Once created it will be automatically removed on process exit.
189
+ #
190
+ # @return [String] A directory.
191
+ def dynamic_fixture_root
192
+ $gu_dynamic_fixture_root || DYNAMIC_FIXTURE_ROOT_LOCK.synchronize {
193
+ $gu_dynamic_fixture_root ||= (
194
+ at_exit_preserving_exit_status {
195
+ FileUtils.remove_entry_secure $gu_dynamic_fixture_root if $gu_dynamic_fixture_root
196
+ $gu_dynamic_fixtures= $gu_dynamic_fixture_root= nil
197
+ }
198
+ Dir.mktmpdir
199
+ )
200
+ }
201
+ end
202
+
203
+ DYNAMIC_FIXTURE_ROOT_LOCK= Mutex.new
204
+
205
+ # Provides the directory name of a generated dynamic fixture. If the fixture hasn't been generated yet, then this
206
+ # will generate it first.
207
+ #
208
+ # @param [Symbol|String] name The dynamic fixture name.
209
+ # @return [String] A directory.
210
+ def dynamic_fixture_dir(name)
211
+ df= get_dynamic_fixture_data(name)
212
+ raise "Undefined dynamic fixture: #{name}" unless df
213
+
214
+ if df[:block]
215
+ $gu_dynamic_fixture_chdir_lock.synchronize {
216
+ # df[:lock].synchronize {
217
+ if df[:block]
218
+
219
+ # Start creating dynamic fixture
220
+ before_dynamic_fixture_creation name
221
+ dir= "#{dynamic_fixture_root}/#{name}"
222
+ Dir.mkdir dir
223
+
224
+ if subdir= df[:dir_name]
225
+ dir+= "/#{subdir}"
226
+ Dir.mkdir dir
227
+ end
228
+
229
+ start_time_stack= (Thread.current[:gu_dynamic_fixture_start_times] ||= [])
230
+ start_time_stack<< Time.now
231
+ begin
232
+ # Create fixture
233
+ Dir.chdir(dir){ instance_eval &df[:block] }
234
+ df.delete :block
235
+ df[:dir]= dir
236
+
237
+ dur= Time.now - start_time_stack.last
238
+ start_time_stack.map!{|t| t + dur}
239
+ after_dynamic_fixture_creation name, dur
240
+ ensure
241
+ start_time_stack.pop
242
+ end
243
+
244
+ end
245
+ }
246
+ end
247
+
248
+ df[:dir].dup
249
+ end
250
+
251
+ #-----------------------------------------------------------------------------------------------------------------
252
+
253
+ extend ClassMethods
254
+
255
+ # @!visibility private
256
+ def self.normalise_dynfix_name(name)
257
+ name.to_sym
258
+ end
259
+
260
+ unless $gu_dynamic_fixtures
261
+ $gu_dynamic_fixtures= {}
262
+ $gu_dynamic_fixture_root= nil
263
+ $gu_dynamic_fixture_chdir_lock= Monitor.new
264
+ end
265
+
266
+ end
267
+ end
268
+ end