coverband 5.0.1 → 5.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
  3. data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  4. data/.github/workflows/main.yml +30 -0
  5. data/CONTRIBUTING.md +1 -0
  6. data/Gemfile +4 -5
  7. data/Gemfile.rails4 +2 -0
  8. data/Gemfile.rails6 +2 -0
  9. data/LICENSE.txt +1 -1
  10. data/README.md +43 -7
  11. data/changes.md +14 -9
  12. data/coverband.gemspec +6 -4
  13. data/lib/coverband/adapters/base.rb +5 -1
  14. data/lib/coverband/adapters/file_store.rb +1 -1
  15. data/lib/coverband/adapters/redis_store.rb +1 -1
  16. data/lib/coverband/at_exit.rb +3 -0
  17. data/lib/coverband/collectors/view_tracker.rb +16 -7
  18. data/lib/coverband/reporters/web.rb +2 -1
  19. data/lib/coverband/utils/dead_methods.rb +63 -0
  20. data/lib/coverband/utils/method_definition_scanner.rb +96 -0
  21. data/lib/coverband/utils/tasks.rb +17 -3
  22. data/lib/coverband/version.rb +1 -1
  23. data/test/coverband/collectors/view_tracker_test.rb +10 -0
  24. data/test/coverband/reporters/web_test.rb +34 -0
  25. data/test/coverband/utils/dead_methods_test.rb +53 -0
  26. data/test/coverband/utils/method_definition_scanner_test.rb +85 -0
  27. data/test/fixtures/casting_invitor.rb +60 -0
  28. data/test/forked/rails_full_stack_test.rb +1 -1
  29. data/test/forked/rails_full_stack_views_test.rb +51 -0
  30. data/test/forked/rails_view_tracker_stack_test.rb +44 -0
  31. data/test/rails4_dummy/app/controllers/dummy_view_controller.rb +16 -0
  32. data/test/rails4_dummy/app/views/dummy_view/show.html.erb +5 -0
  33. data/test/rails4_dummy/app/views/dummy_view/show_haml.html.haml +4 -0
  34. data/test/rails4_dummy/app/views/dummy_view/show_slim.html.slim +4 -0
  35. data/test/rails4_dummy/config/application.rb +1 -0
  36. data/test/rails4_dummy/config/routes.rb +3 -0
  37. data/test/rails5_dummy/app/controllers/dummy_view_controller.rb +16 -0
  38. data/test/rails5_dummy/app/views/dummy_view/show.html.erb +5 -0
  39. data/test/rails5_dummy/app/views/dummy_view/show_haml.html.haml +4 -0
  40. data/test/rails5_dummy/app/views/dummy_view/show_slim.html.slim +4 -0
  41. data/test/rails5_dummy/config/application.rb +2 -0
  42. data/test/rails5_dummy/config/coverband.rb +3 -1
  43. data/test/rails5_dummy/config/routes.rb +3 -0
  44. data/test/rails6_dummy/app/controllers/dummy_view_controller.rb +16 -0
  45. data/test/rails6_dummy/app/views/dummy_view/show.html.erb +5 -0
  46. data/test/rails6_dummy/app/views/dummy_view/show_haml.html.haml +4 -0
  47. data/test/rails6_dummy/app/views/dummy_view/show_slim.html.slim +4 -0
  48. data/test/rails6_dummy/config/application.rb +1 -0
  49. data/test/rails6_dummy/config/routes.rb +3 -0
  50. data/test/test_helper.rb +1 -0
  51. data/views/view_tracker.erb +2 -2
  52. metadata +42 -3
  53. data/.travis.yml +0 -48
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ if defined?(RubyVM::AbstractSyntaxTree)
4
+ module Coverband
5
+ module Utils
6
+ class MethodDefinitionScanner
7
+ attr_reader :path
8
+
9
+ def initialize(path)
10
+ @path = path
11
+ end
12
+
13
+ def scan
14
+ scan_node(RubyVM::AbstractSyntaxTree.parse_file(path), nil)
15
+ end
16
+
17
+ def self.scan(path)
18
+ new(path).scan
19
+ end
20
+
21
+ class MethodBody
22
+ def initialize(method_definition)
23
+ @method_definition = method_definition
24
+ end
25
+
26
+ def coverage?(file_coverage)
27
+ body_coverage =
28
+ file_coverage[(first_line_number - 1)..(last_line_number - 1)]
29
+ body_coverage.map(&:to_i).any?(&:positive?)
30
+ end
31
+
32
+ private
33
+
34
+ def first_line_number
35
+ @method_definition.first_line_number + 1
36
+ end
37
+
38
+ def last_line_number
39
+ @method_definition.last_line_number - 1
40
+ end
41
+ end
42
+
43
+ class MethodDefinition
44
+ attr_reader :last_line_number,
45
+ :first_line_number,
46
+ :name,
47
+ :class_name,
48
+ :file_path
49
+
50
+ def initialize(
51
+ first_line_number:,
52
+ last_line_number:,
53
+ name:,
54
+ class_name:,
55
+ file_path:
56
+ )
57
+ @first_line_number = first_line_number
58
+ @last_line_number = last_line_number
59
+ @name = name
60
+ @class_name = class_name
61
+ @file_path = file_path
62
+ end
63
+
64
+ def body
65
+ MethodBody.new(self)
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ def scan_node(node, class_name)
72
+ definitions = []
73
+ return definitions unless node.is_a?(RubyVM::AbstractSyntaxTree::Node)
74
+ current_class = node.type == :CLASS ? node.children.first.children.last : class_name
75
+ if node.type == :DEFN
76
+ definitions <<
77
+ MethodDefinition.new(
78
+ first_line_number: node.first_lineno,
79
+ last_line_number: node.last_lineno,
80
+ name: node.children.first,
81
+ class_name: current_class,
82
+ file_path: path
83
+ )
84
+ end
85
+ definitions + scan_children(node, current_class)
86
+ end
87
+
88
+ def scan_children(node, current_class)
89
+ node.children.flatten.compact.map { |child|
90
+ scan_node(child, current_class)
91
+ }.flatten
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -9,11 +9,25 @@ namespace :coverband do
9
9
  Coverband::Reporters::ConsoleReport.report(Coverband.configuration.store)
