routes_coverage 0.4.3 → 0.6.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
- SHA1:
3
- metadata.gz: 66fbe57b380af966b736a53bfeeccf4c27600825
4
- data.tar.gz: aebe5c80b8aac0abdcc4f13f8cade0ea801d4b56
2
+ SHA256:
3
+ metadata.gz: b697ab555d0362fa18dae5161266efbeb99eff4c1f276f7d06039952a6fc2f7c
4
+ data.tar.gz: 5e0fbeb4d54958bfb46f307c42ef5a54ceec49af49f699943881c6a665cca251
5
5
  SHA512:
6
- metadata.gz: 8a64c5c7fc2014e3f6dc6cb530c9aaae53140c6eadcdf8df96ad82d77f6acd7ec94f4a40d815177e503380aa306f5ae4bda1e7b2812850684bfe3c7c36f0cc05
7
- data.tar.gz: f5b7376fed952b708fca9747b2e2c0b13a065bd86e8185882b3845dbb27750fbded954007ea5541f9123dc07c04992febeb139343d0a2ed82c58745d3cdd726e
6
+ metadata.gz: c810351185cd998b8370c357d28bb06736e1c3de4202e12aab641b7ab22ed182f9e9f2b31a4cefe54dd9cdd52fe6a20a0187b088649e6652107b9edfce6bad39
7
+ data.tar.gz: e7af918230387e897334154962115d6d6f9b6e38d1e870bd5070529cf88d8fc2708cf6c57baf1d9f1f66b14beacd62f5eb1ae21d33de41ed279e671e4dcf7449
data/README.md CHANGED
@@ -37,10 +37,12 @@ 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
 
43
44
  config.routes_coverage.groups["Some Route group title"] = %r{^/somespace/}
