routes_coverage 0.5.1 → 0.7.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 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