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