coco 0.13.0 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CONTRIBUTORS +9 -0
  3. data/Changelog.markdown +42 -0
  4. data/Gemfile.lock +36 -55
  5. data/LICENSE +7 -0
  6. data/README.markdown +124 -73
  7. data/Rakefile +10 -37
  8. data/VERSION +1 -1
  9. data/lib/coco.rb +9 -21
  10. data/lib/coco/configuration.rb +59 -20
  11. data/lib/coco/cover.rb +1 -0
  12. data/lib/coco/cover/coverage_result.rb +72 -16
  13. data/lib/coco/cover/coverage_stat.rb +20 -8
  14. data/lib/coco/cover/summary.rb +50 -0
  15. data/lib/coco/deprecated_message.rb +31 -0
  16. data/lib/coco/formatter.rb +2 -2
  17. data/lib/coco/formatter/colored_string.rb +1 -1
  18. data/lib/coco/formatter/console_formatter.rb +24 -19
  19. data/lib/coco/formatter/context.rb +10 -39
  20. data/lib/coco/formatter/html_formatter.rb +7 -13
  21. data/lib/coco/formatter/html_index_formatter.rb +20 -16
  22. data/lib/coco/formatter/index_context.rb +37 -0
  23. data/lib/coco/formatter/index_line.rb +21 -0
  24. data/lib/coco/formatter/template.rb +2 -3
  25. data/lib/coco/helpers.rb +88 -68
  26. data/lib/coco/lister/source_lister.rb +23 -26
  27. data/lib/coco/lister/uncovered_lister.rb +6 -9
  28. data/lib/coco/project.rb +65 -0
  29. data/lib/coco/theme.rb +15 -0
  30. data/lib/coco/writer/file_writer.rb +5 -5
  31. data/lib/coco/writer/html_directory.rb +16 -8
  32. data/lib/coco/writer/html_files_writer.rb +9 -6
  33. data/lib/coco/writer/html_index_writer.rb +6 -3
  34. data/template/css/dark.css +178 -0
  35. data/template/css/{coco.css → light.css} +22 -9
  36. data/template/file.erb +3 -3
  37. data/template/index.erb +35 -33
  38. data/template/js/coco.js +18 -0
  39. metadata +34 -58
  40. data/COPYING +0 -674
  41. data/lib/coco/formatter/formatter.rb +0 -23
  42. data/template/img/coconut16.png +0 -0
  43. data/template/img/licenses +0 -19
data/Rakefile CHANGED
@@ -3,49 +3,22 @@ require 'rake/dsl_definition'
3
3
  require 'rake'
4
4
  require 'rspec/core/rake_task'
5
5
 
6
- def ruby_files_for_shell
7
- files = Dir.glob 'lib/**/*.rb'
8
- files.join(' ')
9
- end
10
-
11
- desc 'Test coco'
12
- task :default => :spec
6
+ desc 'Test with all tools'
7
+ task :default => :all_tests
13
8
 
14
- desc 'Test coco'
15
- RSpec::Core::RakeTask.new(:spec) do |t|
9
+ desc 'Test with RSpec'
10
+ RSpec::Core::RakeTask.new(:rspec) do |t|
16
11
  t.rspec_opts = ['--color --order=random']
17
12
  end
18
13
 
19
- desc 'Check for code smells with reek'
20
- task :reek do
21
- puts 'Checking for code smells.'
22
- puts '-------------------------'
23
- system "reek #{ruby_files_for_shell}"
14
+ desc 'Test with Cucumber'
15
+ task :cucumber do
16
+ exec 'cucumber'
24
17
  end
25
18
 
26
- desc 'Check for duplicate code with flay'
27
- task :flay do
28
- puts 'Checking for duplicate code.'
29
- puts '----------------------------'
30
- exec "flay lib"
19
+ task :all_tests do
20
+ Rake::Task['rspec'].execute
21
+ Rake::Task['cucumber'].execute
31
22
  end