10
10
  end
11
11
 
12
+ if defined?(RubyVM::AbstractSyntaxTree)
13
+ require "coverband/utils/dead_methods"
14
+
15
+ desc "Output all dead methods"
16
+ task :dead_methods do
17
+ Coverband::Utils::DeadMethods.output_all
18
+ end
19
+ end
20
+
12
21
  desc "report runtime Coverband code coverage"
13
22
  task :coverage_server do
14
- Rake.application["environment"].invoke if Rake::Task.task_defined?("environment")
15
- Coverband.configuration.store.merge_mode = true if Coverband.configuration.store.is_a?(Coverband::Adapters::FileStore)
16
- Rack::Server.start app: Coverband::Reporters::Web.new, Port: ENV.fetch("COVERBAND_COVERAGE_PORT", 1022).to_i
23
+ if Rake::Task.task_defined?("environment")
24
+ Rake.application["environment"].invoke
25
+ end
26
+ if Coverband.configuration.store.is_a?(Coverband::Adapters::FileStore)
27
+ Coverband.configuration.store.merge_mode = true
28
+ end
29
+ Rack::Server.start app: Coverband::Reporters::Web.new,
30
+ Port: ENV.fetch("COVERBAND_COVERAGE_PORT", 9022).to_i
17
31
  end
18
32
 
19
33
  ###
@@ -5,5 +5,5 @@
5
5
  # use format '4.2.1.rc.1' ~> 4.2.1.rc to prerelease versions like v4.2.1.rc.2 and v4.2.1.rc.3
6
6
  ###
7
7
  module Coverband
8
- VERSION = "5.0.1"
8
+ VERSION = '5.1.0'
9
9
  end
@@ -42,6 +42,16 @@ class ReporterTest < Minitest::Test
42
42
  assert_equal [file_path], tracker.used_views.keys
43
43
  end
44
44
 
45
+ test "track partials that include the word _mailer in the path" do
46
+ Coverband::Collectors::ViewTracker.expects(:supported_version?).returns(true)
47
+ store = fake_store
48
+ file_path = "#{File.expand_path(Coverband.configuration.root)}/_mailer/file"
49
+ tracker = Coverband::Collectors::ViewTracker.new(store: store, roots: "dir")
50
+ tracker.track_views("name", "start", "finish", "id", identifier: file_path)
51
+ tracker.report_views_tracked
52
+ assert_equal [file_path], tracker.used_views.keys
53
+ end
54
+
45
55
  test "ignore partials that include the folder vendor in the path" do
46
56
  Coverband::Collectors::ViewTracker.expects(:supported_version?).returns(true)
47
57
  store = fake_store
@@ -43,4 +43,38 @@ if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.2.0")
43
43
  end
44
44
  end
45
45
  end
46
+
47
+ module Coverband
48
+ class AuthWebTest < Minitest::Test
49
+ include Rack::Test::Methods
50
+
51
+ def setup
52
+ super
53
+ @store = Coverband.configuration.store
54
+ Coverband.configure do |config|
55
+ config.password = "test_pass"
56
+ end
57
+ end
58
+
59
+ def app
60
+ Coverband::Reporters::Web.new
61
+ end
62
+
63
+ def teardown
64
+ super
65
+ end
66
+
67
+ test "renders index with basic auth" do
68
+ basic_authorize "anything", "test_pass"
69
+ get "/"
70
+ assert last_response.ok?
71
+ assert_match "Coverband Home", last_response.body
72
+ end
73
+
74
+ test "renders 401 auth error when not provided" do
75
+ get "/"
76
+ assert_equal 401, last_response.status
77
+ end
78
+ end
79
+ end
46
80
  end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path("../../test_helper", File.dirname(__FILE__))
