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 +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
|