ice_nine 0.1.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 (51) hide show
  1. data/.gitignore +37 -0
  2. data/.pelusa.yml +9 -0
  3. data/.rvmrc +1 -0
  4. data/.travis.yml +17 -0
  5. data/.yardopts +1 -0
  6. data/Gemfile +32 -0
  7. data/LICENSE +20 -0
  8. data/README.md +54 -0
  9. data/Rakefile +9 -0
  10. data/TODO +3 -0
  11. data/config/flay.yml +3 -0
  12. data/config/flog.yml +2 -0
  13. data/config/roodi.yml +16 -0
  14. data/config/site.reek +91 -0
  15. data/config/yardstick.yml +2 -0
  16. data/ice_nine.gemspec +21 -0
  17. data/lib/ice_nine.rb +65 -0
  18. data/lib/ice_nine/freezer.rb +95 -0
  19. data/lib/ice_nine/freezer/array.rb +26 -0
  20. data/lib/ice_nine/freezer/hash.rb +30 -0
  21. data/lib/ice_nine/freezer/no_freeze.rb +48 -0
  22. data/lib/ice_nine/freezer/range.rb +29 -0
  23. data/lib/ice_nine/freezer/struct.rb +29 -0
  24. data/lib/ice_nine/version.rb +5 -0
  25. data/spec/rcov.opts +7 -0
  26. data/spec/spec.opts +3 -0
  27. data/spec/spec_helper.rb +23 -0
  28. data/spec/unit/ice_nine/class_methods/deep_freeze_spec.rb +193 -0
  29. data/spec/unit/ice_nine/freezer/array/class_methods/deep_freeze_spec.rb +27 -0
  30. data/spec/unit/ice_nine/freezer/class_methods/deep_freeze_spec.rb +30 -0
  31. data/spec/unit/ice_nine/freezer/class_methods/element_reference_spec.rb +85 -0
  32. data/spec/unit/ice_nine/freezer/false_class/class_methods/deep_freeze_spec.rb +22 -0
  33. data/spec/unit/ice_nine/freezer/hash/class_methods/deep_freeze_spec.rb +40 -0
  34. data/spec/unit/ice_nine/freezer/nil_class/class_methods/deep_freeze_spec.rb +22 -0
  35. data/spec/unit/ice_nine/freezer/no_freeze/class_methods/deep_freeze_spec.rb +19 -0
  36. data/spec/unit/ice_nine/freezer/numeric/class_methods/deep_freeze_spec.rb +24 -0
  37. data/spec/unit/ice_nine/freezer/range/class_methods/deep_freeze_spec.rb +30 -0
  38. data/spec/unit/ice_nine/freezer/struct/class_methods/deep_freeze_spec.rb +27 -0
  39. data/spec/unit/ice_nine/freezer/symbol/class_methods/deep_freeze_spec.rb +22 -0
  40. data/spec/unit/ice_nine/freezer/true_class/class_methods/deep_freeze_spec.rb +22 -0
  41. data/tasks/metrics/ci.rake +7 -0
  42. data/tasks/metrics/flay.rake +43 -0
  43. data/tasks/metrics/flog.rake +46 -0
  44. data/tasks/metrics/heckle.rake +210 -0
  45. data/tasks/metrics/metric_fu.rake +31 -0
  46. data/tasks/metrics/reek.rake +11 -0
  47. data/tasks/metrics/roodi.rake +17 -0
  48. data/tasks/metrics/yardstick.rake +25 -0
  49. data/tasks/spec.rake +46 -0
  50. data/tasks/yard.rake +11 -0
  51. metadata +150 -0
