coverfield 0.1.0 → 0.3.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 +16 -1
- data/README.md +17 -15
- data/bin/coverfield +11 -54
- data/coverfield.gemspec +5 -3
- data/lib/coverfield.rb +37 -0
- data/lib/coverfield/cli.rb +73 -0
- data/lib/coverfield/config.rb +42 -0
- data/lib/coverfield/report.rb +80 -0
- data/lib/coverfield/source/class.rb +37 -2
- data/lib/coverfield/source/file.rb +27 -32
- data/lib/coverfield/source/file_methods.rb +2 -1
- data/lib/coverfield/source/method.rb +4 -0
- data/lib/coverfield/source/nocov_range.rb +3 -0
- data/lib/coverfield/source/test_file.rb +15 -6
- data/lib/coverfield/version.rb +1 -1
- data/spec/lib/coverfield/report_spec.rb +64 -0
- data/spec/spec_helper.rb +11 -0
- metadata +24 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 555473b54c96dd9e9ae8104e1489048f1779c6f7
|
4
|
+
data.tar.gz: 76961b00f2fee8690d255cafc26f3cab2218ef85
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a0bb29b2a0704b40627ace26e352ce8ca4107c2ec18a090686baa5f23beed1bb98b20cedcf2bfff5e43db6e16e95fe81547c531a9011c9b999b5d2a200449dfc
|
7
|
+
data.tar.gz: 9b5806d01aea4cb702640e7378eae01a51ef9001f4441b2e6ff63f2c5b3bc38fb1f2b8ecbad0e9588fce329f7c3b5b4e81a28ec1fbc626621755c066b41cfccd
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
coverfield (0.
|
4
|
+
coverfield (0.3.0)
|
5
5
|
colorize (~> 0.7)
|
6
6
|
rubocop (~> 0.40)
|
7
7
|
|
@@ -10,10 +10,24 @@ GEM
|
|
10
10
|
specs:
|
11
11
|
ast (2.3.0)
|
12
12
|
colorize (0.7.7)
|
13
|
+
diff-lcs (1.2.5)
|
13
14
|
parser (2.3.1.2)
|
14
15
|
ast (~> 2.2)
|
15
16
|
powerpack (0.1.1)
|
16
17
|
rainbow (2.1.0)
|
18
|
+
rspec (3.4.0)
|
19
|
+
rspec-core (~> 3.4.0)
|
20
|
+
rspec-expectations (~> 3.4.0)
|
21
|
+
rspec-mocks (~> 3.4.0)
|
22
|
+
rspec-core (3.4.4)
|
23
|
+
rspec-support (~> 3.4.0)
|
24
|
+
rspec-expectations (3.4.0)
|
25
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
26
|
+
rspec-support (~> 3.4.0)
|
27
|
+
rspec-mocks (3.4.1)
|
28
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
29
|
+
rspec-support (~> 3.4.0)
|
30
|
+
rspec-support (3.4.1)
|
17
31
|
rubocop (0.40.0)
|
18
32
|
parser (>= 2.3.1.0, < 3.0)
|
19
33
|
powerpack (~> 0.1)
|
@@ -29,6 +43,7 @@ PLATFORMS
|
|
29
43
|
DEPENDENCIES
|
30
44
|
bundler (~> 1.12)
|
31
45
|
coverfield!
|
46
|
+
rspec (~> 3.4)
|
32
47
|
|
33
48
|
BUNDLED WITH
|
34
49
|
1.12.5
|
data/README.md
CHANGED
@@ -2,24 +2,24 @@
|
|
2
2
|
|
3
3
|
[![Gem Version](http://img.shields.io/gem/v/coverfield.svg)](http://badge.fury.io/rb/coverfield)
|
4
4
|
|
5
|
-
**Warning:**
|
5
|
+
**Warning:** This is a Beta Release!
|
6
6
|
|
7
7
|
One day I found a class in my ruby app with > 95% coverage in
|
8
8
|
[SimpleCov](https://github.com/colszowka/simplecov) but without any dedicated
|
9
9
|
spec. SimpleCov is an awesome tool if you want to get an idea of your test
|
10
|
-
coverage, but it has a flaw: It doesn't look whether
|
10
|
+
coverage, but it has a flaw: It doesn't look whether there's a spec for a
|
11
11
|
class/module/method or not, it just reports if a line of code was executed while
|
12
|
-
the spec suite run
|
12
|
+
the spec suite run through. In my case a class which was used relative often so
|
13
13
|
gathered nearly 100% coverage, but I've never wrote any line of testing code
|
14
14
|
specific vor that class.
|
15
15
|
|
16
16
|
To get an overview which classes, modules and methods my test suite covers (and
|
17
17
|
more important: which not), I've wrote a small script, which scans all
|
18
18
|
production code, finds the classes and methods and checks if there are dedicated
|
19
|
-
tests for that methods
|
19
|
+
tests for that methods within the specs. It compiles a report which clearly
|
20
20
|
tells me, what specs are missing.
|
21
21
|
|
22
|
-
This script is not a substitution for SimpleCov! SimpleCov is a wonderful
|
22
|
+
This script is not a substitution for SimpleCov! SimpleCov is a wonderful piece
|
23
23
|
of software and it's coverage report is very important. Think of Coverfield as a
|
24
24
|
additional tool for examining the quality of your test suite under another
|
25
25
|
aspect and from a different view.
|
@@ -30,12 +30,13 @@ covered by specs and which not. SimpleCov then tells you how well the body of
|
|
30
30
|
those methods is tested and if there are edge cases which are not touched by the
|
31
31
|
test suite.
|
32
32
|
|
33
|
+
[Click here for a screenshot of the coverfield generated report](https://twitter.com/phortx/status/745256593625911296).
|
34
|
+
|
33
35
|
|
34
36
|
|
35
37
|
## Future
|
36
38
|
|
37
|
-
This
|
38
|
-
there's plenty to do (specs for example).
|
39
|
+
This gem is still in beta phase and there's plenty to do (specs for example).
|
39
40
|
|
40
41
|
And I wrote [Christoph Olszowka](https://github.com/colszowka) to ask
|
41
42
|
him if there is a chance that this will be included to SimpleCov in some
|
@@ -47,7 +48,7 @@ way. I would really appreciate that!
|
|
47
48
|
Big thanks and Kudos to the folks of
|
48
49
|
[RuboCop](https://github.com/bbatsov/rubocop)! Coverfield is based on the AST
|
49
50
|
Ruby Parser, which is part of the RuboCop gem. Without that awesome peace of
|
50
|
-
software,
|
51
|
+
software, coverfield would have been much more code and much more complicated.
|
51
52
|
Thank you very much, nice work!
|
52
53
|
|
53
54
|
Also thanks to [Christoph Olszowka](https://github.com/colszowka) for his
|
@@ -59,26 +60,27 @@ will be some kind of integration in the future.
|
|
59
60
|
|
60
61
|
Install via `gem install coverfield`.
|
61
62
|
|
62
|
-
Then just call `coverfield lib/ app/` in your apps root dir.
|
63
|
+
Then just call `coverfield -u lib/ app/` in your apps root dir.
|
64
|
+
|
65
|
+
For more info take a look at the usage information of `coverfield -h`.
|
63
66
|
|
64
67
|
|
65
68
|
### Considerations
|
66
69
|
|
67
70
|
Coverfield requires you to have a specific architecture of your RSpec Suite.
|
68
71
|
|
69
|
-
1.
|
70
|
-
|
71
|
-
tested by the spec. For example the spec for the file
|
72
|
+
1. Within the spec directory all specs are placed in the same path as the file
|
73
|
+
which is tested by the spec. For example the spec for the file
|
72
74
|
`/lib/some/nice_class.rb` have to be placed in
|
73
75
|
`/spec/lib/some/nice_class_spec.rb` or `/spec/some/nice_class_spec.rb`.
|
74
76
|
And the spec for the file `/app/models/post.rb` goes to
|
75
77
|
`/spec/app/models/post.rb` or `/spec/models/post.rb`
|
76
78
|
[Why?](http://stackoverflow.com/questions/14180003/rspec-naming-conventions-for-files-and-directory-structure)
|
77
|
-
|
79
|
+
2. The first `describe` call have to be built like that:
|
78
80
|
`describe Some::NiceClass do` assuming, that `/lib/some/nice_code.rb` defines
|
79
81
|
the class `Some::NiceClass`.
|
80
82
|
[Why?](http://rspec.info/documentation/3.4/rspec-core/#Basic_Structure)
|
81
|
-
|
83
|
+
3. All inner `describe` calls for the methods have to be built like that:
|
82
84
|
`describe '#method_name' do`. The `#` is optional and may also be a `.`.
|
83
85
|
[Why?](http://betterspecs.org/#describe)
|
84
|
-
|
86
|
+
4. All dependencies of your app have to be installed (`bundle install`).
|
data/bin/coverfield
CHANGED
@@ -1,65 +1,22 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
# Run that script in your apps root.
|
3
|
+
# Run that script in your apps root.
|
4
4
|
|
5
|
-
require 'bundler'
|
6
|
-
require 'rubocop'
|
7
5
|
require 'colorize'
|
8
|
-
|
9
|
-
APP_ROOT = Bundler.root.to_s
|
10
|
-
|
11
6
|
require 'coverfield'
|
12
|
-
require 'coverfield/
|
13
|
-
require 'coverfield/
|
14
|
-
require 'coverfield/source/test_file'
|
15
|
-
|
16
|
-
|
17
|
-
# Ensure there are paths to search for
|
18
|
-
paths = ARGV
|
19
|
-
paths = [APP_ROOT + '/lib'] if paths.empty?
|
20
|
-
|
21
|
-
|
22
|
-
# Find files
|
23
|
-
target_finder = RuboCop::TargetFinder.new(RuboCop::ConfigStore.new)
|
24
|
-
target_files = target_finder.find(paths)
|
25
|
-
|
26
|
-
|
27
|
-
# Map all found files to SourceFiles
|
28
|
-
source_files = target_files.map { |file| Coverfield::Source::File.new(file) }
|
29
|
-
|
30
|
-
|
31
|
-
# Initialize counter variables
|
32
|
-
total_covered = 0
|
33
|
-
total_methods = 0
|
34
|
-
total_relevant_methods = 0
|
7
|
+
require 'coverfield/cli'
|
8
|
+
require 'coverfield/report'
|
35
9
|
|
36
|
-
# Iterate over all found files and their classes for fancy output
|
37
|
-
source_files.each do |file|
|
38
|
-
file.classes.each do |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
|
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
|
43
10
|
|
44
|
-
|
45
|
-
|
46
|
-
|
11
|
+
# Load the Command Line Interface to parse the options and arguments.
|
12
|
+
# That also initializes the config, which will be available via cli.config
|
13
|
+
cli = Coverfield::CLI.new
|
47
14
|
|
48
|
-
total_methods += cls.method_count
|
49
|
-
total_relevant_methods += cls.relevant_method_count
|
50
|
-
total_covered += file.coverage
|
51
15
|
|
52
|
-
|
53
|
-
|
54
|
-
end
|
16
|
+
# Find all source files based on the include_paths in the config
|
17
|
+
source_files = Coverfield.find_source_files(cli.config)
|
55
18
|
|
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 + '%'
|
59
19
|
|
60
|
-
|
61
|
-
|
62
|
-
puts
|
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."
|
65
|
-
puts
|
20
|
+
# Create and print the report
|
21
|
+
report = Coverfield::Report.new(cli.config, source_files)
|
22
|
+
puts report.full_report
|
data/coverfield.gemspec
CHANGED
@@ -12,16 +12,18 @@ Gem::Specification.new do |gem|
|
|
12
12
|
gem.description = 'Smarter Ruby/RSpec coverage reports: Tells you which classes, modules and methods don\'t have any specs'
|
13
13
|
gem.summary = 'Smarter Ruby/RSpec coverage reports'
|
14
14
|
gem.license = 'GPL-3.0'
|
15
|
-
|
16
15
|
gem.required_ruby_version = '>= 2.3.0'
|
17
16
|
|
18
|
-
|
17
|
+
|
18
|
+
gem.add_dependency 'rubocop', '~> 0.40'
|
19
19
|
gem.add_dependency 'colorize', '~> 0.7'
|
20
20
|
|
21
21
|
gem.add_development_dependency 'bundler', '~> 1.12'
|
22
|
+
gem.add_development_dependency 'rspec', '~> 3.4'
|
23
|
+
|
22
24
|
|
23
25
|
gem.files = `git ls-files`.split("\n")
|
24
26
|
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
25
27
|
gem.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
|
26
28
|
gem.require_paths = ['lib']
|
27
|
-
end
|
29
|
+
end
|
data/lib/coverfield.rb
CHANGED
@@ -1,4 +1,41 @@
|
|
1
1
|
module Coverfield
|
2
2
|
module Source
|
3
3
|
end
|
4
|
+
|
5
|
+
class << self
|
6
|
+
# Finds all ruby source files in the given paths
|
7
|
+
# Returns an array of Coverfield::Source::File instances
|
8
|
+
public def find_source_files(config)
|
9
|
+
require 'rubocop'
|
10
|
+
require 'coverfield/source/file'
|
11
|
+
|
12
|
+
# Find files
|
13
|
+
target_finder = RuboCop::TargetFinder.new(RuboCop::ConfigStore.new)
|
14
|
+
target_files = target_finder.find(config.include_paths)
|
15
|
+
|
16
|
+
|
17
|
+
# Map all found files to SourceFiles
|
18
|
+
target_files = target_files.map { |file| Coverfield::Source::File.new(config, file) }
|
19
|
+
|
20
|
+
# Debug output
|
21
|
+
dump_file_list(target_files) if config.debug
|
22
|
+
|
23
|
+
# Return the file list
|
24
|
+
target_files
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
# Dumps the list of found files
|
29
|
+
private def dump_file_list(file_list)
|
30
|
+
puts "Found #{file_list.size} source files:".blue
|
31
|
+
|
32
|
+
file_list.each do |file|
|
33
|
+
test_file_word = (file.test_file.file_exists? ? 'a'.green : 'no'.red)
|
34
|
+
puts " - #{file.relative_file_name} with #{test_file_word} test file"
|
35
|
+
end
|
36
|
+
|
37
|
+
puts
|
38
|
+
puts
|
39
|
+
end
|
40
|
+
end
|
4
41
|
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'coverfield/config'
|
3
|
+
|
4
|
+
# Command Line Interface: Parses the options and constructs a
|
5
|
+
# Coverfield::Config, which is available via the `config` method
|
6
|
+
class Coverfield::CLI
|
7
|
+
attr_reader :config
|
8
|
+
|
9
|
+
# Constructor
|
10
|
+
public def initialize
|
11
|
+
# Create the config store
|
12
|
+
@config = Coverfield::Config.new
|
13
|
+
|
14
|
+
# Parse the CLI options and write them to the config store
|
15
|
+
parse_cli_options
|
16
|
+
|
17
|
+
# Determine all source pathes
|
18
|
+
@config.include_paths = include_paths
|
19
|
+
|
20
|
+
# Debug output
|
21
|
+
@config.dump_config if @config.debug
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
# Parses the CLI options and writes them to the config store
|
26
|
+
private def parse_cli_options
|
27
|
+
opt_parser = OptionParser.new do |opt|
|
28
|
+
opt.banner = 'Usage: coverfield [OPTION]... [FILE]...'
|
29
|
+
opt.separator ''
|
30
|
+
opt.separator 'Options:'
|
31
|
+
|
32
|
+
# --uncovered-only
|
33
|
+
opt.on('-u', '--uncovered-only', "Don't print classes with 100% coverage") do
|
34
|
+
@config.uncovered_only = true
|
35
|
+
end
|
36
|
+
|
37
|
+
# --skip-summary
|
38
|
+
opt.on('-s', '--skip-summary', "Don't print the coverage summary") do
|
39
|
+
@config.skip_summary = true
|
40
|
+
end
|
41
|
+
|
42
|
+
# --spec-dir
|
43
|
+
opt.on('-d', '--spec-dir=DIR', "Sets the directory which contains the specs. Default is 'spec/'") do |dir|
|
44
|
+
@config.spec_dir = dir
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
# --debug
|
49
|
+
opt.on('-D', '--debug', 'Enables debug output') do
|
50
|
+
@config.debug = true
|
51
|
+
end
|
52
|
+
|
53
|
+
# --help
|
54
|
+
opt.on('-h', '--help', 'Prints usage informations') do
|
55
|
+
puts opt
|
56
|
+
exit
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
opt_parser.parse!
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
# Determines all pathes where ruby source files should be searched in
|
65
|
+
private def include_paths
|
66
|
+
# Ensure there are paths to search for
|
67
|
+
paths = ARGV
|
68
|
+
paths = [config.app_root + '/lib'] if paths.empty?
|
69
|
+
|
70
|
+
# Filter everything, that doesn't exist
|
71
|
+
paths.select { |path| File.exists?(path) }
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# A simple config store
|
2
|
+
class Coverfield::Config
|
3
|
+
attr_accessor :uncovered_only,
|
4
|
+
:skip_summary,
|
5
|
+
:include_paths,
|
6
|
+
:app_root,
|
7
|
+
:spec_dir,
|
8
|
+
:debug
|
9
|
+
|
10
|
+
# Constructor
|
11
|
+
public def initialize
|
12
|
+
@uncovered_only = false
|
13
|
+
@skip_summary = false
|
14
|
+
@debug = false
|
15
|
+
@include_paths = []
|
16
|
+
@spec_dir = 'spec/'
|
17
|
+
|
18
|
+
# Bundler already contains a good logic to determine the apps root
|
19
|
+
require 'bundler'
|
20
|
+
@app_root = Bundler.root.to_s
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
# Returns the full absolute path to the spec dir
|
25
|
+
public def spec_path
|
26
|
+
@app_root + '/' + @spec_dir
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
# Prints all options
|
31
|
+
public def dump_config
|
32
|
+
puts 'Options:'.blue
|
33
|
+
puts " Uncovered only: #{@uncovered_only}"
|
34
|
+
puts " Skip summary: #{@skip_summary}"
|
35
|
+
puts " Debug mode: #{@debug}"
|
36
|
+
puts " Include paths: #{@include_paths}"
|
37
|
+
puts " App root: #{@app_root}"
|
38
|
+
puts " Spec directory: #{@spec_dir} (= #{spec_path})"
|
39
|
+
puts
|
40
|
+
puts
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
class Coverfield::Report
|
2
|
+
public def initialize(config, source_files)
|
3
|
+
@config = config
|
4
|
+
@source_files = source_files
|
5
|
+
end
|
6
|
+
|
7
|
+
|
8
|
+
public def full_report
|
9
|
+
# Some counter variables
|
10
|
+
@total_covered = 0
|
11
|
+
@total_methods = 0
|
12
|
+
@total_relevant_methods = 0
|
13
|
+
|
14
|
+
# Will contain the report
|
15
|
+
report = ''
|
16
|
+
|
17
|
+
# Iterate over all found files and generate a report for each file
|
18
|
+
report << @source_files.map(&method(:for_file)).reject(&:empty?).join("\n")
|
19
|
+
|
20
|
+
# Report summary
|
21
|
+
unless @config.skip_summary
|
22
|
+
# Some calculations
|
23
|
+
total_uncovered = @total_relevant_methods - @total_covered
|
24
|
+
relevant_percent = (@total_relevant_methods * 100 / @total_methods).round.to_s + '%'
|
25
|
+
covered_percent = (@total_covered * 100 / @total_methods).round.to_s + '%'
|
26
|
+
uncovered_percent = (total_uncovered * 100 / @total_methods).round.to_s + '%'
|
27
|
+
|
28
|
+
# Generate summary
|
29
|
+
report << "\nThere are #{@total_methods.to_s.yellow} methods in total.\n"
|
30
|
+
report << "#{@total_relevant_methods.to_s.yellow} (#{relevant_percent.yellow}) of them are relevant for coverage.\n"
|
31
|
+
report << "And #{@total_covered.to_s.yellow} (#{covered_percent.yellow}) methods are covered by tests.\n"
|
32
|
+
report << "Thus there are #{total_uncovered.to_s.yellow} (#{uncovered_percent.yellow}) uncovered methods.\n"
|
33
|
+
end
|
34
|
+
|
35
|
+
report + "\n"
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
# Tells if a line should be displayed according to the amount of
|
40
|
+
# relevant_methods, if the class is covered and the config
|
41
|
+
private def should_display_line?(relevant_methods, covered)
|
42
|
+
relevant_methods > 0 && !(@config.uncovered_only && covered)
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
private def for_file(file)
|
47
|
+
report = ''
|
48
|
+
|
49
|
+
# Generate a report for each class in that file
|
50
|
+
file.classes.each do |cls|
|
51
|
+
covered = cls.coverage == cls.relevant_method_count
|
52
|
+
|
53
|
+
if should_display_line?(cls.relevant_method_count, covered)
|
54
|
+
class_name = cls.full_qualified_name.to_s.light_blue
|
55
|
+
coverage = "#{cls.coverage}/#{cls.relevant_method_count}/#{cls.method_count}"
|
56
|
+
report << "#{covered ? '[X]'.green : '[ ]'.red} Found class: #{class_name} with #{covered ? coverage.green : coverage.red} covered methods.\n".bold
|
57
|
+
report << " => Source file: #{file.relative_file_name.light_blue}\n"
|
58
|
+
|
59
|
+
if file.test_file.file_exists?
|
60
|
+
report << " => Test file: #{file.test_file.relative_file_name.light_blue}\n"
|
61
|
+
else
|
62
|
+
report << " => Test file: #{'Not found'.light_red} (expected one of #{file.allowed_test_files.join(', ')})\n"
|
63
|
+
end
|
64
|
+
|
65
|
+
# Write the hints to the report
|
66
|
+
if cls.hints.any?
|
67
|
+
report << cls.hints.map{ |hint| " - #{hint}" }.reject(&:empty?).join("\n")
|
68
|
+
report << "\n"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Increase the counter variables
|
73
|
+
@total_methods += cls.method_count
|
74
|
+
@total_relevant_methods += cls.relevant_method_count
|
75
|
+
@total_covered += cls.coverage
|
76
|
+
end
|
77
|
+
|
78
|
+
report
|
79
|
+
end
|
80
|
+
end
|
@@ -1,7 +1,8 @@
|
|
1
1
|
require 'coverfield/source/method'
|
2
2
|
|
3
|
+
# Represents a class within a source file
|
3
4
|
class Coverfield::Source::Class
|
4
|
-
attr_reader :name, :module_name, :node, :methods, :source_file
|
5
|
+
attr_reader :name, :module_name, :node, :methods, :source_file, :coverage, :hints
|
5
6
|
|
6
7
|
# Constructor
|
7
8
|
public def initialize(class_name, module_name, node, source_file)
|
@@ -10,25 +11,59 @@ class Coverfield::Source::Class
|
|
10
11
|
@node = node
|
11
12
|
@methods = []
|
12
13
|
@source_file = source_file
|
14
|
+
@coverage = 0
|
15
|
+
@hints = []
|
13
16
|
find_methods
|
14
17
|
end
|
15
18
|
|
19
|
+
|
20
|
+
# Returns the full qualified name like Coverfield::Source::Class
|
16
21
|
public def full_qualified_name
|
17
22
|
name = @name
|
18
23
|
name = "#{@module_name}::#{name}" unless @module_name.empty?
|
19
24
|
name
|
20
25
|
end
|
21
26
|
|
27
|
+
|
28
|
+
# Returns the amount of methods, which should be covered by tests
|
22
29
|
public def relevant_method_count
|
23
30
|
relevant_methods = @methods.select { |m| !m.nocov?}
|
24
31
|
relevant_methods.size
|
25
32
|
end
|
26
33
|
|
34
|
+
|
35
|
+
# Returns the total amount of methods within the class
|
27
36
|
public def method_count
|
28
37
|
@methods.size
|
29
38
|
end
|
30
39
|
|
31
|
-
|
40
|
+
|
41
|
+
# Calculates the coverage of that class based on the relevant methods
|
42
|
+
# and sets the @coverage and the @hints fields
|
43
|
+
public def calculate_coverage
|
44
|
+
@coverage = 0
|
45
|
+
@hints = []
|
46
|
+
test_file = source_file.test_file
|
47
|
+
|
48
|
+
methods.each do |method|
|
49
|
+
# Is the method covered?
|
50
|
+
if test_file.cover?(full_qualified_name, method.name)
|
51
|
+
@coverage += 1
|
52
|
+
else
|
53
|
+
# Should it be covered?
|
54
|
+
if method.nocov?
|
55
|
+
@coverage += 1
|
56
|
+
else
|
57
|
+
# If it should be covered, but isn't, create a hint
|
58
|
+
method_name = "#{name}.#{method.name}".red
|
59
|
+
@hints << "Missing test for #{method_name}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
# Finds all methods within the class
|
32
67
|
private def find_methods
|
33
68
|
node.each_node(:def) do |node|
|
34
69
|
@methods << Coverfield::Source::Method.new(*node, self)
|
@@ -3,19 +3,21 @@ require 'coverfield/source/test_file'
|
|
3
3
|
require 'coverfield/source/class'
|
4
4
|
require 'coverfield/source/nocov_range'
|
5
5
|
|
6
|
+
# Represents a ruby source file which consists of classes
|
6
7
|
class Coverfield::Source::File
|
7
8
|
include Coverfield::Source::FileMethods
|
8
9
|
|
9
|
-
attr_reader :classes, :test_file
|
10
|
+
attr_reader :classes, :test_file
|
11
|
+
|
10
12
|
|
11
13
|
# Constructor
|
12
|
-
public def initialize(file_name)
|
14
|
+
public def initialize(config, file_name)
|
15
|
+
@config = config
|
13
16
|
@file_name = file_name
|
14
17
|
@classes = []
|
15
|
-
@coverage = 0
|
16
|
-
@hints = []
|
17
18
|
@nocov_ranges = []
|
18
19
|
|
20
|
+
# Ignore empty files
|
19
21
|
unless File.zero?(file_name)
|
20
22
|
parse_code
|
21
23
|
find_nocov_ranges
|
@@ -28,28 +30,13 @@ class Coverfield::Source::File
|
|
28
30
|
end
|
29
31
|
|
30
32
|
|
31
|
-
#
|
33
|
+
# Iterates over all classes and calculates their test coverage
|
32
34
|
private def calculate_coverage
|
33
|
-
|
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
|
35
|
+
classes.each { |cls| cls.calculate_coverage }
|
50
36
|
end
|
51
37
|
|
52
38
|
|
39
|
+
# Tells if a method node is located within two :nocov: tags
|
53
40
|
public def nocov?(method_body_node)
|
54
41
|
@nocov_ranges.each do |nocov_range|
|
55
42
|
return true if nocov_range.includes?(method_body_node)
|
@@ -59,18 +46,20 @@ class Coverfield::Source::File
|
|
59
46
|
end
|
60
47
|
|
61
48
|
|
62
|
-
#
|
49
|
+
# Finds all class definitions within that file
|
63
50
|
private def find_classes
|
64
51
|
@processed_source.ast.each_node(:class) do |node|
|
65
52
|
name, superclass, body = *node
|
66
53
|
_scope, const_name, value = *name
|
67
54
|
module_name = node.parent_module_name
|
68
55
|
|
56
|
+
# If the module_name is 'Object', the notation is not Coverfield::Source::TestFile but nested modules/class
|
69
57
|
if module_name == 'Object'
|
70
58
|
nothing, scope_name, nothing = *_scope
|
71
59
|
module_name = scope_name.to_s
|
72
60
|
end
|
73
61
|
|
62
|
+
# Create a new class object and push that to the @classes array
|
74
63
|
@classes << Coverfield::Source::Class.new(const_name, module_name, node, self)
|
75
64
|
end
|
76
65
|
end
|
@@ -78,24 +67,30 @@ class Coverfield::Source::File
|
|
78
67
|
|
79
68
|
# Find the spec file for that class
|
80
69
|
private def find_test_file
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
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)
|
70
|
+
allowed_test_files.each do |file|
|
71
|
+
@test_file = Coverfield::Source::TestFile.new(@config, file)
|
72
|
+
|
73
|
+
# break
|
74
|
+
return false if test_file.file_exists?
|
89
75
|
end
|
90
76
|
end
|
91
77
|
|
92
78
|
|
79
|
+
public def allowed_test_files
|
80
|
+
template = (@config.spec_dir + relative_file_name).gsub('.rb', '_spec.rb')
|
81
|
+
allowed_files = *template
|
82
|
+
allowed_files << template.gsub(/^\/(lib|app)/, '')
|
83
|
+
allowed_files
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
# Collects all :nocov: tag ranges in the file
|
93
88
|
private def find_nocov_ranges
|
94
89
|
first = true
|
95
90
|
line = 0
|
96
91
|
|
97
92
|
@processed_source.comments.each do |comment|
|
98
|
-
if comment.type == :inline && comment.text.strip =~ /^#\s
|
93
|
+
if comment.type == :inline && comment.text.strip =~ /^#\s*:nocov:/
|
99
94
|
if first
|
100
95
|
line = comment.loc.expression.first_line
|
101
96
|
else
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# Mixin for shared methods between TestFile and File
|
1
2
|
module Coverfield::Source::FileMethods
|
2
3
|
attr_reader :file_name
|
3
4
|
|
@@ -9,6 +10,6 @@ module Coverfield::Source::FileMethods
|
|
9
10
|
|
10
11
|
# Returns the file name relative to the app root
|
11
12
|
public def relative_file_name
|
12
|
-
@file_name.gsub(
|
13
|
+
@file_name.gsub(@config.app_root + '/', '')
|
13
14
|
end
|
14
15
|
end
|
@@ -1,6 +1,8 @@
|
|
1
|
+
# Represents a method within a class
|
1
2
|
class Coverfield::Source::Method
|
2
3
|
attr_reader :name, :args, :body, :source_class
|
3
4
|
|
5
|
+
# Constructor
|
4
6
|
public def initialize(method_name, args, body, source_class)
|
5
7
|
@name = method_name
|
6
8
|
@args = args
|
@@ -8,6 +10,8 @@ class Coverfield::Source::Method
|
|
8
10
|
@source_class = source_class
|
9
11
|
end
|
10
12
|
|
13
|
+
|
14
|
+
# Tells whether the method should be covered by a test or not
|
11
15
|
public def nocov?
|
12
16
|
@source_class.source_file.nocov? @body
|
13
17
|
end
|
@@ -1,9 +1,12 @@
|
|
1
|
+
# Represents a range of lines in a source file which is wrapped in :nocov: tags
|
1
2
|
class Coverfield::Source::NocovRange
|
3
|
+
# Consructor
|
2
4
|
public def initialize(first_line, last_line)
|
3
5
|
@first_line = first_line
|
4
6
|
@last_line = last_line
|
5
7
|
end
|
6
8
|
|
9
|
+
# Tells if a node is within that nocov rage
|
7
10
|
public def includes?(node)
|
8
11
|
source_range = node.source_range
|
9
12
|
source_range.first_line > @first_line && source_range.last_line < @last_line
|
@@ -1,14 +1,18 @@
|
|
1
1
|
require 'coverfield/source/file_methods'
|
2
2
|
|
3
|
+
# Represents a spec file
|
3
4
|
class Coverfield::Source::TestFile
|
4
5
|
include Coverfield::Source::FileMethods
|
5
6
|
|
7
|
+
|
6
8
|
# Constructor
|
7
|
-
public def initialize(file_name)
|
8
|
-
@
|
9
|
-
@
|
9
|
+
public def initialize(config, file_name)
|
10
|
+
@config = config
|
11
|
+
@file_name = config.app_root + '/' + file_name
|
12
|
+
@file_exists = File.exists?(file_name) && !File.zero?(file_name)
|
10
13
|
@describes = {}
|
11
14
|
|
15
|
+
# If the file doesn't exist, do nothing
|
12
16
|
if file_exists?
|
13
17
|
parse_code
|
14
18
|
find_describes
|
@@ -24,7 +28,7 @@ class Coverfield::Source::TestFile
|
|
24
28
|
end
|
25
29
|
|
26
30
|
|
27
|
-
# Tells if that file covers a
|
31
|
+
# Tells if that file covers a method of a class pair
|
28
32
|
public def cover?(class_name, method_name)
|
29
33
|
return false unless file_exists?
|
30
34
|
|
@@ -37,9 +41,14 @@ class Coverfield::Source::TestFile
|
|
37
41
|
end
|
38
42
|
|
39
43
|
|
40
|
-
#
|
44
|
+
# Helper method which builts the full qualified class name out of
|
45
|
+
# a describe arguments node
|
41
46
|
private def get_spec_class_name(describe_args_node)
|
47
|
+
# If the argument is already a string, there's nothing to do
|
42
48
|
return describe_args_node if describe_args_node.is_a?(String)
|
49
|
+
|
50
|
+
# Otherwise it's a constant chain like Coverfield::Source::TestFile
|
51
|
+
# which will be concatenated
|
43
52
|
subject_ary = []
|
44
53
|
|
45
54
|
describe_args_node.each_node(:const) do |const_part|
|
@@ -78,8 +87,8 @@ class Coverfield::Source::TestFile
|
|
78
87
|
value = value.to_s
|
79
88
|
end
|
80
89
|
|
90
|
+
# Remove the . or # from the string
|
81
91
|
@describes[current_subject] << value.strip.gsub(/^(?:\.|#)(.+)$/i, '\1')
|
82
|
-
|
83
92
|
end
|
84
93
|
|
85
94
|
first_describe = false
|
data/lib/coverfield/version.rb
CHANGED
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'coverfield/config'
|
2
|
+
require 'coverfield/report'
|
3
|
+
|
4
|
+
describe Coverfield::Report do
|
5
|
+
subject { Coverfield::Report }
|
6
|
+
|
7
|
+
let(:config) { Coverfield::Config.new }
|
8
|
+
|
9
|
+
describe '#should_display_line?' do
|
10
|
+
context "while it shouldn't skip covered classes" do
|
11
|
+
subject { Coverfield::Report.new(config, []) }
|
12
|
+
|
13
|
+
context 'and there are no relevant methods' do
|
14
|
+
it 'returns false' do
|
15
|
+
expect(subject.instance_eval { should_display_line?(0, true) }).to eq(false)
|
16
|
+
expect(subject.instance_eval { should_display_line?(0, false) }).to eq(false)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
context 'and there are relevant methods' do
|
21
|
+
context 'coverage is 100%' do
|
22
|
+
it 'returns true' do
|
23
|
+
expect(subject.instance_eval { should_display_line?(1, true) }).to eq(true)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
context 'and coverage is below 100%' do
|
28
|
+
it 'returns true' do
|
29
|
+
expect(subject.instance_eval { should_display_line?(1, false) }).to eq(true)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context 'while it should skip covered classes' do
|
36
|
+
subject do
|
37
|
+
cfg = config
|
38
|
+
cfg.uncovered_only = true
|
39
|
+
Coverfield::Report.new(cfg, [])
|
40
|
+
end
|
41
|
+
|
42
|
+
context 'and there are no relevant methods' do
|
43
|
+
it 'returns false' do
|
44
|
+
expect(subject.instance_eval { should_display_line?(0, true) }).to eq(false)
|
45
|
+
expect(subject.instance_eval { should_display_line?(0, false) }).to eq(false)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
context 'and there are relevant methods' do
|
50
|
+
context 'coverage is 100%' do
|
51
|
+
it 'returns false' do
|
52
|
+
expect(subject.instance_eval { should_display_line?(1, true) }).to eq(false)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
context 'and coverage is below 100%' do
|
57
|
+
it 'returns true' do
|
58
|
+
expect(subject.instance_eval { should_display_line?(1, false) }).to eq(true)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# Extend loadpath for simpler require statements
|
2
|
+
$: << Dir.pwd + '/lib/'
|
3
|
+
|
4
|
+
|
5
|
+
RSpec.configure do |config|
|
6
|
+
# Configure the Rspec to only accept the new syntax on new projects, to avoid
|
7
|
+
# having the 2 syntax all over the place.
|
8
|
+
config.expect_with :rspec do |c|
|
9
|
+
c.syntax = :expect
|
10
|
+
end
|
11
|
+
end
|
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.
|
4
|
+
version: 0.3.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-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rubocop
|
@@ -52,6 +52,20 @@ dependencies:
|
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '1.12'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.4'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.4'
|
55
69
|
description: 'Smarter Ruby/RSpec coverage reports: Tells you which classes, modules
|
56
70
|
and methods don''t have any specs'
|
57
71
|
email:
|
@@ -69,6 +83,9 @@ files:
|
|
69
83
|
- bin/coverfield
|
70
84
|
- coverfield.gemspec
|
71
85
|
- lib/coverfield.rb
|
86
|
+
- lib/coverfield/cli.rb
|
87
|
+
- lib/coverfield/config.rb
|
88
|
+
- lib/coverfield/report.rb
|
72
89
|
- lib/coverfield/source/class.rb
|
73
90
|
- lib/coverfield/source/file.rb
|
74
91
|
- lib/coverfield/source/file_methods.rb
|
@@ -76,6 +93,8 @@ files:
|
|
76
93
|
- lib/coverfield/source/nocov_range.rb
|
77
94
|
- lib/coverfield/source/test_file.rb
|
78
95
|
- lib/coverfield/version.rb
|
96
|
+
- spec/lib/coverfield/report_spec.rb
|
97
|
+
- spec/spec_helper.rb
|
79
98
|
homepage: http://github.com/phortx/coverfield
|
80
99
|
licenses:
|
81
100
|
- GPL-3.0
|
@@ -100,4 +119,6 @@ rubygems_version: 2.5.1
|
|
100
119
|
signing_key:
|
101
120
|
specification_version: 4
|
102
121
|
summary: Smarter Ruby/RSpec coverage reports
|
103
|
-
test_files:
|
122
|
+
test_files:
|
123
|
+
- spec/lib/coverfield/report_spec.rb
|
124
|
+
- spec/spec_helper.rb
|