32
23
 
33
- desc 'Check various code metrics'
34
- task :metrics do
35
- puts 'Checking various metrics.'
36
- puts '========================='
37
- Rake::Task['reek'].execute
38
- Rake::Task['flay'].execute
39
- end
40
24
 
41
- namespace :doc do
42
- desc 'Generate documentation for developpers'
43
- task :create do
44
- exec 'yardoc'
45
- end
46
-
47
- desc 'Delete documentation'
48
- task :clean do
49
- rm_rf 'doc'
50
- end
51
- end
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.13.0
1
+ 0.14.0
@@ -1,38 +1,26 @@
1
+ require 'coco/project'
2
+ require 'coco/theme'
1
3
  require 'coco/formatter'
2
4
  require 'coco/cover'
3
5
  require 'coco/writer'
4
6
  require 'coco/helpers'
5
7
  require 'coco/configuration'
6
8
  require 'coco/lister'
9
+ require 'coco/deprecated_message'
7
10
 
8
11
  require 'coverage'
9
12
 
10
13
  # Public: Main namespace of Coco, a code coverage utilily for
11
- # Ruby from 1.9.3 to 2.1.
14
+ # Ruby from 2.0 to 2.3.
15
+ #
12
16
  module Coco
13
- ROOT = File.expand_path(File.dirname(__FILE__) + '/..').freeze
17
+ ROOT = File.expand_path(File.dirname(__FILE__) + '/..').freeze
14
18
  end
15
19
 
16
20
  Coverage.start
17
21
 
22
+ # The coverage's analysis happens at the very end of the test suite.
23
+ #
18
24
  at_exit do
19
- config = Coco::Configuration.new
20
- if config.user_wants_to_run?
21
- result = Coco::CoverageResult.new(config, Coverage.result)
22
- covered = result.covered_from_domain
23
-
24
- sources = Coco::SourceLister.new(config).list
25
- uncovered = Coco::UncoveredLister.new(sources, result.all_from_domain).list
26
-
27
- console_formatter = Coco::ConsoleFormatter.new(covered, uncovered,
28
- config[:threshold])
29
- puts console_formatter.format(config[:single_line_report])
30
- puts console_formatter.link if config[:show_link_in_terminal]
31
-
32
- html_files = Coco::HtmlFormatter.new(covered).format
33
- Coco::HtmlFilesWriter.new(html_files).write
34
-
35
- index = Coco::HtmlIndexFormatter.new(covered, uncovered).format
36
- Coco::HtmlIndexWriter.new(index).write
37
- end
25
+ Coco::Project.run(Coverage.result)
38
26
  end
@@ -18,27 +18,36 @@ module Coco
18
18
  #
19
19
  # Note you can set the threshold above 100% (to be sure to see all
20
20
  # files) but you cannot set it under 0.
21
+ #
21
22
  class Configuration < Hash
22
23
 
24
+ DEFAULT_OPTIONS = {
25
+ threshold: 100,
26
+ include: ['lib'],
27
+ exclude: %w( spec test ),
28
+ single_line_report: true,
29
+ always_run: true,
30
+ show_link_in_terminal: false,
31
+ exclude_above_threshold: true,
32
+ theme: 'light',
33
+ }.freeze
34
+
23
35
  # Public: Initialize a Configuration.
36
+ #
24
37
  def initialize
25
- self[:threshold] = 100
26
- self[:directories] = ['lib']
27
- self[:excludes] = ['spec', 'test']
28
- self[:single_line_report] = false
29
- self[:always_run] = true
30
- self[:show_link_in_terminal] = false
31
- self[:exclude_above_threshold] = true
38
+ merge!(DEFAULT_OPTIONS)
32
39
  if File.exist?('.coco.yml')
33
- self.merge!(YAML.load_file('.coco.yml'))
40
+ merge!(YAML.load_file('.coco.yml'))
34
41
  # Deprecated: Support of '.coco' file will be removed in v1.0.
