coverfield 0.0.4 → 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.
- checksums.yaml +4 -4
- data/Gemfile.lock +2 -2
- data/README.md +12 -6
- data/bin/coverfield +23 -13
- data/lib/coverfield/source/class.rb +37 -0
- data/lib/coverfield/source/file.rb +109 -0
- data/lib/coverfield/{file_methods.rb → source/file_methods.rb} +2 -2
- data/lib/coverfield/source/method.rb +14 -0
- data/lib/coverfield/source/nocov_range.rb +11 -0
- data/lib/coverfield/{test_file.rb → source/test_file.rb} +17 -3
- data/lib/coverfield/version.rb +2 -2
- data/lib/coverfield.rb +3 -2
- metadata +8 -6
- data/lib/coverfield/source_class.rb +0 -26
- data/lib/coverfield/source_file.rb +0 -66
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c06502048cd2cfbb46d8383572891c9a6a4ebff8
|
4
|
+
data.tar.gz: 1156c6922c0181fd7267ed4bd3fe9a370127aad5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1be8dfd4b92c3e2b0d6188c3fee72e8f28868994d284e600bad14eaba2a317932e0f186337f44c8290fe8934e3406308a2004d3d489f0a98a897af582dba5fcb
|
7
|
+
data.tar.gz: b33d06bb0e8d0bd9155294110fc245ec8f5ed3c351e438000c2add9d6ac6f4d1c4a84360c5e682cbfa51c38083695032de21206c59ac38508116221079506b30
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,5 +1,9 @@
|
|
1
1
|
# Coverfield
|
2
2
|
|
3
|
+
[](http://badge.fury.io/rb/coverfield)
|
4
|
+
|
5
|
+
**Warning:** Alpha Release, do not rely on or use in production yet!
|
6
|
+
|
3
7
|
One day I found a class in my ruby app with > 95% coverage in
|
4
8
|
[SimpleCov](https://github.com/colszowka/simplecov) but without any dedicated
|
5
9
|
spec. SimpleCov is an awesome tool if you want to get an idea of your test
|
@@ -30,11 +34,11 @@ test suite.
|
|
30
34
|
|
31
35
|
## Future
|
32
36
|
|
33
|
-
This project has still prototype character
|
34
|
-
|
37
|
+
This project has still prototype character (this is an alpha release) and
|
38
|
+
there's plenty to do (specs for example).
|
35
39
|
|
36
|
-
And I
|
37
|
-
him if there is a chance that this
|
40
|
+
And I wrote [Christoph Olszowka](https://github.com/colszowka) to ask
|
41
|
+
him if there is a chance that this will be included to SimpleCov in some
|
38
42
|
way. I would really appreciate that!
|
39
43
|
|
40
44
|
|
@@ -66,8 +70,9 @@ Coverfield requires you to have a specific architecture of your RSpec Suite.
|
|
66
70
|
2. Within `spec` all specs are placed in the same path as the file which is
|
67
71
|
tested by the spec. For example the spec for the file
|
68
72
|
`/lib/some/nice_class.rb` have to be placed in
|
69
|
-
`/spec/lib/some/nice_class_spec.rb
|
70
|
-
`/app/models/post.rb` goes to
|
73
|
+
`/spec/lib/some/nice_class_spec.rb` or `/spec/some/nice_class_spec.rb`.
|
74
|
+
And the spec for the file `/app/models/post.rb` goes to
|
75
|
+
`/spec/app/models/post.rb` or `/spec/models/post.rb`
|
71
76
|
[Why?](http://stackoverflow.com/questions/14180003/rspec-naming-conventions-for-files-and-directory-structure)
|
72
77
|
3. The first `describe` call have to be built like that:
|
73
78
|
`describe Some::NiceClass do` assuming, that `/lib/some/nice_code.rb` defines
|
@@ -76,3 +81,4 @@ Coverfield requires you to have a specific architecture of your RSpec Suite.
|
|
76
81
|
4. All inner `describe` calls for the methods have to be built like that:
|
77
82
|
`describe '#method_name' do`. The `#` is optional and may also be a `.`.
|
78
83
|
[Why?](http://betterspecs.org/#describe)
|
84
|
+
5. All dependencies of your app have to be installed (`bundle install`).
|
data/bin/coverfield
CHANGED
@@ -9,9 +9,9 @@ require 'colorize'
|
|
9
9
|
APP_ROOT = Bundler.root.to_s
|
10
10
|
|
11
11
|
require 'coverfield'
|
12
|
-
require 'coverfield/
|
13
|
-
require 'coverfield/
|
14
|
-
require 'coverfield/test_file'
|
12
|
+
require 'coverfield/source/class'
|
13
|
+
require 'coverfield/source/file'
|
14
|
+
require 'coverfield/source/test_file'
|
15
15
|
|
16
16
|
|
17
17
|
# Ensure there are paths to search for
|
@@ -25,31 +25,41 @@ target_files = target_finder.find(paths)
|
|
25
25
|
|
26
26
|
|
27
27
|
# Map all found files to SourceFiles
|
28
|
-
source_files = target_files.map { |file| Coverfield::
|
28
|
+
source_files = target_files.map { |file| Coverfield::Source::File.new(file) }
|
29
29
|
|
30
|
-
overall_covered = 0
|
31
|
-
overall_methods = 0
|
32
30
|
|
33
|
-
#
|
31
|
+
# Initialize counter variables
|
32
|
+
total_covered = 0
|
33
|
+
total_methods = 0
|
34
|
+
total_relevant_methods = 0
|
35
|
+
|
36
|
+
# Iterate over all found files and their classes for fancy output
|
34
37
|
source_files.each do |file|
|
35
38
|
file.classes.each do |cls|
|
36
|
-
class_name =
|
37
|
-
coverage = "#{file.coverage}/#{cls.
|
38
|
-
covered = file.coverage == cls.
|
39
|
+
class_name = cls.full_qualified_name.to_s.light_blue
|
40
|
+
coverage = "#{file.coverage}/#{cls.relevant_method_count}/#{cls.method_count}"
|
41
|
+
covered = file.coverage == cls.relevant_method_count
|
39
42
|
puts "#{covered ? '[X]'.green : '[ ]'.red} Found class: #{class_name} with #{covered ? coverage.green : coverage.red} covered methods in #{file.relative_file_name.light_blue}".bold
|
40
43
|
|
41
44
|
file.hints.each do |hint|
|
42
45
|
puts " - #{hint}"
|
43
46
|
end
|
44
47
|
|
45
|
-
|
46
|
-
|
48
|
+
total_methods += cls.method_count
|
49
|
+
total_relevant_methods += cls.relevant_method_count
|
50
|
+
total_covered += file.coverage
|
47
51
|
|
48
52
|
puts
|
49
53
|
end
|
50
54
|
end
|
51
55
|
|
56
|
+
relevant_percent = (total_relevant_methods * 100 / total_methods).round.to_s + '%'
|
57
|
+
covered_percent = (total_covered * 100 / total_methods).round.to_s + '%'
|
58
|
+
uncovered_percent = ((total_relevant_methods - total_covered) * 100 / total_methods).round.to_s + '%'
|
52
59
|
|
53
60
|
puts
|
54
|
-
puts "
|
61
|
+
puts "There are #{total_methods.to_s.yellow} methods in total."
|
62
|
+
puts "#{total_relevant_methods.to_s.yellow} (#{relevant_percent.yellow}) of them are relevant for coverage."
|
63
|
+
puts "And #{total_covered.to_s.yellow} (#{covered_percent.yellow}) methods are covered by tests."
|
64
|
+
puts "Thus there are #{(total_relevant_methods - total_covered).to_s.yellow} (#{uncovered_percent.yellow}) uncovered methods."
|
55
65
|
puts
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'coverfield/source/method'
|
2
|
+
|
3
|
+
class Coverfield::Source::Class
|
4
|
+
attr_reader :name, :module_name, :node, :methods, :source_file
|
5
|
+
|
6
|
+
# Constructor
|
7
|
+
public def initialize(class_name, module_name, node, source_file)
|
8
|
+
@name = class_name
|
9
|
+
@module_name = module_name
|
10
|
+
@node = node
|
11
|
+
@methods = []
|
12
|
+
@source_file = source_file
|
13
|
+
find_methods
|
14
|
+
end
|
15
|
+
|
16
|
+
public def full_qualified_name
|
17
|
+
name = @name
|
18
|
+
name = "#{@module_name}::#{name}" unless @module_name.empty?
|
19
|
+
name
|
20
|
+
end
|
21
|
+
|
22
|
+
public def relevant_method_count
|
23
|
+
relevant_methods = @methods.select { |m| !m.nocov?}
|
24
|
+
relevant_methods.size
|
25
|
+
end
|
26
|
+
|
27
|
+
public def method_count
|
28
|
+
@methods.size
|
29
|
+
end
|
30
|
+
|
31
|
+
# Finds all methods
|
32
|
+
private def find_methods
|
33
|
+
node.each_node(:def) do |node|
|
34
|
+
@methods << Coverfield::Source::Method.new(*node, self)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'coverfield/source/file_methods'
|
2
|
+
require 'coverfield/source/test_file'
|
3
|
+
require 'coverfield/source/class'
|
4
|
+
require 'coverfield/source/nocov_range'
|
5
|
+
|
6
|
+
class Coverfield::Source::File
|
7
|
+
include Coverfield::Source::FileMethods
|
8
|
+
|
9
|
+
attr_reader :classes, :test_file, :coverage, :hints
|
10
|
+
|
11
|
+
# Constructor
|
12
|
+
public def initialize(file_name)
|
13
|
+
@file_name = file_name
|
14
|
+
@classes = []
|
15
|
+
@coverage = 0
|
16
|
+
@hints = []
|
17
|
+
@nocov_ranges = []
|
18
|
+
|
19
|
+
unless File.zero?(file_name)
|
20
|
+
parse_code
|
21
|
+
find_nocov_ranges
|
22
|
+
find_classes
|
23
|
+
find_test_file
|
24
|
+
calculate_coverage
|
25
|
+
end
|
26
|
+
rescue Exception => e
|
27
|
+
raise RuntimeError, "Error while processing file #{file_name}: #{e.message}", e.backtrace
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
# Calculates the number of covered methods of this file and sets @coverage and @hints
|
32
|
+
private def calculate_coverage
|
33
|
+
@coverage = 0
|
34
|
+
|
35
|
+
classes.each do |cls|
|
36
|
+
cls.methods.each do |method|
|
37
|
+
if test_file.cover?(cls.full_qualified_name, method.name)
|
38
|
+
@coverage += 1
|
39
|
+
else
|
40
|
+
method_name = "#{cls.name}.#{method.name}".red
|
41
|
+
|
42
|
+
if method.nocov?
|
43
|
+
@coverage += 1
|
44
|
+
else
|
45
|
+
@hints << "Missing test for #{method_name} in #{test_file.relative_file_name.yellow}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
public def nocov?(method_body_node)
|
54
|
+
@nocov_ranges.each do |nocov_range|
|
55
|
+
return true if nocov_range.includes?(method_body_node)
|
56
|
+
end
|
57
|
+
|
58
|
+
false
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
# Find class definitions
|
63
|
+
private def find_classes
|
64
|
+
@processed_source.ast.each_node(:class) do |node|
|
65
|
+
name, superclass, body = *node
|
66
|
+
_scope, const_name, value = *name
|
67
|
+
module_name = node.parent_module_name
|
68
|
+
|
69
|
+
if module_name == 'Object'
|
70
|
+
nothing, scope_name, nothing = *_scope
|
71
|
+
module_name = scope_name.to_s
|
72
|
+
end
|
73
|
+
|
74
|
+
@classes << Coverfield::Source::Class.new(const_name, module_name, node, self)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
# Find the spec file for that class
|
80
|
+
private def find_test_file
|
81
|
+
spec_path = APP_ROOT + '/spec'
|
82
|
+
relative_file_name = @file_name.to_s.gsub(APP_ROOT, '')
|
83
|
+
@test_file = Coverfield::Source::TestFile.new(spec_path + relative_file_name.gsub('.rb', '_spec.rb'))
|
84
|
+
|
85
|
+
# When no file was found also try without '/lib' or '/app'
|
86
|
+
unless @test_file.file_exists?
|
87
|
+
relative_file_name.gsub!(/^\/(lib|app)/, '')
|
88
|
+
@test_file = Coverfield::Source::TestFile.new(spec_path + relative_file_name)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
|
93
|
+
private def find_nocov_ranges
|
94
|
+
first = true
|
95
|
+
line = 0
|
96
|
+
|
97
|
+
@processed_source.comments.each do |comment|
|
98
|
+
if comment.type == :inline && comment.text.strip =~ /^#\s*\:nocov\:/
|
99
|
+
if first
|
100
|
+
line = comment.loc.expression.first_line
|
101
|
+
else
|
102
|
+
@nocov_ranges << Coverfield::Source::NocovRange.new(line, comment.loc.expression.first_line)
|
103
|
+
end
|
104
|
+
|
105
|
+
first = !first
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
module Coverfield::FileMethods
|
1
|
+
module Coverfield::Source::FileMethods
|
2
2
|
attr_reader :file_name
|
3
3
|
|
4
4
|
# Parse the source code
|
@@ -11,4 +11,4 @@ module Coverfield::FileMethods
|
|
11
11
|
public def relative_file_name
|
12
12
|
@file_name.gsub(APP_ROOT + '/', '')
|
13
13
|
end
|
14
|
-
end
|
14
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class Coverfield::Source::Method
|
2
|
+
attr_reader :name, :args, :body, :source_class
|
3
|
+
|
4
|
+
public def initialize(method_name, args, body, source_class)
|
5
|
+
@name = method_name
|
6
|
+
@args = args
|
7
|
+
@body = body
|
8
|
+
@source_class = source_class
|
9
|
+
end
|
10
|
+
|
11
|
+
public def nocov?
|
12
|
+
@source_class.source_file.nocov? @body
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class Coverfield::Source::NocovRange
|
2
|
+
public def initialize(first_line, last_line)
|
3
|
+
@first_line = first_line
|
4
|
+
@last_line = last_line
|
5
|
+
end
|
6
|
+
|
7
|
+
public def includes?(node)
|
8
|
+
source_range = node.source_range
|
9
|
+
source_range.first_line > @first_line && source_range.last_line < @last_line
|
10
|
+
end
|
11
|
+
end
|
@@ -1,5 +1,7 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
require 'coverfield/source/file_methods'
|
2
|
+
|
3
|
+
class Coverfield::Source::TestFile
|
4
|
+
include Coverfield::Source::FileMethods
|
3
5
|
|
4
6
|
# Constructor
|
5
7
|
public def initialize(file_name)
|
@@ -37,6 +39,7 @@ class Coverfield::TestFile
|
|
37
39
|
|
38
40
|
# Small helper method which builts the full qualified class name out of a describe arguments node
|
39
41
|
private def get_spec_class_name(describe_args_node)
|
42
|
+
return describe_args_node if describe_args_node.is_a?(String)
|
40
43
|
subject_ary = []
|
41
44
|
|
42
45
|
describe_args_node.each_node(:const) do |const_part|
|
@@ -52,6 +55,7 @@ class Coverfield::TestFile
|
|
52
55
|
private def find_describes
|
53
56
|
# Contains the current test subject where alls test methods should be associated with
|
54
57
|
current_subject = nil
|
58
|
+
first_describe = true
|
55
59
|
|
56
60
|
# Iterate over all send nodes (method calls)
|
57
61
|
@processed_source.ast.each_node(:send) do |node|
|
@@ -60,15 +64,25 @@ class Coverfield::TestFile
|
|
60
64
|
|
61
65
|
# We only care if it's a describe() call
|
62
66
|
if method_name == :describe
|
63
|
-
if args.const_type?
|
67
|
+
if first_describe || args.const_type?
|
64
68
|
# If it's a const, it's the first describe, which describes the class/module to test
|
65
69
|
current_subject = get_spec_class_name(args)
|
66
70
|
@describes[current_subject] = []
|
67
71
|
else
|
68
72
|
# Otherwise, get the String argument, it will contain something like '#method_name'
|
69
73
|
value, nothing = *args.each_node(:str).first
|
74
|
+
|
75
|
+
if value == nil
|
76
|
+
# That happens if the argument is a symbol
|
77
|
+
value, nothing = *args.each_node(:sym).first
|
78
|
+
value = value.to_s
|
79
|
+
end
|
80
|
+
|
70
81
|
@describes[current_subject] << value.strip.gsub(/^(?:\.|#)(.+)$/i, '\1')
|
82
|
+
|
71
83
|
end
|
84
|
+
|
85
|
+
first_describe = false
|
72
86
|
end
|
73
87
|
end
|
74
88
|
end
|
data/lib/coverfield/version.rb
CHANGED
data/lib/coverfield.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: coverfield
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Benjamin Klein
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-06-
|
11
|
+
date: 2016-06-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rubocop
|
@@ -69,10 +69,12 @@ files:
|
|
69
69
|
- bin/coverfield
|
70
70
|
- coverfield.gemspec
|
71
71
|
- lib/coverfield.rb
|
72
|
-
- lib/coverfield/
|
73
|
-
- lib/coverfield/
|
74
|
-
- lib/coverfield/
|
75
|
-
- lib/coverfield/
|
72
|
+
- lib/coverfield/source/class.rb
|
73
|
+
- lib/coverfield/source/file.rb
|
74
|
+
- lib/coverfield/source/file_methods.rb
|
75
|
+
- lib/coverfield/source/method.rb
|
76
|
+
- lib/coverfield/source/nocov_range.rb
|
77
|
+
- lib/coverfield/source/test_file.rb
|
76
78
|
- lib/coverfield/version.rb
|
77
79
|
homepage: http://github.com/phortx/coverfield
|
78
80
|
licenses:
|
@@ -1,26 +0,0 @@
|
|
1
|
-
class Coverfield::SourceClass
|
2
|
-
attr_reader :name, :module_name, :node, :methods
|
3
|
-
|
4
|
-
# Constructor
|
5
|
-
public def initialize(class_name, module_name, node)
|
6
|
-
@name = class_name
|
7
|
-
@module_name = module_name
|
8
|
-
@node = node
|
9
|
-
@methods = []
|
10
|
-
find_methods
|
11
|
-
end
|
12
|
-
|
13
|
-
public def full_qualified_name
|
14
|
-
name = @name
|
15
|
-
name = "#{@module_name}::#{name}" unless @module_name.empty?
|
16
|
-
name
|
17
|
-
end
|
18
|
-
|
19
|
-
# Finds all methods
|
20
|
-
private def find_methods
|
21
|
-
node.each_node(:def) do |node|
|
22
|
-
method_name, args, body = *node
|
23
|
-
@methods << method_name
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
@@ -1,66 +0,0 @@
|
|
1
|
-
require 'coverfield/file_methods'
|
2
|
-
|
3
|
-
class Coverfield::SourceFile
|
4
|
-
include Coverfield::FileMethods
|
5
|
-
|
6
|
-
attr_reader :classes, :test_file, :coverage, :hints
|
7
|
-
|
8
|
-
# Constructor
|
9
|
-
public def initialize(file_name)
|
10
|
-
@file_name = file_name
|
11
|
-
@classes = []
|
12
|
-
@coverage = 0
|
13
|
-
@hints = []
|
14
|
-
|
15
|
-
unless File.zero?(file_name)
|
16
|
-
parse_code
|
17
|
-
find_classes
|
18
|
-
find_test_file
|
19
|
-
calculate_coverage
|
20
|
-
end
|
21
|
-
rescue Exception => e
|
22
|
-
raise RuntimeError, "Error while processing file #{file_name}: #{e.message}", e.backtrace
|
23
|
-
end
|
24
|
-
|
25
|
-
|
26
|
-
# Calculates the number of covered methods of this file and sets @coverage and @hints
|
27
|
-
public def calculate_coverage
|
28
|
-
@coverage = 0
|
29
|
-
|
30
|
-
classes.each do |cls|
|
31
|
-
cls.methods.each do |method_name|
|
32
|
-
if test_file.cover?(cls.full_qualified_name, method_name)
|
33
|
-
@coverage += 1
|
34
|
-
else
|
35
|
-
method_name = "#{cls.name}.#{method_name}".red
|
36
|
-
@hints << "Missing test for #{method_name} in #{test_file.relative_file_name.yellow}"
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
|
43
|
-
# Find class definitions
|
44
|
-
private def find_classes
|
45
|
-
@processed_source.ast.each_node(:class) do |node|
|
46
|
-
name, superclass, body = *node
|
47
|
-
_scope, const_name, value = *name
|
48
|
-
module_name = node.parent_module_name
|
49
|
-
|
50
|
-
if module_name == 'Object'
|
51
|
-
nothing, scope_name, nothing = *_scope
|
52
|
-
module_name = scope_name.to_s
|
53
|
-
end
|
54
|
-
|
55
|
-
@classes << Coverfield::SourceClass.new(const_name, module_name, node)
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
|
60
|
-
# Find the spec file for that class
|
61
|
-
private def find_test_file
|
62
|
-
relative_file_name = @file_name.to_s.gsub(APP_ROOT, '')
|
63
|
-
relative_file_name.gsub!('/app', '')
|
64
|
-
@test_file = Coverfield::TestFile.new(APP_ROOT + '/spec' + relative_file_name.gsub('.rb', '_spec.rb'))
|
65
|
-
end
|
66
|
-
end
|