ruact 0.0.11 → 0.0.12
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 +4 -4
- data/CHANGELOG.md +110 -48
- data/CONTRIBUTING.md +233 -0
- data/README.md +14 -4
- data/RELEASING.md +125 -139
- data/lib/ruact/packaging.rb +68 -0
- data/lib/ruact/version.rb +1 -1
- data/vendor/javascript/vite-plugin-ruact/flight-client.test.mjs +321 -0
- data/vendor/javascript/vite-plugin-ruact/package-lock.json +11 -0
- data/vendor/javascript/vite-plugin-ruact/package.json +1 -0
- data/vendor/javascript/vite-plugin-ruact/runtime/flight-client.js +14 -3
- metadata +6 -107
- data/.codecov.yml +0 -31
- data/.github/workflows/ci.yml +0 -284
- data/.github/workflows/server-functions-bench.yml +0 -54
- data/.rubocop.yml +0 -107
- data/.rubocop_todo.yml +0 -63
- data/Rakefile +0 -10
- data/bench/server_functions_dispatch_bench.rb +0 -276
- data/bench/server_functions_dispatch_bench.results.md +0 -150
- data/docs/internal/README.md +0 -9
- data/docs/internal/decisions/server-functions-api.md +0 -2236
- data/spec/benchmarks/baseline.json +0 -12
- data/spec/benchmarks/render_pipeline_benchmark_spec.rb +0 -109
- data/spec/fixtures/flight/README.md +0 -136
- data/spec/fixtures/flight/array.txt +0 -1
- data/spec/fixtures/flight/as_json_object.txt +0 -2
- data/spec/fixtures/flight/bigint.txt +0 -1
- data/spec/fixtures/flight/boolean_false.txt +0 -1
- data/spec/fixtures/flight/boolean_true.txt +0 -1
- data/spec/fixtures/flight/client_component_with_props.txt +0 -2
- data/spec/fixtures/flight/client_reference.txt +0 -2
- data/spec/fixtures/flight/hash.txt +0 -1
- data/spec/fixtures/flight/infinity.txt +0 -1
- data/spec/fixtures/flight/nan.txt +0 -1
- data/spec/fixtures/flight/negative_infinity.txt +0 -1
- data/spec/fixtures/flight/nil.txt +0 -1
- data/spec/fixtures/flight/number_float.txt +0 -1
- data/spec/fixtures/flight/number_integer.txt +0 -1
- data/spec/fixtures/flight/react_element_no_props.txt +0 -1
- data/spec/fixtures/flight/redirect_row.txt +0 -1
- data/spec/fixtures/flight/serializable_object.txt +0 -2
- data/spec/fixtures/flight/string_basic.txt +0 -1
- data/spec/fixtures/flight/string_dollar_escape.txt +0 -1
- data/spec/fixtures/flight/undefined.txt +0 -1
- data/spec/fixtures/readme/children-error.html.erb +0 -3
- data/spec/fixtures/readme/children-error.txt +0 -1
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/errors_demo/new.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/exploding_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/ghost_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/rootless_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/unwired_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/layouts/bare_host.html.erb +0 -16
- data/spec/fixtures/story_7_9_views/layouts/exploding_host.html.erb +0 -24
- data/spec/fixtures/story_7_9_views/layouts/rootless_host.html.erb +0 -15
- data/spec/fixtures/story_7_9_views/layouts/ruact_host.html.erb +0 -17
- data/spec/readme_demo_message_spec.rb +0 -67
- data/spec/readme_spec.rb +0 -282
- data/spec/ruact/client_manifest_spec.rb +0 -270
- data/spec/ruact/component_contract_spec.rb +0 -119
- data/spec/ruact/configuration_spec.rb +0 -518
- data/spec/ruact/controller_request_spec.rb +0 -671
- data/spec/ruact/controller_spec.rb +0 -343
- data/spec/ruact/doctor_spec.rb +0 -769
- data/spec/ruact/erb_preprocessor_hook_spec.rb +0 -55
- data/spec/ruact/erb_preprocessor_spec.rb +0 -361
- data/spec/ruact/errors_spec.rb +0 -93
- data/spec/ruact/flight/renderer_spec.rb +0 -133
- data/spec/ruact/flight/serializer_spec.rb +0 -494
- data/spec/ruact/html_converter_spec.rb +0 -375
- data/spec/ruact/install_generator_spec.rb +0 -1549
- data/spec/ruact/layout_source_spec.rb +0 -108
- data/spec/ruact/manifest_resolver_spec.rb +0 -174
- data/spec/ruact/query_request_spec.rb +0 -706
- data/spec/ruact/query_spec.rb +0 -105
- data/spec/ruact/railtie_spec.rb +0 -155
- data/spec/ruact/render_context_spec.rb +0 -58
- data/spec/ruact/render_pipeline_concurrency_spec.rb +0 -78
- data/spec/ruact/render_pipeline_spec.rb +0 -928
- data/spec/ruact/scaffold_generator_spec.rb +0 -1849
- data/spec/ruact/serializable_spec.rb +0 -179
- data/spec/ruact/server_bucket_request_spec.rb +0 -785
- data/spec/ruact/server_function_name_spec.rb +0 -53
- data/spec/ruact/server_functions/backtrace_cleaner_spec.rb +0 -63
- data/spec/ruact/server_functions/bucket_two_payload_spec.rb +0 -200
- data/spec/ruact/server_functions/codegen_spec.rb +0 -397
- data/spec/ruact/server_functions/error_payload_spec.rb +0 -222
- data/spec/ruact/server_functions/error_suggestion_spec.rb +0 -79
- data/spec/ruact/server_functions/introspection_spec.rb +0 -135
- data/spec/ruact/server_functions/name_bridge_spec.rb +0 -212
- data/spec/ruact/server_functions/query_context_spec.rb +0 -72
- data/spec/ruact/server_functions/query_source_spec.rb +0 -193
- data/spec/ruact/server_functions/railtie_integration_spec.rb +0 -215
- data/spec/ruact/server_functions/rake_spec.rb +0 -86
- data/spec/ruact/server_functions/route_source_spec.rb +0 -202
- data/spec/ruact/server_functions/snapshot_spec.rb +0 -96
- data/spec/ruact/server_functions/snapshot_writer_spec.rb +0 -71
- data/spec/ruact/server_rescue_request_spec.rb +0 -416
- data/spec/ruact/server_spec.rb +0 -179
- data/spec/ruact/server_upload_request_spec.rb +0 -311
- data/spec/ruact/signed_references_spec.rb +0 -164
- data/spec/ruact/string_distance_spec.rb +0 -38
- data/spec/ruact/tasks_json_introspection_spec.rb +0 -141
- data/spec/ruact/testing/have_ruact_component_spec.rb +0 -170
- data/spec/ruact/testing/no_production_load_spec.rb +0 -41
- data/spec/ruact/validation_errors_spec.rb +0 -116
- data/spec/ruact/view_helper_spec.rb +0 -131
- data/spec/spec_helper.rb +0 -77
- data/spec/support/fixtures/pixel.png +0 -0
- data/spec/support/flight_wire_parser.rb +0 -21
- data/spec/support/flight_wire_parser_spec.rb +0 -93
- data/spec/support/matchers/flight_fixture_matcher.rb +0 -130
- data/spec/support/matchers/flight_fixture_matcher_spec.rb +0 -250
- data/spec/support/rails_stub.rb +0 -115
data/spec/ruact/query_spec.rb
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Story 9.4 — unit spec for the Ruact::Query base class. Deliberately NO Rails
|
|
4
|
-
# boot (AC3): a query subclass is exercised with a plain double standing in for
|
|
5
|
-
# the dispatch context, proving `CatalogQuery.new(fake_context).categories` is
|
|
6
|
-
# unit-testable in isolation.
|
|
7
|
-
require "spec_helper"
|
|
8
|
-
|
|
9
|
-
module Ruact
|
|
10
|
-
RSpec.describe Query, :story_9_4 do
|
|
11
|
-
let(:fake_user) { { "id" => 42 } }
|
|
12
|
-
let(:fake_params) { { "q" => "ruby" } }
|
|
13
|
-
let(:fake_request) { instance_double(Object) }
|
|
14
|
-
let(:fake_session) { { "token" => "abc" } }
|
|
15
|
-
let(:context) do
|
|
16
|
-
double(
|
|
17
|
-
current_user: fake_user,
|
|
18
|
-
params: fake_params,
|
|
19
|
-
request: fake_request,
|
|
20
|
-
session: fake_session
|
|
21
|
-
)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
let(:query_class) do
|
|
25
|
-
Class.new(described_class) do
|
|
26
|
-
def categories
|
|
27
|
-
%w[books games]
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def whoami
|
|
31
|
-
current_user
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def search
|
|
35
|
-
params["q"]
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
describe "Story 9.4 — context accessors delegate to the injected context (AC3)" do
|
|
41
|
-
subject(:query) { query_class.new(context) }
|
|
42
|
-
|
|
43
|
-
it "exposes current_user from the context" do
|
|
44
|
-
expect(query.whoami).to eq(fake_user)
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
it "exposes params from the context" do
|
|
48
|
-
expect(query.search).to eq("ruby")
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
it "exposes request from the context" do
|
|
52
|
-
expect(query.request).to be(fake_request)
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
it "exposes session from the context" do
|
|
56
|
-
expect(query.session).to eq(fake_session)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
it "is unit-testable with a plain fake context and no Rails boot (AC3)" do
|
|
60
|
-
expect(query_class.new(context).categories).to eq(%w[books games])
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
describe "Story 9.4 — accessor methods are inherited, never own methods of the subclass (AC1)" do
|
|
65
|
-
it "keeps current_user/params/request/session OUT of the subclass's public_instance_methods(false)" do
|
|
66
|
-
own = query_class.public_instance_methods(false)
|
|
67
|
-
expect(own).to contain_exactly(:categories, :whoami, :search)
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
it "defines the accessors on Ruact::Query itself (inherited by every subclass)" do
|
|
71
|
-
expect(described_class.public_instance_methods(false))
|
|
72
|
-
.to include(:current_user, :params, :request, :session)
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
describe "Story 9.4 — ruact_skip_before_action class macro (AC4 / D1)" do
|
|
77
|
-
it "records the callback with its options on the query class" do
|
|
78
|
-
klass = Class.new(described_class)
|
|
79
|
-
klass.ruact_skip_before_action(:require_login, only: :categories)
|
|
80
|
-
expect(klass.__ruact_skipped_callbacks).to eq([[[:require_login], { only: :categories }]])
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
it "accepts multiple callbacks in one call, mirroring Rails' skip_before_action" do
|
|
84
|
-
klass = Class.new(described_class)
|
|
85
|
-
klass.ruact_skip_before_action(:require_login, :check_tenant)
|
|
86
|
-
expect(klass.__ruact_skipped_callbacks).to eq([[%i[require_login check_tenant], {}]])
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
it "accumulates across calls in declaration order" do
|
|
90
|
-
klass = Class.new(described_class)
|
|
91
|
-
klass.ruact_skip_before_action(:require_login)
|
|
92
|
-
klass.ruact_skip_before_action(:check_tenant, raise: false)
|
|
93
|
-
expect(klass.__ruact_skipped_callbacks)
|
|
94
|
-
.to eq([[[:require_login], {}], [[:check_tenant], { raise: false }]])
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
it "keeps the recorded skips per-class — sibling query classes never share them" do
|
|
98
|
-
klass_a = Class.new(described_class)
|
|
99
|
-
klass_b = Class.new(described_class)
|
|
100
|
-
klass_a.ruact_skip_before_action(:require_login)
|
|
101
|
-
expect(klass_b.__ruact_skipped_callbacks).to be_empty
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
end
|
data/spec/ruact/railtie_spec.rb
DELETED
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "spec_helper"
|
|
4
|
-
require "pathname"
|
|
5
|
-
require "logger"
|
|
6
|
-
require "socket"
|
|
7
|
-
|
|
8
|
-
# The Rails stub is provided by spec/support/rails_stub.rb.
|
|
9
|
-
# We explicitly require the railtie here (was skipped in ruact.rb because
|
|
10
|
-
# Rails was not defined when that file loaded).
|
|
11
|
-
require "ruact/railtie"
|
|
12
|
-
|
|
13
|
-
RSpec.describe Ruact::Railtie do
|
|
14
|
-
let(:missing_path) { Pathname.new("/nonexistent/path/react-client-manifest.json") }
|
|
15
|
-
let(:fake_logger) { instance_double(Logger, warn: nil, info: nil) }
|
|
16
|
-
|
|
17
|
-
before do
|
|
18
|
-
Rails.logger = fake_logger
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
describe ".detect_streaming_mode! (AC#1–3)" do
|
|
22
|
-
after { Ruact.streaming_mode = nil }
|
|
23
|
-
|
|
24
|
-
context "when Puma is defined" do
|
|
25
|
-
before do
|
|
26
|
-
puma = Module.new { const_set(:Server, Class.new) }
|
|
27
|
-
stub_const("Puma", puma)
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
it "sets streaming_mode to :enabled" do
|
|
31
|
-
described_class.detect_streaming_mode!
|
|
32
|
-
expect(Ruact.streaming_mode).to eq(:enabled)
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
it "logs streaming: enabled (Puma detected)" do
|
|
36
|
-
described_class.detect_streaming_mode!
|
|
37
|
-
expect(fake_logger).to have_received(:info)
|
|
38
|
-
.with(a_string_including("streaming: enabled (Puma detected)"))
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
context "when Unicorn is defined" do
|
|
43
|
-
before { stub_const("Unicorn", Module.new) }
|
|
44
|
-
|
|
45
|
-
it "sets streaming_mode to :buffered" do
|
|
46
|
-
described_class.detect_streaming_mode!
|
|
47
|
-
expect(Ruact.streaming_mode).to eq(:buffered)
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
it "logs streaming: buffered (Unicorn detected)" do
|
|
51
|
-
described_class.detect_streaming_mode!
|
|
52
|
-
expect(fake_logger).to have_received(:info)
|
|
53
|
-
.with(a_string_including("streaming: buffered (Unicorn detected)"))
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
context "when PhusionPassenger is defined" do
|
|
58
|
-
before { stub_const("PhusionPassenger", Module.new) }
|
|
59
|
-
|
|
60
|
-
it "sets streaming_mode to :buffered" do
|
|
61
|
-
described_class.detect_streaming_mode!
|
|
62
|
-
expect(Ruact.streaming_mode).to eq(:buffered)
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
it "logs streaming: buffered (Passenger detected)" do
|
|
66
|
-
described_class.detect_streaming_mode!
|
|
67
|
-
expect(fake_logger).to have_received(:info)
|
|
68
|
-
.with(a_string_including("streaming: buffered (Passenger detected)"))
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
context "when no recognized server constant is defined" do
|
|
73
|
-
it "sets streaming_mode to :buffered" do
|
|
74
|
-
described_class.detect_streaming_mode!
|
|
75
|
-
expect(Ruact.streaming_mode).to eq(:buffered)
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
it "logs server unknown — defaulting to safe mode" do
|
|
79
|
-
described_class.detect_streaming_mode!
|
|
80
|
-
expect(fake_logger).to have_received(:info)
|
|
81
|
-
.with(a_string_including("server unknown — defaulting to safe mode"))
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
describe ".check_manifest!" do
|
|
87
|
-
context "with missing manifest in development (AC#5, #7)" do
|
|
88
|
-
before do
|
|
89
|
-
Rails.env = ActiveSupport::StringInquirer.new("development")
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
it "does not raise" do
|
|
93
|
-
expect { described_class.check_manifest!(missing_path) }.not_to raise_error
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
it "logs a [ruact] prefixed warning" do
|
|
97
|
-
described_class.check_manifest!(missing_path)
|
|
98
|
-
expect(fake_logger).to have_received(:warn)
|
|
99
|
-
.with(a_string_starting_with("[ruact]"))
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
it "includes the manifest path in the warning" do
|
|
103
|
-
described_class.check_manifest!(missing_path)
|
|
104
|
-
expect(fake_logger).to have_received(:warn)
|
|
105
|
-
.with(a_string_including(missing_path.to_s))
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
context "with missing manifest in production (AC#6)" do
|
|
110
|
-
before { Rails.env = ActiveSupport::StringInquirer.new("production") }
|
|
111
|
-
after { Rails.env = ActiveSupport::StringInquirer.new("development") }
|
|
112
|
-
|
|
113
|
-
it "raises ManifestError" do
|
|
114
|
-
expect { described_class.check_manifest!(missing_path) }
|
|
115
|
-
.to raise_error(Ruact::ManifestError)
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
it "error message contains 'run vite build before deploying'" do
|
|
119
|
-
expect { described_class.check_manifest!(missing_path) }
|
|
120
|
-
.to raise_error(Ruact::ManifestError, /run vite build before deploying/)
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
describe ".check_vite!" do
|
|
126
|
-
context "when Vite is running (AC#4)" do
|
|
127
|
-
before do
|
|
128
|
-
allow(TCPSocket).to receive(:new).and_return(instance_double(TCPSocket, close: nil))
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
it "does not log a warning" do
|
|
132
|
-
described_class.check_vite!
|
|
133
|
-
expect(fake_logger).not_to have_received(:warn)
|
|
134
|
-
end
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
context "when Vite is not running (AC#4)" do
|
|
138
|
-
before do
|
|
139
|
-
allow(TCPSocket).to receive(:new).and_raise(Errno::ECONNREFUSED)
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
it "logs a [ruact] prefixed warning" do
|
|
143
|
-
described_class.check_vite!
|
|
144
|
-
expect(fake_logger).to have_received(:warn)
|
|
145
|
-
.with(a_string_starting_with("[ruact]"))
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
it "mentions Vite and port 5173" do
|
|
149
|
-
described_class.check_vite!
|
|
150
|
-
expect(fake_logger).to have_received(:warn)
|
|
151
|
-
.with(a_string_including("localhost:5173"))
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
|
-
end
|
|
155
|
-
end
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "spec_helper"
|
|
4
|
-
|
|
5
|
-
module Ruact
|
|
6
|
-
RSpec.describe RenderContext do
|
|
7
|
-
subject(:ctx) { described_class.new }
|
|
8
|
-
|
|
9
|
-
describe "#register" do
|
|
10
|
-
it "appends a new component entry" do
|
|
11
|
-
ctx.register("NavBar", { "currentUser" => 1 })
|
|
12
|
-
expect(ctx.components.length).to eq(1)
|
|
13
|
-
expect(ctx.components.first[:name]).to eq("NavBar")
|
|
14
|
-
expect(ctx.components.first[:props]).to eq({ "currentUser" => 1 })
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
it "returns a token of the form __RUACT_<index>__" do
|
|
18
|
-
token = ctx.register("Foo", {})
|
|
19
|
-
expect(token).to eq("__RUACT_0__")
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
it "increments token indices across successive registrations" do
|
|
23
|
-
t0 = ctx.register("A", {})
|
|
24
|
-
t1 = ctx.register("B", {})
|
|
25
|
-
t2 = ctx.register("C", {})
|
|
26
|
-
expect([t0, t1, t2]).to eq(%w[__RUACT_0__ __RUACT_1__ __RUACT_2__])
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
describe "#components" do
|
|
31
|
-
it "starts empty" do
|
|
32
|
-
expect(ctx.components).to eq([])
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
describe "#by_token" do
|
|
37
|
-
it "finds a registered component by its token" do
|
|
38
|
-
ctx.register("NavBar", { "x" => 1 })
|
|
39
|
-
entry = ctx.by_token("__RUACT_0__")
|
|
40
|
-
expect(entry[:name]).to eq("NavBar")
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
it "returns nil for an unknown token" do
|
|
44
|
-
expect(ctx.by_token("__RUACT_99__")).to be_nil
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
describe "isolation" do
|
|
49
|
-
it "two contexts are independent" do
|
|
50
|
-
a = described_class.new
|
|
51
|
-
b = described_class.new
|
|
52
|
-
a.register("A", {})
|
|
53
|
-
expect(b.components).to be_empty
|
|
54
|
-
expect(a.components.length).to eq(1)
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "spec_helper"
|
|
4
|
-
|
|
5
|
-
module Ruact
|
|
6
|
-
RSpec.describe "Concurrent render isolation" do
|
|
7
|
-
let(:manifest) do
|
|
8
|
-
ClientManifest.from_hash(
|
|
9
|
-
(0..7).to_h do |i|
|
|
10
|
-
["ThreadComponent#{i}", { "id" => "/tc#{i}.js",
|
|
11
|
-
"name" => "ThreadComponent#{i}",
|
|
12
|
-
"chunks" => ["/tc#{i}.js"] }]
|
|
13
|
-
end
|
|
14
|
-
)
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
let(:pipeline) { RenderPipeline.new(manifest) }
|
|
18
|
-
|
|
19
|
-
# Per Story 7.1 AC4: prove thread isolation deterministically with a
|
|
20
|
-
# countdown latch (Mutex+Queue) so all threads arrive at the render call
|
|
21
|
-
# simultaneously, fixed seed for reproducibility, ≥4 threads to outnumber
|
|
22
|
-
# typical CI cores, and ≥100 iterations to expose any race that fires
|
|
23
|
-
# ≥1% of the time.
|
|
24
|
-
let(:thread_count) { 4 }
|
|
25
|
-
let(:iterations) { 100 }
|
|
26
|
-
|
|
27
|
-
def run_isolation_iteration(iter, count, pipeline)
|
|
28
|
-
srand(0xC0DE + iter)
|
|
29
|
-
ready_latch = Queue.new
|
|
30
|
-
release = Queue.new
|
|
31
|
-
results = Array.new(count)
|
|
32
|
-
results_mu = Mutex.new
|
|
33
|
-
|
|
34
|
-
threads = Array.new(count) do |tid|
|
|
35
|
-
Thread.new do
|
|
36
|
-
ctx = Object.new
|
|
37
|
-
ctx.instance_variable_set(:@tid, tid)
|
|
38
|
-
binding_ctx = ctx.instance_eval { binding }
|
|
39
|
-
ready_latch << :ready
|
|
40
|
-
release.pop
|
|
41
|
-
output = pipeline.render(
|
|
42
|
-
{ erb: "<ThreadComponent#{tid} thread_id={@tid} />", binding: binding_ctx },
|
|
43
|
-
mode: :string
|
|
44
|
-
)
|
|
45
|
-
results_mu.synchronize { results[tid] = output }
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
count.times { ready_latch.pop }
|
|
50
|
-
count.times { release << :go }
|
|
51
|
-
threads.each(&:join)
|
|
52
|
-
results
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
it "isolates each render's component registry from other concurrent renders" do
|
|
56
|
-
iterations.times do |iter|
|
|
57
|
-
results = run_isolation_iteration(iter, thread_count, pipeline)
|
|
58
|
-
|
|
59
|
-
results.each_with_index do |output, tid|
|
|
60
|
-
expect(output).to include_flight_row(
|
|
61
|
-
class: :import, payload: array_including("ThreadComponent#{tid}")
|
|
62
|
-
), "iter #{iter} thread #{tid}: missing own component import"
|
|
63
|
-
expect(output).to include_flight_row(
|
|
64
|
-
class: :model, payload: array_including(hash_including("thread_id" => tid))
|
|
65
|
-
), "iter #{iter} thread #{tid}: missing own thread_id prop"
|
|
66
|
-
|
|
67
|
-
(0...thread_count).each do |other_tid|
|
|
68
|
-
next if other_tid == tid
|
|
69
|
-
|
|
70
|
-
expect(output).not_to include_flight_row(
|
|
71
|
-
class: :import, payload: array_including("ThreadComponent#{other_tid}")
|
|
72
|
-
), "iter #{iter} thread #{tid} leaked ThreadComponent#{other_tid}"
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|