35
42
  elsif File.exist?('.coco')
36
- warn('Please use `.coco.yml` instead of `.coco`.')
37
- warn('Support for `.coco` will be removed in future versions.')
38
- self.merge!(YAML.load_file('.coco'))
43
+ warn(DeprecatedMessage.for_legacy_config_file)
44
+ merge!(YAML.load_file('.coco'))
39
45
  end
40
46
 
47
+ ensure_known_theme
41
48
  ensure_threeshold_compatibility
49
+ ensure_directories_compatibility
50
+ ensure_excludes_compatibility
42
51
  expand_directories
43
52
  remove_directories
44
53
  end
@@ -49,13 +58,13 @@ module Coco
49
58
  # If the configuration key :always_run is set to true, we always
50
59
  # run the coverage.
51
60
  # In case the configuration key :always_run is set to false, we have
52
- # to check for an environement variable named 'COCO' to decide if
61
+ # to check for an environment variable named 'COCO' to decide if
53
62
  # we launch the coverage or not. When 'COCO' doesn't exist, or is
54
63
  # the empty string, or '0', or 'false', we don't run coverage.
55
64
  # When 'COCO' is set to any other value, we start coverage.
56
65
  #
57
66
  # Returns true if coverage should start.
58
- def user_wants_to_run?
67
+ def run_this_time?
59
68
  if self[:always_run]
60
69
  true
61
70
  else
@@ -66,27 +75,57 @@ module Coco
66
75
  private
67
76
 
68
77
  def expand_directories
69
- self[:excludes].each do |file_or_dir|
78
+ self[:exclude].each do |file_or_dir|
70
79
  add_files file_or_dir if File.directory?(file_or_dir)
71
80
  end
72
81
  end
73
82
 
74
83
  def add_files(dir)
75
- Helpers.rb_files_from(dir).each {|file| self[:excludes] << file }
84
+ Helpers.rb_files_from(dir).each { |file| self[:exclude] << file }
76
85
  end
77
86
 
78
87
  def remove_directories
79
- self[:excludes].delete_if {|file_or_dir| File.directory?(file_or_dir) }
88
+ self[:exclude].delete_if { |file_or_dir| File.directory?(file_or_dir) }
80
89
  end
81
90
 
82
91
  def ensure_threeshold_compatibility
83
- if !self[:threeshold].nil?
84
- warn('Please change `threeshold` to `threshold`.')
85
- warn('Support for the misspelt `threeshold` configuration key will be removed in future COCO versions.')
92
+ if threeshold_present?
93
+ warn(DeprecatedMessage.for_threeshold)
86
94
  self[:threshold] = self[:threeshold]
87
95
  end
88
96
  end
89
97
 
90
- end
98
+ def threeshold_present?
99
+ self[:threeshold]
100
+ end
101
+
102
+ def ensure_directories_compatibility
103
+ if directories_present?
104
+ warn(DeprecatedMessage.for_directories)
105
+ self[:include] = self[:directories]
106
+ end
107
+ end
108
+
109
+ def directories_present?
110
+ self[:directories]
111
+ end
91
112
 
113
+ def ensure_excludes_compatibility
114
+ if excludes_present?
115
+ warn(DeprecatedMessage.for_excludes)
116
+ self[:exclude] = self[:excludes]
117
+ end
118
+ end
119
+
120
+ def excludes_present?
121
+ self[:excludes]
122
+ end
123
+
124
+ def ensure_known_theme
125
+ unless %w( light dark ).include?(self[:theme])
126
+ warn("\n\nThe theme '#{self[:theme]}' didn't exist.\n\n")
127
+ self[:theme] = 'light'
128
+ end
129
+ end
130
+ end
92
131
  end
@@ -1,2 +1,3 @@
1
1
  require 'coco/cover/coverage_stat'
2
2
  require 'coco/cover/coverage_result'
3
+ require 'coco/cover/summary'
@@ -2,56 +2,112 @@ module Coco
2
2
 
