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,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Constable
|
|
4
|
+
# Catches the test that only passes because something else ran first.
|
|
5
|
+
#
|
|
6
|
+
# A new or changed investigation is run twice in CI: once completely alone, in its own
|
|
7
|
+
# forked process before the suite has touched anything, and once in ordinary full-suite
|
|
8
|
+
# context. The two answers should agree. When they don't, the test is depending on state
|
|
9
|
+
# it never set up, and that is a failure now rather than a mystery three months from now
|
|
10
|
+
# when someone reorders a file.
|
|
11
|
+
#
|
|
12
|
+
# Only new and changed investigations are audited, because identity is a content hash:
|
|
13
|
+
# an unchanged body keeps its hash, and a body that already passed this audit does not
|
|
14
|
+
# need to pay for it on every future run.
|
|
15
|
+
class OrderAudit
|
|
16
|
+
MESSAGE = "ORDER DEPENDENT TEST"
|
|
17
|
+
|
|
18
|
+
attr_reader :config, :storage
|
|
19
|
+
|
|
20
|
+
def initialize(config: Constable.config, storage: Constable.storage, enabled: nil)
|
|
21
|
+
@config = config
|
|
22
|
+
@storage = storage
|
|
23
|
+
@enabled = enabled.nil? ? ci? : enabled
|
|
24
|
+
@isolated = {}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def enabled? = @enabled && Process.respond_to?(:fork)
|
|
28
|
+
|
|
29
|
+
def ci? = !ENV["CI"].to_s.empty?
|
|
30
|
+
|
|
31
|
+
# Investigations whose content hash the blotter has never seen -- new tests, or tests
|
|
32
|
+
# whose body actually changed.
|
|
33
|
+
def candidates(investigations)
|
|
34
|
+
return [] unless enabled?
|
|
35
|
+
|
|
36
|
+
known = begin
|
|
37
|
+
@storage.known_identities.map { |entry| entry[:identity] }
|
|
38
|
+
rescue StandardError
|
|
39
|
+
[]
|
|
40
|
+
end
|
|
41
|
+
investigations.reject { |inv| known.include?(inv.identity) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# The "alone" half of the audit. Runs before the suite so each child inherits a process
|
|
45
|
+
# that has not yet run a single test.
|
|
46
|
+
def record_isolated!(investigations)
|
|
47
|
+
candidates(investigations).each do |investigation|
|
|
48
|
+
@isolated[investigation.identity] = run_alone(investigation)
|
|
49
|
+
end
|
|
50
|
+
@isolated
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The "in context" half. Returns the results rewritten to failures where the two
|
|
54
|
+
# answers disagree.
|
|
55
|
+
def audit(results)
|
|
56
|
+
return results if @isolated.empty?
|
|
57
|
+
|
|
58
|
+
results.map do |result|
|
|
59
|
+
expected = @isolated[result.identity]
|
|
60
|
+
next result if expected.nil?
|
|
61
|
+
next result if expected == simple_status(result)
|
|
62
|
+
|
|
63
|
+
order_dependent(result, expected)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def order_dependent?(result)
|
|
68
|
+
expected = @isolated[result.identity]
|
|
69
|
+
!expected.nil? && expected != simple_status(result)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def simple_status(result)
|
|
75
|
+
result.passed? ? :passed : :failed
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def order_dependent(result, isolated_status)
|
|
79
|
+
in_suite = simple_status(result)
|
|
80
|
+
result.status = :failed
|
|
81
|
+
result.failure = Failure.new(
|
|
82
|
+
message: "#{MESSAGE}: passed #{describe(isolated_status)} but #{describe(in_suite)} " \
|
|
83
|
+
"in full-suite context. This investigation depends on state another test " \
|
|
84
|
+
"leaves behind rather than on its own briefing.",
|
|
85
|
+
context: "alone: #{isolated_status}\nin suite: #{in_suite}",
|
|
86
|
+
backtrace: []
|
|
87
|
+
)
|
|
88
|
+
result
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def describe(status)
|
|
92
|
+
status == :passed ? "alone" : "failed alone"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Forks a child that runs exactly one investigation and reports back a single byte.
|
|
96
|
+
# A crash in the child is a failure, not a hang in the parent.
|
|
97
|
+
def run_alone(investigation)
|
|
98
|
+
reader, writer = IO.pipe
|
|
99
|
+
|
|
100
|
+
pid = fork do
|
|
101
|
+
reader.close
|
|
102
|
+
instance = Case.constable_instance_for(investigation)
|
|
103
|
+
status =
|
|
104
|
+
begin
|
|
105
|
+
# The same full lifecycle the Runner drives -- before_setup, briefings, body,
|
|
106
|
+
# teardowns, after_teardown -- because an audit that skipped half of it would
|
|
107
|
+
# be comparing two different tests and calling the difference an order bug.
|
|
108
|
+
Isolation.with_rollback(investigation.tier) { instance.run_investigation(investigation) }
|
|
109
|
+
"P"
|
|
110
|
+
rescue StandardError
|
|
111
|
+
"F"
|
|
112
|
+
ensure
|
|
113
|
+
instance._constable_dsl_teardown if instance.respond_to?(:_constable_dsl_teardown)
|
|
114
|
+
end
|
|
115
|
+
writer.write(status)
|
|
116
|
+
writer.close
|
|
117
|
+
exit!(0)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
writer.close
|
|
121
|
+
byte = reader.read
|
|
122
|
+
reader.close
|
|
123
|
+
Process.waitpid(pid)
|
|
124
|
+
|
|
125
|
+
byte == "P" ? :passed : :failed
|
|
126
|
+
rescue StandardError
|
|
127
|
+
nil
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Constable
|
|
4
|
+
# Rails' own testing behaviour, borrowed for a Constable::Case.
|
|
5
|
+
#
|
|
6
|
+
# class IntegrationCase < Constable::Case
|
|
7
|
+
# include Constable::RailsSupport::Integration
|
|
8
|
+
# tier :integration
|
|
9
|
+
# end
|
|
10
|
+
#
|
|
11
|
+
# Rails ships the request stack and the browser stack as ordinary modules -- there is no
|
|
12
|
+
# need to reimplement `post users_path` or `click_on "Save"`, and every reimplementation
|
|
13
|
+
# would be a slightly wrong one. What those modules *do* assume is Minitest's lifecycle:
|
|
14
|
+
# `setup`/`teardown` class macros and the `before_setup`/`after_teardown` instance hooks.
|
|
15
|
+
# Constable::Case answers that contract (see its "Minitest lifecycle compatibility"
|
|
16
|
+
# section), so the modules drop straight in.
|
|
17
|
+
#
|
|
18
|
+
# Nothing here is loaded until a case_helper asks for it. `require "constable"` must
|
|
19
|
+
# still work in a process with no Rails at all -- that is the whole premise of the :unit
|
|
20
|
+
# tier -- so every Rails constant below is reached through a lazy require.
|
|
21
|
+
module RailsSupport
|
|
22
|
+
# The request stack: get/post/patch/put/delete, `response`, `follow_redirect!`,
|
|
23
|
+
# cookies, and the application's URL helpers (`articles_url`, `article_path`).
|
|
24
|
+
#
|
|
25
|
+
# Include it into a tier base class, not into an individual case.
|
|
26
|
+
module Integration
|
|
27
|
+
def self.included(base)
|
|
28
|
+
behavior = RailsSupport.integration_behavior
|
|
29
|
+
RailsSupport.shim_fixture_paths(base)
|
|
30
|
+
base.include(behavior)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# The browser stack: Capybara's DSL (`visit`, `click_on`, `fill_in`, `page`), Capybara's
|
|
35
|
+
# Minitest assertions, Rails' `driven_by`/`served_by` configuration and its failure
|
|
36
|
+
# screenshots.
|
|
37
|
+
#
|
|
38
|
+
# Rails' own ActionDispatch::SystemTestCase is a class, not a module, so it cannot be
|
|
39
|
+
# mixed in. What it actually does, though, is a short list -- driver selection, a Puma
|
|
40
|
+
# server, session reset, screenshots, URL helpers off the Capybara host -- and each of
|
|
41
|
+
# those is reproduced below against the same Rails objects rather than reinvented.
|
|
42
|
+
module System
|
|
43
|
+
DEFAULT_HOST = "http://127.0.0.1"
|
|
44
|
+
|
|
45
|
+
def self.included(base)
|
|
46
|
+
RailsSupport.load_system!
|
|
47
|
+
|
|
48
|
+
base.extend(ClassMethods)
|
|
49
|
+
base.include(::Capybara::DSL)
|
|
50
|
+
base.include(::Capybara::Minitest::Assertions)
|
|
51
|
+
base.include(::ActionDispatch::SystemTesting::TestHelpers::ScreenshotHelper)
|
|
52
|
+
base.include(::ActionDispatch::SystemTesting::TestHelpers::SetupAndTeardown)
|
|
53
|
+
base.include(InstanceMethods)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
module ClassMethods
|
|
57
|
+
# Same signature as ActionDispatch::SystemTestCase.driven_by, and it builds the
|
|
58
|
+
# same driver object, so every driver Rails supports is supported here.
|
|
59
|
+
def driven_by(driver, using: :chrome, screen_size: [1400, 1400], options: {}, &capabilities)
|
|
60
|
+
self.constable_driver = ::ActionDispatch::SystemTesting::Driver.new(
|
|
61
|
+
driver, using: using, screen_size: screen_size, options: options, &capabilities
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def served_by(host:, port:)
|
|
66
|
+
::Capybara.server_host = host
|
|
67
|
+
::Capybara.server_port = port
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Inherited the same way `tier` is: declare the driver once on SystemCase and every
|
|
71
|
+
# case below it is driven the same way.
|
|
72
|
+
def constable_driver
|
|
73
|
+
return @constable_driver if defined?(@constable_driver) && @constable_driver
|
|
74
|
+
|
|
75
|
+
superclass.respond_to?(:constable_driver) ? superclass.constable_driver : nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
attr_writer :constable_driver
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
module InstanceMethods
|
|
82
|
+
# Registering the driver with Capybara is deferred to the first investigation that
|
|
83
|
+
# actually runs, so merely loading a suite that contains system cases never starts
|
|
84
|
+
# a browser.
|
|
85
|
+
def before_setup
|
|
86
|
+
driver = self.class.constable_driver || self.class.driven_by(:selenium)
|
|
87
|
+
driver.use
|
|
88
|
+
super if defined?(super)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Rails' screenshot helper asks the test whether it failed. Constable knows the
|
|
92
|
+
# answer while teardown runs, because the lifecycle stashes the exception first.
|
|
93
|
+
def failed? = !constable_failure.nil?
|
|
94
|
+
def passed? = constable_failure.nil?
|
|
95
|
+
|
|
96
|
+
# Minitest hangs arbitrary reporter data off each test; the screenshot helper
|
|
97
|
+
# writes the failure image path into it. Constable has no such reporter channel,
|
|
98
|
+
# so this is somewhere harmless for it to land.
|
|
99
|
+
# rubocop:disable Naming/MemoizedInstanceVariableName -- class-level and instance
|
|
100
|
+
# state on a Case carries a constable_ prefix so it can never collide with an
|
|
101
|
+
# instance variable a user's own case sets.
|
|
102
|
+
def metadata = (@constable_metadata ||= {})
|
|
103
|
+
# rubocop:enable Naming/MemoizedInstanceVariableName
|
|
104
|
+
|
|
105
|
+
# Capybara's assertions bump Minitest's counter directly (`self.assertions += 1`).
|
|
106
|
+
# Constable keeps the same tally under its own name, so point one at the other
|
|
107
|
+
# rather than let `assert_text` die counting.
|
|
108
|
+
def assertions = assertion_count
|
|
109
|
+
|
|
110
|
+
def assertions=(count)
|
|
111
|
+
@assertion_count = count
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private
|
|
115
|
+
|
|
116
|
+
# Capybara drives a real server over HTTP, so a system case's URL helpers must
|
|
117
|
+
# generate absolute URLs pointed at that server -- not the relative paths an
|
|
118
|
+
# integration case wants. This is Rails' own arrangement, kept verbatim.
|
|
119
|
+
def url_helpers
|
|
120
|
+
@url_helpers ||= build_url_helpers
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def build_url_helpers
|
|
124
|
+
app = ::ActionDispatch.test_app
|
|
125
|
+
return nil unless app
|
|
126
|
+
|
|
127
|
+
Class.new do
|
|
128
|
+
include app.routes.url_helpers
|
|
129
|
+
include app.routes.mounted_helpers
|
|
130
|
+
|
|
131
|
+
def url_options = default_url_options.reverse_merge(host: app_host)
|
|
132
|
+
|
|
133
|
+
def app_host
|
|
134
|
+
::Capybara.app_host || ::Capybara.current_session.server_url || DEFAULT_HOST
|
|
135
|
+
end
|
|
136
|
+
end.new
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def method_missing(name, ...)
|
|
140
|
+
helpers = url_helpers
|
|
141
|
+
return super unless helpers.respond_to?(name)
|
|
142
|
+
|
|
143
|
+
helpers.public_send(name, ...)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def respond_to_missing?(name, include_private = false)
|
|
147
|
+
url_helpers.respond_to?(name) || super
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
class << self
|
|
153
|
+
# ActionDispatch::IntegrationTest::Behavior is an ActiveSupport::Concern, so it has
|
|
154
|
+
# to be included into the *class* rather than into a module in front of it --
|
|
155
|
+
# otherwise its `included` block configures the wrapper module instead of the case.
|
|
156
|
+
def integration_behavior
|
|
157
|
+
require "action_dispatch"
|
|
158
|
+
# Behavior mixes in ActionController::TemplateAssertions without requiring it --
|
|
159
|
+
# inside a booted app something else always has by then.
|
|
160
|
+
require "action_controller"
|
|
161
|
+
require "action_dispatch/testing/integration"
|
|
162
|
+
::ActionDispatch::IntegrationTest::Behavior
|
|
163
|
+
rescue LoadError, NameError => e
|
|
164
|
+
raise Constable::ConfigurationError, <<~MESSAGE
|
|
165
|
+
Constable::RailsSupport::Integration needs Action Pack's integration test
|
|
166
|
+
helpers, and they could not be loaded (#{e.class}: #{e.message}).
|
|
167
|
+
|
|
168
|
+
An :integration case drives the real request stack, so Rails has to be booted
|
|
169
|
+
before the tier base class is defined. In test/case_helper.rb that means:
|
|
170
|
+
|
|
171
|
+
require_relative "../config/environment"
|
|
172
|
+
require "constable"
|
|
173
|
+
|
|
174
|
+
must come before `class IntegrationCase < Constable::Case`.
|
|
175
|
+
MESSAGE
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def load_system!
|
|
179
|
+
require "action_dispatch"
|
|
180
|
+
# Loading this file is what starts the Capybara/Puma server plumbing -- Rails does
|
|
181
|
+
# the setup at require time rather than in a hook we could call ourselves.
|
|
182
|
+
require "action_dispatch/system_test_case"
|
|
183
|
+
require "capybara/minitest"
|
|
184
|
+
::ActionDispatch::SystemTesting::Driver
|
|
185
|
+
rescue LoadError, NameError => e
|
|
186
|
+
raise Constable::ConfigurationError, <<~MESSAGE
|
|
187
|
+
Constable::RailsSupport::System needs Rails' system testing support and
|
|
188
|
+
Capybara, and they could not be loaded (#{e.class}: #{e.message}).
|
|
189
|
+
|
|
190
|
+
System cases are opt-in. Add them to your Gemfile's :test group:
|
|
191
|
+
|
|
192
|
+
gem "capybara"
|
|
193
|
+
gem "selenium-webdriver"
|
|
194
|
+
|
|
195
|
+
and make sure test/case_helper.rb requires config/environment before it defines
|
|
196
|
+
SystemCase.
|
|
197
|
+
MESSAGE
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# `rails/test_help` registers an :action_dispatch_integration_test load hook that
|
|
201
|
+
# does `self.fixture_paths += ActiveSupport::TestCase.fixture_paths`, on the
|
|
202
|
+
# assumption that anything including Behavior is an ActiveSupport::TestCase. A Case
|
|
203
|
+
# is not one, and Constable does not use Rails fixtures, so give the hook somewhere
|
|
204
|
+
# harmless to write rather than let it take the whole suite down.
|
|
205
|
+
def shim_fixture_paths(base)
|
|
206
|
+
return if base.respond_to?(:fixture_paths)
|
|
207
|
+
|
|
208
|
+
base.singleton_class.send(:attr_accessor, :fixture_paths)
|
|
209
|
+
base.fixture_paths = []
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Constable
|
|
4
|
+
# The one file in the gem that cannot exist without Rails.
|
|
5
|
+
#
|
|
6
|
+
# `rails generate scaffold Post title:string` does not know what a test file looks
|
|
7
|
+
# like. It asks whatever generator is registered as the app's test framework, and
|
|
8
|
+
# unless something says otherwise that is always test_unit. So an app can install
|
|
9
|
+
# Constable, write its whole suite in cases, and still have every `rails generate`
|
|
10
|
+
# quietly drop Minitest files into test/ -- for the one framework the app deliberately
|
|
11
|
+
# replaced. Registering here is what closes that gap, and it is exactly what
|
|
12
|
+
# rspec-rails does for the same reason.
|
|
13
|
+
#
|
|
14
|
+
# Loaded conditionally from lib/constable.rb, never unconditionally: `require
|
|
15
|
+
# "constable"` has to keep working in a process with no Rails app at all, which is the
|
|
16
|
+
# whole premise of the :unit tier. That guard is also why there is no
|
|
17
|
+
# `require "rails/railtie"` at the top of this file -- by the time anything reaches
|
|
18
|
+
# here, Rails::Railtie is already defined, and requiring it from a bare Ruby process
|
|
19
|
+
# would only turn a clean NameError into a confusing one from inside railties.
|
|
20
|
+
class Railtie < ::Rails::Railtie
|
|
21
|
+
config.app_generators do |g|
|
|
22
|
+
# `fixture: false` is not an oversight. Constable has no fixtures: a witness builds
|
|
23
|
+
# exactly what one investigation needs and throws it away again, which is the same
|
|
24
|
+
# reason there is no before(:all). Generating a fixtures.yml alongside a case would
|
|
25
|
+
# be handing the suite the shared mutable state it exists to prevent.
|
|
26
|
+
g.test_framework :constable, fixture: false
|
|
27
|
+
|
|
28
|
+
# Rails resolves these two separately from :test_framework -- its own test_unit
|
|
29
|
+
# railtie claims all three. Claiming only the first would leave
|
|
30
|
+
# `rails generate integration_test` and `rails generate system_test` writing
|
|
31
|
+
# Minitest into a Constable app, which is the bug this railtie exists to fix.
|
|
32
|
+
g.integration_tool :constable
|
|
33
|
+
g.system_tests :constable
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Constable
|
|
4
|
+
# The roll call of every case file loaded into this process.
|
|
5
|
+
#
|
|
6
|
+
# `Constable::Case.inherited` reports each new subclass here as it's defined, so by the
|
|
7
|
+
# time the Runner has required the case files it can ask one object what there is to
|
|
8
|
+
# run. Dockets are deliberately left off the roll -- they're anonymous subclasses that
|
|
9
|
+
# belong to the case that opened them, and they're reached through it.
|
|
10
|
+
class Registry
|
|
11
|
+
include Enumerable
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@cases = []
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Called from the `inherited` hook. Dockets and duplicates are ignored.
|
|
18
|
+
def register(case_class)
|
|
19
|
+
return case_class if case_class.respond_to?(:docket?) && case_class.docket?
|
|
20
|
+
return case_class if @cases.include?(case_class)
|
|
21
|
+
|
|
22
|
+
@cases << case_class
|
|
23
|
+
case_class
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Top-level case classes in definition order. Tier base classes appear here too --
|
|
27
|
+
# they simply have no investigations of their own.
|
|
28
|
+
def cases = @cases.dup
|
|
29
|
+
|
|
30
|
+
# Every investigation across every loaded case, dockets included.
|
|
31
|
+
def investigations = @cases.flat_map(&:investigations)
|
|
32
|
+
|
|
33
|
+
# Cases that actually declared something to run.
|
|
34
|
+
def sworn_cases = @cases.reject { |klass| klass.investigations.empty? }
|
|
35
|
+
|
|
36
|
+
def investigations_in(file)
|
|
37
|
+
path = file.to_s
|
|
38
|
+
investigations.select { |inv| inv.file.to_s == path || inv.relative_file == path }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def find_case(name)
|
|
42
|
+
@cases.find { |klass| klass.constable_display_name == name.to_s || klass.name == name.to_s }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def each(&) = @cases.each(&)
|
|
46
|
+
|
|
47
|
+
def size = @cases.size
|
|
48
|
+
def empty? = @cases.empty?
|
|
49
|
+
def include?(case_class) = @cases.include?(case_class)
|
|
50
|
+
|
|
51
|
+
# Test isolation, ours as much as anyone's.
|
|
52
|
+
def clear
|
|
53
|
+
@cases = []
|
|
54
|
+
self
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|