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 +4 -4
- data/CHANGELOG.md +18 -0
- data/README.md +4 -1
- data/lib/routes_coverage/auditor.rb +156 -0
- data/lib/routes_coverage/middleware.rb +2 -20
- data/lib/routes_coverage/result.rb +14 -12
- data/lib/routes_coverage/version.rb +1 -1
- data/lib/routes_coverage.rb +55 -5
- data/routes_coverage.gemspec +1 -1
- metadata +7 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4f992227e3172a9ee4ea678d302b2d6655cdf16feed78020947375cc627a0254
|
4
|
+
data.tar.gz: 4f5dae5790bac97b3675d085d771a17175e8744f00fed659625592dc471c8a27
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
13
|
-
|
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
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
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
|
-
|
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
|
data/lib/routes_coverage.rb
CHANGED
@@ -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 :
|
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
|
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
|
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
|
data/routes_coverage.gemspec
CHANGED
@@ -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"
|
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.
|
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:
|
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:
|
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:
|
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.
|
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
|