constable-rails 0.1.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.
Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +88 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +515 -0
  5. data/exe/constable +7 -0
  6. data/lib/constable/case.rb +336 -0
  7. data/lib/constable/cli.rb +475 -0
  8. data/lib/constable/cold_case/minitest.rb +342 -0
  9. data/lib/constable/cold_case/rspec.rb +334 -0
  10. data/lib/constable/cold_case.rb +280 -0
  11. data/lib/constable/config.rb +125 -0
  12. data/lib/constable/coverage.rb +951 -0
  13. data/lib/constable/diff.rb +212 -0
  14. data/lib/constable/dsl.rb +833 -0
  15. data/lib/constable/identity.rb +121 -0
  16. data/lib/constable/importer/modernizer.rb +860 -0
  17. data/lib/constable/importer/reopener.rb +468 -0
  18. data/lib/constable/importer.rb +51 -0
  19. data/lib/constable/investigation.rb +67 -0
  20. data/lib/constable/isolation.rb +171 -0
  21. data/lib/constable/jail.rb +399 -0
  22. data/lib/constable/log_router.rb +197 -0
  23. data/lib/constable/matchers.rb +834 -0
  24. data/lib/constable/order_audit.rb +130 -0
  25. data/lib/constable/rails_support.rb +213 -0
  26. data/lib/constable/railtie.rb +36 -0
  27. data/lib/constable/registry.rb +57 -0
  28. data/lib/constable/reporter.rb +625 -0
  29. data/lib/constable/result.rb +149 -0
  30. data/lib/constable/runner.rb +697 -0
  31. data/lib/constable/selection.rb +205 -0
  32. data/lib/constable/storage/adapter.rb +91 -0
  33. data/lib/constable/storage/mysql_adapter.rb +125 -0
  34. data/lib/constable/storage/postgres_adapter.rb +125 -0
  35. data/lib/constable/storage/sqlite_adapter.rb +84 -0
  36. data/lib/constable/storage.rb +847 -0
  37. data/lib/constable/version.rb +5 -0
  38. data/lib/constable/warrants.rb +290 -0
  39. data/lib/constable-rails.rb +16 -0
  40. data/lib/constable.rb +151 -0
  41. data/lib/generators/constable/base.rb +99 -0
  42. data/lib/generators/constable/channel/channel_generator.rb +20 -0
  43. data/lib/generators/constable/channel/templates/channel_case.rb.tt +29 -0
  44. data/lib/generators/constable/controller/controller_generator.rb +25 -0
  45. data/lib/generators/constable/controller/templates/controller_case.rb.tt +32 -0
  46. data/lib/generators/constable/generator/generator_generator.rb +31 -0
  47. data/lib/generators/constable/generator/templates/generator_case.rb.tt +28 -0
  48. data/lib/generators/constable/helper/helper_generator.rb +23 -0
  49. data/lib/generators/constable/helper/templates/helper_case.rb.tt +19 -0
  50. data/lib/generators/constable/import_generator.rb +137 -0
  51. data/lib/generators/constable/install_generator.rb +188 -0
  52. data/lib/generators/constable/integration/integration_generator.rb +27 -0
  53. data/lib/generators/constable/integration/templates/request_case.rb.tt +22 -0
  54. data/lib/generators/constable/job/job_generator.rb +20 -0
  55. data/lib/generators/constable/job/templates/job_case.rb.tt +33 -0
  56. data/lib/generators/constable/mailbox/mailbox_generator.rb +20 -0
  57. data/lib/generators/constable/mailbox/templates/mailbox_case.rb.tt +26 -0
  58. data/lib/generators/constable/mailer/mailer_generator.rb +32 -0
  59. data/lib/generators/constable/mailer/templates/mailer_case.rb.tt +34 -0
  60. data/lib/generators/constable/mailer/templates/preview.rb.tt +14 -0
  61. data/lib/generators/constable/model/model_generator.rb +31 -0
  62. data/lib/generators/constable/model/templates/model_case.rb.tt +37 -0
  63. data/lib/generators/constable/resource/resource_generator.rb +27 -0
  64. data/lib/generators/constable/scaffold/scaffold_generator.rb +42 -0
  65. data/lib/generators/constable/scaffold/templates/api_controller_case.rb.tt +54 -0
  66. data/lib/generators/constable/scaffold/templates/controller_case.rb.tt +70 -0
  67. data/lib/generators/constable/scaffold/templates/system_case.rb.tt +53 -0
  68. data/lib/generators/constable/system/system_generator.rb +20 -0
  69. data/lib/generators/constable/system/templates/system_case.rb.tt +18 -0
  70. data/lib/generators/constable/templates/authenticatable.rb.tt +31 -0
  71. data/lib/generators/constable/templates/case_helper.rb.tt +179 -0
  72. data/lib/generators/constable/templates/config.yml.tt +67 -0
  73. data/lib/generators/constable/templates/example_case.rb.tt +56 -0
  74. data/lib/generators/constable/templates/matchers.rb.tt +36 -0
  75. data/lib/generators/constable/templates/rubocop.yml.tt +12 -0
  76. metadata +209 -0
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "logger"
5
+
6
+ module Constable
7
+ # stdout is results only.
8
+ #
9
+ # A test run's stdout belongs to the reporter -- glyphs while it runs, the summary when
10
+ # it finishes. Rails.logger, ActiveRecord's SQL logging and request/response logging all
11
+ # go to log/test.log and nowhere else, so a failure never has to be dug out of a wall of
12
+ # scrolled-past SQL. `constable test --verbose` is the escape hatch: the same lines still
13
+ # land in the file, and are additionally teed to stdout for active debugging.
14
+ #
15
+ # Constable::LogRouter.route!(verbose: options[:verbose])
16
+ #
17
+ # Rails is never required at load time -- a :unit-tier run boots without an app at all --
18
+ # so without Rails this is a no-op that says why.
19
+ module LogRouter
20
+ DEFAULT_PATH = "log/test.log"
21
+
22
+ # What route! did, for a caller that wants to say so (or a test that wants to check).
23
+ Routing = Struct.new(:routed, :path, :verbose, :reason, keyword_init: true) do
24
+ def routed? = !!routed
25
+ def verbose? = !!verbose
26
+
27
+ def to_s
28
+ return reason.to_s unless routed?
29
+
30
+ verbose? ? "logs → #{path} (teed to stdout)" : "logs → #{path}"
31
+ end
32
+ end
33
+
34
+ # Writes to the log file and, in verbose mode, to stdout as well. Teeing at the IO
35
+ # keeps this independent of which logger-broadcast API a given Rails version ships.
36
+ class Tee
37
+ attr_reader :targets
38
+
39
+ def initialize(*targets)
40
+ @targets = targets.compact
41
+ end
42
+
43
+ def write(*args)
44
+ @targets.sum { |target| target.write(*args).to_i }
45
+ end
46
+
47
+ def <<(text)
48
+ @targets.each { |target| target << text }
49
+ self
50
+ end
51
+
52
+ def print(*args) = @targets.each { |target| target.print(*args) }
53
+ def puts(*args) = @targets.each { |target| target.puts(*args) }
54
+ def flush = @targets.each { |target| target.flush if target.respond_to?(:flush) }
55
+ def sync = true
56
+ def tty? = false
57
+
58
+ def sync=(value)
59
+ @targets.each { |target| target.sync = value if target.respond_to?(:sync=) }
60
+ end
61
+
62
+ # Never closes stdout/stderr out from under the process.
63
+ def close
64
+ @targets.each do |target|
65
+ next if target.equal?($stdout) || target.equal?($stderr)
66
+
67
+ target.close if target.respond_to?(:close) && !target.closed?
68
+ end
69
+ end
70
+ end
71
+
72
+ # Everything in a Rails app that is allowed to talk. Each entry is resolved lazily and
73
+ # skipped when its constant isn't loaded, so a unit-tier app with no ActionMailer, or a
74
+ # Rails version that dropped a component, routes the rest without complaint.
75
+ LOGGER_TARGETS = [
76
+ ["Rails", :logger=, :logger],
77
+ ["ActiveRecord::Base", :logger=, :logger],
78
+ ["ActionController::Base", :logger=, :logger],
79
+ ["ActionView::Base", :logger=, :logger],
80
+ ["ActionMailer::Base", :logger=, :logger],
81
+ ["ActiveJob::Base", :logger=, :logger],
82
+ ["ActiveStorage", :logger=, :logger],
83
+ ["ActiveSupport::LogSubscriber", :logger=, :logger]
84
+ ].freeze
85
+
86
+ class << self
87
+ # Routes every Rails-side logger at log/test.log. Returns a Routing describing what
88
+ # happened; `verbose: true` additionally tees the same lines to `io`.
89
+ def route!(verbose: false, path: nil, io: $stdout, root: nil)
90
+ target = path ? File.expand_path(path.to_s, root || Constable.root) : default_path(root)
91
+
92
+ unless rails_loaded?
93
+ reason = "Rails is not loaded — nothing to route, stdout is already results only"
94
+ return @current = Routing.new(routed: false, path: target, verbose: verbose, reason: reason)
95
+ end
96
+
97
+ file = open_log(target)
98
+ logger = build_logger(verbose ? Tee.new(file, io) : file)
99
+
100
+ remember_previous!
101
+ apply(logger)
102
+
103
+ @current = Routing.new(routed: true, path: target, verbose: verbose, reason: nil)
104
+ end
105
+
106
+ # Puts back whatever the app had before route!. Only useful in-process (our own
107
+ # suite, or a REPL); a real run never wants its logs back on stdout.
108
+ def restore!
109
+ (@previous || {}).each do |constant_name, (setter, logger)|
110
+ constant = resolve(constant_name)
111
+ constant&.public_send(setter, logger)
112
+ end
113
+ close_file
114
+ @previous = nil
115
+ @current = nil
116
+ true
117
+ end
118
+
119
+ attr_reader :current
120
+
121
+ def routed? = !!@current&.routed?
122
+
123
+ def default_path(root = nil)
124
+ File.expand_path(DEFAULT_PATH, root || Constable.root)
125
+ end
126
+
127
+ private
128
+
129
+ def rails_loaded?
130
+ defined?(Rails) && Rails.respond_to?(:logger)
131
+ end
132
+
133
+ def open_log(path)
134
+ FileUtils.mkdir_p(File.dirname(path))
135
+ close_file
136
+ @file = File.open(path, "a")
137
+ @file.sync = true
138
+ @file
139
+ end
140
+
141
+ def close_file
142
+ @file.close if @file && !@file.closed?
143
+ @file = nil
144
+ rescue IOError
145
+ @file = nil
146
+ end
147
+
148
+ # A plain message-only format: test.log is read by a human chasing one request, not
149
+ # parsed by a log shipper. Tagged logging is preserved where Rails offers it, since
150
+ # Rails' own middleware calls `logger.tagged`.
151
+ def build_logger(device)
152
+ logger = ::Logger.new(device)
153
+ logger.level = ::Logger::DEBUG
154
+ logger.formatter = ->(_severity, _time, _progname, message) { "#{message}\n" }
155
+
156
+ if defined?(ActiveSupport::TaggedLogging)
157
+ ActiveSupport::TaggedLogging.new(logger)
158
+ else
159
+ logger
160
+ end
161
+ end
162
+
163
+ def apply(logger)
164
+ each_target do |constant, setter, _getter|
165
+ constant.public_send(setter, logger)
166
+ end
167
+
168
+ # SQL comment logging chases backtraces on every query -- expensive, and pointless
169
+ # when nobody is reading the file live.
170
+ active_record = resolve("ActiveRecord::Base")
171
+ active_record.verbose_query_logs = false if active_record.respond_to?(:verbose_query_logs=)
172
+ end
173
+
174
+ def remember_previous!
175
+ @previous = {}
176
+ each_target do |constant, setter, getter|
177
+ @previous[constant.to_s] = [setter, (constant.public_send(getter) if constant.respond_to?(getter))]
178
+ end
179
+ end
180
+
181
+ def each_target
182
+ LOGGER_TARGETS.each do |constant_name, setter, getter|
183
+ constant = resolve(constant_name)
184
+ next unless constant.respond_to?(setter)
185
+
186
+ yield constant, setter, getter
187
+ end
188
+ end
189
+
190
+ def resolve(constant_name)
191
+ Object.const_get(constant_name)
192
+ rescue NameError
193
+ nil
194
+ end
195
+ end
196
+ end
197
+ end