yardstick 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 (42) hide show
  1. data/.document +5 -0
  2. data/.gitignore +10 -0
  3. data/LICENSE +20 -0
  4. data/README.markdown +117 -0
  5. data/Rakefile +24 -0
  6. data/VERSION +1 -0
  7. data/bin/yardstick +7 -0
  8. data/deps.rip +1 -0
  9. data/lib/yardstick/autoload.rb +16 -0
  10. data/lib/yardstick/cli.rb +76 -0
  11. data/lib/yardstick/core_ext/object.rb +13 -0
  12. data/lib/yardstick/measurable.rb +78 -0
  13. data/lib/yardstick/measurement.rb +217 -0
  14. data/lib/yardstick/measurement_set.rb +130 -0
  15. data/lib/yardstick/method.rb +111 -0
  16. data/lib/yardstick/ordered_set.rb +115 -0
  17. data/lib/yardstick/processor.rb +65 -0
  18. data/lib/yardstick/rake/measurement.rb +101 -0
  19. data/lib/yardstick/rake/verify.rb +179 -0
  20. data/lib/yardstick/rule.rb +61 -0
  21. data/lib/yardstick/rule_set.rb +18 -0
  22. data/lib/yardstick/yard_ext.rb +20 -0
  23. data/lib/yardstick.rb +52 -0
  24. data/spec/public/yardstick/cli_spec.rb +108 -0
  25. data/spec/public/yardstick/measurement_set_spec.rb +265 -0
  26. data/spec/public/yardstick/measurement_spec.rb +256 -0
  27. data/spec/public/yardstick/method_spec.rb +356 -0
  28. data/spec/public/yardstick/rake/measurement_spec.rb +173 -0
  29. data/spec/public/yardstick/rake/verify_spec.rb +229 -0
  30. data/spec/public/yardstick_spec.rb +70 -0
  31. data/spec/rcov.opts +6 -0
  32. data/spec/semipublic/yardstick/rule_spec.rb +28 -0
  33. data/spec/spec.opts +4 -0
  34. data/spec/spec_helper.rb +45 -0
  35. data/tasks/ci.rake +1 -0
  36. data/tasks/heckle.rake +52 -0
  37. data/tasks/metrics.rake +5 -0
  38. data/tasks/rdoc.rake +15 -0
  39. data/tasks/spec.rake +22 -0
  40. data/tasks/yardstick.rake +9 -0
  41. data/yardstick.gemspec +90 -0
  42. metadata +113 -0
