coverfield 0.0.4 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Gem Version](http://img.shields.io/gem/v/coverfield.svg)](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
|