ruact 0.0.10 → 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 +123 -48
- data/CONTRIBUTING.md +233 -0
- data/README.md +16 -6
- 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 -1
- data/spec/benchmarks/render_pipeline_benchmark_spec.rb +0 -92
- 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
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "spec_helper"
|
|
4
|
-
require "active_support/all"
|
|
5
|
-
require "action_dispatch"
|
|
6
|
-
require "ruact/server_functions/route_source"
|
|
7
|
-
|
|
8
|
-
module Ruact
|
|
9
|
-
module ServerFunctions
|
|
10
|
-
# Story 9.3 — route introspection: Rails.application.routes (filtered to
|
|
11
|
-
# non-GET routes on controllers that include Ruact::Server) → v2 mutation
|
|
12
|
-
# entries with the derivation table locked in the 2026-06-09 ADR addendum.
|
|
13
|
-
RSpec.describe RouteSource, :story_9_3 do
|
|
14
|
-
# Builds an isolated RouteSet so specs never depend on a host app's routes.
|
|
15
|
-
def route_set(&blk)
|
|
16
|
-
rs = ActionDispatch::Routing::RouteSet.new
|
|
17
|
-
rs.draw(&blk)
|
|
18
|
-
rs
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
# All controllers count as Ruact::Server hosts (isolates the derivation
|
|
22
|
-
# table from constant-resolution); the filter behaviour is tested
|
|
23
|
-
# separately below.
|
|
24
|
-
def collect(routes, overrides: {})
|
|
25
|
-
described_class.collect(
|
|
26
|
-
routes,
|
|
27
|
-
host_predicate: ->(_controller) { true },
|
|
28
|
-
overrides_for: ->(controller) { overrides.fetch(controller, {}) }
|
|
29
|
-
)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def ids(entries) = entries.map { |e| e["js_identifier"] }
|
|
33
|
-
|
|
34
|
-
describe "derivation table (AC3)" do
|
|
35
|
-
it "names the RESTful collection create as singular verb+resource" do
|
|
36
|
-
rs = route_set { resources :posts, only: %i[create] }
|
|
37
|
-
entry = collect(rs).fetch(0)
|
|
38
|
-
expect(entry["js_identifier"]).to eq("createPost")
|
|
39
|
-
expect(entry["http_method"]).to eq("POST")
|
|
40
|
-
expect(entry["path"]).to eq("/posts")
|
|
41
|
-
expect(entry["segments"]).to eq([])
|
|
42
|
-
expect(entry["kind"]).to eq("action")
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
it "names update (member) singular and prefers PATCH over the PUT alias" do
|
|
46
|
-
rs = route_set { resources :posts, only: %i[update] }
|
|
47
|
-
entries = collect(rs)
|
|
48
|
-
expect(entries.size).to eq(1) # PATCH + PUT collapse to one entry
|
|
49
|
-
entry = entries.fetch(0)
|
|
50
|
-
expect(entry["js_identifier"]).to eq("updatePost")
|
|
51
|
-
expect(entry["http_method"]).to eq("PATCH")
|
|
52
|
-
expect(entry["path"]).to eq("/posts/:id")
|
|
53
|
-
expect(entry["segments"]).to eq(["id"])
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
it "names destroy (member) singular" do
|
|
57
|
-
rs = route_set { resources :posts, only: %i[destroy] }
|
|
58
|
-
entry = collect(rs).fetch(0)
|
|
59
|
-
expect(entry["js_identifier"]).to eq("destroyPost")
|
|
60
|
-
expect(entry["http_method"]).to eq("DELETE")
|
|
61
|
-
expect(entry["path"]).to eq("/posts/:id")
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
it "skips the GET RESTful actions (index/show/new/edit)" do
|
|
65
|
-
rs = route_set { resources :posts }
|
|
66
|
-
expect(ids(collect(rs))).to match_array(%w[createPost updatePost destroyPost])
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
it "names a custom member route singular (verb-action + singular resource)" do
|
|
70
|
-
rs = route_set do
|
|
71
|
-
resources :posts do
|
|
72
|
-
member { post :publish }
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
entry = collect(rs).find { |e| e["js_identifier"] == "publishPost" }
|
|
76
|
-
expect(entry).not_to be_nil
|
|
77
|
-
expect(entry["http_method"]).to eq("POST")
|
|
78
|
-
expect(entry["path"]).to eq("/posts/:id/publish")
|
|
79
|
-
expect(entry["segments"]).to eq(["id"])
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
it "names a custom collection route plural" do
|
|
83
|
-
rs = route_set do
|
|
84
|
-
resources :posts do
|
|
85
|
-
collection { post :publish_all }
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
entry = collect(rs).find { |e| e["js_identifier"] == "publishAllPosts" }
|
|
89
|
-
expect(entry).not_to be_nil
|
|
90
|
-
expect(entry["path"]).to eq("/posts/publish_all")
|
|
91
|
-
expect(entry["segments"]).to eq([])
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
it "names singular-resource create/destroy singular" do
|
|
95
|
-
rs = route_set { resource :session, only: %i[create destroy] }
|
|
96
|
-
expect(ids(collect(rs))).to match_array(%w[createSession destroySession])
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
it "prefixes namespaced controllers (verb + Namespace + Resource)" do
|
|
100
|
-
rs = route_set { namespace(:admin) { resources :posts, only: %i[create update] } }
|
|
101
|
-
expect(ids(collect(rs))).to match_array(%w[createAdminPost updateAdminPost])
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
it "deep-prefixes multi-level namespaces" do
|
|
105
|
-
rs = route_set do
|
|
106
|
-
namespace(:admin) { namespace(:reports) { resources :posts, only: %i[create] } }
|
|
107
|
-
end
|
|
108
|
-
expect(ids(collect(rs))).to eq(%w[createAdminReportsPost])
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
it "sorts entries by js_identifier for deterministic output" do
|
|
112
|
-
rs = route_set { resources :posts }
|
|
113
|
-
expect(ids(collect(rs))).to eq(%w[createPost destroyPost updatePost])
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
it "classifies a custom-param member route (param: :slug) as member → singular" do
|
|
117
|
-
rs = route_set do
|
|
118
|
-
resources :posts, param: :slug do
|
|
119
|
-
member { post :publish }
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
entry = collect(rs).find { |e| e["action"] == "publish" }
|
|
123
|
-
expect(entry["js_identifier"]).to eq("publishPost")
|
|
124
|
-
expect(entry["segments"]).to eq(["slug"])
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
it "classifies a nested collection route (only parent :id present) as collection → plural" do
|
|
128
|
-
rs = route_set do
|
|
129
|
-
resources :posts, only: [] do
|
|
130
|
-
resources :comments, only: [] do
|
|
131
|
-
collection { post :flag_all }
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
entry = collect(rs).find { |e| e["action"] == "flag_all" }
|
|
136
|
-
expect(entry["js_identifier"]).to eq("flagAllComments")
|
|
137
|
-
end
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
describe "rename override (AC4 input)" do
|
|
141
|
-
it "uses the per-controller override identifier when present" do
|
|
142
|
-
rs = route_set do
|
|
143
|
-
resources :posts do
|
|
144
|
-
collection { post :publish_all }
|
|
145
|
-
end
|
|
146
|
-
end
|
|
147
|
-
entries = collect(rs, overrides: { "posts" => { "publish_all" => "publishEverything" } })
|
|
148
|
-
expect(ids(entries)).to include("publishEverything")
|
|
149
|
-
expect(ids(entries)).not_to include("publishAllPosts")
|
|
150
|
-
end
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
describe "collision detection (AC4)" do
|
|
154
|
-
it "fails loudly naming BOTH origins when two routes map to the same identifier" do
|
|
155
|
-
rs = route_set do
|
|
156
|
-
resources :posts, only: %i[create]
|
|
157
|
-
resources :comments, only: %i[create]
|
|
158
|
-
end
|
|
159
|
-
# Force a collision: comments#create is renamed onto posts#create's name.
|
|
160
|
-
overrides = { "comments" => { "create" => "createPost" } }
|
|
161
|
-
expect do
|
|
162
|
-
collect(rs, overrides: overrides)
|
|
163
|
-
end.to raise_error(Ruact::ConfigurationError, /naming collision/) { |err|
|
|
164
|
-
expect(err.message).to include("posts#create")
|
|
165
|
-
expect(err.message).to include("comments#create")
|
|
166
|
-
expect(err.message).to include("createPost")
|
|
167
|
-
}
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
it "boot succeeds once the colliding route is renamed to a free identifier (full cycle)" do
|
|
171
|
-
rs = route_set do
|
|
172
|
-
resources :posts, only: %i[create]
|
|
173
|
-
resources :comments, only: %i[create]
|
|
174
|
-
end
|
|
175
|
-
overrides = { "comments" => { "create" => "createComment" } }
|
|
176
|
-
ids_out = ids(collect(rs, overrides: overrides))
|
|
177
|
-
expect(ids_out).to eq(%w[createComment createPost])
|
|
178
|
-
end
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
describe "host filter (Ruact::Server only)" do
|
|
182
|
-
it "skips routes whose controller is not a Ruact::Server host" do
|
|
183
|
-
rs = route_set do
|
|
184
|
-
resources :posts, only: %i[create]
|
|
185
|
-
resources :widgets, only: %i[create]
|
|
186
|
-
end
|
|
187
|
-
entries = described_class.collect(
|
|
188
|
-
rs,
|
|
189
|
-
host_predicate: ->(controller) { controller == "posts" },
|
|
190
|
-
overrides_for: ->(_c) { {} }
|
|
191
|
-
)
|
|
192
|
-
expect(ids(entries)).to eq(%w[createPost])
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
it "skips GET/HEAD routes even on a host controller" do
|
|
196
|
-
rs = route_set { get "/posts/search", to: "posts#search" }
|
|
197
|
-
expect(collect(rs)).to be_empty
|
|
198
|
-
end
|
|
199
|
-
end
|
|
200
|
-
end
|
|
201
|
-
end
|
|
202
|
-
end
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "spec_helper"
|
|
4
|
-
require "tmpdir"
|
|
5
|
-
|
|
6
|
-
module Ruact
|
|
7
|
-
module ServerFunctions
|
|
8
|
-
RSpec.describe Snapshot, :story_9_3 do
|
|
9
|
-
let(:frozen_time) { Time.utc(2026, 5, 13, 12, 34, 56) }
|
|
10
|
-
let(:entries) do
|
|
11
|
-
[
|
|
12
|
-
{ "js_identifier" => "createPost", "kind" => "action", "http_method" => "POST",
|
|
13
|
-
"path" => "/posts", "segments" => [], "controller" => "posts", "action" => "create" }
|
|
14
|
-
]
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
describe ".dump_v2" do
|
|
18
|
-
it "wraps entries in a version-2 snapshot Hash" do
|
|
19
|
-
snap = described_class.dump_v2(entries, now: frozen_time)
|
|
20
|
-
expect(snap[:version]).to eq(2)
|
|
21
|
-
expect(snap[:generated_at]).to eq(frozen_time.iso8601)
|
|
22
|
-
expect(snap[:functions]).to eq(entries)
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
describe ".generate_v2! (write-if-changed)" do
|
|
27
|
-
around do |example|
|
|
28
|
-
Dir.mktmpdir do |dir|
|
|
29
|
-
@tmpdir = dir
|
|
30
|
-
example.run
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
let(:path) { File.join(@tmpdir, "server-functions.json") }
|
|
35
|
-
|
|
36
|
-
it "writes a version-2 bridge on first call" do
|
|
37
|
-
expect(described_class.generate_v2!(entries: entries, path: path, now: frozen_time)).to be(true)
|
|
38
|
-
parsed = JSON.parse(File.read(path))
|
|
39
|
-
expect(parsed.fetch("version")).to eq(2)
|
|
40
|
-
expect(parsed.fetch("functions").first.fetch("js_identifier")).to eq("createPost")
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
it "short-circuits when entries are unchanged (no timestamp churn)" do
|
|
44
|
-
described_class.generate_v2!(entries: entries, path: path, now: frozen_time)
|
|
45
|
-
expect(described_class.generate_v2!(entries: entries, path: path, now: Time.now.utc)).to be(false)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
it "rewrites when entries change" do
|
|
49
|
-
described_class.generate_v2!(entries: entries, path: path, now: frozen_time)
|
|
50
|
-
more = entries + [{ "js_identifier" => "destroyPost", "kind" => "action",
|
|
51
|
-
"http_method" => "DELETE", "path" => "/posts/:id",
|
|
52
|
-
"segments" => ["id"], "controller" => "posts", "action" => "destroy" }]
|
|
53
|
-
expect(described_class.generate_v2!(entries: more, path: path, now: frozen_time)).to be(true)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
it "creates the parent directory if missing" do
|
|
57
|
-
nested = File.join(@tmpdir, "deep", "nested", "server-functions.json")
|
|
58
|
-
expect(described_class.generate_v2!(entries: entries, path: nested, now: frozen_time)).to be(true)
|
|
59
|
-
expect(File.exist?(nested)).to be(true)
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
it "recovers from a corrupted existing file by overwriting it" do
|
|
63
|
-
File.write(path, "{ not json")
|
|
64
|
-
expect(described_class.generate_v2!(entries: entries, path: path, now: frozen_time)).to be(true)
|
|
65
|
-
expect(JSON.parse(File.read(path)).fetch("version")).to eq(2)
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
# Story 13.4 — the new typed-query metadata must survive the JSON
|
|
69
|
-
# round-trip unchanged AND keep the file byte-stable (no churn on a
|
|
70
|
-
# second pass with identical entries).
|
|
71
|
-
context "with typed-query params metadata (Story 13.4)", :story_13_4 do
|
|
72
|
-
let(:query_entries) do
|
|
73
|
-
[
|
|
74
|
-
{ "js_identifier" => "searchUsers", "kind" => "query", "http_method" => "GET",
|
|
75
|
-
"path" => "/q/searchUsers", "segments" => [], "accepts_params" => true,
|
|
76
|
-
"params" => [{ "name" => "term", "required" => true },
|
|
77
|
-
{ "name" => "limit", "required" => false }],
|
|
78
|
-
"params_rest" => false, "controller" => "CatalogQuery", "action" => "search_users" }
|
|
79
|
-
]
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
it "round-trips the params metadata unchanged" do
|
|
83
|
-
described_class.generate_v2!(entries: query_entries, path: path, now: frozen_time)
|
|
84
|
-
parsed = JSON.parse(File.read(path))
|
|
85
|
-
expect(parsed.fetch("functions")).to eq(query_entries)
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
it "is byte-stable: a second pass with identical entries writes nothing" do
|
|
89
|
-
described_class.generate_v2!(entries: query_entries, path: path, now: frozen_time)
|
|
90
|
-
expect(described_class.generate_v2!(entries: query_entries, path: path, now: Time.now.utc)).to be(false)
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
end
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "spec_helper"
|
|
4
|
-
require "tmpdir"
|
|
5
|
-
|
|
6
|
-
module Ruact
|
|
7
|
-
module ServerFunctions
|
|
8
|
-
RSpec.describe SnapshotWriter, :story_8_0a do
|
|
9
|
-
around do |example|
|
|
10
|
-
Dir.mktmpdir do |dir|
|
|
11
|
-
@tmpdir = dir
|
|
12
|
-
example.run
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
let(:path) { File.join(@tmpdir, "out.txt") }
|
|
17
|
-
|
|
18
|
-
describe ".write_if_changed! (Story 8.0a — atomic, byte-aware writer)" do
|
|
19
|
-
it "writes the file when it does not yet exist and returns true" do
|
|
20
|
-
expect(described_class.write_if_changed!(path: path, content: "hello\n"))
|
|
21
|
-
.to be(true)
|
|
22
|
-
expect(File.read(path)).to eq("hello\n")
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
it "skips writing when the existing content matches byte-for-byte" do
|
|
26
|
-
File.write(path, "hello\n")
|
|
27
|
-
original_mtime = File.mtime(path)
|
|
28
|
-
sleep 1.05
|
|
29
|
-
|
|
30
|
-
expect(described_class.write_if_changed!(path: path, content: "hello\n"))
|
|
31
|
-
.to be(false)
|
|
32
|
-
expect(File.mtime(path)).to eq(original_mtime)
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
it "writes when the existing content differs by even a single byte" do
|
|
36
|
-
File.write(path, "hello\n")
|
|
37
|
-
expect(described_class.write_if_changed!(path: path, content: "hello!\n"))
|
|
38
|
-
.to be(true)
|
|
39
|
-
expect(File.read(path)).to eq("hello!\n")
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
it "creates missing parent directories" do
|
|
43
|
-
nested = File.join(@tmpdir, "a", "b", "c", "out.txt")
|
|
44
|
-
described_class.write_if_changed!(path: nested, content: "x")
|
|
45
|
-
expect(File.read(nested)).to eq("x")
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
it "writes via a same-directory tmpfile so partial reads never see " \
|
|
49
|
-
"a torn file (Story 8.0a)" do
|
|
50
|
-
described_class.write_if_changed!(path: path, content: "atomic\n")
|
|
51
|
-
# After the write the temp sibling must not linger.
|
|
52
|
-
siblings = Dir.children(@tmpdir)
|
|
53
|
-
expect(siblings).to eq(["out.txt"])
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
it "raises Ruact::ConfigurationError when the parent directory is unwritable " \
|
|
57
|
-
"(Story 8.0a)" do
|
|
58
|
-
read_only = File.join(@tmpdir, "ro")
|
|
59
|
-
FileUtils.mkdir_p(read_only)
|
|
60
|
-
FileUtils.chmod(0o500, read_only)
|
|
61
|
-
target = File.join(read_only, "nested", "out.txt")
|
|
62
|
-
|
|
63
|
-
expect { described_class.write_if_changed!(path: target, content: "x") }
|
|
64
|
-
.to raise_error(Ruact::ConfigurationError, /cannot create/)
|
|
65
|
-
ensure
|
|
66
|
-
FileUtils.chmod(0o700, read_only) if read_only && File.exist?(read_only)
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
end
|