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.
- data/.corvid/Gemfile +28 -0
- data/.corvid/features.yml +4 -0
- data/.corvid/plugins.yml +4 -0
- data/.corvid/stats_cfg.rb +13 -0
- data/.corvid/todo_cfg.rb +15 -0
- data/.corvid/versions.yml +2 -0
- data/.simplecov +6 -3
- data/CHANGELOG.md +45 -2
- data/Gemfile +6 -3
- data/Gemfile.lock +34 -37
- data/Guardfile +10 -4
- data/RELEASE.md +2 -0
- data/Rakefile +1 -1
- data/golly-utils.gemspec +19 -10
- data/lib/golly-utils/attr_declarative.rb +120 -49
- data/lib/golly-utils/callbacks.rb +211 -19
- data/lib/golly-utils/child_process.rb +28 -8
- data/lib/golly-utils/delegator.rb +14 -3
- data/lib/golly-utils/ruby_ext/classes_and_types.rb +120 -0
- data/lib/golly-utils/ruby_ext/enumerable.rb +16 -0
- data/lib/golly-utils/ruby_ext/env_helpers.rb +17 -0
- data/lib/golly-utils/ruby_ext/kernel.rb +18 -0
- data/lib/golly-utils/ruby_ext/options.rb +28 -0
- data/lib/golly-utils/ruby_ext/pretty_error_messages.rb +1 -1
- data/lib/golly-utils/singleton.rb +130 -0
- data/lib/golly-utils/testing/dynamic_fixtures.rb +268 -0
- data/lib/golly-utils/testing/file_helpers.rb +117 -0
- data/lib/golly-utils/testing/helpers_base.rb +20 -0
- data/lib/golly-utils/testing/rspec/arrays.rb +85 -0
- data/lib/golly-utils/testing/rspec/base.rb +9 -0
- data/lib/golly-utils/testing/rspec/deferrable_specs.rb +111 -0
- data/lib/golly-utils/testing/rspec/files.rb +262 -0
- data/lib/golly-utils/{test/spec → testing/rspec}/within_time.rb +17 -7
- data/lib/golly-utils/version.rb +2 -1
- data/test/bootstrap/all.rb +8 -1
- data/test/bootstrap/spec.rb +1 -1
- data/test/bootstrap/unit.rb +1 -1
- data/test/spec/child_process_spec.rb +1 -1
- data/test/spec/testing/dynamic_fixtures_spec.rb +131 -0
- data/test/spec/testing/rspec/arrays_spec.rb +33 -0
- data/test/spec/testing/rspec/files_spec.rb +300 -0
- data/test/unit/attr_declarative_test.rb +79 -13
- data/test/unit/callbacks_test.rb +103 -5
- data/test/unit/delegator_test.rb +25 -1
- data/test/unit/ruby_ext/classes_and_types_test.rb +103 -0
- data/test/unit/ruby_ext/enumerable_test.rb +12 -0
- data/test/unit/ruby_ext/options_test.rb +29 -0
- data/test/unit/singleton_test.rb +59 -0
- metadata +100 -10
- data/Gemfile.corvid +0 -27
- data/lib/golly-utils/ruby_ext.rb +0 -2
- data/lib/golly-utils/ruby_ext/subclasses.rb +0 -17
- data/lib/golly-utils/test/spec/deferrable_specs.rb +0 -85
- 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,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
|