virtus 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (85) hide show
  1. data/.gitignore +3 -0
  2. data/.rvmrc +1 -1
  3. data/Gemfile +20 -1
  4. data/History.txt +21 -0
  5. data/README.markdown +2 -2
  6. data/Rakefile +1 -2
  7. data/VERSION +1 -1
  8. data/config/flay.yml +3 -0
  9. data/config/flog.yml +2 -0
  10. data/config/roodi.yml +18 -0
  11. data/config/site.reek +91 -0
  12. data/config/yardstick.yml +2 -0
  13. data/lib/virtus.rb +51 -45
  14. data/lib/virtus/attribute.rb +301 -0
  15. data/lib/virtus/attribute/array.rb +17 -0
  16. data/lib/virtus/attribute/boolean.rb +60 -0
  17. data/lib/virtus/attribute/date.rb +35 -0
  18. data/lib/virtus/attribute/date_time.rb +34 -0
  19. data/lib/virtus/attribute/decimal.rb +24 -0
  20. data/lib/virtus/attribute/float.rb +33 -0
  21. data/lib/virtus/attribute/hash.rb +18 -0
  22. data/lib/virtus/attribute/integer.rb +30 -0
  23. data/lib/virtus/{attributes → attribute}/numeric.rb +2 -3
  24. data/lib/virtus/{attributes → attribute}/object.rb +2 -1
  25. data/lib/virtus/attribute/string.rb +31 -0
  26. data/lib/virtus/attribute/time.rb +34 -0
  27. data/lib/virtus/class_methods.rb +25 -8
  28. data/lib/virtus/instance_methods.rb +48 -9
  29. data/lib/virtus/support/chainable.rb +4 -6
  30. data/lib/virtus/typecast/boolean.rb +27 -0
  31. data/lib/virtus/typecast/numeric.rb +82 -0
  32. data/lib/virtus/typecast/time.rb +162 -0
  33. data/spec/integration/virtus/attributes/attribute/typecast_spec.rb +4 -4
  34. data/spec/integration/virtus/class_methods/attribute_spec.rb +1 -1
  35. data/spec/integration/virtus/class_methods/attributes_spec.rb +3 -2
  36. data/spec/integration/virtus/class_methods/const_missing_spec.rb +2 -2
  37. data/spec/rcov.opts +6 -0
  38. data/spec/spec_helper.rb +0 -9
  39. data/spec/unit/shared/attribute.rb +8 -8
  40. data/spec/unit/virtus/{attributes → attribute}/array_spec.rb +1 -1
  41. data/spec/unit/virtus/attribute/attribute_spec.rb +12 -0
  42. data/spec/unit/virtus/{attributes → attribute}/boolean_spec.rb +4 -4
  43. data/spec/unit/virtus/{attributes → attribute}/date_spec.rb +13 -7
  44. data/spec/unit/virtus/{attributes → attribute}/date_time_spec.rb +31 -10
  45. data/spec/unit/virtus/{attributes → attribute}/decimal_spec.rb +18 -18
  46. data/spec/unit/virtus/{attributes → attribute}/float_spec.rb +18 -18
  47. data/spec/unit/virtus/{attributes → attribute}/hash_spec.rb +1 -1
  48. data/spec/unit/virtus/{attributes → attribute}/integer_spec.rb +18 -18
  49. data/spec/unit/virtus/attribute/numeric/class_methods/descendants_spec.rb +15 -0
  50. data/spec/unit/virtus/attribute/object/class_methods/descendants_spec.rb +16 -0
  51. data/spec/unit/virtus/{attributes → attribute}/string_spec.rb +2 -2
  52. data/spec/unit/virtus/{attributes → attribute}/time_spec.rb +19 -9
  53. data/spec/unit/virtus/class_methods/new_spec.rb +7 -7
  54. data/spec/unit/virtus/determine_type_spec.rb +4 -4
  55. data/spec/unit/virtus/instance_methods/attribute_get_spec.rb +1 -1
  56. data/spec/unit/virtus/instance_methods/attribute_set_spec.rb +2 -2
  57. data/spec/unit/virtus/instance_methods/attributes_spec.rb +2 -2
  58. data/tasks/metrics/ci.rake +7 -0
  59. data/tasks/metrics/flay.rake +41 -0
  60. data/tasks/metrics/flog.rake +43 -0
  61. data/tasks/metrics/heckle.rake +261 -0
  62. data/tasks/metrics/metric_fu.rake +29 -0
  63. data/tasks/metrics/reek.rake +9 -0
  64. data/tasks/metrics/roodi.rake +15 -0
  65. data/tasks/metrics/yardstick.rake +23 -0
  66. data/tasks/spec.rake +26 -0
  67. data/tasks/yard.rake +9 -0
  68. data/virtus.gemspec +48 -33
  69. metadata +51 -41
  70. data/lib/virtus/attributes/array.rb +0 -8
  71. data/lib/virtus/attributes/attribute.rb +0 -214
  72. data/lib/virtus/attributes/boolean.rb +0 -39
  73. data/lib/virtus/attributes/date.rb +0 -44
  74. data/lib/virtus/attributes/date_time.rb +0 -43
  75. data/lib/virtus/attributes/decimal.rb +0 -24
  76. data/lib/virtus/attributes/float.rb +0 -20
  77. data/lib/virtus/attributes/hash.rb +0 -8
  78. data/lib/virtus/attributes/integer.rb +0 -20
  79. data/lib/virtus/attributes/string.rb +0 -11
  80. data/lib/virtus/attributes/time.rb +0 -45
  81. data/lib/virtus/attributes/typecast/numeric.rb +0 -32
  82. data/lib/virtus/attributes/typecast/time.rb +0 -27
  83. data/spec/unit/virtus/attributes/attribute_spec.rb +0 -13
  84. data/spec/unit/virtus/attributes/numeric/class_methods/descendants_spec.rb +0 -15
  85. data/spec/unit/virtus/attributes/object/class_methods/descendants_spec.rb +0 -16