@@ -0,0 +1,85 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+ require 'ice_nine/freezer'
5
+ require 'ice_nine/freezer/struct'
6
+
7
+ describe IceNine::Freezer, '.[]' do
8
+ subject { object[mod] }
9
+
10
+ let(:object) { described_class }
11
+ let(:freezer) { object }
12
+
13
+ describe 'when the module matches a descendant' do
14
+ let(:freezer) { Class.new(object) }
15
+ let(:mod) { Class }
16
+
17
+ before do
18
+ object.const_set(mod.name, freezer)
19
+ end
20
+
21
+ after do
22
+ object.send(:remove_const, mod.name)
23
+ end
24
+
25
+ it 'returns the freezer' do
26
+ should be(freezer)
27
+ end
28
+ end
29
+
30
+ describe 'when the module matches a descendant inside a namespace' do
31
+ let(:namespace) { Class.new(object) }
32
+ let(:freezer) { Class.new(object) }
33
+ let(:mod) { Application::User }
34
+
35
+ before :all do
36
+ module ::Application
37
+ class User; end
38
+ end
39
+ end
40
+
41
+ after :all do
42
+ ::Application.send(:remove_const, :User)
43
+ Object.send(:remove_const, :Application)
44
+ end
45
+
46
+ before do
47
+ namespace.const_set(:User, freezer)
48
+ object.const_set(:Application, namespace)
49
+ end
50
+
51
+ after do
52
+ namespace.send(:remove_const, :User)
53
+ object.send(:remove_const, :Application)
54
+ end
55
+
56
+ it 'returns the freezer' do
57
+ should be(freezer)
58
+ end
59
+ end
60
+
61
+ describe 'when the module is a struct' do
62
+ let(:mod) { Struct.new(:a) }
63
+ let(:freezer) { IceNine::Freezer::Struct }
64
+
65
+ it 'returns the freezer' do
66
+ should be(freezer)
67
+ end
68
+ end
69
+
70
+ describe 'when the module does not match a descendant' do
71
+ let(:mod) { Object }
72
+
73
+ it 'returns the freezer' do
74
+ should be(freezer)
75
+ end
76
+ end
77
+
78
+ describe 'when the module is anonymous' do
79
+ let(:mod) { Class.new }
80
+
81
+ it 'returns the freezer' do
82
+ should be(freezer)
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,22 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+ require 'ice_nine/freezer/no_freeze'
5
+
6
+ describe IceNine::Freezer::FalseClass, '.deep_freeze' do
7
+ subject { object.deep_freeze(value) }
8
+
9
+ let(:object) { described_class }
10
+
11
+ context 'with a false object' do
12
+ let(:value) { false }
13
+
14
+ it 'returns the object' do
15
+ should be(value)
16
+ end
17
+
18
+ it 'does not freeze the object' do
19
+ expect { subject }.should_not change(value, :frozen?).from(false)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,40 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+ require 'ice_nine'
5
+
6
+ describe IceNine::Freezer::Hash, '.deep_freeze' do
7
+ subject { object.deep_freeze(value) }
8
+
9
+ let(:object) { described_class }
10
+
11
+ context 'with a Hash object' do
12
+ let(:value) { { Object.new => Object.new } }
13
+
14
+ it 'returns the object' do
15
+ should be(value)
16
+ end
17
+
18
+ it 'freezes the object' do
19
+ expect { subject }.should change(value, :frozen?).from(false).to(true)
20
+ end
21
+
22
+ it 'freezes each key in the Hash' do
23
+ subject.keys.select(&:frozen?).should == subject.keys
24
+ end
25
+
26
+ it 'freezes each value in the Hash' do
27
+ subject.values.select(&:frozen?).should == subject.values
28
+ end
29
+
30
+ if RUBY_VERSION >= '1.9' and RUBY_ENGINE == 'rbx'
31
+ it 'does not freeze the Hash state' do
32
+ subject.instance_variable_get(:@state).should_not be_frozen
33
+ end
34
+
35
+ it 'does not freeze the Hash entries' do
36
+ subject.instance_variable_get(:@entries).should_not be_frozen
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,22 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+ require 'ice_nine/freezer/no_freeze'
5
+
6
+ describe IceNine::Freezer::NilClass, '.deep_freeze' do
7
+ subject { object.deep_freeze(value) }
8
+
9
+ let(:object) { described_class }
10
+
11
+ context 'with a nil object' do
12
+ let(:value) { nil }
13
+
14
+ it 'returns the object' do
15
+ should be(value)
16
+ end
17
+
18
+ it 'does not freeze the object' do
19
+ expect { subject }.should_not change(value, :frozen?).from(false)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,19 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+ require 'ice_nine/freezer/no_freeze'
5
+
6
+ describe IceNine::Freezer::NoFreeze, '.deep_freeze' do
7
+ subject { object.deep_freeze(value) }
8
+
9
+ let(:object) { described_class }
10
+ let(:value) { stub('value') }
11
+
12
+ it 'returns the object' do
13
+ should be(value)
14
+ end
15
+
16
+ it 'does not freeze the object' do
17
+ expect { subject }.should_not change(value, :frozen?).from(false)
18
+ end
19
+ end
@@ -0,0 +1,24 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+ require 'ice_nine/freezer/no_freeze'
5
+
6
+ describe IceNine::Freezer::Numeric, '.deep_freeze' do
7
+ subject { object.deep_freeze(value) }
8
+
9
+ let(:object) { described_class }
10
+
11
+ [ 0.0, 0, 0x7fffffffffffffff ].each do |value|
12
+ context "with a #{value.class} object" do
13
+ let(:value) { value }
14
+
15
+ it 'returns the object' do
16
+ should be(value)
17
+ end
18
+
19
+ it 'does not freeze the object' do
20
+ expect { subject }.should_not change(value, :frozen?).from(false)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,30 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+ require 'ice_nine'
5
+
6
+ describe IceNine::Freezer::Range, '.deep_freeze' do
7
+ subject { object.deep_freeze(value) }
8
+
9
+ let(:object) { described_class }
10
+
11
+ context 'with a Range' do
12
+ let(:value) { 'a'..'z' }
13
+
14
+ it 'returns the object' do
15
+ should be(value)
16
+ end
17
+
18
+ it 'freezes the object' do
19
+ expect { subject }.should change(value, :frozen?).from(false).to(true)
20
+ end
21
+
22
+ it 'freeze the first object in the Range' do
23
+ subject.begin.should be_frozen
24
+ end
25
+
26
+ it 'freeze the last object in the Range' do
27
+ subject.end.should be_frozen
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,27 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+ require 'ice_nine'
5
+
6
+ describe IceNine::Freezer::Struct, '.deep_freeze' do
7
+ subject { object.deep_freeze(value) }
8
+
9
+ let(:object) { described_class }
10
+
11
+ context 'with a Struct' do
12
+ let(:value) { klass.new('1') }
13
+ let(:klass) { Struct.new(:a) }
14
+
15
+ it 'returns the object' do
16
+ should be(value)
17
+ end
18
+
19
+ it 'freezes the object' do
20
+ expect { subject }.should change(value, :frozen?).from(false).to(true)
21
+ end
22
+
23
+ it 'freezes each value in the Struct' do
24
+ subject.values.select(&:frozen?).should == subject.values
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,22 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+ require 'ice_nine/freezer/no_freeze'
5
+
6
+ describe IceNine::Freezer::Symbol, '.deep_freeze' do
7
+ subject { object.deep_freeze(value) }
8
+
9
+ let(:object) { described_class }
10
+
11
+ context 'with a Symbol object' do
12
+ let(:value) { :symbol }
13
+
14
+ it 'returns the object' do
15
+ should be(value)
16
+ end
17
+
18
+ it 'does not freeze the object' do
19
+ expect { subject }.should_not change(value, :frozen?).from(false)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+ require 'ice_nine/freezer/no_freeze'
5
+
6
+ describe IceNine::Freezer::TrueClass, '.deep_freeze' do
7
+ subject { object.deep_freeze(value) }
8
+
9
+ let(:object) { described_class }
10
+
11
+ context 'with a true object' do
12
+ let(:value) { true }
13
+
14
+ it 'returns the object' do
15
+ should be(value)
16
+ end
17
+
18
+ it 'does not freeze the object' do
19
+ expect { subject }.should_not change(value, :frozen?).from(false)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,7 @@
1
+ desc 'Run metrics with Heckle'
2
+ task :ci => %w[ ci:metrics heckle ]
3
+
4
+ namespace :ci do
5
+ desc 'Run metrics'
6
+ task :metrics => %w[ verify_measurements flog flay reek roodi metrics:all ]
7
+ end
@@ -0,0 +1,43 @@
1
+ # encoding: utf-8
2
+
3
+ begin
4
+ require 'flay'
5
+ require 'yaml'
6
+
7
+ config = YAML.load_file(File.expand_path('../../../config/flay.yml', __FILE__)).freeze
8
+ threshold = config.fetch('threshold').to_i
9
+ total_score = config.fetch('total_score').to_f
10
+ files = Flay.expand_dirs_to_files(config.fetch('path', 'lib'))
11
+
12
+ # original code by Marty Andrews:
13
+ # http://blog.martyandrews.net/2009/05/enforcing-ruby-code-quality.html
14
+ desc 'Analyze for code duplication'
15
+ task :flay do
16
+ # run flay once without a threshold to ensure the max mass matches the threshold
17
+ flay = Flay.new(:fuzzy => false, :verbose => false, :mass => 0)
18
+ flay.process(*files)
19
+
20
+ max = flay.masses.map { |hash, mass| mass.to_f / flay.hashes[hash].size }.max
21
+ unless max.nil? || max >= threshold
22
+ raise "Adjust flay threshold down to #{max}"
23
+ end
24
+
25
+ total = flay.masses.reduce(0.0) { |total, (hash, mass)| total + (mass.to_f / flay.hashes[hash].size) }
26
+ unless total == total_score
27
+ raise "Flay total is now #{total}, but expected #{total_score}"
28
+ end
29
+
30
+ # run flay a second time with the threshold set
31
+ flay = Flay.new(:fuzzy => false, :verbose => false, :mass => threshold.succ)
32
+ flay.process(*files)
33
+
34
+ if flay.masses.any?
35
+ flay.report
36
+ raise "#{flay.masses.size} chunks of code have a duplicate mass > #{threshold}"
37
+ end
38
+ end
39
+ rescue LoadError
40
+ task :flay do
41
+ abort 'Flay is not available. In order to run flay, you must: gem install flay'
42
+ end
43
+ end
@@ -0,0 +1,46 @@
1
+ # encoding: utf-8
2
+
3
+ begin
4
+ require 'flog'
5
+ require 'yaml'
6
+
7
+ class Float
8
+ def round_to(n)
9
+ (self * 10**n).round.to_f * 10**-n
10
+ end
11
+ end
12
+
13
+ config = YAML.load_file(File.expand_path('../../../config/flog.yml', __FILE__)).freeze
14
+ threshold = config.fetch('threshold').to_f.round_to(1)
15
+
16
+ # original code by Marty Andrews:
17
+ # http://blog.martyandrews.net/2009/05/enforcing-ruby-code-quality.html
18
+ desc 'Analyze for code complexity'
19
+ task :flog do
20
+ flog = Flog.new
21
+ flog.flog Array(config.fetch('path', 'lib'))
22
+
23
+ totals = flog.totals.select { |name, score| name[-5, 5] != '#none' }.
24
+ map { |name, score| [ name, score.round_to(1) ] }.
25
+ sort_by { |name, score| score }
26
+
27
+ last = totals.last
28
+ max = last[1] if last
29
+ unless max.nil? || max >= threshold
30
+ raise "Adjust flog score down to #{max}"
31
+ end
32
+
33
+ bad_methods = totals.select { |name, score| score > threshold }
34
+ if bad_methods.any?
35
+ bad_methods.reverse_each do |name, score|
36
+ puts '%8.1f: %s' % [ score, name ]
37
+ end
38
+
39
+ raise "#{bad_methods.size} methods have a flog complexity > #{threshold}"
40
+ end
41
+ end
42
+ rescue LoadError
43
+ task :flog do
44
+ abort 'Flog is not available. In order to run flog, you must: gem install flog'
45
+ end
46
+ end
@@ -0,0 +1,210 @@
1
+ # encoding: utf-8
2
+
3
+ $LOAD_PATH.unshift(File.expand_path('../../../lib', __FILE__))
4
+
5
+ # original code by Ashley Moran:
6
+ # http://aviewfromafar.net/2007/11/1/rake-task-for-heckling-your-specs
7
+
8
+ begin
9
+ require 'pathname'
10
+ require 'active_support/inflector'
11
+ require 'heckle'
12
+ require 'mspec'
13
+ require 'mspec/utils/name_map'
14
+
15
+ SKIP_METHODS = %w[ blank_slate_method_added ].freeze
16
+
17
+ class NameMap
18
+ def file_name(method, constant)
19
+ map = MAP[method]
20
+ name = if map
21
+ map[constant] || map[:default]
22
+ else
23
+ method.gsub(/[?!=]\z/, '')
24
+ end
25
+ "#{name}_spec.rb"
26
+ end
27
+ end
28
+
29
+ desc 'Heckle each module and class'
30
+ task :heckle => :rcov do
31
+ unless Ruby2Ruby::VERSION == '1.2.2'
32
+ raise "ruby2ruby version #{Ruby2Ruby::VERSION} may not work properly, 1.2.2 *only* is recommended for use with heckle"
33
+ end
34
+
35
+ require 'ice_nine'
36
+
37
+ root_module_regexp = Regexp.union(
38
+ 'IceNine'
39
+ )
40
+
41
+ spec_dir = Pathname('spec/unit')
42
+
43
+ NameMap::MAP.each do |op, method|
44
+ next if method.kind_of?(Hash)
45
+ NameMap::MAP[op] = { :default => method }
46
+ end
47
+
48
+ aliases = Hash.new { |h,mod| h[mod] = Hash.new { |h,method| h[method] = method } }
49
+ map = NameMap.new
50
+
51
+ heckle_caught_modules = Hash.new { |hash, key| hash[key] = [] }
52
+ unhandled_mutations = 0
53
+
54
+ ObjectSpace.each_object(Module) do |mod|
55
+ next unless mod.name =~ /\A#{root_module_regexp}(?::|\z)/
56
+
57
+ spec_prefix = spec_dir.join(mod.name.underscore)
58
+
59
+ specs = []
60
+
61
+ # get the public class methods
62
+ metaclass = class << mod; self end
63
+ ancestors = metaclass.ancestors
64
+
65
+ spec_class_methods = mod.singleton_methods(false)
66
+
67
+ spec_class_methods.reject! do |method|
68
+ %w[ yaml_new yaml_tag_subclasses? included nesting constants ].include?(method.to_s)
69
+ end
70
+
71
+ if mod.ancestors.include?(Singleton)
72
+ spec_class_methods.reject! { |method| method.to_s == 'instance' }
73
+ end
74
+
75
+ # get the protected and private class methods
76
+ other_class_methods = metaclass.protected_instance_methods(false) |
77
+ metaclass.private_instance_methods(false)
78
+
79
+ ancestors.each do |ancestor|
80
+ other_class_methods -= ancestor.protected_instance_methods(false) |
81
+ ancestor.private_instance_methods(false)
82
+ end
83
+
84
+ other_class_methods.reject! do |method|
85
+ method.to_s == 'allocate' || SKIP_METHODS.include?(method.to_s)
86
+ end
87
+
88
+ other_class_methods.reject! do |method|
89
+ next unless spec_class_methods.any? { |specced| specced.to_s == $1 }
90
+
91
+ spec_class_methods << method
92
+ end
93
+
94
+ spec_class_methods -= other_class_methods
95
+
96
+ # get the instances methods
97
+ spec_methods = mod.public_instance_methods(false)
98
+
99
+ other_methods = mod.protected_instance_methods(false) |
100
+ mod.private_instance_methods(false)
101
+
102
+ other_methods.reject! do |method|
103
+ next unless spec_methods.any? { |specced| specced.to_s == $1 }
104
+
105
+ spec_methods << method
106
+ end
107
+
108
+ # map the class methods to spec files
109
+ spec_class_methods.each do |method|
110
+ method = aliases[mod.name][method]
111
+ next if SKIP_METHODS.include?(method.to_s)
112
+
113
+ spec_file = spec_prefix.join('class_methods').join(map.file_name(method, mod.name))
114
+
115
+ unless spec_file.file?
116
+ raise "No spec file #{spec_file} for #{mod}.#{method}"
117
+ next
118
+ end
119
+
120
+ specs << [ ".#{method}", [ spec_file ] ]
121
+ end
122
+
123
+ # map the instance methods to spec files
124
+ spec_methods.each do |method|
125
+ method = aliases[mod.name][method]
126
+ next if SKIP_METHODS.include?(method.to_s)
127
+
128
+ spec_file = spec_prefix.join(map.file_name(method, mod.name))
129
+
130
+ unless spec_file.file?
131
+ raise "No spec file #{spec_file} for #{mod}##{method}"
132
+ next
133
+ end
134
+
135
+ specs << [ "##{method}", [ spec_file ] ]
136
+ end
137
+
138
+ # non-public methods are considered covered if they can be mutated
139
+ # and any spec fails for the current or descendant modules
140
+ other_methods.each do |method|
141
+ descedant_specs = []
142
+
143
+ ObjectSpace.each_object(Module) do |descedant|
144
+ next unless descedant.name =~ /\A#{root_module_regexp}(?::|\z)/ && mod >= descedant
145
+ descedant_spec_prefix = spec_dir.join(descedant.name.underscore)
146
+ descedant_specs << descedant_spec_prefix
147
+
148
+ if method.to_s == 'initialize'
149
+ descedant_specs.concat(Pathname.glob(descedant_spec_prefix.join('class_methods/new_spec.rb')))
150
+ end
151
+ end
152
+
153
+ specs << [ "##{method}", descedant_specs ]
154
+ end
155
+
156
+ other_class_methods.each do |method|
157
+ descedant_specs = []
158
+
159
+ ObjectSpace.each_object(Module) do |descedant|
160
+ next unless descedant.name =~ /\A#{root_module_regexp}(?::|\z)/ && mod >= descedant
161
+ descedant_specs << spec_dir.join(descedant.name.underscore).join('class_methods')
162
+ end
163
+
164
+ specs << [ ".#{method}", descedant_specs ]
165
+ end
166
+
167
+ specs.sort.each do |(method, spec_files)|
168
+ puts "Heckling #{mod}#{method}"
169
+ IO.popen("spec #{spec_files.join(' ')} --heckle '#{mod}#{method}'") do |pipe|
170
+ while line = pipe.gets
171
+ case line = line.chomp
172
+ when "The following mutations didn't cause test failures:"
173
+ heckle_caught_modules[mod.name] << method
174
+ when '+++ mutation'
175
+ unhandled_mutations += 1
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end
181
+
182
+ if unhandled_mutations > 0
183
+ error_message_lines = [ "*************\n" ]
184
+
185
+ error_message_lines << "Heckle found #{unhandled_mutations} " \
186
+ "mutation#{"s" unless unhandled_mutations == 1} " \
187
+ "that didn't cause spec violations\n"
188
+
189
+ heckle_caught_modules.each do |mod, methods|
190
+ error_message_lines << "#{mod} contains the following " \
191
+ 'poorly-specified methods:'
192
+ methods.each do |method|
193
+ error_message_lines << " - #{method}"
194
+ end
195
+ error_message_lines << ''
196
+ end
197
+
198
+ error_message_lines << 'Get your act together and come back ' \
199
+ 'when your specs are doing their job!'
200
+
201
+ raise error_message_lines.join("\n")
202
+ else
203
+ puts 'Well done! Your code withstood a heckling.'
204
+ end
205
+ end
206
+ rescue LoadError => e
207
+ task :heckle do
208
+ abort 'Heckle or mspec is not available. In order to run heckle, you must: gem install heckle mspec' << e
209
+ end
210
+ end