3
3
  # Compute results of interest from the big results information (from
4
4
  # Coverage.result)
5
+ #
5
6
  class CoverageResult
6
7
 
7
8
  # Returns a Hash coverage for all the sources that live in the root
8
9
  # project folder.
9
- attr_reader :all_from_domain
10
+ #
11
+ attr_reader :coverable_files
10
12
 
11
13
  # Returns a Hash coverage for sources that are not sufficiently
12
14
  # covered. More technically, the sources that live in the root
13
15
  # project folder and for which the coverage percentage is under the
14
16
  # threshold.
15
- attr_reader :covered_from_domain
17
+ #
18
+ attr_reader :not_covered_enough
16
19
 
17
20
  # Public: Initialize a CoverageResult.
18
21
  #
19
22
  # config - Hash
20
- # raw_results - Hash results obtained from Coverage.result.
23
+ # raw_results - The Hash from Coverage.result. Keys are filenames
24
+ # and values are an Array representing each lines of
25
+ # the file :
26
+ # + nil : Unreacheable (comments, etc).
27
+ # + 0 : Not hit.
28
+ # + 1 or more : Number of hits.
29
+ #
21
30
  def initialize(config, raw_results)
22
- @exclude_files = config[:excludes]
31
+ raise ArgumentError if config[:threshold] < 0
32
+
33
+ @exclude_files = config[:exclude]
23
34
  @threshold = config[:threshold]
24
- raise ArgumentError if @threshold < 0
25
35
  @result = raw_results
36
+
26
37
  exclude_external_sources
27
- exclude_files_user_dont_want
28
- if config[:exclude_above_threshold]
29
- @covered_from_domain = exclude_sources_above_threshold
30
- else
31
- @covered_from_domain = @all_from_domain
32
- end
38
+ exclude_files_user_dont_want if @exclude_files
39
+
40
+ @not_covered_enough = if config[:exclude_above_threshold]
41
+ exclude_sources_above_threshold
42
+ else
43
+ @coverable_files
44
+ end
45
+ end
46
+
47
+ # Public: Count the number of "coverable" files.
48
+ #
49
+ # Returns the Fixnum number of files.
50
+ #
51
+ def count
52
+ coverable_files.size
53
+ end
54
+
55
+ # Public: Count the number of uncovered files, that is, files with a
56
+ # coverage rate of 0%.
57
+ #
58
+ # Returns the Fixnum number of uncovered files.
59
+ #
60
+ def uncovered_count
61
+ not_covered_enough.select do |_, hits|
62
+ CoverageStat.coverage_percent(hits).zero?
63
+ end.size
64
+ end
65
+
66
+ # Public: Computes the average coverage rate.
67
+ # The formula is simple:
68
+ #
69
+ # N = number of files
70
+ # f = a file
71
+ # average = sum(f_i%) / N
72
+ #
73
+ # In words: Take the sum of the coverage's percentage of all files
74
+ # and divide this sum by the number of files.
75
+ #
76
+ # Returns the Fixnum rounded average rate of coverage.
77
+ #
78
+ def average
79
+ files_present? ? (sum / count).round : 0
33
80
  end
34
81
 
35
82
  private
36
83
 
37
84
  def exclude_external_sources
38
85
  here = Dir.pwd
39
- @all_from_domain = @result.select {|key, value| key.start_with?(here) }
86
+ @coverable_files = @result.select { |key, _| key.start_with?(here) }
40
87
  end
41
88
 
42
89
  def exclude_files_user_dont_want
43
- return if @exclude_files.nil?
44
90
  @exclude_files.each do |filename|
45
- @all_from_domain.delete(File.expand_path(filename))
91
+ @coverable_files.delete(File.expand_path(filename))
46
92
  end
47
93
  end
48
94
 
49
95
  def exclude_sources_above_threshold
