coverfield 0.1.0 → 0.3.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 +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
|
[](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
|