routes_coverage 0.5.1 → 0.7.0

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
  SHA256:
3
- metadata.gz: 6b8b13d46985bf003169ae46c148f11a0601baae2d06281553d5baf307b9a80b
4
- data.tar.gz: 9cd59c55dae847b403d6d46cae8e0a98e0a3cb584a69887e80bc78369138a748
3
+ metadata.gz: 4f992227e3172a9ee4ea678d302b2d6655cdf16feed78020947375cc627a0254
4
+ data.tar.gz: 4f5dae5790bac97b3675d085d771a17175e8744f00fed659625592dc471c8a27
5
5
  SHA512:
6
- metadata.gz: 91e649e374663f6c6e8ac187eb452b0ea3117037fbf51f92216aa4789ad64828e744fa1ba6e677c4e77b4062a50125df03bf3099866abc6fa6871d557d333168
7
- data.tar.gz: 3230aa30d9332d6df0b7b3549ad8b1b22b34934da67db613cbf74b0288982125523c1c073cda0587383224f7610a7ac070953ccd79aa3e622d5e638683c51f8c
6
+ metadata.gz: 97e2aaf45f30572215e26fe51370257f00ee92129c38404050f569d5faf3d290690ddd1f5b5022c366edbb09b3b3fe5b16e9c6a50814ce86c2a4d6fc313d8be0
7
+ data.tar.gz: 432330b7f4a1641f03bb9bcfed7dc2a85868a2b6637cc2ba3015454471f1bc7a33889730feb7fdb8257b0456a536defbceeff320c88a899d4dd19500882fe2f4
data/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ -
6
+
7
+ ## 0.7.0
8
+
9
+ - Support for Rails 7 (added tests, main code was already working)
10
+ - Fixed: errors on Rails 3 and ruby 2.3
11
+ - New feature: `require 'routes_coverage/auditor'; RoutesCoverage::Auditor.new.print_missing_actions` detects actions present in routes, but not present in controllers.
12
+ `print_unused_actions` - the other direction. Useful for routes cleanup.
13
+
14
+ - Known bug: collecting coverage data from engines is still not supported :(
15
+
16
+ ## <= 0.6.0
17
+
18
+ In commit history, sorry
data/README.md CHANGED
@@ -37,6 +37,7 @@ RSpec.configure do |config|
37
37
  config.routes_coverage.perform_report = ENV['ROUTES_COVERAGE'] # only generate report if env var is set
38
38
 
39
39
  config.routes_coverage.exclude_put_fallbacks = true # exclude non-hit PUT-requests where a matching PATCH exists
40
+ config.routes_coverage.include_from_controller_tests = true # include results from controller tests
40
41
  config.routes_coverage.exclude_patterns << %r{PATCH /reqs} # excludes all requests matching regex
41
42
  config.routes_coverage.exclude_namespaces << 'somenamespace' # excludes /somenamespace/*
42
43
 
@@ -70,7 +71,9 @@ or
70
71
  RoutesCoverage.settings.format = :full_text
71
72
  ```
72
73
 
73
-
74
+ Note that coverage from `include_from_controller_tests` (disabled by default) is not a true routes coverage.
75
+ Rounting is not tested in controller tests (which are deprecated in Rails 5),
76
+ but sometimes you may already have a lot of controller tests and an intent to improve green-path/business level coverage
74
77
 
75
78
  ## Development
76
79
 
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RoutesCoverage
4
+ class Auditor
5
+ def logger
6
+ @logger ||= Logger.new($stdout).tap do |log|
7
+ log.formatter = ->(_severity, _datetime, _progname, msg) { "#{msg}\n" }
8
+ end
9
+ end
10
+
11
+ def controllers
12
+ @controllers ||= begin
13
+ logger.info "Eager-loading app to collect controllers"
14
+ Rails.application.eager_load!
15
+
16
+ logger.info "Collecting controllers"
17
+ if defined?(ActionController::API)
18
+ ActionController::Base.descendants + ActionController::API.descendants
19
+ else
20
+ # older rails
21
+ ActionController::Base.descendants
22
+ end
23
+ end
24
+ end
25
+
26
+ def controllers_hash
27
+ @controllers_hash ||= controllers.index_by { |controller| controller.name.sub(/Controller$/, "").underscore }
28
+ end
29
+
30
+ def controller_class_by_name(controller_name)
31
+ controller = controllers_hash[controller_name]
32
+ return controller if controller
33
+
34
+ @missing_controllers ||= Set.new
35
+ return if @missing_controllers.include?(controller_name)
36
+
37
+ controllers_hash[controller_name] ||= "#{controller_name}_controller".classify.constantize
38
+ logger.warn "Controller #{controller_name} was not collected, but exists"
39
+ controllers_hash[controller_name]
40
+ rescue ArgumentError => e
41
+ @missing_controllers << controller_name
42
+ logger.warn "Controller #{controller_name} failed to load: #{e}"
43
+ nil
44
+ rescue NameError
45
+ @missing_controllers << controller_name
46
+ logger.warn "Controller #{controller_name} looks not existing"
47
+ nil
48
+ end
49
+
50
+ def existing_actions_usage_hash
51
+ @existing_actions_usage_hash ||= begin
52
+ logger.info "Collecting actions"
53
+ controller_actions = controllers.map do |controller|
54
+ # cannot use controller.controller_name - it has no namespace, same thing without demodulize:
55
+ controller_name = controller.name.sub(/Controller$/, "").underscore
56
+ controller.action_methods.map { |action| "#{controller_name}##{action}" }
57
+ end
58
+ controller_actions.flatten.map { |action| [action, 0] }.to_h
59
+ end
60
+ end
61
+
62
+ def all_routes
63
+ # NB: there're no engines
64
+ @all_routes ||= RoutesCoverage._collect_all_routes
65
+ end
66
+
67
+ def perform
68
+ require 'routes_coverage'
69
+ routes = all_routes
70
+
71
+ @missing_actions = Hash.new(0)
72
+ @existing_actions_usage_hash = nil
73
+ routes.each do |route|
74
+ next unless route.respond_to?(:requirements) && route.requirements[:controller]
75
+
76
+ action = "#{route.requirements[:controller]}##{route.requirements[:action]}"
77
+ if existing_actions_usage_hash[action]
78
+ existing_actions_usage_hash[action] += 1
79
+ else
80
+ # there may be inheritance or implicit renders
81
+ controller_instance = controller_class_by_name(route.requirements[:controller])&.new
82
+ unless controller_instance&.available_action?(route.requirements[:action])
83
+ if controller_instance.respond_to?(route.requirements[:action])
84
+ logger.warn "No action, but responds: #{action}"
85
+ end
86
+ @missing_actions[action] += 1
87
+ end
88
+ end
89
+ end
90
+ end
91
+
92
+ def missing_actions
93
+ perform unless @missing_actions
94
+ @missing_actions
95
+ end
96
+
97
+ def unused_actions
98
+ perform unless @existing_actions_usage_hash
99
+
100
+ root = "#{Rails.root}/" # rubocop:disable Rails/FilePath
101
+ @unused_actions ||= begin
102
+ # methods with special suffixes are obviously not actions, reduce noise:
103
+ unused_actions_from_hash = existing_actions_usage_hash.reject do |action, count|
104
+ count.positive? || action.end_with?('?') || action.end_with?('!') || action.end_with?('=')
105
+ end
106
+
107
+ unused_actions_from_hash.keys.map do |action|
108
+ controller_name, action_name = action.split('#', 2)
109
+ controller = controller_class_by_name(controller_name)&.new
110
+ method = controller.method(action_name.to_sym)
111
+ if method&.source_location && method.source_location.first.start_with?(root)
112
+ "#{method.source_location.first.sub(root, '')}:#{method.source_location.second} - #{action}"
113
+ else
114
+ action
115
+ end
116
+ end.uniq.sort
117
+ end
118
+ end
119
+
120
+ def print_missing_actions
121
+ logger.info "\nMissing #{missing_actions.count} actions:"
122
+
123
+ # NB: for singular `resource` there may be unnecessary `index` in suggestions
124
+ restful_actions = %w[index new create show edit update destroy].freeze
125
+
126
+ declared_restful = all_routes.select { |route|
127
+ route.respond_to?(:requirements) && route.requirements[:controller] &&
128
+ restful_actions.include?(route.requirements[:action])
129
+ }.group_by{ |route| route.requirements[:controller]}
130
+
131
+ missing_actions.keys.map { |action| action.split('#', 2) }.group_by(&:first).each do |(controller, actions)|
132
+ missing = actions.map(&:last)
133
+ next if missing.empty?
134
+
135
+ undeclared_restful = restful_actions - declared_restful[controller].map{|r| r.requirements[:action] }
136
+ logger.info([
137
+ "#{controller}:",
138
+ (if (restful_actions & missing).any?
139
+ "#{(missing & restful_actions).join(', ')}"\
140
+ ", except: %i[#{(restful_actions & (missing + undeclared_restful)).join(' ')}]"\
141
+ ", only: %i[#{(restful_actions - (missing + undeclared_restful)).join(' ')}]"
142
+ end),
143
+ (if (missing - restful_actions).any?
144
+ ", Missing custom: #{(missing - restful_actions).join(', ')}"
145
+ end)
146
+
147
+ ].compact.join(' '))
148
+ end
149
+ end
150
+
151
+ def print_unused_actions
152
+ logger.info "Unused #{unused_actions.count} actions:"
153
+ unused_actions.each { |action| logger.info action }
154
+ end
155
+ end
156
+ end
@@ -9,27 +9,9 @@ module RoutesCoverage
9
9
  def call(original_env)
10
10
  # router changes env/request during recognition so need a copy:
11
11
  env = original_env.dup
12
- req = ::Rails.application.routes.request_class.new env
13
- ::Rails.application.routes.router.recognize(req) do |route, parameters5, parameters4|
14
- parameters = parameters5 || parameters4
15
- dispatcher = route.app
16
- if dispatcher.respond_to?(:dispatcher?)
17
- req.path_parameters = parameters
18
- dispatcher = nil unless dispatcher.matches?(req) # && dispatcher.dispatcher?
19
- else # rails < 4.2
20
- dispatcher = route.app
21
- req.env['action_dispatch.request.path_parameters'] =
22
- (env['action_dispatch.request.path_parameters'] || {}).merge(parameters)
23
- while dispatcher.is_a?(ActionDispatch::Routing::Mapper::Constraints)
24
- dispatcher = (dispatcher.app if dispatcher.matches?(env))
25
- end
26
- end
27
- next unless dispatcher
12
+ req = ::Rails.application.routes.request_class.new(env)
13
+ RoutesCoverage._touch_request(req)
28
14
 
29
- RoutesCoverage._touch_route(route)
30
- # there may be multiple matching routes - we should match only first
31
- break
32
- end
33
15
  # TODO: detect 404s? and maybe other route errors?
34
16
  @app.call(original_env)
35
17
  end
@@ -90,20 +90,22 @@ module RoutesCoverage
90
90
  attr_reader :all_routes, :route_hit_counts
91
91
 
92
92
  def expected_routes
93
- return @expected_routes if @expected_routes
94
-
95
- filter_regex = Regexp.union(@settings.exclude_patterns)
96
- namespaces_regex = Regexp.union(@settings.exclude_namespaces.map { |n| %r{^/#{n}} })
93
+ @expected_routes ||= begin
94
+ filter_regex = Regexp.union(@settings.exclude_patterns)
95
+ namespaces_regex = Regexp.union(@settings.exclude_namespaces.map { |n| %r{^/#{n}} })
96
+
97
+ routes_groups = all_routes.group_by do |r|
98
+ # rails <=4 has regex in verb
99
+ verb = r.verb.is_a?(Regexp) && r.verb.inspect.gsub(/[^\w]/, '') || r.verb
100
+ (
101
+ ("#{verb} #{r.path.spec}".strip =~ filter_regex) ||
102
+ (r.path.spec.to_s =~ namespaces_regex)
103
+ ).present?
104
+ end
97
105
 
98
- routes_groups = all_routes.group_by do |r|
99
- (
100
- ("#{r.verb.to_s[8..-3]} #{r.path.spec}".strip =~ filter_regex) ||
101
- (r.path.spec.to_s =~ namespaces_regex)
102
- ).present?
106
+ @excluded_routes = routes_groups[true] || []
107
+ routes_groups[false] || []
103
108
  end
104
-
105
- @excluded_routes = routes_groups[true] || []
106
- @expected_routes = routes_groups[false] || []
107
109
  end
108
110
 
109
111
  def pending_routes
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RoutesCoverage
4
- VERSION = "0.5.1"
4
+ VERSION = "0.7.0"
5
5
  end
@@ -10,17 +10,43 @@ require "routes_coverage/formatters/full_text"
10
10
  require "routes_coverage/formatters/html"
11
11
 
12
12
  module RoutesCoverage
13
+ module ActionControllerTestCase
14
+ def process(action, *args)
15
+ return super unless RoutesCoverage.settings.include_from_controller_tests
16
+
17
+ super.tap { RoutesCoverage._touch_request(@request) }
18
+ end
19
+ end
20
+
21
+ module ActionControllerTestCaseKvargs
22
+ def process(action, **kvargs)
23
+ return super unless RoutesCoverage.settings.include_from_controller_tests
24
+
25
+ super.tap { RoutesCoverage._touch_request(@request) }
26
+ end
27
+ end
28
+
13
29
  class Railtie < ::Rails::Railtie
14
30
  railtie_name :routes_coverage
15
31
 
16
32
  initializer "request_coverage.inject_test_middleware" do
17
33
  ::Rails.application.middleware.use RoutesCoverage::Middleware if RoutesCoverage.enabled?
34
+
35
+ ActiveSupport.on_load(:action_controller_test_case) do |klass|
36
+ if Rails.version >= '5.1'
37
+ klass.prepend RoutesCoverage::ActionControllerTestCaseKvargs
38
+ else
39
+ klass.prepend RoutesCoverage::ActionControllerTestCase
40
+ end
41
+ end
18
42
  end
19
43
  end
20
44
 
21
45
  class Settings
22
46
  attr_reader :exclude_patterns, :exclude_namespaces, :groups
23
- attr_accessor :exclude_put_fallbacks, :perform_report, :minimum_coverage, :round_precision, :format
47
+ attr_accessor :perform_report, :format, :minimum_coverage, :round_precision,
48
+ :exclude_put_fallbacks,
49
+ :include_from_controller_tests
24
50
 
25
51
  def initialize
26
52
  @exclude_patterns = []
@@ -90,7 +116,7 @@ module RoutesCoverage
90
116
  end
91
117
 
92
118
  if ungroupped_routes.any?
93
- groups["Ungroupped"] = Result.new(ungroupped_routes, route_hit_count.slice(ungroupped_routes), settings)
119
+ groups["Ungroupped"] = Result.new(ungroupped_routes, route_hit_count.slice(*ungroupped_routes), settings)
94
120
  end
95
121
  end
96
122
 
@@ -144,7 +170,7 @@ module RoutesCoverage
144
170
  when :constraints
145
171
  value.all? do |constraint_name, constraint_value|
146
172
  if constraint_value.present?
147
- route.constraints[constraint_name] && route.constraints[constraint_name].match?(constraint_value)
173
+ route.constraints[constraint_name] && route.constraints[constraint_name].match(constraint_value)
148
174
  else
149
175
  route.constraints[constraint_name].blank?
150
176
  end
@@ -152,14 +178,38 @@ module RoutesCoverage
152
178
  end
153
179
  end
154
180
  else
155
- route.path.spec.to_s.match?(matcher)
181
+ route.path.spec.to_s.match(matcher)
156
182
  end
157
183
  end
158
184
 
159
- [group_name, Result.new(group_routes, route_hit_count.slice(group_routes), settings)]
185
+ [group_name, Result.new(group_routes, route_hit_count.slice(*group_routes), settings)]
160
186
  end.to_h
161
187
  end
162
188
 
189
+ # NB: router changes env/request during recognition
190
+ def self._touch_request(req)
191
+ ::Rails.application.routes.router.recognize(req) do |route, parameters5, parameters4|
192
+ parameters = parameters5 || parameters4
193
+ dispatcher = route.app
194
+ if dispatcher.respond_to?(:dispatcher?)
195
+ req.path_parameters = parameters
196
+ dispatcher = nil unless dispatcher.matches?(req) # && dispatcher.dispatcher?
197
+ else # rails < 4.2
198
+ dispatcher = route.app
199
+ req.env['action_dispatch.request.path_parameters'] =
200
+ (req.env['action_dispatch.request.path_parameters'] || {}).merge(parameters)
201
+ while dispatcher.is_a?(ActionDispatch::Routing::Mapper::Constraints)
202
+ dispatcher = (dispatcher.app if dispatcher.matches?(req.env))
203
+ end
204
+ end
205
+ next unless dispatcher
206
+
207
+ RoutesCoverage._touch_route(route)
208
+ # there may be multiple matching routes - we should match only first
209
+ break
210
+ end
211
+ end
212
+
163
213
  def self._touch_route(route)
164
214
  reset! unless route_hit_count
165
215
  route_hit_count[route] += 1
@@ -26,7 +26,7 @@ Gem::Specification.new do |spec|
26
26
  spec.require_paths = ["lib"]
27
27
 
28
28
  spec.add_development_dependency 'appraisal'
29
- spec.add_development_dependency "bundler", ">= 2.2.10"
29
+ spec.add_development_dependency "bundler" #, ">= 2.2.10"
30
30
  spec.add_development_dependency "minitest"
31
31
  spec.add_development_dependency "rake", ">= 12.3.3"
32
32
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: routes_coverage
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vasily Fedoseyev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-11-13 00:00:00.000000000 Z
11
+ date: 2022-04-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: appraisal
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: 2.2.10
33
+ version: '0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: 2.2.10
40
+ version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: minitest
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -74,6 +74,7 @@ executables: []
74
74
  extensions: []
75
75
  extra_rdoc_files: []
76
76
  files:
77
+ - CHANGELOG.md
77
78
  - LICENSE.txt
78
79
  - README.md
79
80
  - compiled_assets/routes.css
@@ -82,6 +83,7 @@ files:
82
83
  - lib/routes_coverage/adapters/atexit.rb
83
84
  - lib/routes_coverage/adapters/rspec.rb
84
85
  - lib/routes_coverage/adapters/simplecov.rb
86
+ - lib/routes_coverage/auditor.rb
85
87
  - lib/routes_coverage/formatters/base.rb
86
88
  - lib/routes_coverage/formatters/full_text.rb
87
89
  - lib/routes_coverage/formatters/html.rb
@@ -111,7 +113,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
111
113
  - !ruby/object:Gem::Version
112
114
  version: '0'
113
115
  requirements: []
114
- rubygems_version: 3.0.9
116
+ rubygems_version: 3.3.3
115
117
  signing_key:
116
118
  specification_version: 4
117
119
  summary: Provides coverage report for your rails routes