50
- @all_from_domain.select do |key, value|
96
+ @coverable_files.select do |_, value|
51
97
  CoverageStat.coverage_percent(value) < @threshold
52
98
  end
53
99
  end
54
100
 
55
- end
101
+ # Returns the Float sum of all files' percentage.
102
+ #
103
+ def sum
104
+ @coverable_files.values.map do |hits|
105
+ CoverageStat.real_percent(hits)
106
+ end.reduce(&:+)
107
+ end
56
108
 
109
+ def files_present?
110
+ count > 0
111
+ end
112
+ end
57
113
  end
@@ -9,20 +9,32 @@ module Coco
9
9
  # * nil: source line will never be reached (like comments).
10
10
  # * 0: source line could be reached, but was not.
11
11
  # * 1 and above: number of time the source line has been reached.
12
+ #
12
13
  module CoverageStat
13
- extend self
14
14
 
15
- # Public: Compute the percentage of code coverage for a file.
15
+ # Public: Compute the decimal percentage of code coverage for a file.
16
16
  # The file is represented by an array of hits.
17
17
  #
18
18
  # hits - Array of Integer.
19
19
  #
20
- # Returns a Integer (rounded) percentage.
21
- def coverage_percent(hits)
20
+ # Returns a Float percentage of coverage.
21
+ #
22
+ def self.real_percent(hits)
22
23
  hits = hits.compact
23
24
  return 0 if hits.empty?
24
25
  one_percent = 100.0 / hits.size
25
- (number_of_covered_lines(hits) * one_percent).to_i
26
+ number_of_covered_lines(hits) * one_percent
27
+ end
28
+
29
+ # Public: Compute the integer percentage of code coverage for a file.
30
+ # The file is represented by an array of hits.
31
+ #
32
+ # hits - Array of Integer.
33
+ #
34
+ # Returns a Integer rounded percentage of coverage.
35
+ #
36
+ def self.coverage_percent(hits)
37
+ real_percent(hits).round
26
38
  end
27
39
 
28
40
  # Compute the total of covered lines in a hits array.
@@ -30,9 +42,9 @@ module Coco
30
42
  # hits - Array of Integer.
31
43
  #
32
44
  # Returns Integer.
33
- def number_of_covered_lines(hits)
34
- hits.select {|hit| hit > 0 }.size
45
+ #
46
+ def self.number_of_covered_lines(hits)
47
+ hits.select { |hit| hit > 0 }.size
35
48
  end
36
-
37
49
  end
38
50
  end
@@ -0,0 +1,50 @@
1
+ module Coco
2
+
3
+ # A very brief summary of the coverage result.
4
+ #
5
+ class Summary
6
+ attr_reader :count, :uncovered_count
7
+
8
+ def initialize(result, uncovered)
9
+ @uncovered_count = uncovered.size
10
+ @coverable_files = result.coverable_files
11
+ @count = @coverable_files.size + @uncovered_count
12
+ end
13
+
14
+ def to_s
15
+ "Cover #{average}% | #{uncovered_count} uncovered | #{count} files"
16
+ end
17
+
18
+ # Public: Computes the average coverage rate.
19
+ # The formula is simple:
20
+ #
21
+ # N = number of files
22
+ # f = a file
23
+ # average = sum(f_i%) / N
24
+ #
25
+ # In words: Take the sum of the coverage's percentage of all files
26
+ # and divide this sum by the number of files.
27
+ #
28
+ # Returns the Fixnum rounded average rate of coverage.
29
+ #
30
+ def average
31
+ files_present? ? (sum / count).round : 0
32
+ end
33
+
34
+ private
35
+
36
+ attr_reader :coverable_files
37
+
38
+ # Returns the Float sum of all files' percentage.
39
+ #
40
+ def sum
41
+ coverable_files.values.map do |hits|
42
+ CoverageStat.real_percent(hits)
43
+ end.reduce(&:+)
44
+ end
45
+
46
+ def files_present?
47
+ count > 0
48
+ end
49
+ end
50
+ end