coverfield 0.1.0 → 0.3.0

This diff has not been reviewed by any users.
Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c06502048cd2cfbb46d8383572891c9a6a4ebff8
4
- data.tar.gz: 1156c6922c0181fd7267ed4bd3fe9a370127aad5
3
+ metadata.gz: 555473b54c96dd9e9ae8104e1489048f1779c6f7
4
+ data.tar.gz: 76961b00f2fee8690d255cafc26f3cab2218ef85
5
5
  SHA512:
6
- metadata.gz: 1be8dfd4b92c3e2b0d6188c3fee72e8f28868994d284e600bad14eaba2a317932e0f186337f44c8290fe8934e3406308a2004d3d489f0a98a897af582dba5fcb
7
- data.tar.gz: b33d06bb0e8d0bd9155294110fc245ec8f5ed3c351e438000c2add9d6ac6f4d1c4a84360c5e682cbfa51c38083695032de21206c59ac38508116221079506b30
6
+ metadata.gz: a0bb29b2a0704b40627ace26e352ce8ca4107c2ec18a090686baa5f23beed1bb98b20cedcf2bfff5e43db6e16e95fe81547c531a9011c9b999b5d2a200449dfc
7
+ data.tar.gz: 9b5806d01aea4cb702640e7378eae01a51ef9001f4441b2e6ff63f2c5b3bc38fb1f2b8ecbad0e9588fce329f7c3b5b4e81a28ec1fbc626621755c066b41cfccd
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- coverfield (0.1.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:** Alpha Release, do not rely on or use in production yet!
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 theres a spec for a
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 thru. In my case a class which was used relative often so
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 whitin the specs. It compiles a report which clearly
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 peave
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 project has still prototype character (this is an alpha release) and
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, covervield would have been much more code and much more complicated.
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. All specs are located in `spec/`.
70
- 2. Within `spec` all specs are placed in the same path as the file which is
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
- 3. The first `describe` call have to be built like that:
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
- 4. All inner `describe` calls for the methods have to be built like that:
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
- 5. All dependencies of your app have to be installed (`bundle install`).
86
+ 4. All dependencies of your app have to be installed (`bundle install`).
@@ -1,65 +1,22 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- # Run that script in your apps root. Important: rubocop and colorize gems have to be installed!
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/source/class'
13
- require 'coverfield/source/file'
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
- file.hints.each do |hint|
45
- puts " - #{hint}"
46
- end
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
- puts
53
- end
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
- 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."
65
- puts
20
+ # Create and print the report
21
+ report = Coverfield::Report.new(cli.config, source_files)
22
+ puts report.full_report
@@ -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
- gem.add_dependency 'rubocop', '~> 0.40'
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
@@ -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
- # Finds all methods
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, :coverage, :hints
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
- # Calculates the number of covered methods of this file and sets @coverage and @hints
33
+ # Iterates over all classes and calculates their test coverage
32
34
  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
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
- # Find class definitions
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
- 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)
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*\:nocov\:/
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(APP_ROOT + '/', '')
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
- @file_name = file_name
9
- @file_exists = File.exists?(@file_name) && !File.zero?(file_name)
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 class_name method_name pair
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
- # Small helper method which builts the full qualified class name out of a describe arguments node
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
@@ -1,5 +1,5 @@
1
1
  module Coverfield
2
- version = '0.1.0'
2
+ version = '0.3.0'
3
3
 
4
4
  def version.to_a
5
5
  split('.').map(&:to_i)
@@ -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
@@ -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.1.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-21 00:00:00.000000000 Z
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