4
+
5
+ if defined?(RubyVM::AbstractSyntaxTree)
6
+ require "coverband/utils/dead_methods"
7
+ module Coverband
8
+ module Utils
9
+ class DeadMethodsTest < Minitest::Test
10
+ attr_accessor :coverband
11
+
12
+ def setup
13
+ super
14
+ @coverband = Coverband::Collectors::Coverage.instance
15
+ end
16
+
17
+ def test_dog_dead_methods
18
+ file_path = require_unique_file
19
+ coverage = [nil, nil, 1, 1, 0, nil, nil]
20
+ dead_methods =
21
+ DeadMethods.scan(file_path: file_path, coverage: coverage)
22
+ assert_equal(1, dead_methods.length)
23
+ dead_method = dead_methods.first
24
+ assert_equal(4, dead_method.first_line_number)
25
+ assert_equal(6, dead_method.last_line_number)
26
+ assert_equal(file_path, dead_method.file_path)
27
+ end
28
+
29
+ def test_all_dead_methods
30
+ require_unique_file
31
+ @coverband.report_coverage
32
+ dead_methods = DeadMethods.scan_all
33
+ dead_method = dead_methods.find { |method| method.class_name == :Dog }
34
+ assert(dead_method)
35
+ assert_equal(4, dead_method.first_line_number)
36
+ assert_equal(6, dead_method.last_line_number)
37
+ end
38
+
39
+ def test_output_all
40
+ require_unique_file
41
+ @coverband.report_coverage
42
+ DeadMethods.output_all
43
+ end
44
+
45
+ def test_dog_methods_not_dead
46
+ file = require_unique_file
47
+ coverage = [nil, nil, 1, 1, 1, nil, nil]
48
+ assert_empty(DeadMethods.scan(file_path: file, coverage: coverage))
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path("../../test_helper", File.dirname(__FILE__))
4
+
5
+ if defined?(RubyVM::AbstractSyntaxTree)
6
+ require "coverband/utils/method_definition_scanner"
7
+ module Coverband
8
+ module Utils
9
+ class MethodBodyTest < Minitest::Test
10
+ def test_no_method_body_coverage
11
+ method_body =
12
+ MethodDefinitionScanner::MethodBody.new(
13
+ MethodDefinitionScanner::MethodDefinition.new(
14
+ first_line_number: 4,
15
+ last_line_number: 6,
16
+ name: :bark,
17
+ class_name: :Dog,
18
+ file_path: "./test/dog.rb"
19
+ )
20
+ )
21
+ refute(method_body.coverage?([nil, nil, 1, 1, 0, nil, 1]))
22
+ end
23
+
24
+ def test_method_body_coverage
25
+ method_body =
26
+ MethodDefinitionScanner::MethodBody.new(
27
+ MethodDefinitionScanner::MethodDefinition.new(
28
+ first_line_number: 4,
29
+ last_line_number: 6,
30
+ name: :bark,
31
+ class_name: :Dog,
32
+ file_path: "./test/dog.rb"
33
+ )
34
+ )
35
+ assert(method_body.coverage?([nil, nil, 1, 1, 1, nil, 1]))
36
+ end
37
+ end
38
+
39
+ class MethodDefinitionScannerTest < Minitest::Test
40
+ def test_scan
41
+ method_definitions = MethodDefinitionScanner.scan("./test/dog.rb")
42
+ assert(method_definitions)
43
+ assert_equal(1, method_definitions.length)
44
+ method_definition = method_definitions.first # assert_equal(4, method.first_line)
45
+ assert_equal(4, method_definition.first_line_number)
46
+ assert_equal(6, method_definition.last_line_number)
47
+ assert_equal(:bark, method_definition.name)
48
+ assert_equal(:Dog, method_definition.class_name)
49
+ end
50
+
51
+ def test_scan_large_class
52
+ method_definitions =
53
+ MethodDefinitionScanner.scan("./test/fixtures/casting_invitor.rb")
54
+ method_first_line_numbers =
55
+ method_definitions.map(&:first_line_number)
56
+ assert_equal(
57
+ [6, 13, 17, 35, 40, 44, 48, 52],
58
+ method_first_line_numbers
59
+ )
60
+ method_last_line_numbers = method_definitions.map(&:last_line_number)
61
+ assert_equal(
62
+ [11, 15, 31, 38, 42, 46, 50, 59],
63
+ method_last_line_numbers
64
+ )
65
+ method_names = method_definitions.map(&:name)
66
+ assert_equal(
67
+ %i[
68
+ initialize
69
+ valid?
70
+ deliver
71
+ invalid_invitees
72
+ invitee_list
73
+ valid_message?
74
+ valid_invitees?
75
+ create_invitation
76
+ ],
77
+ method_names
78
+ )
79
+ class_names = method_definitions.map(&:class_name)
80
+ assert_equal(8.times.map { :CastingInviter }, class_names)
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,60 @@
1
+ class CastingInviter
2
+ EMAIL_REGEX = /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/
3
+
4
+ attr_reader :message, :invitees, :casting
5
+
6
+ def initialize(attributes = {})
7
+ @message = attributes[:message] || ""
8
+ @invitees = attributes[:invitees] || ""
9
+ @sender = attributes[:sender]
10
+ @casting = attributes[:casting]
11
+ end
12
+
13
+ def valid?
14
+ valid_message? && valid_invitees?
15
+ end
16
+
17
+ def deliver
18
+ if valid?
19
+ invitee_list.each do |email|
20
+ invitation = create_invitation(email)
21
+ Mailer.invitation_notification(invitation, @message)
22
+ end
23
+ else
24
+ failure_message =
25
+ "Your #{
26
+ @casting
27
+ } message couldn’t be sent. Invitees emails or message are invalid"
28
+ invitation = create_invitation(@sender)
29
+ Mailer.invitation_notification(invitation, failure_message)
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def invalid_invitees
36
+ @invalid_invitees ||=
37
+ invitee_list.map { |item| item unless item.match(EMAIL_REGEX) }.compact
38
+ end
39
+
40
+ def invitee_list
41
+ @invitee_list ||= @invitees.gsub(/\s+/, "").split(/[\n,;]+/)
42
+ end
43
+
44
+ def valid_message?
45
+ @message.present?
46
+ end
47
+
48
+ def valid_invitees?
49
+ invalid_invitees.empty?
50
+ end
51
+
52
+ def create_invitation(email)
53
+ Invitation.create(
54
+ casting: @casting,
55
+ sender: @sender,
56
+ invitee_email: email,
57
+ status: "pending"
58
+ )
59
+ end
60
+ end
@@ -35,7 +35,7 @@ class RailsFullStackTest < Minitest::Test
35
35
  end
