upkeep-rails 0.2.5-aarch64-linux-gnu
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/LICENSE.txt +21 -0
- data/README.md +244 -0
- data/docs/drafts/turbo-streams-is-cache-invalidation-you-write-by-hand.md +240 -0
- data/docs/how-it-works.md +329 -0
- data/docs/plans/sqlglot-active-record-migration.md +211 -0
- data/docs/plans/turbo-frame-subscription-composition.md +90 -0
- data/docs/spikes/SQLGLOT_VS_AREL_FINDINGS.md +136 -0
- data/docs/spikes/query-source-analysis-comparison/FINDINGS.md +169 -0
- data/docs/spikes/sqlglot-first-active-record/README.md +26 -0
- data/docs/spikes/sqlglot-query-analysis/FINDINGS.md +110 -0
- data/docs/spikes/sqlglot-query-analysis/PULSE_1802_FINDINGS.md +95 -0
- data/docs/spikes/sqlglot-semantic-bindings/README.md +29 -0
- data/lib/generators/upkeep/install/install_generator.rb +192 -0
- data/lib/generators/upkeep/install/templates/create_upkeep_subscriptions.rb.erb +49 -0
- data/lib/generators/upkeep/install/templates/subscription.js +288 -0
- data/lib/generators/upkeep/install/templates/upkeep.rb +65 -0
- data/lib/upkeep/active_record_query.rb +248 -0
- data/lib/upkeep/capture/request.rb +150 -0
- data/lib/upkeep/dag/subscription_shape.rb +244 -0
- data/lib/upkeep/dag.rb +454 -0
- data/lib/upkeep/delivery/action_cable_adapter.rb +48 -0
- data/lib/upkeep/delivery/async_dispatcher.rb +102 -0
- data/lib/upkeep/delivery/broadcast_transport.rb +89 -0
- data/lib/upkeep/delivery/transport.rb +194 -0
- data/lib/upkeep/delivery/turbo_streams.rb +339 -0
- data/lib/upkeep/delivery.rb +7 -0
- data/lib/upkeep/dependencies.rb +600 -0
- data/lib/upkeep/herb/developer_report.rb +135 -0
- data/lib/upkeep/herb/manifest_cache.rb +83 -0
- data/lib/upkeep/herb/manifest_diff.rb +183 -0
- data/lib/upkeep/herb/source_instrumenter.rb +149 -0
- data/lib/upkeep/herb/template_manifest.rb +548 -0
- data/lib/upkeep/invalidation/collection_append.rb +84 -0
- data/lib/upkeep/invalidation/collection_member_replace.rb +78 -0
- data/lib/upkeep/invalidation/collection_prepend.rb +84 -0
- data/lib/upkeep/invalidation/collection_remove.rb +57 -0
- data/lib/upkeep/invalidation/planner.rb +411 -0
- data/lib/upkeep/invalidation.rb +7 -0
- data/lib/upkeep/rails/action_view_capture.rb +1007 -0
- data/lib/upkeep/rails/activation_token.rb +55 -0
- data/lib/upkeep/rails/cable/channel.rb +165 -0
- data/lib/upkeep/rails/cable/subscriber_identity.rb +361 -0
- data/lib/upkeep/rails/cable.rb +4 -0
- data/lib/upkeep/rails/client_subscription.rb +65 -0
- data/lib/upkeep/rails/cluster_guard.rb +57 -0
- data/lib/upkeep/rails/configuration.rb +252 -0
- data/lib/upkeep/rails/controller_runtime.rb +187 -0
- data/lib/upkeep/rails/install.rb +28 -0
- data/lib/upkeep/rails/job_runtime.rb +43 -0
- data/lib/upkeep/rails/railtie.rb +44 -0
- data/lib/upkeep/rails/replay.rb +244 -0
- data/lib/upkeep/rails/testing.rb +259 -0
- data/lib/upkeep/rails.rb +466 -0
- data/lib/upkeep/replay.rb +462 -0
- data/lib/upkeep/runtime.rb +1276 -0
- data/lib/upkeep/shared_streams.rb +86 -0
- data/lib/upkeep/sql_dependency_analysis.rb +553 -0
- data/lib/upkeep/sqlglot/libsqlglot_rust.so +0 -0
- data/lib/upkeep/sqlglot/native.rb +121 -0
- data/lib/upkeep/sqlglot/native_library.rb +23 -0
- data/lib/upkeep/sqlglot.rb +367 -0
- data/lib/upkeep/subscriptions/active_record_store.rb +398 -0
- data/lib/upkeep/subscriptions/active_record_subscription_persistence.rb +411 -0
- data/lib/upkeep/subscriptions/active_registry.rb +80 -0
- data/lib/upkeep/subscriptions/base_store.rb +110 -0
- data/lib/upkeep/subscriptions/json_snapshot.rb +98 -0
- data/lib/upkeep/subscriptions/layered_reverse_index.rb +125 -0
- data/lib/upkeep/subscriptions/lookup_instrumentation.rb +32 -0
- data/lib/upkeep/subscriptions/persistent_reverse_index.rb +228 -0
- data/lib/upkeep/subscriptions/registrar.rb +36 -0
- data/lib/upkeep/subscriptions/reverse_index.rb +313 -0
- data/lib/upkeep/subscriptions/shape.rb +117 -0
- data/lib/upkeep/subscriptions/store.rb +349 -0
- data/lib/upkeep/subscriptions.rb +7 -0
- data/lib/upkeep/targeting.rb +146 -0
- data/lib/upkeep/version.rb +5 -0
- data/lib/upkeep-rails.rb +3 -0
- data/lib/upkeep.rb +15 -0
- data/upkeep-rails.gemspec +66 -0
- metadata +327 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "action_controller"
|
|
4
|
+
require "active_support/hash_with_indifferent_access"
|
|
5
|
+
require "stringio"
|
|
6
|
+
|
|
7
|
+
module Upkeep
|
|
8
|
+
module Rails
|
|
9
|
+
module Replay
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
class RackSession < ActiveSupport::HashWithIndifferentAccess
|
|
13
|
+
def enabled? = true
|
|
14
|
+
|
|
15
|
+
def loaded? = true
|
|
16
|
+
|
|
17
|
+
def id = self[:session_id]
|
|
18
|
+
|
|
19
|
+
def id_was = id
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def render(recipe)
|
|
23
|
+
replay = recipe.replay
|
|
24
|
+
|
|
25
|
+
case replay
|
|
26
|
+
when ::Upkeep::Replay::ControllerPage
|
|
27
|
+
render_controller_page(replay)
|
|
28
|
+
when ::Upkeep::Replay::ControllerTurboFrame
|
|
29
|
+
render_controller_turbo_frame(replay)
|
|
30
|
+
when ::Upkeep::Replay::Template
|
|
31
|
+
render_template(replay)
|
|
32
|
+
when ::Upkeep::Replay::Fragment
|
|
33
|
+
render_fragment(replay)
|
|
34
|
+
when ::Upkeep::Replay::Collection
|
|
35
|
+
render_collection(replay)
|
|
36
|
+
when ::Upkeep::Replay::CollectionMember
|
|
37
|
+
render_collection_member(replay)
|
|
38
|
+
else
|
|
39
|
+
raise "unknown Rails replay recipe type: #{replay.class.name}"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def render_controller_page(replay, env: replay.env)
|
|
44
|
+
controller = constantize(replay.controller_class)
|
|
45
|
+
_status, _headers, body = ControllerRuntime.suppress do
|
|
46
|
+
controller.action(replay.action).call(rack_env(env))
|
|
47
|
+
end
|
|
48
|
+
collect_response_body(body)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def render_controller_turbo_frame(replay)
|
|
52
|
+
html = render_controller_page(
|
|
53
|
+
replay,
|
|
54
|
+
env: replay.env.merge("HTTP_TURBO_FRAME" => replay.target_id)
|
|
55
|
+
)
|
|
56
|
+
target = Targeting::Target.new("turbo_frame", replay.target_id, "Turbo Frame replay")
|
|
57
|
+
frame_html = Targeting::Extraction.extract_target_html(html, target)
|
|
58
|
+
frame = Nokogiri::HTML5.fragment(frame_html).at_css("turbo-frame")
|
|
59
|
+
frame["src"] = turbo_frame_src(replay.env)
|
|
60
|
+
frame.to_html
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def turbo_frame_src(env)
|
|
64
|
+
env = env.transform_keys(&:to_s)
|
|
65
|
+
path = "#{env["SCRIPT_NAME"]}#{env["PATH_INFO"]}"
|
|
66
|
+
query = env["QUERY_STRING"].to_s
|
|
67
|
+
query.empty? ? path : "#{path}?#{query}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def render_template(replay)
|
|
71
|
+
template_context = replay_template_context
|
|
72
|
+
renderer_for(replay).render(
|
|
73
|
+
template: replay.template,
|
|
74
|
+
locals: revive_hash(replay.locals, template: template_context),
|
|
75
|
+
assigns: revive_hash(replay.assigns, template: template_context)
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def render_fragment(replay)
|
|
80
|
+
template_context = replay_template_context
|
|
81
|
+
renderer_for(replay).render(
|
|
82
|
+
partial: partial_path(replay.template),
|
|
83
|
+
locals: revive_hash(replay.locals, template: template_context),
|
|
84
|
+
assigns: revive_hash(replay.assigns, template: template_context)
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def render_collection(replay)
|
|
89
|
+
template_context = replay_template_context
|
|
90
|
+
options = revive_hash(replay.options, template: template_context)
|
|
91
|
+
collection = revive_value(replay.collection, template: template_context)
|
|
92
|
+
|
|
93
|
+
if replay.derived_partial?
|
|
94
|
+
renderer_for(replay).render(collection)
|
|
95
|
+
else
|
|
96
|
+
renderer_for(replay).render(options.merge(
|
|
97
|
+
partial: replay.partial,
|
|
98
|
+
collection: collection
|
|
99
|
+
))
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def render_collection_member(replay)
|
|
104
|
+
template_context = replay_template_context
|
|
105
|
+
options = revive_hash(replay.options, template: template_context)
|
|
106
|
+
record = revive_value(replay.record, template: template_context)
|
|
107
|
+
locals = options.fetch(:locals, {})
|
|
108
|
+
local_name = (options[:as] || inferred_local_name(replay.partial)).to_sym
|
|
109
|
+
|
|
110
|
+
renderer_for(replay).render(
|
|
111
|
+
partial: replay.partial,
|
|
112
|
+
locals: locals.merge(local_name => record)
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def renderer_for(replay)
|
|
117
|
+
if replay.controller_class
|
|
118
|
+
constantize(replay.controller_class).renderer
|
|
119
|
+
else
|
|
120
|
+
::ActionController::Base.renderer
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def rack_env(env)
|
|
125
|
+
env = application_env_config.merge(
|
|
126
|
+
env.each_with_object({}) { |(key, value), copy| copy[key.to_s] = revive_env_value(value) }
|
|
127
|
+
)
|
|
128
|
+
env["rack.input"] = StringIO.new
|
|
129
|
+
env["rack.errors"] ||= StringIO.new
|
|
130
|
+
env
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def application_env_config
|
|
134
|
+
return {} unless defined?(::Rails) && ::Rails.respond_to?(:application)
|
|
135
|
+
|
|
136
|
+
application = ::Rails.application
|
|
137
|
+
application.respond_to?(:env_config) ? application.env_config : {}
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def revive_hash(values, template: replay_template_context)
|
|
141
|
+
values = values.entries if values.is_a?(::Upkeep::Replay::HashValue)
|
|
142
|
+
|
|
143
|
+
values.each_with_object({}) do |(key, value), revived|
|
|
144
|
+
revived[key.to_sym] = revive_value(value, template: template)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def revive_value(snapshot, template: replay_template_context)
|
|
149
|
+
snapshot = ::Upkeep::Replay.value(snapshot)
|
|
150
|
+
|
|
151
|
+
case snapshot
|
|
152
|
+
when ::Upkeep::Replay::ActiveRecordValue
|
|
153
|
+
constantize(snapshot.model).find(snapshot.id)
|
|
154
|
+
when ::Upkeep::Replay::ActiveRecordRelationValue
|
|
155
|
+
constantize(snapshot.model).find_by_sql(snapshot.sql)
|
|
156
|
+
when ::Upkeep::Replay::ArrayValue
|
|
157
|
+
snapshot.items.map { |item| revive_value(item, template: template) }
|
|
158
|
+
when ::Upkeep::Replay::HashValue
|
|
159
|
+
revive_hash(snapshot.entries, template: template)
|
|
160
|
+
when ::Upkeep::Replay::LiteralValue
|
|
161
|
+
snapshot.value
|
|
162
|
+
when ::Upkeep::Replay::RailsFormBuilderValue
|
|
163
|
+
revive_form_builder(snapshot, template: template)
|
|
164
|
+
else
|
|
165
|
+
raise "unsupported Rails replay value type: #{snapshot.class.name}"
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def revive_form_builder(snapshot, template:)
|
|
170
|
+
constantize(snapshot.builder_class).new(
|
|
171
|
+
snapshot.object_name,
|
|
172
|
+
revive_value(snapshot.object, template: template),
|
|
173
|
+
template,
|
|
174
|
+
revive_hash(snapshot.options, template: template)
|
|
175
|
+
)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def replay_template_context
|
|
179
|
+
::ActionView::Base.empty
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def revive_env_value(value)
|
|
183
|
+
return revive_replay_session(value) if replay_session_snapshot?(value)
|
|
184
|
+
return revive_replay_warden(value) if replay_warden_snapshot?(value)
|
|
185
|
+
|
|
186
|
+
case value
|
|
187
|
+
when Hash
|
|
188
|
+
value.transform_values { |nested_value| revive_env_value(nested_value) }
|
|
189
|
+
when Array
|
|
190
|
+
value.map { |nested_value| revive_env_value(nested_value) }
|
|
191
|
+
else
|
|
192
|
+
value
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def replay_session_snapshot?(value)
|
|
197
|
+
return false unless value.is_a?(Hash)
|
|
198
|
+
|
|
199
|
+
type = value["__upkeep_replay_type"] || value[:__upkeep_replay_type]
|
|
200
|
+
type == "rack_session"
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def revive_replay_session(snapshot)
|
|
204
|
+
values = snapshot["values"] || snapshot[:values] || {}
|
|
205
|
+
RackSession.new(revive_env_value(values))
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def replay_warden_snapshot?(value)
|
|
209
|
+
return false unless value.is_a?(Hash)
|
|
210
|
+
|
|
211
|
+
type = value["__upkeep_replay_type"] || value[:__upkeep_replay_type]
|
|
212
|
+
type == "warden"
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def revive_replay_warden(snapshot)
|
|
216
|
+
users = snapshot["users"] || snapshot[:users] || {}
|
|
217
|
+
revived_users = users.to_h do |scope, user_snapshot|
|
|
218
|
+
value = ::Upkeep::Replay::Value.from_h(user_snapshot)
|
|
219
|
+
[scope, revive_value(value)]
|
|
220
|
+
end
|
|
221
|
+
Runtime::ObservedWarden.new(revived_users)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def partial_path(template)
|
|
225
|
+
template.to_s.sub(%r{(^|/)_([^/]+)\z}, "\\1\\2")
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def inferred_local_name(partial)
|
|
229
|
+
File.basename(partial.to_s).sub(/\A_/, "").to_sym
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def constantize(name)
|
|
233
|
+
name.to_s.split("::").reduce(Object) { |namespace, constant_name| namespace.const_get(constant_name) }
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def collect_response_body(body)
|
|
237
|
+
body.each.to_a.join
|
|
238
|
+
ensure
|
|
239
|
+
body.close if body.respond_to?(:close)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Upkeep
|
|
4
|
+
module Rails
|
|
5
|
+
# Test helpers for asserting the public Upkeep subscription lifecycle from
|
|
6
|
+
# Rails request, integration, and system tests.
|
|
7
|
+
module Testing
|
|
8
|
+
CHANGE_FACTS_THREAD_KEY = :upkeep_rails_testing_change_facts
|
|
9
|
+
@broadcast_capture_mutex = Mutex.new
|
|
10
|
+
@broadcast_captures = []
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
# Drains the async delivery dispatcher when a test needs deterministic
|
|
14
|
+
# broadcast assertions.
|
|
15
|
+
#
|
|
16
|
+
# Production code should not call this; normal app delivery runs
|
|
17
|
+
# on the in-process dispatcher.
|
|
18
|
+
#
|
|
19
|
+
# @return [void]
|
|
20
|
+
def drain_delivery!
|
|
21
|
+
Upkeep::Rails.send(:drain_delivery_dispatcher!)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Captures facts passed into Upkeep delivery while the block runs. This
|
|
25
|
+
# exposes the same committed-change payloads the planner sees, without
|
|
26
|
+
# broadcasting or altering application code.
|
|
27
|
+
#
|
|
28
|
+
# @return [Array(Object, Array<Hash>)] the block result and captured facts.
|
|
29
|
+
def capture_change_facts
|
|
30
|
+
previous = Thread.current[CHANGE_FACTS_THREAD_KEY]
|
|
31
|
+
facts = []
|
|
32
|
+
Thread.current[CHANGE_FACTS_THREAD_KEY] = facts
|
|
33
|
+
|
|
34
|
+
[yield, facts]
|
|
35
|
+
ensure
|
|
36
|
+
Thread.current[CHANGE_FACTS_THREAD_KEY] = previous
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Records delivery facts for capture_change_facts. Internal test hook;
|
|
40
|
+
# production delivery calls this only when a capture is active.
|
|
41
|
+
def record_change_facts(changes)
|
|
42
|
+
facts = Thread.current[CHANGE_FACTS_THREAD_KEY]
|
|
43
|
+
return unless facts
|
|
44
|
+
|
|
45
|
+
facts.concat(Array(changes).map { |change| clone_change_fact(change) })
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @return [Boolean] true when the current thread is capturing change facts.
|
|
49
|
+
def capturing_change_facts?
|
|
50
|
+
!!Thread.current[CHANGE_FACTS_THREAD_KEY]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Captures rendered Upkeep delivery payloads while the block runs.
|
|
54
|
+
# This observes the batch after planning/rendering and before the
|
|
55
|
+
# app-specific transport adapter, so tests stay deterministic across
|
|
56
|
+
# ActionCable adapters.
|
|
57
|
+
def capture_broadcasts
|
|
58
|
+
broadcasts = []
|
|
59
|
+
broadcast_capture_mutex.synchronize { broadcast_captures << broadcasts }
|
|
60
|
+
|
|
61
|
+
yield
|
|
62
|
+
drain_delivery!
|
|
63
|
+
broadcasts.dup
|
|
64
|
+
ensure
|
|
65
|
+
broadcast_capture_mutex.synchronize { broadcast_captures.delete(broadcasts) } if broadcasts
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @return [Boolean] true when any thread is capturing delivery batches.
|
|
69
|
+
def capturing_broadcasts?
|
|
70
|
+
broadcast_capture_mutex.synchronize { broadcast_captures.any? }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Records a rendered delivery batch for active capture_broadcasts
|
|
74
|
+
# blocks. Internal test hook; production delivery calls this only when
|
|
75
|
+
# a capture is active.
|
|
76
|
+
def record_delivery_batch(batch)
|
|
77
|
+
captures = broadcast_capture_mutex.synchronize { broadcast_captures.dup }
|
|
78
|
+
return if captures.empty?
|
|
79
|
+
|
|
80
|
+
bodies = batch.envelopes.map(&:body)
|
|
81
|
+
return if bodies.empty?
|
|
82
|
+
|
|
83
|
+
broadcast_capture_mutex.synchronize do
|
|
84
|
+
captures.each do |capture|
|
|
85
|
+
capture.concat(bodies) if broadcast_captures.include?(capture)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Runs the invalidation planner against committed-change facts without
|
|
91
|
+
# enqueueing delivery or broadcasting.
|
|
92
|
+
#
|
|
93
|
+
# @param changes [Hash, Array<Hash>] one or more change facts.
|
|
94
|
+
# @param store [#reverse_index] subscription store to inspect.
|
|
95
|
+
# @return [Hash] concise planner match report.
|
|
96
|
+
def match_report(changes, store: Upkeep::Rails.subscriptions)
|
|
97
|
+
changes = changes.is_a?(Hash) ? [changes] : Array(changes)
|
|
98
|
+
lookup_payloads = []
|
|
99
|
+
subscription = ActiveSupport::Notifications.subscribe("lookup_subscription_index.upkeep") do |event|
|
|
100
|
+
lookup_payloads << event.payload.dup
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
plan = Upkeep::Invalidation::Planner.new(store: store).plan(changes)
|
|
104
|
+
|
|
105
|
+
{
|
|
106
|
+
candidate_entries: plan.candidate_entries.size,
|
|
107
|
+
matched_entries: plan.matched_entries.size,
|
|
108
|
+
miss_reason: match_miss_reason(plan, changes, lookup_payloads),
|
|
109
|
+
targets: plan.targets.map { |target| match_report_target(target) }
|
|
110
|
+
}
|
|
111
|
+
ensure
|
|
112
|
+
ActiveSupport::Notifications.unsubscribe(subscription) if subscription
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
attr_reader :broadcast_capture_mutex, :broadcast_captures
|
|
118
|
+
|
|
119
|
+
def clone_change_fact(change)
|
|
120
|
+
case change
|
|
121
|
+
when Hash
|
|
122
|
+
change.to_h.transform_values { |value| clone_change_fact(value) }
|
|
123
|
+
when Array
|
|
124
|
+
change.map { |value| clone_change_fact(value) }
|
|
125
|
+
else
|
|
126
|
+
begin
|
|
127
|
+
change.dup
|
|
128
|
+
rescue TypeError
|
|
129
|
+
change
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def match_miss_reason(plan, changes, lookup_payloads)
|
|
135
|
+
return nil if plan.targets.any?
|
|
136
|
+
return "no_changes" if changes.empty?
|
|
137
|
+
|
|
138
|
+
lookup_payloads.reverse_each do |payload|
|
|
139
|
+
return payload.fetch(:miss_reason) if payload.key?(:miss_reason)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
return "no_matching_subscriber" if plan.candidate_entries.empty?
|
|
143
|
+
return "dependencies_did_not_match_change" if plan.matched_entries.empty?
|
|
144
|
+
|
|
145
|
+
"no_renderable_target"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def match_report_target(target)
|
|
149
|
+
{
|
|
150
|
+
subscription_id: target.subscription_id,
|
|
151
|
+
subscriber_id: target.subscriber_id,
|
|
152
|
+
subscriber_ids: target.subscriber_ids,
|
|
153
|
+
target: {
|
|
154
|
+
kind: target.target.kind,
|
|
155
|
+
id: target.target.id,
|
|
156
|
+
reason: target.target.reason
|
|
157
|
+
},
|
|
158
|
+
shared_stream_target: {
|
|
159
|
+
kind: target.shared_stream_target.kind,
|
|
160
|
+
id: target.shared_stream_target.id
|
|
161
|
+
},
|
|
162
|
+
frame_id: target.frame_id,
|
|
163
|
+
identity_signature: target.identity_signature,
|
|
164
|
+
action: target.action,
|
|
165
|
+
matched_dependency_keys: target.matched_dependency_keys,
|
|
166
|
+
deoptimization_reason: target.deoptimization_reason
|
|
167
|
+
}
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Asserts that the last successful HTML response injected an Upkeep
|
|
172
|
+
# subscription marker and registered a subscription in the configured
|
|
173
|
+
# store.
|
|
174
|
+
#
|
|
175
|
+
# @param message [String, nil] optional assertion failure message.
|
|
176
|
+
# @return [void]
|
|
177
|
+
def assert_upkeep_subscription_registered(message = nil)
|
|
178
|
+
assert_select "upkeep-subscription-source[data-upkeep-subscription]"
|
|
179
|
+
assert Upkeep::Rails.subscriptions.subscriptions.any?,
|
|
180
|
+
message || "expected Upkeep to register at least one subscription"
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Returns the most recently registered Upkeep subscription.
|
|
184
|
+
#
|
|
185
|
+
# @return [Upkeep::Subscriptions::Subscription, nil]
|
|
186
|
+
def upkeep_subscription
|
|
187
|
+
Upkeep::Rails.subscriptions.subscriptions.last
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Returns every ActionCable stream name that can receive broadcasts for a
|
|
191
|
+
# subscription, including shared streams.
|
|
192
|
+
#
|
|
193
|
+
# @param subscription [Upkeep::Subscriptions::Subscription]
|
|
194
|
+
# @return [Array<String>]
|
|
195
|
+
# @raise [ArgumentError] when no subscription is registered.
|
|
196
|
+
def upkeep_stream_names(subscription = upkeep_subscription)
|
|
197
|
+
raise ArgumentError, "no Upkeep subscription is registered" unless subscription
|
|
198
|
+
|
|
199
|
+
([Delivery::ActionCableAdapter.subscription_stream_name_for(subscription.id)] +
|
|
200
|
+
subscription.metadata.fetch(:shared_stream_names, [])).uniq
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Activates the registered subscription so delivery lookup can find it.
|
|
204
|
+
#
|
|
205
|
+
# @param subscription [Upkeep::Subscriptions::Subscription]
|
|
206
|
+
# @return [Upkeep::Subscriptions::Subscription]
|
|
207
|
+
# @raise [ArgumentError] when no subscription is registered.
|
|
208
|
+
# @raise [Upkeep::Subscriptions::NotFound] when activation fails.
|
|
209
|
+
def activate_upkeep_subscription!(subscription = upkeep_subscription)
|
|
210
|
+
raise ArgumentError, "no Upkeep subscription is registered" unless subscription
|
|
211
|
+
|
|
212
|
+
activated = Upkeep::Rails.subscriptions.activate(subscription.id)
|
|
213
|
+
raise Upkeep::Subscriptions::NotFound, subscription.id unless activated
|
|
214
|
+
|
|
215
|
+
subscription
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Captures rendered Upkeep broadcasts while the block runs. This observes
|
|
219
|
+
# Upkeep after planning/rendering and before the application ActionCable
|
|
220
|
+
# adapter, so tests stay deterministic regardless of the host app's cable
|
|
221
|
+
# adapter.
|
|
222
|
+
#
|
|
223
|
+
# @param subscription [Upkeep::Subscriptions::Subscription]
|
|
224
|
+
# @return [Array<String>]
|
|
225
|
+
# @raise [ArgumentError] when called without a block or subscription.
|
|
226
|
+
def capture_upkeep_broadcasts(subscription = upkeep_subscription, &block)
|
|
227
|
+
raise ArgumentError, "capture_upkeep_broadcasts requires a block" unless block
|
|
228
|
+
raise ArgumentError, "no Upkeep subscription is registered" unless subscription
|
|
229
|
+
|
|
230
|
+
Upkeep::Rails::Testing.capture_broadcasts(&block)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Drains async Upkeep delivery for deterministic test assertions.
|
|
234
|
+
#
|
|
235
|
+
# @return [void]
|
|
236
|
+
def drain_upkeep_delivery!
|
|
237
|
+
Upkeep::Rails::Testing.drain_delivery!
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Captures facts passed into Upkeep delivery while the block runs.
|
|
241
|
+
#
|
|
242
|
+
# @return [Array(Object, Array<Hash>)] the block result and captured facts.
|
|
243
|
+
def capture_upkeep_change_facts(&block)
|
|
244
|
+
raise ArgumentError, "capture_upkeep_change_facts requires a block" unless block
|
|
245
|
+
|
|
246
|
+
Upkeep::Rails::Testing.capture_change_facts(&block)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Returns a dry-run invalidation planner report for one or more change
|
|
250
|
+
# facts against the configured Upkeep subscription store.
|
|
251
|
+
#
|
|
252
|
+
# @param changes [Hash, Array<Hash>] one or more change facts.
|
|
253
|
+
# @return [Hash]
|
|
254
|
+
def upkeep_match_report(changes)
|
|
255
|
+
Upkeep::Rails::Testing.match_report(changes)
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|