@@ -0,0 +1,15 @@
1
+ require 'spec_helper'
2
+
3
+ describe Virtus::Attribute::Numeric, '.descendants' do
4
+ subject { described_class.descendants }
5
+
6
+ let(:known_descendants) do
7
+ [ Virtus::Attribute::Decimal,
8
+ Virtus::Attribute::Float,
9
+ Virtus::Attribute::Integer ]
10
+ end
11
+
12
+ it "should return all known attribute classes" do
13
+ subject.should eql(known_descendants)
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ require 'spec_helper'
2
+
3
+ describe Virtus::Attribute::Object, '.descendants' do
4
+ subject { described_class.descendants }
5
+
6
+ let(:known_descendants) do
7
+ [ Virtus::Attribute::Array, Virtus::Attribute::Boolean,
8
+ Virtus::Attribute::Date, Virtus::Attribute::DateTime,
9
+ Virtus::Attribute::Numeric, Virtus::Attribute::Hash,
10
+ Virtus::Attribute::String, Virtus::Attribute::Time ]
11
+ end
12
+
13
+ it "should return all known attribute classes" do
14
+ subject.should eql(known_descendants)
15
+ end
16
+ end
@@ -1,6 +1,6 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe Virtus::Attributes::String do
3
+ describe Virtus::Attribute::String do
4
4
  it_should_behave_like 'Attribute' do
5
5
  let(:attribute_name) { :email }
6
6
  let(:attribute_value) { 'red john' }
@@ -15,6 +15,6 @@ describe Virtus::Attributes::String do
15
15
 
16
16
  subject { attribute.typecast(value) }
17
17
 
18
- it { should == typecast_value }
18
+ it { should eql(typecast_value) }
19
19
  end
20
20
  end
@@ -1,6 +1,6 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe Virtus::Attributes::Time do
3
+ describe Virtus::Attribute::Time do
4
4
  it_should_behave_like 'Attribute' do
5
5
  let(:attribute_name) { :birthday }
6
6
  let(:attribute_value) { Time.now }
@@ -9,7 +9,7 @@ describe Virtus::Attributes::Time do
9
9
 
10
10
  describe '#typecast' do
11
11
  let(:model) { Class.new { include Virtus } }
