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