ice_nine 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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