virtus 0.0.2 → 0.0.3

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