@@ -0,0 +1,130 @@
1
+ require 'rational'
2
+
3
+ module Yardstick
4
+ class MeasurementSet < OrderedSet
5
+
6
+ # The total number of measurements
7
+ #
8
+ # @example
9
+ # measurements.total # => 570
10
+ #
11
+ # @return [Integer]
12
+ # total measurements
13
+ #
14
+ # @api public
15
+ def total
16
+ length
17
+ end
18
+
19
+ # The number of successful measurements
20
+ #
21
+ # @example
22
+ # measurements.successful # => 561
23
+ #
24
+ # @return [Integer]
25
+ # successful measurements
26
+ #
27
+ # @api public
28
+ def successful
29
+ select { |measurement| measurement.ok? }.length
30
+ end
31
+
32
+ # The number of failed measurements
33
+ #
34
+ # @example
35
+ # measurements.failed # => 9
36
+ #
37
+ # @return [Integer]
38
+ # failed measurements
39
+ #
40
+ # @api public
41
+ def failed
42
+ total - successful
43
+ end
44
+
45
+ # The percentage of successful measurements
46
+ #
47
+ # @example
48
+ # coverage = measurements.coverage # => Rational(561, 570)
49
+ # '%.1f%%' % (coverage * 100) # => "98.4%"
50
+ #
51
+ # @return [Integer, Rational]
52
+ # the coverage percentage
53
+ #
54
+ # @api public
55
+ def coverage
56
+ empty? ? 0 : Rational(successful, total)
57
+ end
58
+
59
+ # Warn the unsuccessful measurements and a summary
60
+ #
61
+ # @example
62
+ # measurements.puts # (outputs measurements results and summary)
63
+ #
64
+ # @param [#puts] io
65
+ # optional object to puts the summary
66
+ #
67
+ # @return [undefined]
68
+ #
69
+ # @api public
70
+ def puts(io = $stdout)
71
+ each { |measurement| measurement.puts(io) }
72
+ puts_summary(io)
73
+ end
74
+
75
+ private
76
+
77
+ # Warn the summary of all measurements
78
+ #
79
+ # @param [#puts] io
80
+ # object to puts the summary
81
+ #
82
+ # @return [undefined]
83
+ #
84
+ # @api private
85
+ def puts_summary(io)
86
+ io.puts("\n#{[ coverage_text, successful_text, failed_text, total_text ].join(' ')}")
87
+ end
88
+
89
+ # The text for the coverage percentage to include in the summary
90
+ #
91
+ # @return [String]
92
+ # the coverage text
93
+ #
94
+ # @api private
95
+ def coverage_text
96
+ 'Coverage: %.1f%%' % (coverage * 100)
97
+ end
98
+
99
+ # The text for the successful measurements to include in the summary
100
+ #
101
+ # @return [String]
102
+ # the successful text
103
+ #
104
+ # @api private
105
+ def successful_text
106
+ 'Success: %d' % successful
107
+ end
108
+
109
+ # The text for the failed measurements to include in the summary
110
+ #
111
+ # @return [String]
112
+ # the failed text
113
+ #
114
+ # @api private
115
+ def failed_text
116
+ 'Failed: %d' % failed
117
+ end
118
+
119
+ # The text for the total measurements to include in the summary
120
+ #
121
+ # @return [String]
122
+ # the total text
123
+ #
124
+ # @api private
125
+ def total_text
126
+ 'Total: %d' % total
127
+ end
128
+
129
+ end # class MeasurementSet
130
+ end # module Yardstick
@@ -0,0 +1,111 @@
1
+ module Yardstick
2
+ module Method
3
+ include Measurable
4
+
5
+ rule 'The method summary should be specified' do
6
+ skip if has_tag?('see')
7
+ summary_text != ''
8
+ end
9
+
10
+ rule 'The method summary should be less than 80 characters in length' do
11
+ summary_text.split(//).length <= 80
12
+ end
13
+
14
+ rule 'The method summary should not end in a period' do
15
+ summary_text[-1, 1] != '.'
16
+ end
17
+
18
+ rule 'The method summary should be a single line' do
19
+ !summary_text.include?("\n")
20
+ end
21
+
22
+ rule 'The public/semipublic method should have an example specified' do
23
+ skip if api?(%w[ private ]) || tag_types('return') == %w[ undefined ]
24
+ has_tag?('example')
25
+ end
26
+
27
+ rule 'The @api tag should be specified' do
28
+ has_tag?('api')
29
+ end
30
+
31
+ rule 'The @api tag must be either public, semipublic or private' do
32
+ %w[ public semipublic private ].include?(tag_text('api'))
33
+ end
34
+
35
+ rule 'A method with protected visibility must have an @api tag of semipublic or private' do
36
+ skip unless visibility == :protected
37
+ api?(%w[ semipublic private ])
38
+ end
39
+
40
+ rule 'A method with private visibility must have an @api tag of private' do
41
+ skip unless visibility == :private
42
+ api?(%w[ private ])
43
+ end
44
+
45
+ rule 'The @return tag should be specified' do
46
+ has_tag?('return')
47
+ end
48
+
49
+ private
50
+
51
+ # The raw text for the summary
52
+ #
53
+ # @return [String]
54
+ # the summary text
55
+ #
56
+ # @api private
57
+ def summary_text
58
+ split(/\r?\n\r?\n/).first || ''
59
+ end
60
+
61
+ # The text for a specified tag
62
+ #
63
+ # @param [String] tag_name
64
+ # the name of the tag
65
+ #
66
+ # @return [String, nil]
67
+ # the tag text if the tag exists
68
+ #
69
+ # @api private
70
+ def tag_text(tag_name)
71
+ tag(tag_name).text if has_tag?(tag_name)
72
+ end
73
+
74
+ # The types for a specified tag
75
+ #
76
+ # @param [String] tag_name
77
+ # the name of the tag
78
+ #
79
+ # @return [Array<String>, nil]
80
+ # a collection of tag types if the tag exists
81
+ #
82
+ # @api private
83
+ def tag_types(tag_name)
84
+ tag(tag_name).types if has_tag?(tag_name)
85
+ end
86
+
87
+ # The method visibility: public, protected or private
88
+ #
89
+ # @return [Symbol]
90
+ # the visibility of the method
91
+ #
92
+ # @api private
93
+ def visibility
94
+ object.visibility
95
+ end
96
+
97
+ # Check if the method API type matches
98
+ #
99
+ # @param [Array<String>] types
100
+ # a collection of API types
101
+ #
102
+ # @return [Boolean]
103
+ # true if the API type matches
104
+ #
105
+ # @api private
106
+ def api?(types)
107
+ types.include?(tag_text('api'))
108
+ end
109
+
110
+ end # module Method
111
+ end # module Yardstick
@@ -0,0 +1,115 @@
1
+ module Yardstick
2
+ class OrderedSet
3
+ include Enumerable
4
+
5
+ # Returns the OrderedSet instance
6
+ #
7
+ # @param [Array] entries
8
+ # optional entries
9
+ #
10
+ # @return [OrderedSet]
11
+ # the ordered set instance
12
+ #
13
+ # @api private
14
+ def initialize(entries = nil)
15
+ @entries = []
16
+ @index = {}
17
+ merge(entries) if entries
18
+ end
19
+
20
+ # Append to the OrderedSet
21
+ #
22
+ # @param [Object] entry
23
+ # the object to append
24
+ #
25
+ # @return [OrderedSet]
26
+ # returns self
27
+ #
28
+ # @api private
29
+ def <<(entry)
30
+ unless include?(entry)
31
+ @index[entry] = @entries.length
32
+ @entries << entry
33
+ end
34
+ self
35
+ end
36
+
37
+ # Merge in another OrderedSet
38
+ #
39
+ # @param [#each] other
40
+ # the other ordered set
41
+ #
42
+ # @return [OrderedSet]
43
+ # returns self
44
+ #
45
+ # @api private
46
+ def merge(other)
47
+ other.each { |entry| self << entry }
48
+ self
49
+ end
50
+
51
+ # Iterate over each entry
52
+ #
53
+ # @yield [entry]
54
+ # yield to the entry
55
+ #
56
+ # @yieldparam [Object] entry
57
+ # an entry in the ordered set
58
+ #
59
+ # @return [OrderedSet]
60
+ # returns self
61
+ #
62
+ # @api private
63
+ def each(&block)
64
+ @entries.each(&block)
65
+ self
66
+ end
67
+
68
+ # Check if there are any entries
69
+ #
70
+ # @return [Boolean]
71
+ # true if there are no entries, false if there are
72
+ #
73
+ # @api private
74
+ def empty?
75
+ @entries.empty?
76
+ end
77
+
78
+ # The number of entries
79
+ #
80
+ # @return [Integer]
81
+ # number of entries
82
+ #
83
+ # @api private
84
+ def length
85
+ @entries.length
86
+ end
87
+
88
+ # Check if the entry exists in the set
89
+ #
90
+ # @param [Object] entry
91
+ # the entry to test for
92
+ #
93
+ # @return [Boolean]
94
+ # true if the entry exists in the set, false if not
95
+ #
96
+ # @api private
97
+ def include?(entry)
98
+ @index.key?(entry)
99
+ end
100
+
101
+ # Return the index for the entry in the set
102
+ #
103
+ # @param [Object] entry
104
+ # the entry to check the set for
105
+ #
106
+ # @return [Integer, nil]
107
+ # the index for the entry, or nil if it does not exist
108
+ #
109
+ # @api private
110
+ def index(entry)
111
+ @index[entry]
112
+ end
113
+
114
+ end # class OrderedSet
115
+ end # module Yardstick
@@ -0,0 +1,65 @@
1
+ module Yardstick
2
+ class Processor
3
+
4
+ # Measure files provided
5
+ #
6
+ # @param [Array<#to_s>, #to_s] path
7
+ # the files to measure
8
+ #
9
+ # @return [MeasurementSet]
10
+ # a collection of measurements
11
+ #
12
+ # @api private
13
+ def self.process_path(path)
14
+ YARD.parse(Array(path).map { |file| file.to_s })
15
+ measurements
16
+ end
17
+
18
+ # Measure string provided
19
+ #
20
+ # @param [#to_str] string
21
+ # the string to measure
22
+ #
23
+ # @return [MeasurementSet]
24
+ # a collection of measurements
25
+ #
26
+ # @api private
27
+ def self.process_string(string)
28
+ YARD.parse_string(string.to_str)
29
+ measurements
30
+ end
31
+
32
+ # Measure method objects in YARD registry
33
+ #
34
+ # @return [MeasurementSet]
35
+ # a collection of measurements
36
+ #
37
+ # @api private
38
+ def self.measurements
39
+ measurements = MeasurementSet.new
40
+ method_objects.each do |method_object|
41
+ measurements.merge(method_object.docstring.measure)
42
+ end
43
+ measurements
44
+ end
45
+
46
+ # Return method objects in YARD registry
47
+ #
48
+ # @return [Array<YARD::CodeObjects::MethodObject>]
49
+ # a collection of method objects
50
+ #
51
+ # @api private
52
+ def self.method_objects
53
+ YARD::Registry.all(:method).sort_by do |method_object|
54
+ [ method_object.file, method_object.line ]
55
+ end
56
+ ensure
57
+ YARD::Registry.clear
58
+ end
59
+
60
+ class << self
61
+ private :measurements, :method_objects
62
+ end
63
+
64
+ end # class Processor
65
+ end # module Yardstick
@@ -0,0 +1,101 @@
1
+ require 'rake'
2
+ require 'rake/tasklib'
3
+
4
+ module Yardstick
5
+ module Rake
6
+ class Measurement < ::Rake::TaskLib
7
+
8
+ # List of paths to measure
9
+ #
10
+ # @param [Array<#to_s>, #to_s] path
11
+ # optional list of paths to measure
12
+ #
13
+ # @return [undefined]
14
+ #
15
+ # @api public
16
+ attr_writer :path
17
+
18
+ # The path to the file where the measurements will be written
19
+ #
20
+ # @param [String, Pathname] output
21
+ # optional output path for measurements
22
+ #
23
+ # @return [undefined]
24
+ #
25
+ # @api public
26
+ def output=(output)
27
+ @output = Pathname(output)
28
+ end
29
+
30
+ # Initializes a Measurement task
31
+ #
32
+ # @example
33
+ # task = Yardstick::Rake::Measurement
34
+ #
35
+ # @param [Symbol] name
36
+ # optional task name
37
+ #
38
+ # @yield [task]
39
+ # yield to self
40
+ #
41
+ # @yieldparam [Yardstick::Rake::Measurement] task
42
+ # the measurement task
43
+ #
44
+ # @return [Yardstick::Rake::Measurement]
45
+ # the measurement task
46
+ #
47
+ # @api public
48
+ def initialize(name = :yardstick_measure)
49
+ @name = name
50
+ @path = 'lib/**/*.rb'
51
+
52
+ self.output = 'measurements/report.txt'
53
+
54
+ yield self if block_given?
55
+
56
+ define
57
+ end
58
+
59
+ # Measure the documentation
60
+ #
61
+ # @example
62
+ # task.yardstick_measure # (output measurement report)
63
+ #
64
+ # @return [undefined]
65
+ #
66
+ # @api public
67
+ def yardstick_measure
68
+ write_report { |io| Yardstick.measure(@path).puts(io) }
69
+ end
70
+
71
+ private
72
+
73
+ # Define the task
74
+ #
75
+ # @return [undefined]
76
+ #
77
+ # @api private
78
+ def define
79
+ desc "Measure docs in #{@path} with yardstick"
80
+ task(@name) { yardstick_measure }
81
+ end
82
+
83
+ # Open up a report for writing
84
+ #
85
+ # @yield [io]
86
+ # yield to an object that responds to #puts
87
+ #
88
+ # @yieldparam [#puts] io
89
+ # the object that responds to #puts
90
+ #
91
+ # @return [undefined]
92
+ #
93
+ # @api private
94
+ def write_report(&block)
95
+ @output.dirname.mkpath
96
+ @output.open('w', &block)
97
+ end
98
+
99
+ end # class Measurement
100
+ end # module Rake
101
+ end # module Yardstick
@@ -0,0 +1,179 @@
1
+ require 'rake'
2
+ require 'rake/tasklib'
3
+
4
+ module Yardstick
5
+ module Rake
6
+ class Verify < ::Rake::TaskLib
7
+
8
+ # Set the threshold
9
+ #
10
+ # @param [Number] threshold
11
+ # the threshold to set
12
+ #
13
+ # @return [undefined]
14
+ #
15
+ # @api public
16
+ attr_writer :threshold
17
+
18
+ # Specify if the threshold should match the coverage
19
+ #
20
+ # @param [Boolean] require_exact_threshold
21
+ # true if the threshold should match the coverage, false if not
22
+ #
23
+ # @return [undefined]
24
+ #
25
+ # @api public
26
+ attr_writer :require_exact_threshold
27
+
28
+ # List of paths to measure
29
+ #
30
+ # @param [Array<#to_s>, #to_s] path
31
+ # optional list of paths to measure
32
+ #
33
+ # @return [undefined]
34
+ #
35
+ # @api public
36
+ attr_writer :path
37
+
38
+ # Specify if the coverage summary should be displayed
39
+ #
40
+ # @param [Boolean] verbose
41
+ # true if the coverage summary should be displayed, false if not
42
+ #
43
+ # @return [undefined]
44
+ #
45
+ # @api public
46
+ attr_writer :verbose
47
+
48
+ # Initialize a Verify task
49
+ #
50
+ # @example
51
+ # task = Yardstick::Rake::Verify.new do |task|
52
+ # task.threshold = 100
53
+ # end
54
+ #
55
+ # @param [Symbol] name
56
+ # optional task name
57
+ #
58
+ # @yield [task]
59
+ # yield to self
60
+ #
61
+ # @yieldparam [Yardstick::Rake::Verify] task
62
+ # the verification task
63
+ #
64
+ # @return [Yardstick::Rake::Verify] task
65
+ # the verification task instance
66
+ #
67
+ # @api public
68
+ def initialize(name = :verify_measurements)
69
+ @name = name
70
+ @require_exact_threshold = true
71
+ @path = 'lib/**/*.rb'
72
+ @verbose = true
73
+
74
+ yield self
75
+
76
+ assert_threshold
77
+ define
78
+ end
79
+
80
+ # Verify the YARD coverage measurements
81
+ #
82
+ # @example
83
+ # task.verify_measurements # output coverage and threshold
84
+ #
85
+ # @return [undefined]
86
+ #
87
+ # @raise [RuntimeError]
88
+ # raised if threshold is not met
89
+ # @raise [RuntimeError]
90
+ # raised if threshold is not equal to the coverage
91
+ #
92
+ # @api public
93
+ def verify_measurements
94
+ puts "Coverage: #{total_coverage}% (threshold: #{@threshold}%)" if verbose
95
+ assert_meets_threshold
96
+ assert_matches_threshold
97
+ end
98
+
99
+ private
100
+
101
+ # Define the task
102
+ #
103
+ # @return [undefined]
104
+ #
105
+ # @api private
106
+ def define
107
+ desc "Verify that yardstick coverage is at least #{@threshold}%"
108
+ task(@name) { verify_measurements }
109
+ end
110
+
111
+ # The total YARD coverage
112
+ #
113
+ # @return [Float]
114
+ # the total coverage
115
+ #
116
+ # @api private
117
+ def total_coverage
118
+ measurements = Yardstick.measure(@path)
119
+ round_percentage(measurements.coverage * 100)
120
+ end
121
+
122
+ # Round percentage to 1/10th of a percent
123
+ #
124
+ # @param [Float] percentage
125
+ # the percentage to round
126
+ #
127
+ # @return [Float]
128
+ # the rounded percentage
129
+ #
130
+ # @api private
131
+ def round_percentage(percentage)
132
+ (percentage * 10).ceil / 10.0
133
+ end
134
+
135
+ # Raise an exception if threshold is not set
136
+ #
137
+ # @return [undefined]
138
+ #
139
+ # @raise [RuntimeError]
140
+ # raised if threshold is not set
141
+ #
142
+ # @api private
143
+ def assert_threshold
144
+ if @threshold.nil?
145
+ raise 'threshold must be set'
146
+ end
147
+ end
148
+
149
+ # Raise an exception if the threshold is not met
150
+ #
151
+ # @return [undefined]
152
+ #
153
+ # @raise [RuntimeError]
154
+ # raised if threshold is not met
155
+ #
156
+ # @api private
157
+ def assert_meets_threshold
158
+ if total_coverage < @threshold
159
+ raise "Coverage must be at least #{@threshold}% but was #{total_coverage}%"
160
+ end
161
+ end
162
+
163
+ # Raise an exception if the threshold is not equal to the coverage
164
+ #
165
+ # @return [undefined]
166
+ #
167
+ # @raise [RuntimeError]
168
+ # raised if threshold is not equal to the coverage
169
+ #
170
+ # @api private
171
+ def assert_matches_threshold
172
+ if @require_exact_threshold && total_coverage > @threshold
173
+ raise "Coverage has increased above the threshold of #{@threshold}% to #{total_coverage}%. You should update your threshold value."
174
+ end
175
+ end
176
+
177
+ end # class Verify
178
+ end # module Rake
179
+ end # module Yardstick