36
36
 
37
37
  # Test eager load data stored separately
38
- dummy_controller = "./test/rails#{Rails::VERSION::MAJOR}_dummy/app/controllers/dummy_controller.rb"
38
+ dummy_controller = "./app/controllers/dummy_controller.rb"
39
39
  store.type = :eager_loading
40
40
  eager_expected = [1, 1, 0, nil, nil]
41
41
  results = store.coverage[dummy_controller]["data"]
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path("../rails_test_helper", File.dirname(__FILE__))
4
+
5
+ class RailsFullStackTest < Minitest::Test
6
+ include Capybara::DSL
7
+ include Capybara::Minitest::Assertions
8
+
9
+ def setup
10
+ super
11
+ rails_setup
12
+ Coverband.report_coverage
13
+ end
14
+
15
+ def teardown
16
+ super
17
+ Capybara.reset_sessions!
18
+ Capybara.use_default_driver
19
+ end
20
+
21
+ test "verify erb haml slim support" do
22
+ visit "/dummy_view/show"
23
+ assert_content("I am no dummy view tracker text")
24
+ Coverband.report_coverage
25
+ Coverband.configuration.view_tracker&.report_views_tracked
26
+ visit "/coverage/view_tracker"
27
+ assert_content("Used Views: (1)")
28
+ assert_content("Unused Views: (2)")
29
+ assert_selector("li.used-views", text: "dummy_view/show.html.erb")
30
+ assert_selector("li.unused-views", text: "dummy_view/show_haml.html.haml")
31
+ assert_selector("li.unused-views", text: "dummy_view/show_slim.html.slim")
32
+
33
+ visit "/dummy_view/show_haml"
34
+ assert_content("I am haml text")
35
+ Coverband.report_coverage
36
+ Coverband.configuration.view_tracker&.report_views_tracked
37
+ visit "/coverage/view_tracker"
38
+ assert_content("Used Views: (2)")
39
+ assert_content("Unused Views: (1)")
40
+ assert_selector("li.used-views", text: "dummy_view/show_haml.html.haml")
41
+
42
+ visit "/dummy_view/show_slim"
43
+ assert_content("I am slim text")
44
+ Coverband.report_coverage
45
+ Coverband.configuration.view_tracker&.report_views_tracked
46
+ visit "/coverage/view_tracker"
47
+ assert_content("Used Views: (3)")
48
+ assert_content("Unused Views: (0)")
49
+ assert_selector("li.used-views", text: "dummy_view/show_slim.html.slim")
50
+ end
51
+ end