45
+ config.routes_coverage.groups["Subdomain"] = { constraints: { subdomain: 'some_subdomain' }, path: '/' }
44
46
  config.routes_coverage.groups["Admin"] = Regexp.union([
45
47
  %r{^/admin/},
46
48
  %r{^/secret_place/},
@@ -69,7 +71,9 @@ or
69
71
  RoutesCoverage.settings.format = :full_text
70
72
  ```
71
73
 
72
-
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
73
77
 
74
78
  ## Development
75
79
 
@@ -1,14 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RoutesCoverage
2
4
  module Adapters
3
5
  class AtExit
4
- def self.use coverer=nil
5
- #NB: at_exit order is important, for example minitest uses it to run, need to install our handler before it
6
+ def self.use(_coverer = nil)
7
+ # NB: at_exit order is important, for example minitest uses it to run, need to install our handler before it
6
8
 
7
9
  RoutesCoverage.reset!
8
10
  at_exit do
9
11
  next if RoutesCoverage.pid != Process.pid
12
+
10
13
  RoutesCoverage.perform_report
11
- exit
14
+ exit # rubocop:disable Rails/Exit
12
15
  end
13
16
  end
14
17
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rspec/core'
2
4
 
3
5
  RSpec.configure do |config|
@@ -1,13 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RoutesCoverage
2
4
  module Adapters
3
5
  class SimpleCov
4
6
  def self.use
5
7
  RoutesCoverage.reset!
6
8
  prev_block = ::SimpleCov.at_exit
7
- ::SimpleCov.at_exit{
9
+ ::SimpleCov.at_exit do
8
10
  RoutesCoverage.perform_report
9
11
  prev_block.call
10
- }
12
+ end
11
13
  end
12
14
  end
13
15
  end
@@ -1,15 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RoutesCoverage
2
4
  module Formatters
3
5
  class Base
4
- def initialize result, groups, settings
6
+ def initialize(result, groups, settings)
5
7
  @result = result
6
8
  @groups = groups
7
9
  @settings = settings
8
10
  end
9
11
 
10
- attr_reader :result
11
- attr_reader :groups
12
- attr_reader :settings
12
+ attr_reader :result, :groups, :settings
13
13
  end
14
14
  end
15
15
  end
@@ -1,11 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RoutesCoverage
2
4
  module Formatters
3
5
  class FullText < SummaryText
4
-
5
6
  class RouteFormatter
6
7
  attr_reader :buffer
7
8
 
8
- def initialize result=nil, _settings=nil, output_hits=false
9
+ def initialize(result = nil, _settings = nil, output_hits: false)
9
10
  @buffer = []
10
11
  @result = result
11
12
  @output_hits = output_hits
@@ -28,49 +29,47 @@ module RoutesCoverage
28
29
  @buffer << draw_header(routes)
29
30
  end
30
31
 
31
- def no_routes _routes_from_rails5=nil
32
+ def no_routes(_routes_from_rails5 = nil)
32
33
  @buffer << "\tNone"
33
34
  end
34
35
 
35
36
  private
36
- HEADER = ['Prefix', 'Verb', 'URI Pattern', 'Controller#Action']
37
+
38
+ HEADER = ['Prefix', 'Verb', 'URI Pattern', 'Controller#Action'].freeze
37
39
  def draw_section(routes)
38
40
  header_lengths = HEADER.map(&:length)
39
41
  name_width, verb_width, path_width, reqs_width = widths(routes).zip(header_lengths).map(&:max)
40
42
 
41
- hits = nil
42
43
  routes.map do |r|
43
- # puts "route is #{r.inspect}"
44
- if @output_hits
45
- # hits = " ?"
46
- if r[:original].respond_to?(:__getobj__)
47
- original_route = r[:original].__getobj__ # SimpleDelegator
48
- else
49
- original_route = r[:original]
50
- end
51
- hits = " #{@result.route_hit_counts[original_route]}"
52
- end
53
- "#{r[:name].rjust(name_width) if @output_prefix} #{r[:verb].ljust(verb_width)} #{r[:path].ljust(path_width)} #{r[:reqs].ljust(reqs_width)}#{hits}"
44
+ # unwrap SimpleDelegator
45
+ original_route = r[:original].respond_to?(:__getobj__) ? r[:original].__getobj__ : r[:original]
46
+
47
+ "#{r[:name].rjust(name_width) if @output_prefix} "\
48
+ "#{r[:verb].ljust(verb_width)} #{r[:path].ljust(path_width)} #{r[:reqs].ljust(reqs_width)}"\
49
+ "#{" #{@result.route_hit_counts[original_route]}" if @output_hits}"
54
50
  end
55
51
  end
56
52
 
57
53
  def draw_header(routes)
58
54
  name_width, verb_width, path_width, reqs_width = widths(routes)
59
55
 
60
- "#{"Prefix".rjust(name_width) if @output_prefix} #{"Verb".ljust(verb_width)} #{"URI Pattern".ljust(path_width)} #{"Controller#Action".ljust(reqs_width)}#{' Hits' if @output_hits}"
56
+ [
57
+ ('Prefix'.rjust(name_width) if @output_prefix),
58
+ 'Verb'.ljust(verb_width),
59
+ 'URI Pattern'.ljust(path_width),
60
+ 'Controller#Action'.ljust(reqs_width),
61
+ ('Hits' if @output_hits)
62
+ ].compact.join(' ')
61
63
  end
62
64
 
63
65
  def widths(routes)
64
- [routes.map { |r| r[:name].length }.max || 0,
65
- routes.map { |r| r[:verb].length }.max || 0,
66
- routes.map { |r| r[:path].length }.max || 0,
67
- routes.map { |r| r[:reqs].length }.max || 0,
68
- ]
66
+ %i[name verb path reqs].map do |key|
67
+ routes.map { |r| r[key].length }.max.to_i
68
+ end
69
69
  end
70
70
  end
71
71
 
72
-
73
- def routes_section formatter, title, routes
72
+ def routes_section(formatter, title, routes)
74
73
  formatter.buffer << title
75
74
 
76
75
  if routes.none?
@@ -83,24 +82,27 @@ module RoutesCoverage
83
82
  formatter.result
84
83
  end
85
84
 
86
- def hit_routes
87
- routes = result.hit_routes
85
+ def collect_routes(routes)
86
+ # rails 3
87
+ return Result::Inspector.new.collect_all_routes(routes) unless Result::Inspector::NEW_RAILS
88
+
89
+ Result::Inspector.new(routes).collect_all_routes
90
+ end
91
+
92
+ def hit_routes_details
88
93
  # engine routes now are in the same list
89
- if Result::Inspector::NEW_RAILS
90
- hit_routes = Result::Inspector.new(result.hit_routes).collect_all_routes
91
- pending_routes = Result::Inspector.new(result.pending_routes).collect_all_routes
92
- else
93
- #rails 3
94
- hit_routes = Result::Inspector.new.collect_all_routes(result.hit_routes)
95
- pending_routes = Result::Inspector.new.collect_all_routes(result.pending_routes)
96
- end
94
+ hit_routes = collect_routes(result.hit_routes)
95
+ pending_routes = collect_routes(result.pending_routes)
96
+
97
+ <<~TXT
98
+ #{routes_section(RouteFormatter.new(result, settings, output_hits: true), 'Covered routes:', hit_routes)}
97
99
 
98
- return routes_section(RouteFormatter.new(result, settings, true), "Covered routes:", hit_routes) + "\n\n" +
99
- routes_section(RouteFormatter.new(result, settings), "Pending routes:", pending_routes)
100
+ #{routes_section(RouteFormatter.new(result, settings), 'Pending routes:', pending_routes)}
101
+ TXT
100
102
  end
101
103
 
102
104
  def format
103
- "#{super}\n\n#{hit_routes}"
105
+ "#{super}\n\n#{hit_routes_details}"
104
106
  end
105
107
  end
106
108
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "erb"
2
4
  require "cgi"
3
5
  require "fileutils"
@@ -23,24 +25,25 @@ module RoutesCoverage
23
25
  ERB.new(File.read(File.join(File.dirname(__FILE__), "html_views", "#{name}.erb")))
24
26
  end
25
27
 
26
-
27
28
  def root(root = nil)
28
- #TODO: config for this
29
+ # TODO: config for this
29
30
  return SimpleCov.root if defined? SimpleCov
30
31
  return @root if defined?(@root) && root.nil?
32
+
31
33
  @root = File.expand_path(root || Dir.getwd)
32
34
  end
33
35
 
34
36
  def coverage_dir(dir = nil)
35
- #TODO: config for this
37
+ # TODO: config for this
36
38
  return SimpleCov.coverage_dir if defined? SimpleCov
37
39
  return @coverage_dir if defined?(@coverage_dir) && dir.nil?
38
- @coverage_path = nil # invalidate cache
40
+
41
+ @output_path = nil # invalidate cache
39
42
  @coverage_dir = (dir || "coverage")
40
43
  end
41
44
 
42
45
  def output_path
43
- @coverage_path ||= File.expand_path(coverage_dir, root).tap do |path|
46
+ @output_path ||= File.expand_path(coverage_dir, root).tap do |path|
44
47
  FileUtils.mkdir_p path
45
48
  end
46
49
  end
@@ -51,8 +54,8 @@ module RoutesCoverage
51
54
  @project_name ||= File.basename(root.split("/").last).capitalize.tr("_", " ")
52
55
  end
53
56
 
54
- def asset_content name
55
- File.read(File.expand_path("../../../compiled_assets/#{name}", File.dirname(__FILE__)))
57
+ def asset_content(name)
58
+ File.read(File.expand_path("../../../compiled_assets/#{name}", __dir__))
56
59
  end
57
60
 
58
61
  def style_asset_link
@@ -83,33 +86,23 @@ module RoutesCoverage
83
86
  end
84
87
  end
85
88
 
86
- def hits_css_class hits
87
- hits > 0 ? 'cov' : 'uncov'
89
+ def hits_css_class(hits)
90
+ hits.positive? ? 'cov' : 'uncov'
88
91
  end
89
92
 
90
93
  def timeago(time)
91
94
  "<abbr class=\"timeago\" title=\"#{time.iso8601}\">#{time.iso8601}</abbr>"
92
95
  end
93
96
 
94
-
95
97
  def all_result_groups
96
- return @all_result_groups if @all_result_groups
97
- @all_result_groups = [
98
- {
99
- id: 'all_routes',
100
- name: 'All Routes',
101
- result: result,
102
- }
103
- ]
104
- @all_result_groups += groups.map do |group_name, group_result|
105
- {
106
- id: group_name.gsub(/^[^a-zA-Z]+/, "").gsub(/[^a-zA-Z0-9\-\_]/, ""),
107
- name: group_name,
108
- result: group_result,
109
- }
110
- end
111
-
112
- @all_result_groups
98
+ @all_result_groups ||= [{ id: 'all_routes', name: 'All Routes', result: result }] +
99
+ groups.map do |group_name, group_result|
100
+ {
101
+ id: group_name.gsub(/^[^a-zA-Z]+/, "").gsub(/[^a-zA-Z0-9\-_]/, ""),
102
+ name: group_name,
103
+ result: group_result
104
+ }
105
+ end
113
106
  end
114
107
  end
115
108
  end
@@ -1,15 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RoutesCoverage
2
4
  module Formatters
3
5
  class SummaryText < Base
4
- def hits_count result
5
- "#{result.hit_routes_count} of #{result.expected_routes_count}#{"(#{result.total_count} total)" if result.expected_routes_count != result.total_count} routes hit#{ " at #{result.avg_hits} hits average" if result.hit_routes_count > 0}"
6
+ def hits_count(result)
7
+ "#{result.hit_routes_count} of #{result.expected_routes_count}"\
8
+ "#{"(#{result.total_count} total)" if result.expected_routes_count != result.total_count}"\
9
+ " routes hit#{" at #{result.avg_hits} hits average" if result.hit_routes_count.positive?}"
6
10
  end
7
11
 
8
12
  def status
9
13
  return unless settings.minimum_coverage
10
- unless result.coverage_pass?
11
- "Coverage is too low"
12
- end
14
+
15
+ "Coverage is too low" unless result.coverage_pass?
13
16
  end
14
17
 
15
18
  def format
@@ -18,9 +21,9 @@ module RoutesCoverage
18
21
  ]
19
22
 
20
23
  if groups.any?
21
- buffer += groups.map{|group_name, group_result|
24
+ buffer += groups.map do |group_name, group_result|
22
25
  " #{group_name}: #{group_result.coverage}% (#{hits_count group_result})"
23
- }
26
+ end
24
27
  end
25
28
 
26
29
  buffer << status
@@ -1,39 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RoutesCoverage
2
4
  class Middleware
3
- def initialize app
5
+ def initialize(app)
4
6
  @app = app
5
7
  end
6
8
 
7
- def call original_env
9
+ def call(original_env)
8
10
  # router changes env/request during recognition so need a copy:
9
11
  env = original_env.dup
10
- req = ::Rails.application.routes.request_class.new env
11
- ::Rails.application.routes.router.recognize(req) do |route, parameters5, parameters4|
12
- parameters = parameters5 || parameters4
13
- dispatcher = route.app
14
- if dispatcher.respond_to?(:dispatcher?)
15
- req.path_parameters = parameters
16
- unless dispatcher.matches?(req) # && dispatcher.dispatcher?
17
- dispatcher = nil
18
- end
19
- else # rails < 4.2
20
- dispatcher = route.app
21
- req.env['action_dispatch.request.path_parameters'] = (env['action_dispatch.request.path_parameters'] || {}).merge(parameters)
22
- while dispatcher.is_a?(ActionDispatch::Routing::Mapper::Constraints) do
23
- if dispatcher.matches?(env)
24
- dispatcher = dispatcher.app
25
- else
26
- dispatcher = nil
27
- end
28
- end
29
- end
30
- next unless dispatcher
12
+ req = ::Rails.application.routes.request_class.new(env)
13
+ RoutesCoverage._touch_request(req)
31
14
 
32
- RoutesCoverage._touch_route(route)
33
- # there may be multiple matching routes - we should match only first
34
- break
35
- end
36
- #TODO: detect 404s? and maybe other route errors?
15
+ # TODO: detect 404s? and maybe other route errors?
37
16
  @app.call(original_env)
38
17
  end
39
18
  end
@@ -1,19 +1,20 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_support/core_ext/string' # needed for rails5 version of inspector
2
4
 
3
5
  module RoutesCoverage
4
6
  class Result
5
-
6
7
  begin
7
8
  require 'action_dispatch/routing/inspector'
8
9
  class Inspector < ActionDispatch::Routing::RoutesInspector
9
10
  NEW_RAILS = true
10
11
  def collect_all_routes
11
12
  res = collect_routes(@routes)
12
- #TODO: test with engines
13
+ # TODO: test with engines
13
14
  @engines.each do |engine_name, engine_routes|
14
- res += engine_routes.map{|er|
15
+ res += engine_routes.map do |er|
15
16
  er.merge({ engine_name: engine_name })
16
- }
17
+ end
17
18
  end
18
19
  res
19
20
  end
@@ -21,39 +22,35 @@ module RoutesCoverage
21
22
  def collect_routes(routes)
22
23
  routes.collect do |route|
23
24
  ActionDispatch::Routing::RouteWrapper.new(route)
24
- end.reject do |route|
25
- route.internal?
26
- end.collect do |route|
25
+ end.reject(&:internal?).collect do |route| # rubocop:disable Style/MultilineBlockChain
27
26
  collect_engine_routes(route)
28
27
 
29
- { name: route.name,
30
- verb: route.verb,
31
- path: route.path,
32
- reqs: route.reqs,
28
+ { name: route.name,
29
+ verb: route.verb,
30
+ path: route.path,
31
+ reqs: route.reqs,
33
32
  # regexp: route.json_regexp, # removed, this is not present in rails5
34
33
  # added:
35
- original: route,
36
- }
34
+ original: route }
37
35
  end
38
36
  end
39
37
  end
40
-
41
38
  rescue LoadError
42
- #rails 3
39
+ # rails 3
43
40
  require 'rails/application/route_inspector'
44
41
  class Inspector < Rails::Application::RouteInspector
45
42
  NEW_RAILS = false
46
- def collect_all_routes(routes)
43
+ def collect_all_routes(routes) # rubocop:disable Lint/DuplicateMethods
47
44
  res = collect_routes(routes)
48
45
  @engines.each do |engine_name, engine_routes|
49
- res += engine_routes.map{|er|
46
+ res += engine_routes.map do |er|
50
47
  er.merge({ engine_name: engine_name })
51
- }
48
+ end
52
49
  end
53
50
  res
54
51
  end
55
52
 
56
- def collect_routes(routes)
53
+ def collect_routes(routes) # rubocop:disable Lint/DuplicateMethods
57
54
  routes = routes.collect do |route|
58
55
  route_reqs = route.requirements
59
56
 
@@ -70,47 +67,43 @@ module RoutesCoverage
70
67
 
71
68
  collect_engine_routes(reqs, rack_app)
72
69
 
73
- { name: route.name.to_s,
74
- verb: route.verb.source.gsub(/[$^]/, ''),
75
- path: route.path.spec.to_s,
76
- reqs: reqs,
70
+ { name: route.name.to_s,
71
+ verb: route.verb.source.gsub(/[$^]/, ''),
72
+ path: route.path.spec.to_s,
73
+ reqs: reqs,
77
74
  # added:
78
- original: route,
79
- }
75
+ original: route }
80
76
  end
81
77
 
82
78
  # Skip the route if it's internal info route
83
79
  routes.reject { |r| r[:path] =~ %r{/rails/info/properties|^#{Rails.application.config.assets.prefix}} }
84
80
  end
85
81
  end
86
-
87
82
  end
88
83
 
89
- def initialize all_routes, hit_routes, settings
84
+ def initialize(all_routes, hit_routes, settings)
90
85
  @all_routes = all_routes
91
86
  @route_hit_counts = hit_routes
92
87
  @settings = settings
93
88
  end
94
89
 
95
- attr_reader :all_routes
96
-
97
- attr_reader :route_hit_counts
90
+ attr_reader :all_routes, :route_hit_counts
98
91
 
99
92
  def expected_routes
100
- return @expected_routes if @expected_routes
101
-
102
- filter_regex = Regexp.union(@settings.exclude_patterns)
103
- namespaces_regex = Regexp.union(@settings.exclude_namespaces.map{|n| /^\/#{n}/})
104
-
105
- routes_groups = all_routes.group_by{|r|
106
- !!(
107
- ("#{r.verb.to_s[8..-3]} #{r.path.spec}".strip =~ filter_regex) ||
108
- (r.path.spec.to_s =~ namespaces_regex)
109
- )
110
- }
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
+ (
99
+ ("#{r.verb.to_s[8..-3]} #{r.path.spec}".strip =~ filter_regex) ||
100
+ (r.path.spec.to_s =~ namespaces_regex)
101
+ ).present?
102
+ end
111
103
 
112
- @excluded_routes = routes_groups[true] || []
113
- @expected_routes = routes_groups[false] || []
104
+ @excluded_routes = routes_groups[true] || []
105
+ routes_groups[false] || []
106
+ end
114
107
  end
115
108
 
116
109
  def pending_routes
@@ -123,11 +116,10 @@ module RoutesCoverage
123
116
  end
124
117
 
125
118
  def hit_routes
126
- #TODO: sort?
119
+ # TODO: sort?
127
120
  @route_hit_counts.keys
128
121
  end
129
122
 
130
-
131
123
  def hit_routes_count
132
124
  @route_hit_counts.size
133
125
  end
@@ -146,6 +138,7 @@ module RoutesCoverage
146
138
 
147
139
  def coverage
148
140
  return 0 unless expected_routes.any?
141
+
149
142
  (hit_routes_count * 100.0 / expected_routes_count).round(@settings.round_precision)
150
143
  end
151
144
 
@@ -158,16 +151,15 @@ module RoutesCoverage
158
151
  end
159
152
 
160
153
  def all_routes_with_hits
161
- if Inspector::NEW_RAILS
162
- res = Inspector.new(all_routes).collect_all_routes
163
- else
164
- res = Inspector.new.collect_all_routes(all_routes)
165
- end
166
- res.each{|val|
154
+ res = if Inspector::NEW_RAILS
155
+ Inspector.new(all_routes).collect_all_routes
156
+ else
157
+ Inspector.new.collect_all_routes(all_routes)
158
+ end
159
+ res.each do |val|
167
160
  val[:hits] = @route_hit_counts[val[:original]] || 0
168
- }
161
+ end
169
162
  res
170
163
  end
171
-
172
164
  end
173
165
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RoutesCoverage
2
- VERSION = "0.4.3"
4
+ VERSION = "0.6.0"
3
5
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "routes_coverage/version"
2
4
  require "routes_coverage/result"
3
5
  require "routes_coverage/middleware"
@@ -8,28 +10,43 @@ require "routes_coverage/formatters/full_text"
8
10
  require "routes_coverage/formatters/html"
9
11
 
10
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
+
11
29
  class Railtie < ::Rails::Railtie
12
30
  railtie_name :routes_coverage
13
31
 
14
32
  initializer "request_coverage.inject_test_middleware" do
15
- if RoutesCoverage.enabled?
16
- ::Rails.application.middleware.use RoutesCoverage::Middleware
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
17
41
  end
18
42
  end
19
43
  end
20
44
 
21
45
  class Settings
22
- attr_reader :exclude_patterns
23
- attr_reader :exclude_namespaces
24
- attr_accessor :exclude_put_fallbacks
25
-
26
- attr_accessor :perform_report
27
- attr_accessor :minimum_coverage
28
- attr_accessor :round_precision
29
-
30
- attr_accessor :format
31
-
32
- attr_reader :groups
46
+ attr_reader :exclude_patterns, :exclude_namespaces, :groups
47
+ attr_accessor :perform_report, :format, :minimum_coverage, :round_precision,
48
+ :exclude_put_fallbacks,
49
+ :include_from_controller_tests
33
50
 
34
51
  def initialize
35
52
  @exclude_patterns = []
@@ -63,96 +80,143 @@ module RoutesCoverage
63
80
  end
64
81
 
65
82
  def self.settings
66
- @@settings ||= Settings.new
83
+ @settings ||= Settings.new
67
84
  end
68
85
 
69
86
  def self.configure
70
- yield self.settings
87
+ yield settings
71
88
  end
72
89
 
73
- mattr_reader :pid
90
+ # used in at_exit adapter to skip subprocesses
91
+ def self.pid
92
+ @pid
93
+ end
94
+
95
+ def self.route_hit_count
96
+ @route_hit_count
97
+ end
74
98
 
75
99
  def self.reset!
76
- @@route_hit_count = Hash.new(0)
77
- @@pid = Process.pid
100
+ @route_hit_count = Hash.new(0)
101
+ @pid = Process.pid
78
102
  end
79
103
 
80
104
  def self.perform_report
81
105
  return unless settings.perform_report
82
106
 
107
+ all_routes = _collect_all_routes
108
+ all_result = Result.new(all_routes, route_hit_count, settings)
109
+ groups = _collect_route_groups(all_routes)
110
+
111
+ if groups.size > 1
112
+ ungroupped_routes = all_routes.reject do |r|
113
+ groups.values.any? do |group_routes|
114
+ group_routes.all_routes.include? r
115
+ end
116
+ end
117
+
118
+ if ungroupped_routes.any?
119
+ groups["Ungroupped"] = Result.new(ungroupped_routes, route_hit_count.slice(*ungroupped_routes), settings)
120
+ end
121
+ end
122
+
123
+ puts
124
+ puts settings.formatter_class.new(all_result, groups, settings).format # rubocop:disable Rails/Output
125
+ end
126
+
127
+ def self._collect_all_routes
83
128
  all_routes = ::Rails.application.routes.routes.routes.dup
84
129
 
85
130
  if defined?(::Sprockets) && defined?(::Sprockets::Environment)
86
- all_routes.reject!{|r| r.app.is_a?(::Sprockets::Environment) }
131
+ all_routes.reject! { |r| r.app.is_a?(::Sprockets::Environment) }
87
132
  end
88
133
 
89
134
  if settings.exclude_put_fallbacks
90
- all_routes.reject!{|put_route|
135
+ all_routes.reject! do |put_route|
91
136
  (
92
137
  put_route.verb == /^PUT$/ ||
93
138
  put_route.verb == "PUT" # rails 5
94
139
  ) &&
95
- put_route.name.nil? &&
96
- @@route_hit_count[put_route] == 0 &&
97
- all_routes.any?{|patch_route|
98
- (
99
- patch_route.verb == /^PATCH$/ ||
100
- patch_route.verb == "PATCH" # rails5
101
- ) &&
102
- patch_route.defaults == put_route.defaults &&
103
- patch_route.ip == put_route.ip &&
104
- patch_route.path.spec.to_s == put_route.path.spec.to_s
105
- }
106
- }
140
+ put_route.name.nil? &&
141
+ route_hit_count[put_route].zero? &&
142
+ all_routes.any? do |patch_route|
143
+ (
144
+ patch_route.verb == /^PATCH$/ ||
145
+ patch_route.verb == "PATCH" # rails5
146
+ ) &&
147
+ patch_route.defaults == put_route.defaults &&
148
+ patch_route.ip == put_route.ip &&
149
+ patch_route.path.spec.to_s == put_route.path.spec.to_s
150
+ end
151
+ end
107
152
  end
153
+ all_routes
154
+ end
108
155
 
109
- all_result = Result.new(
110
- all_routes,
111
- @@route_hit_count,
112
- settings
113
- )
114
-
115
-
116
- groups = Hash[settings.groups.map{|group_name, regex|
117
- [group_name,
118
- Result.new(
119
- all_routes.select{|r| r.path.spec.to_s =~ regex},
120
- Hash[@@route_hit_count.select{|r,_hits| r.path.spec.to_s =~ regex}],
121
- settings
122
- )
123
- ]
124
- }]
156
+ def self._collect_route_groups(all_routes)
157
+ settings.groups.map do |group_name, matcher|
158
+ group_routes = all_routes.select do |route|
159
+ if matcher.respond_to?(:call)
160
+ matcher.call(route)
161
+ elsif matcher.is_a?(Hash)
162
+ matcher.all? do |key, value|
163
+ case key
164
+ when :path
165
+ route.path.spec.to_s =~ value
166
+ when :action
167
+ route.requirements[:action]&.match(value)
168
+ when :controller
169
+ route.requirements[:controller]&.match(value)
170
+ when :constraints
171
+ value.all? do |constraint_name, constraint_value|
172
+ if constraint_value.present?
173
+ route.constraints[constraint_name] && route.constraints[constraint_name].match?(constraint_value)
174
+ else
175
+ route.constraints[constraint_name].blank?
176
+ end
177
+ end
178
+ end
179
+ end
180
+ else
181
+ route.path.spec.to_s.match?(matcher)
182
+ end
183
+ end
125
184
 
126
- if groups.size > 1
127
- ungroupped_routes = all_routes.reject{|r|
128
- groups.values.any?{|group_routes|
129
- group_routes.all_routes.include? r
130
- }
131
- }
185
+ [group_name, Result.new(group_routes, route_hit_count.slice(*group_routes), settings)]
186
+ end.to_h
187
+ end
132
188
 
133
- if ungroupped_routes.any?
134
- groups["Ungroupped"] = Result.new(
135
- ungroupped_routes,
136
- Hash[@@route_hit_count.select{|r,_hits| ungroupped_routes.include? r}],
137
- settings
138
- )
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
+ (env['action_dispatch.request.path_parameters'] || {}).merge(parameters)
201
+ while dispatcher.is_a?(ActionDispatch::Routing::Mapper::Constraints)
202
+ dispatcher = (dispatcher.app if dispatcher.matches?(env))
203
+ end
139
204
  end
140
- end
205
+ next unless dispatcher
141
206
 
142
- puts
143
- puts settings.formatter_class.new(all_result, groups, settings).format
207
+ RoutesCoverage._touch_route(route)
208
+ # there may be multiple matching routes - we should match only first
209
+ break
210
+ end
144
211
  end
145
212
 
146
-
147
- def self._touch_route route
148
- reset! unless @@route_hit_count
149
- @@route_hit_count[route] += 1
213
+ def self._touch_route(route)
214
+ reset! unless route_hit_count
215
+ route_hit_count[route] += 1
150
216
  end
151
217
  end
152
218
 
153
- if defined? RSpec
154
- require "routes_coverage/adapters/rspec"
155
- end
219
+ require "routes_coverage/adapters/rspec" if defined? RSpec
156
220
 
157
221
  if RoutesCoverage.enabled?
158
222
  if defined?(SimpleCov) && SimpleCov.running
@@ -1,5 +1,6 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
5
  require 'routes_coverage/version'
5
6
 
@@ -9,18 +10,23 @@ Gem::Specification.new do |spec|
9
10
  spec.authors = ["Vasily Fedoseyev"]
10
11
  spec.email = ["vasilyfedoseyev@gmail.com"]
11
12
 
12
- spec.summary = %q{Provides coverage report for your rails routes}
13
- spec.description = %q{Generates coverage report for routes hit by your request/integration/feature tests including capybara ones}
13
+ spec.summary = "Provides coverage report for your rails routes"
14
+ spec.description = "Generates coverage report for routes hit by your request/integration/feature tests "\
15
+ "including capybara ones"
14
16
  spec.homepage = "https://github.com/Vasfed/routes_coverage"
15
17
  spec.license = "MIT"
16
18
 
17
- spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
- f.match(%r{^(test|spec|features|assets)/})
19
+ spec.required_ruby_version = ">= 2.0"
20
+
21
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
22
+ f.match(%r{^(test|spec|features|assets|bin|gemfiles)/}) ||
23
+ f.start_with?('.') ||
24
+ %w[Appraisals Gemfile Rakefile].include?(f)
19
25
  end
20
26
  spec.require_paths = ["lib"]
21
27
 
22
- spec.add_development_dependency "bundler", "~> 1.14"
23
- spec.add_development_dependency "rake", "~> 10.0"
24
- spec.add_development_dependency "minitest"
25
28
  spec.add_development_dependency 'appraisal'
29
+ spec.add_development_dependency "bundler", ">= 2.2.10"
30
+ spec.add_development_dependency "minitest"
31
+ spec.add_development_dependency "rake", ">= 12.3.3"
26
32
  end
metadata CHANGED
@@ -1,43 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: routes_coverage
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.3
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vasily Fedoseyev
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-04-09 00:00:00.000000000 Z
11
+ date: 2021-11-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: bundler
14
+ name: appraisal
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '1.14'
19
+ version: '0'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '1.14'
26
+ version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
- name: rake
28
+ name: bundler
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '10.0'
33
+ version: 2.2.10
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: '10.0'
40
+ version: 2.2.10
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: minitest
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -53,19 +53,19 @@ dependencies:
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
55
  - !ruby/object:Gem::Dependency
56
- name: appraisal
56
+ name: rake
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - ">="
60
60
  - !ruby/object:Gem::Version
61
- version: '0'
61
+ version: 12.3.3
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
- version: '0'
68
+ version: 12.3.3
69
69
  description: Generates coverage report for routes hit by your request/integration/feature
70
70
  tests including capybara ones
71
71
  email:
@@ -74,26 +74,10 @@ executables: []
74
74
  extensions: []
75
75
  extra_rdoc_files: []
76
76
  files:
77
- - ".gitattributes"
78
- - ".gitignore"
79
- - ".travis.yml"
80
- - Appraisals
81
- - Gemfile
82
77
  - LICENSE.txt
83
78
  - README.md
84
- - Rakefile
85
- - bin/console
86
- - bin/setup
87
79
  - compiled_assets/routes.css
88
80
  - compiled_assets/routes.js
89
- - gemfiles/.bundle/config
90
- - gemfiles/rails_3.gemfile
91
- - gemfiles/rails_40.gemfile
92
- - gemfiles/rails_40_rspec.gemfile
93
- - gemfiles/rails_40_simplecov.gemfile
94
- - gemfiles/rails_42.gemfile
95
- - gemfiles/rails_5.gemfile
96
- - gemfiles/rails_51.gemfile
97
81
  - lib/routes_coverage.rb
98
82
  - lib/routes_coverage/adapters/atexit.rb
99
83
  - lib/routes_coverage/adapters/rspec.rb
@@ -112,7 +96,7 @@ homepage: https://github.com/Vasfed/routes_coverage
112
96
  licenses:
113
97
  - MIT
114
98
  metadata: {}
115
- post_install_message:
99
+ post_install_message:
116
100
  rdoc_options: []
117
101
  require_paths:
118
102
  - lib
@@ -120,16 +104,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
120
104
  requirements:
121
105
  - - ">="
122
106
  - !ruby/object:Gem::Version
123
- version: '0'
107
+ version: '2.0'
124
108
  required_rubygems_version: !ruby/object:Gem::Requirement
125
109
  requirements:
126
110
  - - ">="
127
111
  - !ruby/object:Gem::Version
128
112
  version: '0'
129
113
  requirements: []
130
- rubyforge_project:
131
- rubygems_version: 2.6.12
132
- signing_key:
114
+ rubygems_version: 3.0.9
115
+ signing_key:
133
116
  specification_version: 4
134
117
  summary: Provides coverage report for your rails routes
135
118
  test_files: []
data/.gitattributes DELETED
@@ -1 +0,0 @@
1
- compiled_assets/* linguist-generated
data/.gitignore DELETED
@@ -1,12 +0,0 @@
1
- /.bundle/
2
- /.yardoc
3
- /Gemfile.lock
4
- /_yardoc/
5
- /coverage/
6
- /doc/
7
- /pkg/
8
- /spec/reports/
9
- /tmp/
10
- /log/
11
- /coverage/
12
- /gemfiles/*.lock
data/.travis.yml DELETED
@@ -1,5 +0,0 @@
1
- sudo: false
2
- language: ruby
3
- rvm:
4
- - 2.3.0
5
- before_install: gem install bundler -v 1.14.6
data/Appraisals DELETED
@@ -1,33 +0,0 @@
1
- # to test against all, run: appraisal rake spec
2
-
3
-
4
- appraise "rails-40" do
5
- gem "rails", "~>4.0.0"
6
- end
7
-
8
- appraise "rails-42" do
9
- gem "rails", "~>4.2.0"
10
- end
11
-
12
- appraise "rails-5" do
13
- gem "rails", "~>5.0.0"
14
- end
15
-
16
- appraise "rails-40+rspec" do
17
- gem "rails", "~>4.0.0"
18
- gem "rspec-rails"
19
- end
20
-
21
- appraise "rails-40+simplecov" do
22
- gem "rails", "~>4.0.0"
23
- gem "simplecov"
24
- end
25
-
26
- appraise "rails-51" do
27
- gem "rails", "~>5.1.0"
28
- end
29
-
30
- appraise 'rails-3' do
31
- gem 'rails', '~>3.2.22'
32
- gem 'test-unit'
33
- end
data/Gemfile DELETED
@@ -1,15 +0,0 @@
1
- source 'https://rubygems.org'
2
-
3
- #NB: gem's dependencies are in routes_coverage.gemspec
4
- #NB: all other non-listed gems should go into Appraisals,
5
- # this file is only for quick tests
6
-
7
- # rails should be included before us
8
- gem 'rails', '5.2.0.rc1'
9
- gem 'simplecov', require: false
10
-
11
- # for assets:
12
- gem 'sprockets'
13
- gem 'sass'
14
-
15
- gemspec
data/Rakefile DELETED
@@ -1,36 +0,0 @@
1
- require 'bundler'
2
- require 'bundler/setup'
3
- require "bundler/gem_tasks"
4
-
5
- require 'rake/testtask'
6
-
7
- Rake::TestTask.new(:spec) do |t|
8
- t.pattern = 'spec/**/*_spec.rb'
9
- t.libs.push 'spec'
10
- end
11
-
12
- Rake::TestTask.new(:dummytest) do |t| t.pattern = 'spec/fixtures/dummy_test.rb' end
13
- Rake::TestTask.new(:dummytest_html) do |t| t.pattern = 'spec/fixtures/dummy_html.rb' end
14
- Rake::TestTask.new(:dummytest_full) do |t| t.pattern = 'spec/fixtures/dummy_test_full.rb' end
15
- Rake::TestTask.new(:dummytest_filter) do |t| t.pattern = 'spec/fixtures/dummy_test_nsfilters.rb' end
16
- Rake::TestTask.new(:dummytest_groups) do |t| t.pattern = 'spec/fixtures/dummy_test_groups.rb' end
17
-
18
-
19
- task :default => :spec
20
-
21
- $:.push File.expand_path("../lib", __FILE__)
22
- require 'routes_coverage/version'
23
-
24
- namespace :assets do
25
- desc "Compiles all assets"
26
- task :compile do
27
- puts "Compiling assets"
28
- require "sprockets"
29
- assets = Sprockets::Environment.new
30
- assets.append_path "assets/javascripts"
31
- assets.append_path "assets/stylesheets"
32
- compiled_path = "compiled_assets"
33
- assets["application.js"].write_to("#{compiled_path}/routes.js")
34
- assets["application.css"].write_to("#{compiled_path}/routes.css")
35
- end
36
- end
data/bin/console DELETED
@@ -1,15 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require "bundler/setup"
4
- require "rails"
5
- require "routes_coverage"
6
-
7
- # You can add fixtures and/or initialization code here to make experimenting
8
- # with your gem easier. You can also use a different console, if you like.
9
-
10
- # (If you use this, don't forget to add pry to your Gemfile!)
11
- # require "pry"
12
- # Pry.start
13
-
14
- require "irb"
15
- IRB.start(__FILE__)
data/bin/setup DELETED
@@ -1,7 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- IFS=$'\n\t'
4
- set -vx
5
-
6
- bundle install
7
- appraisal
@@ -1,2 +0,0 @@
1
- ---
2
- BUNDLE_RETRY: "1"
@@ -1,11 +0,0 @@
1
- # This file was generated by Appraisal
2
-
3
- source "https://rubygems.org"
4
-
5
- gem "rails", "~>3.2.22"
6
- gem "simplecov", require: false
7
- gem "sprockets"
8
- gem "sass"
9
- gem "test-unit"
10
-
11
- gemspec path: "../"
@@ -1,10 +0,0 @@
1
- # This file was generated by Appraisal
2
-
3
- source "https://rubygems.org"
4
-
5
- gem "rails", "~>4.0.0"
6
- gem "simplecov", require: false
7
- gem "sprockets"
8
- gem "sass"
9
-
10
- gemspec path: "../"
@@ -1,11 +0,0 @@
1
- # This file was generated by Appraisal
2
-
3
- source "https://rubygems.org"
4
-
5
- gem "rails", "~>4.0.0"
6
- gem "simplecov", require: false
7
- gem "sprockets"
8
- gem "sass"
9
- gem "rspec-rails"
10
-
11
- gemspec path: "../"
@@ -1,10 +0,0 @@
1
- # This file was generated by Appraisal
2
-
3
- source "https://rubygems.org"
4
-
5
- gem "rails", "~>4.0.0"
6
- gem "simplecov"
7
- gem "sprockets"
8
- gem "sass"
9
-
10
- gemspec path: "../"
@@ -1,10 +0,0 @@
1
- # This file was generated by Appraisal
2
-
3
- source "https://rubygems.org"
4
-
5
- gem "rails", "~>4.2.0"
6
- gem "simplecov", require: false
7
- gem "sprockets"
8
- gem "sass"
9
-
10
- gemspec path: "../"
@@ -1,10 +0,0 @@
1
- # This file was generated by Appraisal
2
-
3
- source "https://rubygems.org"
4
-
5
- gem "rails", "~>5.0.0"
6
- gem "simplecov", require: false
7
- gem "sprockets"
8
- gem "sass"
9
-
10
- gemspec path: "../"
@@ -1,10 +0,0 @@
1
- # This file was generated by Appraisal
2
-
3
- source "https://rubygems.org"
4
-
5
- gem "rails", "~>5.1.0"
6
- gem "simplecov", require: false
7
- gem "sprockets"
8
- gem "sass"
9
-
10
- gemspec path: "../"