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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +88 -0
- data/LICENSE.txt +21 -0
- data/README.md +515 -0
- data/exe/constable +7 -0
- data/lib/constable/case.rb +336 -0
- data/lib/constable/cli.rb +475 -0
- data/lib/constable/cold_case/minitest.rb +342 -0
- data/lib/constable/cold_case/rspec.rb +334 -0
- data/lib/constable/cold_case.rb +280 -0
- data/lib/constable/config.rb +125 -0
- data/lib/constable/coverage.rb +951 -0
- data/lib/constable/diff.rb +212 -0
- data/lib/constable/dsl.rb +833 -0
- data/lib/constable/identity.rb +121 -0
- data/lib/constable/importer/modernizer.rb +860 -0
- data/lib/constable/importer/reopener.rb +468 -0
- data/lib/constable/importer.rb +51 -0
- data/lib/constable/investigation.rb +67 -0
- data/lib/constable/isolation.rb +171 -0
- data/lib/constable/jail.rb +399 -0
- data/lib/constable/log_router.rb +197 -0
- data/lib/constable/matchers.rb +834 -0
- data/lib/constable/order_audit.rb +130 -0
- data/lib/constable/rails_support.rb +213 -0
- data/lib/constable/railtie.rb +36 -0
- data/lib/constable/registry.rb +57 -0
- data/lib/constable/reporter.rb +625 -0
- data/lib/constable/result.rb +149 -0
- data/lib/constable/runner.rb +697 -0
- data/lib/constable/selection.rb +205 -0
- data/lib/constable/storage/adapter.rb +91 -0
- data/lib/constable/storage/mysql_adapter.rb +125 -0
- data/lib/constable/storage/postgres_adapter.rb +125 -0
- data/lib/constable/storage/sqlite_adapter.rb +84 -0
- data/lib/constable/storage.rb +847 -0
- data/lib/constable/version.rb +5 -0
- data/lib/constable/warrants.rb +290 -0
- data/lib/constable-rails.rb +16 -0
- data/lib/constable.rb +151 -0
- data/lib/generators/constable/base.rb +99 -0
- data/lib/generators/constable/channel/channel_generator.rb +20 -0
- data/lib/generators/constable/channel/templates/channel_case.rb.tt +29 -0
- data/lib/generators/constable/controller/controller_generator.rb +25 -0
- data/lib/generators/constable/controller/templates/controller_case.rb.tt +32 -0
- data/lib/generators/constable/generator/generator_generator.rb +31 -0
- data/lib/generators/constable/generator/templates/generator_case.rb.tt +28 -0
- data/lib/generators/constable/helper/helper_generator.rb +23 -0
- data/lib/generators/constable/helper/templates/helper_case.rb.tt +19 -0
- data/lib/generators/constable/import_generator.rb +137 -0
- data/lib/generators/constable/install_generator.rb +188 -0
- data/lib/generators/constable/integration/integration_generator.rb +27 -0
- data/lib/generators/constable/integration/templates/request_case.rb.tt +22 -0
- data/lib/generators/constable/job/job_generator.rb +20 -0
- data/lib/generators/constable/job/templates/job_case.rb.tt +33 -0
- data/lib/generators/constable/mailbox/mailbox_generator.rb +20 -0
- data/lib/generators/constable/mailbox/templates/mailbox_case.rb.tt +26 -0
- data/lib/generators/constable/mailer/mailer_generator.rb +32 -0
- data/lib/generators/constable/mailer/templates/mailer_case.rb.tt +34 -0
- data/lib/generators/constable/mailer/templates/preview.rb.tt +14 -0
- data/lib/generators/constable/model/model_generator.rb +31 -0
- data/lib/generators/constable/model/templates/model_case.rb.tt +37 -0
- data/lib/generators/constable/resource/resource_generator.rb +27 -0
- data/lib/generators/constable/scaffold/scaffold_generator.rb +42 -0
- data/lib/generators/constable/scaffold/templates/api_controller_case.rb.tt +54 -0
- data/lib/generators/constable/scaffold/templates/controller_case.rb.tt +70 -0
- data/lib/generators/constable/scaffold/templates/system_case.rb.tt +53 -0
- data/lib/generators/constable/system/system_generator.rb +20 -0
- data/lib/generators/constable/system/templates/system_case.rb.tt +18 -0
- data/lib/generators/constable/templates/authenticatable.rb.tt +31 -0
- data/lib/generators/constable/templates/case_helper.rb.tt +179 -0
- data/lib/generators/constable/templates/config.yml.tt +67 -0
- data/lib/generators/constable/templates/example_case.rb.tt +56 -0
- data/lib/generators/constable/templates/matchers.rb.tt +36 -0
- data/lib/generators/constable/templates/rubocop.yml.tt +12 -0
- 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
|