12
- let(:attribute) { model.attribute(:birthday, Virtus::Attributes::Time) }
12
+ let(:attribute) { model.attribute(:birthday, Virtus::Attribute::Time) }
13
13
 
14
14
  let(:year) { 1983 }
15
15
  let(:month) { 11 }
@@ -22,12 +22,22 @@ describe Virtus::Attributes::Time do
22
22
 
23
23
  shared_examples_for "a correct time" do
24
24
  it { should be_kind_of(Time) }
25
- its(:year) { should == year }
26
- its(:month) { should == month }
27
- its(:day) { should == day }
28
- its(:hour) { should == hour }
29
- its(:min) { should == min }
30
- its(:sec) { should == sec }
25
+ its(:year) { should eql(year) }
26
+ its(:month) { should eql(month) }
27
+ its(:day) { should eql(day) }
28
+ its(:hour) { should eql(hour) }
29
+ its(:min) { should eql(min) }
30
+ its(:sec) { should eql(sec) }
31
+ end
32
+
33
+ context 'with a date' do
34
+ let(:hour) { 0 }
35
+ let(:min) { 0 }
36
+ let(:sec) { 0 }
37
+
38
+ it_should_behave_like "a correct time" do
39
+ let(:value) { DateTime.new(year, month, day, hour, min, sec) }
40
+ end
31
41
  end
32
42
 
33
43
  context 'with a date time' do
@@ -65,7 +75,7 @@ describe Virtus::Attributes::Time do
65
75
 
66
76
  context 'with a non-date value' do
67
77
  let(:value) { '2999' }
68
- it { should == value }
78
+ it { should equal(value) }
69
79
  end
70
80
  end
71
81
  end
@@ -1,6 +1,6 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe Virtus::Attributes, '.new' do
3
+ describe Virtus::Attribute, '.new' do
4
4
  let(:model) do
5
5
  Class.new do
6
6
  include Virtus
@@ -11,9 +11,9 @@ describe Virtus::Attributes, '.new' do
11
11
  attr_accessor :age
12
12
 
13
13
  def self.new(attributes)
14
- model = super
15
- model.age = 28
16
- model
14
+ instance = super
15
+ instance.age = 28
16
+ instance
17
17
  end
18
18
 
19
19
  def initialize(attributes = {})
@@ -28,14 +28,14 @@ describe Virtus::Attributes, '.new' do
28
28
  end
29
29
 
30
30
  it "sets the attributes" do
31
- object.name.should == "john"
31
+ object.name.should eql("john")
32
32
  end
33
33
 
34
34
  it "calls custom #initialize" do
35
- object.email.should == "john@domain.com"
35
+ object.email.should eql("john@domain.com")
36
36
  end
37
37
 
38
38
  it "calls custom .new" do
39
- object.age.should == 28
39
+ object.age.should eql(28)
40
40
  end
41
41
  end
@@ -2,10 +2,10 @@ require 'spec_helper'
2
2
 
3
3
  describe Virtus, '.determine_type' do
4
4
 
5
- (Virtus::Attributes::Object.descendants - [
6
- Virtus::Attributes::Boolean,
7
- Virtus::Attributes::Object,
8
- Virtus::Attributes::Numeric ]).each do |attribute_class|
5
+ (Virtus::Attribute::Object.descendants - [
6
+ Virtus::Attribute::Boolean,
7
+ Virtus::Attribute::Object,
8
+ Virtus::Attribute::Numeric ]).each do |attribute_class|
9
9
 
10
10
  context "with #{attribute_primitive = attribute_class.primitive}" do
11
11
  subject { described_class.determine_type(attribute_primitive) }
@@ -17,6 +17,6 @@ describe Virtus::InstanceMethods, '#attribute_get' do
17
17
  end
18
18
 
19
19
  it "returns the value of an attribute" do
20
- object.attribute_get(:name).should == value
20
+ object.attribute_get(:name).should eql(value)
21
21
  end
22
22
  end
@@ -21,10 +21,10 @@ describe Virtus::InstanceMethods, '#attribute_set' do
21
21
  end
22
22
 
23
23
  it "returns the value" do
24
- object.attribute_set(:name, value).should == value
24
+ object.attribute_set(:name, value).should eql(value)
25
25
  end
26
26
 
