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,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