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,117 @@
1
+ require 'golly-utils/testing/helpers_base'
2
+ require 'tmpdir'
3
+
4
+ module GollyUtils::Testing::Helpers
5
+
6
+ # Creates an empty, temporary directory and changes the current directory into it.
7
+ #
8
+ # @example
9
+ # puts Dir.pwd # => /home/david/my_project
10
+ # inside_empty_dir {
11
+ # puts Dir.pwd # => /tmp/abcdef123567
12
+ # }
13
+ # # Empty directory now removed
14
+ # puts Dir.pwd # => /home/david/my_project
15
+ #
16
+ # @param [nil|String] dir_name If not `nil`, then the empty directory name will be set to the provided value.
17
+ # @overload inside_empty_dir(dir_name = nil, &block)
18
+ # When a block is given, after yielding, the current directory is restored and the temp directory deleted.
19
+ # @yieldparam [String] dir The newly-created temp dir.
20
+ # @return The result of yielding.
21
+ # @overload inside_empty_dir(dir_name = nil)
22
+ # When no block is given the current directory is *not* restored and the temp directory *not* deleted.
23
+ # @return [String] The newly-created temp dir.
24
+ def inside_empty_dir(dir_name=nil)
25
+ if block_given?
26
+ Dir.mktmpdir {|dir|
27
+ if dir_name
28
+ dir= File.join dir, dir_name
29
+ Dir.mkdir dir
30
+ end
31
+ Dir.chdir(dir) {
32
+ (@tmp_dir_stack ||= [])<< :inside_empty_dir
33
+ begin
34
+ return yield dir
35
+ ensure
36
+ @tmp_dir_stack.pop
37
+ end
38
+ }
39
+ }
40
+ else
41
+ x= {}
42
+ x[:old_dir]= Dir.pwd
43
+ x[:tmp_dir]= Dir.mktmpdir
44
+ if dir_name
45
+ x[:tmp_dir]= File.join x[:tmp_dir], dir_name
46
+ Dir.mkdir x[:tmp_dir]
47
+ end
48
+ Dir.chdir x[:tmp_dir]
49
+ (@tmp_dir_stack ||= [])<< x
50
+ x[:tmp_dir]
51
+ end
52
+ end
53
+
54
+ # Indicates whether the current directory is one made by {#inside_empty_dir}.
55
+ #
56
+ # @return [Boolean]
57
+ def in_tmp_dir?
58
+ @tmp_dir_stack && !@tmp_dir_stack.empty? or false
59
+ end
60
+
61
+ # To be used in conjunction with {#inside_empty_dir}.
62
+ #
63
+ # @example
64
+ # inside_empty_dir
65
+ # begin
66
+ # # Do stuff in empty dir
67
+ # ensure
68
+ # step_out_of_tmp_dir
69
+ # end
70
+ #
71
+ # @return [nil]
72
+ def step_out_of_tmp_dir
73
+ if @tmp_dir_stack
74
+ x= @tmp_dir_stack.pop
75
+ raise "You cannot call step_out_of_tmp_dir() within the yield block of #{x}()" if x.is_a?(Symbol)
76
+ Dir.chdir x[:old_dir]
77
+ FileUtils.rm_rf x[:tmp_dir]
78
+ end
79
+ nil
80
+ end
81
+
82
+ # Returns a list of all files in a directory tree.
83
+ #
84
+ # @param [String, nil] dir The directory to inspect, or `nil` to indicate the current directory.
85
+ # @return [Array<String>] A sorted array of files. Filenames will be relative to the provided directory.
86
+ def get_files(dir=nil)
87
+ get_dir_entries(dir){|f| File.file? f }
88
+ end
89
+
90
+ # Returns a list of all subdirectories in a directory.
91
+ #
92
+ # @param [String, nil] dir The directory to inspect, or `nil` to indicate the current directory.
93
+ # @return [Array<String>] A sorted array of dirs. Filenames will be relative to the provided directory. `.` and `..`
94
+ # will never be returned.
95
+ def get_dirs(dir=nil)
96
+ get_dir_entries(dir){|f| File.directory? f }
97
+ end
98
+
99
+ # Returns a list of all files, directories, symlinks, etc in a directory tree.
100
+ #
101
+ # @param [String, nil] dir The directory to inspect, or `nil` to indicate the current directory.
102
+ # @param select_filter An optional filter to be applied to each entry where negative calls result in the entry being
103
+ # discarded.
104
+ # @return [Array<String>] A sorted array of dir entries. Filenames will be relative to the provided directory. `.` and
105
+ # `..` will never be returned.
106
+ def get_dir_entries(dir=nil, &select_filter)
107
+ if dir
108
+ Dir.chdir(dir){ get_dir_entries &select_filter }
109
+ else
110
+ Dir.glob('**/*',File::FNM_DOTMATCH)
111
+ .reject{|f| /(?:^|[\\\/]+)\.{1,2}$/ === f } # Ignore: . .. dir/. dir/..
112
+ .select{|f| select_filter ? select_filter.call(f) : true }
113
+ .sort
114
+ end
115
+ end
116
+
117
+ end
@@ -0,0 +1,20 @@
1
+ module GollyUtils
2
+ # This module is the parent module for utility code that client may find useful when writing tests.
3
+ #
4
+ # (This has nothing to do with GollyUtils' own, internal tests.)
5
+ module Testing
6
+ module Helpers
7
+
8
+ # @!visibility private
9
+ def self.included(base)
10
+ base.extend ClassMethods
11
+ end
12
+
13
+ module ClassMethods
14
+ # @!visibility private
15
+ SELF= self
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,85 @@
1
+ require 'golly-utils/testing/rspec/base'
2
+ require 'golly-utils/ruby_ext/enumerable'
3
+
4
+ module GollyUtils::Testing::RSpecMatchers
5
+
6
+ #-----------------------------------------------------------------------------------------------------------------
7
+
8
+ # @!visibility private
9
+ class EqualsArray
10
+
11
+ def initialize(expected)
12
+ raise "Array expected, not #{expected.inspect}" unless expected.is_a?(Array)
13
+ @expected= expected
14
+ end
15
+
16
+ def matches?(tgt)
17
+ raise "Array expected, not #{tgt.inspect}" unless tgt.is_a?(Array)
18
+ @tgt= tgt
19
+ tgt == @expected
20
+ end
21
+
22
+ def failure_message_for_should
23
+ @common= @tgt & @expected
24
+ @missing= @expected - @common - @tgt
25
+ @extra= @tgt - @common - @expected
26
+
27
+ msg= unless @missing.empty? and @extra.empty?
28
+ # Element mismatch
29
+ m("Missing",@missing) + m("Unexpected",@extra)
30
+ else
31
+ # Check freq differences
32
+ tgt_freq= @tgt.frequency_map
33
+ expected_freq= @expected.frequency_map
34
+ freq_diff= tgt_freq.inject({}){|h,kv|
35
+ e,a = kv
36
+ diff= a - expected_freq[e]
37
+ h[e]= diff unless diff == 0
38
+ h
39
+ }
40
+ if !freq_diff.empty?
41
+ # Freq differences
42
+ "Both arrays contain the same elements but have different frequencies of occurrance.\nFrequency differences: (neg=not enough, pos=too many)\n #{freq_diff.inspect}"
43
+ elsif @tgt.sort_by(&:to_s) == @expected.sort_by(&:to_s)
44
+ # Order difference
45
+ "They're in different orders." + m("Actual",@tgt,false) + m("Expected",@expected,false)
46
+ else
47
+ # Unknown difference
48
+ m("Actual",@tgt,false) + m("Expected",@expected,false)
49
+ end
50
+ end
51
+
52
+ "expected that arrays would match. #{msg}"
53
+ end
54
+
55
+ def failure_message_for_should_not
56
+ "expected that arrays would not match." + m("Contents",@expected)
57
+ end
58
+
59
+ private
60
+ def m(name, array, sort=true)
61
+ return '' if array.empty?
62
+ array= array.sort_by(&:to_s) if sort
63
+ "\n#{name} elements: #{array.inspect}"
64
+ end
65
+ end
66
+
67
+ # Passes if an array is the same as the target array.
68
+ #
69
+ # The advantage of calling this rather than `==` is that the error messages on failure here are customised for array
70
+ # comparison and will provide much more useful description of why the arrays don't match.
71
+ #
72
+ # @note The order and frequency of elements matters; call `sort` or `uniq` first if required.
73
+ #
74
+ # @example
75
+ # %w[a a b].should equal_array %w[a a b]
76
+ # %w[b a b].should_not equal_array %w[a a b]
77
+ # files.should equal_array(expected)
78
+ def equal_array(expected)
79
+ return be_nil if expected.nil?
80
+ EqualsArray.new(expected)
81
+ end
82
+
83
+ #-----------------------------------------------------------------------------------------------------------------
84
+
85
+ end
@@ -0,0 +1,9 @@
1
+ require 'golly-utils/testing/helpers_base'
2
+
3
+ module GollyUtils::Testing::RSpecMatchers
4
+ end
5
+
6
+ RSpec::configure do |config|
7
+ config.include GollyUtils::Testing::Helpers
8
+ config.include GollyUtils::Testing::RSpecMatchers
9
+ end
@@ -0,0 +1,111 @@
1
+ module GollyUtils
2
+ module Testing
3
+
4
+ # With this module you can create specs that don't start running until manually started elsewhere in another spec.
5
+ #
6
+ # This is only really useful when writing integration tests in spec format, where specs (aka examples) are not
7
+ # isolated tests but single components of a larger-scale test. This is especially the case when checking
8
+ # asynchronous events, or managing dependencies on state of an external entity.
9
+ #
10
+ # ## Usage
11
+ # * Extend (not include) {DeferrableSpecs}.
12
+ # * Create specs/examples using {ClassMethods#deferrable_spec deferrable_spec}.
13
+ # * In other specs/examples, call {InstanceMethods#start_deferred_tests start_deferred_tests} to start deferred tests.
14
+ #
15
+ # @example
16
+ # describe 'Integration test #2' do
17
+ # extend GollyUtils::Testing::DeferrableSpecs
18
+ #
19
+ # it("Register client, notices and default prefs") do
20
+ # scenario.register_client
21
+ # scenario.register_notification_groups
22
+ # scenario.register_notices
23
+ # scenario.set_default_preferences
24
+ #
25
+ # start_deferred_test :email1
26
+ # start_deferred_test :email2
27
+ # start_deferred_test :mq
28
+ # end
29
+ #
30
+ # # Deferred until registration of client, notices and preferences are complete
31
+ # deferrable_spec(:email1, "Sends emails (to unregistered contacts)") do
32
+ # assert_sends_email ...
33
+ # end
34
+ #
35
+ # ...
36
+ #
37
+ # end
38
+ module DeferrableSpecs
39
+
40
+ # @!visibility private
41
+ def self.extended(base)
42
+ base.send :include, InstanceMethods
43
+ base.extend ClassMethods
44
+ base.instance_eval{ @deferrable_specs= {} }
45
+ end
46
+
47
+ module ClassMethods
48
+ # @!visibility private
49
+ attr_reader :deferrable_specs
50
+
51
+ # Declares a test case that will start paused and not run until allowed within another test case.
52
+ #
53
+ # @param [Symbol] key A unique key that will later be used to refer back to this test case.
54
+ # @param [String] name The name of the test case.
55
+ # @raise If the given key has already been used.
56
+ # @see InstanceMethods#start_deferred_tests
57
+ def deferrable_spec(key, name, &block)
58
+ raise "Invalid test key; please pass a Symbol." unless key.is_a?(Symbol)
59
+ raise "Invalid test name; please pass a String." unless name.is_a?(String)
60
+ raise "You must provide a block of test code. This test needs to do something." if block.nil?
61
+ raise "The key #{key.inspect} has already been used." if deferrable_specs[key]
62
+ deferrable_specs[key]= {block: block}
63
+ class_eval <<-EOB
64
+ it(#{name.inspect}){ deferred_join #{key.inspect} }
65
+ EOB
66
+ end
67
+
68
+ end
69
+
70
+ module InstanceMethods
71
+
72
+
73
+ # Triggers one or more deferred tests to start running in the background.
74
+ #
75
+ # @param [Symbol] first_key The identifying key of the deferred test to start. (The name must match that given
76
+ # in {ClassMethods#deferrable_spec}).
77
+ # @param [Array<Symbol>] other_keys Keys of additional tests to start.
78
+ # @raise If a test name is invalid (i.e. hasn't been declared).
79
+ # @raise If a test has already been started.
80
+ # @return [true]
81
+ def start_deferred_tests(first_key,*other_keys)
82
+ ([first_key]+other_keys).flatten.uniq.each do |key|
83
+ raise "Unknown defferable test: #{key}" unless d= self.class.deferrable_specs[key]
84
+ raise "Test already started: #{key}" if d[:thread]
85
+ s= self.dup
86
+ d[:thread]= Thread.new{ s.instance_eval &b }
87
+ end
88
+ true
89
+ end
90
+
91
+ alias start_deferred_test start_deferred_tests
92
+
93
+ private
94
+
95
+ # Waits for a deferred test to run in the background and complete.
96
+ def deferred_join(key)
97
+ raise "Unknown defferable test: #{key}" unless d= self.class.deferrable_specs[key]
98
+ if t= d[:thread]
99
+ t.join
100
+ else
101
+ #raise "Test hasn't started: #{key}"
102
+ warn "Deferrable spec #{key.inspect} wasn't deferred. Start it elsewhere with start_deferred_test #{key.inspect}"
103
+ raise "Test block missing." unless b= d[:block]
104
+ b.call
105
+ end
106
+ end
107
+
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,262 @@
1
+ require 'golly-utils/testing/rspec/base'
2
+ require 'golly-utils/testing/file_helpers'
3
+
4
+ module GollyUtils::Testing::Helpers::ClassMethods
5
+
6
+ # Runs each RSpec example in a new, empty directory.
7
+ #
8
+ # Old directories are deleted at the end of each example, and the original current-directory restored.
9
+ #
10
+ # @param [nil|String] dir_name If not `nil`, then the empty directory name will be set to the provided value.
11
+ # @return [void]
12
+ def run_each_in_empty_dir(dir_name=nil)
13
+ eval <<-EOB
14
+ around :each do |ex|
15
+ inside_empty_dir(#{dir_name.inspect}){ ex.run }
16
+ end
17
+ EOB
18
+ end
19
+
20
+ # Runs each RSpec example in a new, empty directory unless the context has already put it in one (for example, via
21
+ # {#run_all_in_empty_dir}).
22
+ #
23
+ # @param [nil|String] dir_name If not `nil`, then the empty directory name will be set to the provided value.
24
+ # @return [void]
25
+ # @see #in_tmp_dir?
26
+ def run_each_in_empty_dir_unless_in_one_already(dir_name=nil)
27
+ eval <<-EOB
28
+ around :each do |ex|
29
+ in_tmp_dir? ? ex.run : inside_empty_dir(#{dir_name.inspect}){ ex.run }
30
+ end
31
+ EOB
32
+ end
33
+
34
+
35
+ # Runs all RSpec examples (in the current context) in a new, empty directory.
36
+ #
37
+ # The directory is deleted after all examples have run, and the original current-directory restored.
38
+ #
39
+ # @param [nil|String] dir_name If not `nil`, then the empty directory name will be set to the provided value.
40
+ # @yield Invokes the given block (if one given) once before any examples run, inside the empty dir, to perform any
41
+ # additional initialisation required.
42
+ # @return [void]
43
+ def run_all_in_empty_dir(dir_name=nil, &block)
44
+ block ||= Proc.new{}
45
+ @@around_all_in_empty_dir_count ||= 0
46
+ @@around_all_in_empty_dir_count += 1
47
+ block_name= :"@@around_all_in_empty_dir_#@@around_all_in_empty_dir_count"
48
+ SELF.class_variable_set block_name, block
49
+ eval <<-EOB
50
+ before(:all){
51
+ inside_empty_dir(#{dir_name.inspect})
52
+ block= ::#{SELF}.class_variable_get(:"#{block_name}")
53
+ instance_exec &block
54
+ }
55
+ after(:all){ step_out_of_tmp_dir }
56
+ EOB
57
+ end
58
+
59
+ end
60
+
61
+ #-----------------------------------------------------------------------------------------------------------------------
62
+
63
+ module GollyUtils::Testing::RSpecMatchers
64
+
65
+ # @!visibility private
66
+ class ExistAsFile
67
+ def matches?(tgt)
68
+ @tgt= tgt
69
+ File.exists? tgt and File.file? tgt
70
+ end
71
+
72
+ def failure_message_for_should
73
+ m "expected that '#@tgt' would be an existing file."
74
+ end
75
+
76
+ def failure_message_for_should_not
77
+ m "expected that '#@tgt' would not exist."
78
+ end
79
+
80
+ private
81
+ def m(msg)
82
+ dir= File.dirname(@tgt)
83
+ if Dir.exists? dir
84
+ Dir.chdir(dir) {
85
+ files= Dir.glob('*',File::FNM_DOTMATCH).select{|f| File.file? f }.sort
86
+ indir= dir == '.' ? '' : " in #{dir}"
87
+ "#{msg}\nFiles found#{indir}: #{files.join ' '}"
88
+ }
89
+ else
90
+ "#{msg}\nDirectory doesn't exist: #{dir}"
91
+ end
92
+ end
93
+ end
94
+
95
+ # Passes if a file exists (relative to the current directory) with a name specified by the target string.
96
+ #
97
+ # Note: This only passes if a file is found; a directory with the same name will fail.
98
+ #
99
+ # @example
100
+ # 'Gemfile'.should exist_as_a_file
101
+ # '/tmp/stuff'.should_not exist_as_a_file
102
+ def exist_as_a_file
103
+ ExistAsFile.new
104
+ end
105
+ alias :exist_as_file :exist_as_a_file
106
+
107
+ #---------------------------------------------------------------------------------------------------------------------
108
+
109
+ # @!visibility private
110
+ class ExistAsDir
111
+ def matches?(tgt)
112
+ @tgt= tgt
113
+ Dir.exists? tgt
114
+ end
115
+
116
+ def failure_message_for_should
117
+ m "expected that '#@tgt' would be an existing directory."
118
+ end
119
+
120
+ def failure_message_for_should_not
121
+ m "expected that '#@tgt' would not exist."
122
+ end
123
+
124
+ private
125
+ def m(msg)
126
+ dir= File.expand_path('..',@tgt).sub(Dir.pwd+'/','')
127
+ if Dir.exists? dir
128
+ Dir.chdir(dir) {
129
+ dirs= Dir.glob('*',File::FNM_DOTMATCH).select{|f| File.directory? f }.sort - %w[. ..]
130
+ indir= dir == '.' ? '' : " in #{dir}"
131
+ "#{msg}\nDirs found#{indir}: #{dirs.join ' '}"
132
+ }
133
+ else
134
+ "#{msg}\nDirectory doesn't exist: #{dir}"
135
+ end
136
+ end
137
+ end
138
+
139
+ # Passes if a directory exists (relative to the current directory) with a name specified by the target string.
140
+ #
141
+ # @example
142
+ # 'lib'.should exist_as_a_dir
143
+ # 'cache/z01'.should_not exist_as_a_dir
144
+ def exist_as_a_dir
145
+ ExistAsDir.new
146
+ end
147
+ alias :exist_as_dir :exist_as_a_dir
148
+
149
+ #---------------------------------------------------------------------------------------------------------------------
150
+
151
+ # @!visibility private
152
+ class FileWithContents
153
+ def initialize
154
+ @contents= []
155
+ @not_contents= []
156
+ @normalisation_fns= []
157
+ end
158
+
159
+ def and(c1,*cn)
160
+ @contents.concat [c1]+cn
161
+ self
162
+ end
163
+
164
+ def and_not(c1,*cn)
165
+ @not_contents.concat [c1]+cn
166
+ self
167
+ end
168
+
169
+ def _and_normalisation_fn(&fn)
170
+ @normalisation_fns<< fn
171
+ self
172
+ end
173
+
174
+ def when_normalised_with(&fn)
175
+ instance_eval <<-EOB
176
+ alias :and :_and_normalisation_fn
177
+ def and_not(*) raise "and_not() cannot be called after when_normalised_with()." end
178
+ EOB
179
+ self.and &fn
180
+ end
181
+ alias :when_normalized_with :when_normalised_with
182
+
183
+ def matches?(file)
184
+ @contents= @contents.flatten.compact.uniq
185
+ @not_contents= @not_contents.flatten.compact.uniq
186
+ file.should ExistAsFile.new
187
+ @filename= file
188
+ @file_content= File.read(file)
189
+
190
+ @normalisation_fns.each do |fn|
191
+ @file_content= fn.(@file_content)
192
+ @contents.map! {|c| c.is_a?(String) ? fn.(c) : c}
193
+ @not_contents.map! {|c| c.is_a?(String) ? fn.(c) : c}
194
+ end
195
+
196
+ @failures= []
197
+ @not_failures= []
198
+ @contents.each {|c| @failures<< c unless c === @file_content }
199
+ @not_contents.each {|c| @not_failures<< c if c === @file_content }
200
+
201
+ @failures.empty? and @not_failures.empty?
202
+ end
203
+
204
+ def failure_message_for_should
205
+ if !@failures.empty?
206
+ expected_msg @failures
207
+ else
208
+ unexpected_msg @not_failures
209
+ end
210
+ end
211
+
212
+ def failure_message_for_should_not
213
+ inv= @contents - @failures
214
+ if !inv.empty?
215
+ unexpected_msg inv
216
+ else
217
+ expected_msg @not_contents - @not_failures
218
+ end
219
+ end
220
+
221
+ private
222
+
223
+ def expected_msg(expected)
224
+ "expected that '#@filename' would have certain content.\n" \
225
+ + expected.map{|f| " Expected: #{f.inspect}" }.join("\n") \
226
+ + "\nActual Content: #{@file_content.inspect}"
227
+ end
228
+
229
+ def unexpected_msg(unexpected)
230
+ "expected that '#@filename' would not have certain content.\n" \
231
+ + unexpected.map{|f| "Unexpected: #{f.inspect}" }.join("\n") \
232
+ + "\nActual Content: #{@file_content.inspect}"
233
+ end
234
+ end
235
+
236
+ # Checks that a file exists and has expected content.
237
+ #
238
+ # @example
239
+ # # Specify a single string to for a straight 1:1 comparison.
240
+ # 'version.txt'.should be_file_with_contents "2\n"
241
+ #
242
+ # # Use regex and the and() method to add multiple expectations
243
+ # 'Gemfile'.should be_file_with_contents(/['"]rspec['"]/).and(/['"]golly-utils['"]/)
244
+ #
245
+ # # Negative checks can be added too
246
+ # 'Gemfile'.should be_file_with_contents(/gemspec/).and_not(/rubygems/)
247
+ #
248
+ # @example With normalisation
249
+ # # You can specify functions to normalise both the file and expectation.
250
+ # 'version.txt'.should be_file_with_contents("2").when_normalised_with(&:chomp)
251
+ #
252
+ # # You can add multiple normalisation functions by specifying and() after the first
253
+ # 'stuff.txt'.should be_file_with_contents(/ABC/)
254
+ # .and(/DEF/)
255
+ # .and(/123\n/)
256
+ # .when_normalised_with(&:upcase)
257
+ # .and(&:chomp)
258
+ def be_file_with_contents(contents, *extra)
259
+ FileWithContents.new.and(contents).and(extra)
260
+ end
261
+ alias :be_file_with_content :be_file_with_contents
262
+ end