27
27
  it "sets value of an attribute" do
28
- object.name.should == value
28
+ object.name.should eql(value)
29
29
  end
30
30
  end
@@ -21,7 +21,7 @@ describe Virtus do
21
21
 
22
22
  describe '#attributes' do
23
23
  it "returns a hash of attributes" do
24
- object.attributes.should == attributes
24
+ object.attributes.should eql(attributes)
25
25
  end
26
26
  end
27
27
 
@@ -31,7 +31,7 @@ describe Virtus do
31
31
  end
32
32
 
33
33
  it "sets attribute values for publicly accessible attributes" do
34
- object.attributes.should == attributes
34
+ object.attributes.should eql(attributes)
35
35
  end
36
36
  end
37
37
  end
@@ -0,0 +1,7 @@
1
+ desc 'Run metrics with Heckle'
2
+ task :ci => [ 'ci:metrics', :heckle ]
3
+
4
+ namespace :ci do
5
+ desc 'Run metrics'
6
+ task :metrics => [ :verify_measurements, :flog, :flay, :reek, :roodi, 'metrics:all' ]
7
+ end
@@ -0,0 +1,41 @@
1
+ begin
2
+ require 'flay'
3
+ require 'yaml'
4
+
5
+ config = YAML.load_file(File.expand_path('../../../config/flay.yml', __FILE__)).freeze
6
+ threshold = config.fetch('threshold').to_i
7
+ total_score = config.fetch('total_score').to_f
8
+ files = Flay.expand_dirs_to_files(config.fetch('path', 'lib'))
9
+
10
+ # original code by Marty Andrews:
11
+ # http://blog.martyandrews.net/2009/05/enforcing-ruby-code-quality.html
12
+ desc 'Analyze for code duplication'
13
+ task :flay do
14
+ # run flay once without a threshold to ensure the max mass matches the threshold
15
+ flay = Flay.new(:fuzzy => false, :verbose => false, :mass => 0)
16
+ flay.process(*files)
17
+
18
+ max = flay.masses.map { |hash, mass| mass.to_f / flay.hashes[hash].size }.max
19
+ unless max >= threshold
20
+ raise "Adjust flay threshold down to #{max}"
21
+ end
22
+
23
+ total = flay.masses.reduce(0.0) { |total, (hash, mass)| total + (mass.to_f / flay.hashes[hash].size) }
24
+ unless total == total_score
25
+ raise "Flay total is now #{total}, but expected #{total_score}"
26
+ end
27
+
28
+ # run flay a second time with the threshold set
29
+ flay = Flay.new(:fuzzy => false, :verbose => false, :mass => threshold.succ)
30
+ flay.process(*files)
31
+
32
+ if flay.masses.any?
33
+ flay.report
34
+ raise "#{flay.masses.size} chunks of code have a duplicate mass > #{threshold}"
35
+ end
36
+ end
37
+ rescue LoadError
38
+ task :flay do
39
+ abort 'Flay is not available. In order to run flay, you must: gem install flay'
40
+ end
41
+ end
@@ -0,0 +1,43 @@
1
+ begin
2
+ require 'flog'
3
+ require 'yaml'
4
+
5
+ class Float
6
+ def round_to(n)
7
+ (self * 10**n).round.to_f * 10**-n
8
+ end
9
+ end
10
+
11
+ config = YAML.load_file(File.expand_path('../../../config/flog.yml', __FILE__)).freeze
12
+ threshold = config.fetch('threshold').to_f.round_to(1)
13
+
14
+ # original code by Marty Andrews:
15
+ # http://blog.martyandrews.net/2009/05/enforcing-ruby-code-quality.html
16
+ desc 'Analyze for code complexity'
17
+ task :flog do
18
+ flog = Flog.new
19
+ flog.flog Array(config.fetch('path', 'lib'))
20
+
21
+ totals = flog.totals.select { |name, score| name[-5, 5] != '#none' }.
22
+ map { |name, score| [ name, score.round_to(1) ] }.
23
+ sort_by { |name, score| score }
24
+
25
+ max = totals.last[1]
26
+ unless max >= threshold
27
+ raise "Adjust flog score down to #{max}"
28
+ end
29
+
30
+ bad_methods = totals.select { |name, score| score > threshold }
31
+ if bad_methods.any?
32
+ bad_methods.reverse_each do |name, score|
33
+ puts '%8.1f: %s' % [ score, name ]
34
+ end
35
+
36
+ raise "#{bad_methods.size} methods have a flog complexity > #{threshold}"
37
+ end
38
+ end
39
+ rescue LoadError
40
+ task :flog do
41
+ abort 'Flog is not available. In order to run flog, you must: gem install flog'
42
+ end
43
+ end
@@ -0,0 +1,261 @@
1
+ $LOAD_PATH.unshift(File.expand_path('../../../lib', __FILE__))
2
+
3
+ # original code by Ashley Moran:
4
+ # http://aviewfromafar.net/2007/11/1/rake-task-for-heckling-your-specs
5
+
6
+ begin
7
+ require 'pathname'
8
+ require 'backports'
9
+ require 'active_support/inflector'
10
+ require 'heckle'
11
+ require 'mspec'
12
+ require 'mspec/utils/name_map'
13
+
14
+ SKIP_METHODS = %w[ blank_slate_method_added ].freeze
15
+
16
+ class NameMap
17
+ def file_name(method, constant)
18
+ map = MAP[method]
19
+ name = if map
20
+ map[constant] || map[:default]
21
+ else
22
+ method.gsub(/[?!=]\z/, '')
23
+ end
24
+ "#{name}_spec.rb"
25
+ end
26
+ end
27
+
28
+ desc 'Heckle each module and class'
29
+ task :heckle => :verify_rcov do
30
+ unless Ruby2Ruby::VERSION == '1.2.2'
31
+ raise "ruby2ruby version #{Ruby2Ruby::VERSION} may not work properly, 1.2.2 *only* is recommended for use with heckle"
32
+ end
33
+
34
+ require 'veritas'
35
+ root_module = 'Veritas'
36
+
37
+ spec_dir = Pathname('spec/unit')
38
+
39
+ NameMap::MAP.each do |op, method|
40
+ next if method.kind_of?(Hash)
41
+ NameMap::MAP[op] = { :default => method }
42
+ end
43
+
44
+ %w[
45
+ Veritas::Relation::Header
46
+ Veritas::Algebra::Difference::Methods
47
+ Veritas::Algebra::Intersection::Methods
48
+ Veritas::Algebra::Join::Methods
49
+ Veritas::Algebra::Product::Methods
50
+ Veritas::Algebra::Projection::Methods
51
+ Veritas::Algebra::Rename::Methods
52
+ Veritas::Algebra::Rename::Aliases
53
+ Veritas::Algebra::Restriction::Methods
54
+ Veritas::Algebra::Union::Methods
55
+ ].each do |mod|
56
+ NameMap::MAP['-'][mod] = 'difference'
57
+ NameMap::MAP['&'][mod] = 'intersect'
58
+ NameMap::MAP['+'][mod] = 'join'
59
+ NameMap::MAP['*'][mod] = 'product'
60
+ NameMap::MAP['|'][mod] = 'union'
61
+ end
62
+
63
+ NameMap::MAP['==']['Veritas::Relation::Operation::Order::Direction'] = 'eql'
64
+
65
+ NameMap::MAP['|']['Veritas::Relation::Operation::Order::DirectionSet'] = 'union'
66
+
67
+ %w[
68
+ Veritas::Function::Connective::Conjunction::Methods
69
+ Veritas::Function::Connective::Disjunction::Methods
70
+ Veritas::Function::Connective::Negation::Methods
71
+ ].each do |mod|
72
+ NameMap::MAP['&'][mod] = 'and'
73
+ NameMap::MAP['|'][mod] = 'or'
74
+ NameMap::MAP['-'][mod] = 'not'
75
+ end
76
+
77
+ aliases = Hash.new { |h,mod| h[mod] = Hash.new { |h,method| h[method] = method } }
78
+
79
+ aliases['Veritas::Attribute::Numeric']['range'] = 'size'
80
+ aliases['Veritas::Attribute::String']['range'] = 'length'
81
+
82
+ aliases['Veritas::Aggregate::Minimum::Methods']['min'] = 'minimum'
83
+ aliases['Veritas::Aggregate::Maximum::Methods']['max'] = 'maximum'
84
+ aliases['Veritas::Aggregate::Mean::Methods']['avg'] = 'mean'
85
+ aliases['Veritas::Aggregate::Mean::Methods']['average'] = 'mean'
86
+ aliases['Veritas::Aggregate::Variance::Methods']['var'] = 'variance'
87
+ aliases['Veritas::Aggregate::StandardDeviation::Methods']['stddev'] = 'standard_deviation'
88
+
89
+ aliases['Veritas::Function::Numeric::Addition::Methods']['+'] = 'add'
90
+ aliases['Veritas::Function::Numeric::Subtraction::Methods']['-'] = 'subtract'
91
+ aliases['Veritas::Function::Numeric::Multiplication::Methods']['*'] = 'multiply'
92
+ aliases['Veritas::Function::Numeric::Division::Methods']['/'] = 'divide'
93
+ aliases['Veritas::Function::Numeric::Absolute::Methods']['abs'] = 'absolute'
94
+ aliases['Veritas::Function::Numeric::Exponentiation::Methods']['**'] = 'exponent'
95
+ aliases['Veritas::Function::Numeric::Exponentiation::Methods']['pow'] = 'exponent'
96
+ aliases['Veritas::Function::Numeric::Modulo::Methods']['%'] = 'modulo'
97
+ aliases['Veritas::Function::Numeric::Modulo::Methods']['mod'] = 'modulo'
98
+ aliases['Veritas::Function::Numeric::Random::Methods']['rand'] = 'random'
99
+ aliases['Veritas::Function::Numeric::SquareRoot::Methods']['sqrt'] = 'square_root'
100
+ aliases['Veritas::Function::Numeric::UnaryPlus::Methods']['+@'] = 'unary_plus'
101
+ aliases['Veritas::Function::Numeric::UnaryMinus::Methods']['-@'] = 'unary_minus'
102
+
103
+ map = NameMap.new
104
+
105
+ heckle_caught_modules = Hash.new { |hash, key| hash[key] = [] }
106
+ unhandled_mutations = 0
107
+
108
+ ObjectSpace.each_object(Module) do |mod|
109
+ next unless mod.name =~ /\A#{root_module}(?::|\z)/
110
+
111
+ spec_prefix = spec_dir.join(mod.name.underscore)
112
+
113
+ specs = []
114
+
115
+ # get the public class methods
116
+ metaclass = class << mod; self end
117
+ ancestors = metaclass.ancestors
118
+
119
+ spec_class_methods = mod.singleton_methods(false)
120
+
121
+ spec_class_methods.reject! do |method|
122
+ %w[ yaml_new yaml_tag_subclasses? included nesting constants ].include?(method.to_s)
123
+ end
124
+
125
+ if mod.ancestors.include?(Singleton)
126
+ spec_class_methods.reject! { |method| method.to_s == 'instance' }
127
+ end
128
+
129
+ # get the protected and private class methods
130
+ other_class_methods = metaclass.protected_instance_methods(false) |
131
+ metaclass.private_instance_methods(false)
132
+
133
+ ancestors.each do |ancestor|
134
+ other_class_methods -= ancestor.protected_instance_methods(false) |
135
+ ancestor.private_instance_methods(false)
136
+ end
137
+
138
+ other_class_methods.reject! do |method|
139
+ method.to_s == 'allocate' || SKIP_METHODS.include?(method.to_s)
140
+ end
141
+
142
+ other_class_methods.reject! do |method|
143
+ next unless spec_class_methods.any? { |specced| specced.to_s == $1 }
144
+
145
+ spec_class_methods << method
146
+ end
147
+
148
+ # get the instances methods
149
+ spec_methods = mod.public_instance_methods(false)
150
+
151
+ other_methods = mod.protected_instance_methods(false) |
152
+ mod.private_instance_methods(false)
153
+
154
+ other_methods.reject! do |method|
155
+ next unless spec_methods.any? { |specced| specced.to_s == $1 }
156
+
157
+ spec_methods << method
158
+ end
159
+
160
+ # map the class methods to spec files
161
+ spec_class_methods.each do |method|
162
+ method = aliases[mod.name][method]
163
+ next if SKIP_METHODS.include?(method.to_s)
164
+
165
+ spec_file = spec_prefix.join('class_methods').join(map.file_name(method, mod.name))
166
+
167
+ unless spec_file.file?
168
+ raise "No spec file #{spec_file} for #{mod}.#{method}"
169
+ end
170
+
171
+ specs << [ ".#{method}", [ spec_file ] ]
172
+ end
173
+
174
+ # map the instance methods to spec files
175
+ spec_methods.each do |method|
176
+ method = aliases[mod.name][method]
177
+ next if SKIP_METHODS.include?(method.to_s)
178
+
179
+ spec_file = spec_prefix.join(map.file_name(method, mod.name))
180
+
181
+ unless spec_file.file?
182
+ raise "No spec file #{spec_file} for #{mod}##{method}"
183
+ end
184
+
185
+ specs << [ "##{method}", [ spec_file ] ]
186
+ end
187
+
188
+ # non-public methods are considered covered if they can be mutated
189
+ # and any spec fails for the current or descendant modules
190
+ other_methods.each do |method|
191
+ descedant_specs = []
192
+
193
+ ObjectSpace.each_object(Module) do |descedant|
194
+ next unless descedant.name =~ /\A#{root_module}(?::|\z)/ && mod >= descedant
195
+ descedant_spec_prefix = spec_dir.join(descedant.name.underscore)
196
+ descedant_specs.concat(Pathname.glob(descedant_spec_prefix.join('*_spec.rb')))
197
+
198
+ if method.to_s == 'initialize'
199
+ descedant_specs.concat(Pathname.glob(descedant_spec_prefix.join('class_methods/new_spec.rb')))
200
+ end
201
+ end
202
+
203
+ specs << [ "##{method}", descedant_specs ]
204
+ end
205
+
206
+ other_class_methods.each do |method|
207
+ descedant_specs = []
208
+
209
+ ObjectSpace.each_object(Module) do |descedant|
210
+ next unless descedant.name =~ /\A#{root_module}(?::|\z)/ && mod >= descedant
211
+ descedant_spec_prefix = spec_dir.join(descedant.name.underscore)
212
+ descedant_specs.concat(Pathname.glob(descedant_spec_prefix.join('class_methods/*_spec.rb')))
213
+ end
214
+
215
+ specs << [ ".#{method}", descedant_specs ]
216
+ end
217
+
218
+ specs.sort.each do |(method, spec_files)|
219
+ puts "Heckling #{mod}#{method}"
220
+ IO.popen("spec #{spec_files.join(' ')} --heckle '#{mod}#{method}'") do |pipe|
221
+ while line = pipe.gets
222
+ case line = line.chomp
223
+ when "The following mutations didn't cause test failures:"
224
+ heckle_caught_modules[mod.name] << method
225
+ when '+++ mutation'
226
+ unhandled_mutations += 1
227
+ end
228
+ end
229
+ end
230
+ end
231
+ end
232
+
233
+ if unhandled_mutations > 0
234
+ error_message_lines = [ "*************\n" ]
235
+
236
+ error_message_lines << "Heckle found #{unhandled_mutations} " \
237
+ "mutation#{"s" unless unhandled_mutations == 1} " \
238
+ "that didn't cause spec violations\n"
239
+
240
+ heckle_caught_modules.each do |mod, methods|
241
+ error_message_lines << "#{mod} contains the following " \
242
+ 'poorly-specified methods:'
243
+ methods.each do |method|
244
+ error_message_lines << " - #{method}"
245
+ end
246
+ error_message_lines << ''
247
+ end
248
+
249
+ error_message_lines << 'Get your act together and come back ' \
250
+ 'when your specs are doing their job!'
251
+
252
+ raise error_message_lines.join("\n")
253
+ else
254
+ puts 'Well done! Your code withstood a heckling.'
255
+ end
256
+ end
257
+ rescue LoadError
258
+ task :heckle do
259
+ abort 'Heckle or mspec is not available. In order to run heckle, you must: gem install heckle mspec'
260
+ end
261
+ end