torikago 0.0.1 → 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.
@@ -10,42 +10,426 @@ class TorikagoEngineContainerTest < Minitest::Test
10
10
  Object.send(:remove_const, :SharedDependency) if Object.const_defined?(:SharedDependency, false)
11
11
  Object.send(:remove_const, :SetupProbe) if Object.const_defined?(:SetupProbe, false)
12
12
  Object.send(:remove_const, :VersionedFormatter) if Object.const_defined?(:VersionedFormatter, false)
13
+ Object.send(:remove_const, :WidgetsController) if Object.const_defined?(:WidgetsController, false)
13
14
  end
14
15
 
15
- def test_call_loads_the_public_api_class_and_executes_call
16
+ def test_invoke_loads_the_public_api_class_and_executes_call
16
17
  with_module_root do |module_root|
17
18
  container = Torikago::EngineContainer.new(name: :foo, module_root: module_root)
18
19
 
19
- result = container.call("Foo::ListProductsQuery")
20
+ result = container.invoke("Foo::ListProductsQuery", :call, constructor_args: [], constructor_kwargs: {}, method_args: [], method_kwargs: {})
20
21
 
21
22
  assert_equal ["coffee-beans", "drip-bag"], result
22
23
  end
23
24
  end
24
25
 
25
- def test_call_reuses_loaded_runtime_files_across_calls
26
+ def test_invoke_reuses_loaded_runtime_files_across_calls
26
27
  with_module_root do |module_root|
27
28
  container = Torikago::EngineContainer.new(name: :foo, module_root: module_root)
28
29
 
29
- first = container.call("Foo::ListProductsQuery")
30
- second = container.call("Foo::ListProductsQuery")
30
+ first = container.invoke("Foo::ListProductsQuery", :call, constructor_args: [], constructor_kwargs: {}, method_args: [], method_kwargs: {})
31
+ second = container.invoke("Foo::ListProductsQuery", :call, constructor_args: [], constructor_kwargs: {}, method_args: [], method_kwargs: {})
31
32
 
32
33
  assert_equal ["coffee-beans", "drip-bag"], first
33
34
  assert_equal ["coffee-beans", "drip-bag"], second
34
35
  end
35
36
  end
36
37
 
37
- def test_call_sets_the_current_box_during_execution
38
+ def test_invoke_sets_the_current_box_during_execution
38
39
  with_module_root do |module_root|
39
40
  container = Torikago::EngineContainer.new(name: :foo, module_root: module_root)
40
41
 
41
- result = container.call("Foo::CurrentBoxQuery")
42
+ result = container.invoke("Foo::CurrentBoxQuery", :call, constructor_args: [], constructor_kwargs: {}, method_args: [], method_kwargs: {})
42
43
 
43
44
  assert_equal :foo, result
44
45
  assert_nil Torikago::CurrentExecution.current_box
45
46
  end
46
47
  end
47
48
 
48
- def test_call_uses_a_configured_entrypoint_directory_when_present
49
+ def test_invoke_shares_root_constants_and_runtime_gateway_with_a_ruby_box
50
+ Dir.mktmpdir("torikago-root-access") do |application_root|
51
+ module_root = File.join(application_root, "modules/foo")
52
+ package_api_dir = File.join(module_root, "app/package_api/foo")
53
+ FileUtils.mkdir_p(package_api_dir)
54
+
55
+ root_constant_file = File.join(application_root, "root_constants.rb")
56
+ File.write(
57
+ root_constant_file,
58
+ <<~RUBY
59
+ class RootOrder
60
+ end
61
+
62
+ class RootCustomerQuery
63
+ def self.call
64
+ "root customer"
65
+ end
66
+ end
67
+ RUBY
68
+ )
69
+ lazy_constant_file = File.join(application_root, "root_lazy_query.rb")
70
+ File.write(
71
+ lazy_constant_file,
72
+ <<~RUBY
73
+ class RootLazyQuery
74
+ def self.call
75
+ "lazy root"
76
+ end
77
+ end
78
+ RUBY
79
+ )
80
+ File.write(
81
+ File.join(package_api_dir, "root_access_query.rb"),
82
+ <<~RUBY
83
+ class Foo::RootAccessQuery < ::RootOrder
84
+ def call
85
+ [
86
+ ::RootCustomerQuery.call,
87
+ ::RootLazyQuery.call,
88
+ self.class.superclass.object_id,
89
+ Torikago.object_id,
90
+ Torikago::Gateway.object_id,
91
+ Torikago::DependencyError.object_id,
92
+ Torikago::Gateway.invoke("Bar::Ping", :call)
93
+ ]
94
+ end
95
+ end
96
+ RUBY
97
+ )
98
+
99
+ assert_ruby_box_child_process(
100
+ <<~RUBY,
101
+ $LOAD_PATH.unshift(ARGV.fetch(0))
102
+ module_root = ARGV.fetch(1)
103
+ root_constant_file = ARGV.fetch(2)
104
+ lazy_constant_file = ARGV.fetch(3)
105
+
106
+ require "torikago"
107
+ load root_constant_file
108
+ autoload :RootLazyQuery, lazy_constant_file
109
+ gateway_probe = Object.new
110
+ gateway_probe.define_singleton_method(:invoke) { |class_name, method_name| [class_name, method_name] }
111
+ Torikago.instance_variable_set(:@gateway, gateway_probe)
112
+
113
+ container = Torikago::EngineContainer.new(
114
+ name: :foo,
115
+ module_root: module_root,
116
+ registered_roots: [module_root]
117
+ )
118
+
119
+ result = container.invoke("Foo::RootAccessQuery", :call, constructor_args: [], constructor_kwargs: {}, method_args: [], method_kwargs: {})
120
+ expected_shared_values = [
121
+ "root customer",
122
+ "lazy root",
123
+ RootOrder.object_id,
124
+ Torikago::Gateway.object_id,
125
+ Torikago::DependencyError.object_id,
126
+ ["Bar::Ping", :call]
127
+ ]
128
+ shared_values = result.values_at(0, 1, 2, 4, 5, 6)
129
+ raise "unexpected result: \#{result.inspect}" unless shared_values == expected_shared_values
130
+ raise "host Torikago namespace leaked into module Box" if result.fetch(3) == Torikago.object_id
131
+
132
+ Object.send(:remove_const, :RootCustomerQuery)
133
+ class RootCustomerQuery
134
+ def self.call
135
+ "reloaded root customer"
136
+ end
137
+ end
138
+ reloaded_result = container.invoke("Foo::RootAccessQuery", :call, constructor_args: [], constructor_kwargs: {}, method_args: [], method_kwargs: {})
139
+ raise "stale root constant: \#{reloaded_result.inspect}" unless reloaded_result.first == "reloaded root customer"
140
+
141
+ puts "ok"
142
+ RUBY
143
+ module_root,
144
+ root_constant_file,
145
+ lazy_constant_file
146
+ )
147
+ end
148
+ end
149
+
150
+ def test_invoke_prefers_module_local_constants_over_root_constants
151
+ Dir.mktmpdir("torikago-root-access") do |application_root|
152
+ module_root = File.join(application_root, "modules/foo")
153
+ package_api_dir = File.join(module_root, "app/package_api/foo")
154
+ FileUtils.mkdir_p(package_api_dir)
155
+
156
+ root_constant_file = File.join(application_root, "root_constant.rb")
157
+ File.write(root_constant_file, "RootPriority = \"root\"\n")
158
+ File.write(
159
+ File.join(package_api_dir, "priority_query.rb"),
160
+ <<~RUBY
161
+ RootPriority = "module"
162
+
163
+ class Foo::PriorityQuery
164
+ def call
165
+ RootPriority
166
+ end
167
+ end
168
+ RUBY
169
+ )
170
+
171
+ assert_ruby_box_child_process(
172
+ <<~RUBY,
173
+ $LOAD_PATH.unshift(ARGV.fetch(0))
174
+ module_root = ARGV.fetch(1)
175
+ root_constant_file = ARGV.fetch(2)
176
+
177
+ require "torikago"
178
+ load root_constant_file
179
+
180
+ container = Torikago::EngineContainer.new(
181
+ name: :foo,
182
+ module_root: module_root,
183
+ registered_roots: [module_root]
184
+ )
185
+
186
+ result = container.invoke("Foo::PriorityQuery", :call, constructor_args: [], constructor_kwargs: {}, method_args: [], method_kwargs: {})
187
+ raise "unexpected result: \#{result.inspect}" unless result == "module"
188
+
189
+ puts "ok"
190
+ RUBY
191
+ module_root,
192
+ root_constant_file
193
+ )
194
+ end
195
+ end
196
+
197
+ def test_invoke_does_not_share_constants_owned_by_a_registered_root
198
+ Dir.mktmpdir("torikago-root-access") do |application_root|
199
+ foo_root = File.join(application_root, "packs/foo")
200
+ bar_root = File.join(application_root, "components/bar")
201
+ package_api_dir = File.join(foo_root, "app/package_api/foo")
202
+ FileUtils.mkdir_p(package_api_dir)
203
+ FileUtils.mkdir_p(bar_root)
204
+
205
+ registered_constant_file = File.join(bar_root, "bar_internal.rb")
206
+ File.write(registered_constant_file, "class BarInternal; end\n")
207
+ File.write(
208
+ File.join(package_api_dir, "forbidden_root_query.rb"),
209
+ <<~RUBY
210
+ class Foo::ForbiddenRootQuery
211
+ def call
212
+ ::BarInternal
213
+ end
214
+ end
215
+ RUBY
216
+ )
217
+
218
+ assert_ruby_box_child_process(
219
+ <<~RUBY,
220
+ $LOAD_PATH.unshift(ARGV.fetch(0))
221
+ foo_root = ARGV.fetch(1)
222
+ bar_root = ARGV.fetch(2)
223
+ registered_constant_file = ARGV.fetch(3)
224
+
225
+ require "torikago"
226
+ load registered_constant_file
227
+
228
+ container = Torikago::EngineContainer.new(
229
+ name: :foo,
230
+ module_root: foo_root,
231
+ registered_roots: [foo_root, bar_root]
232
+ )
233
+
234
+ begin
235
+ container.invoke("Foo::ForbiddenRootQuery", :call, constructor_args: [], constructor_kwargs: {}, method_args: [], method_kwargs: {})
236
+ raise "registered constant was unexpectedly shared"
237
+ rescue NameError => error
238
+ raise unless error.message.include?("BarInternal")
239
+ end
240
+
241
+ puts "ok"
242
+ RUBY
243
+ foo_root,
244
+ bar_root,
245
+ registered_constant_file
246
+ )
247
+ end
248
+ end
249
+
250
+ def test_invoke_does_not_resolve_a_qualified_module_constant_from_root
251
+ Dir.mktmpdir("torikago-root-access") do |application_root|
252
+ module_root = File.join(application_root, "modules/foo")
253
+ package_api_dir = File.join(module_root, "app/package_api/foo")
254
+ FileUtils.mkdir_p(package_api_dir)
255
+
256
+ root_constant_file = File.join(application_root, "order.rb")
257
+ File.write(root_constant_file, "class Order; end\n")
258
+ File.write(
259
+ File.join(package_api_dir, "qualified_constant_query.rb"),
260
+ <<~RUBY
261
+ class Foo::QualifiedConstantQuery
262
+ def call
263
+ Foo::Order
264
+ end
265
+ end
266
+ RUBY
267
+ )
268
+
269
+ assert_ruby_box_child_process(
270
+ <<~RUBY,
271
+ $LOAD_PATH.unshift(ARGV.fetch(0))
272
+ module_root = ARGV.fetch(1)
273
+ root_constant_file = ARGV.fetch(2)
274
+
275
+ require "torikago"
276
+ load root_constant_file
277
+
278
+ container = Torikago::EngineContainer.new(
279
+ name: :foo,
280
+ module_root: module_root,
281
+ registered_roots: [module_root]
282
+ )
283
+
284
+ begin
285
+ container.invoke("Foo::QualifiedConstantQuery", :call, constructor_args: [], constructor_kwargs: {}, method_args: [], method_kwargs: {})
286
+ raise "Foo::Order unexpectedly resolved to ::Order"
287
+ rescue NameError => error
288
+ raise unless error.message.include?("Foo::Order")
289
+ end
290
+
291
+ puts "ok"
292
+ RUBY
293
+ module_root,
294
+ root_constant_file
295
+ )
296
+ end
297
+ end
298
+
299
+ def test_invoke_does_not_share_a_root_namespace_with_a_registered_descendant
300
+ Dir.mktmpdir("torikago-root-access") do |application_root|
301
+ foo_root = File.join(application_root, "modules/foo")
302
+ bar_root = File.join(application_root, "modules/bar")
303
+ package_api_dir = File.join(foo_root, "app/package_api/foo")
304
+ FileUtils.mkdir_p(package_api_dir)
305
+ FileUtils.mkdir_p(bar_root)
306
+
307
+ root_namespace_file = File.join(application_root, "shared_namespace.rb")
308
+ registered_constant_file = File.join(bar_root, "bar_internal.rb")
309
+ File.write(root_namespace_file, "module SharedNamespace; end\n")
310
+ File.write(registered_constant_file, "class SharedNamespace::BarInternal; end\n")
311
+ File.write(
312
+ File.join(package_api_dir, "shared_namespace_query.rb"),
313
+ <<~RUBY
314
+ class Foo::SharedNamespaceQuery
315
+ def call
316
+ ::SharedNamespace::BarInternal
317
+ end
318
+ end
319
+ RUBY
320
+ )
321
+
322
+ assert_ruby_box_child_process(
323
+ <<~RUBY,
324
+ $LOAD_PATH.unshift(ARGV.fetch(0))
325
+ foo_root = ARGV.fetch(1)
326
+ bar_root = ARGV.fetch(2)
327
+ root_namespace_file = ARGV.fetch(3)
328
+ registered_constant_file = ARGV.fetch(4)
329
+
330
+ require "torikago"
331
+ load root_namespace_file
332
+ load registered_constant_file
333
+
334
+ container = Torikago::EngineContainer.new(
335
+ name: :foo,
336
+ module_root: foo_root,
337
+ registered_roots: [foo_root, bar_root]
338
+ )
339
+
340
+ begin
341
+ container.invoke("Foo::SharedNamespaceQuery", :call, constructor_args: [], constructor_kwargs: {}, method_args: [], method_kwargs: {})
342
+ raise "registered descendant was unexpectedly shared"
343
+ rescue NameError => error
344
+ raise unless error.message.include?("SharedNamespace")
345
+ end
346
+
347
+ puts "ok"
348
+ RUBY
349
+ foo_root,
350
+ bar_root,
351
+ root_namespace_file,
352
+ registered_constant_file
353
+ )
354
+ end
355
+ end
356
+
357
+ def test_invoke_does_not_strip_bundler_env_during_public_api_execution
358
+ with_module_root do |module_root|
359
+ File.write(
360
+ File.join(module_root, "app/package_api/foo/env_query.rb"),
361
+ <<~RUBY
362
+ class Foo::EnvQuery
363
+ def call
364
+ [ENV["RUBYOPT"], ENV["BUNDLER_SETUP"]]
365
+ end
366
+ end
367
+ RUBY
368
+ )
369
+
370
+ old_rubyopt = ENV["RUBYOPT"]
371
+ old_bundler_setup = ENV["BUNDLER_SETUP"]
372
+ ENV["RUBYOPT"] = "-rbundler/setup -w"
373
+ ENV["BUNDLER_SETUP"] = "true"
374
+
375
+ container = Torikago::EngineContainer.new(
376
+ name: :foo,
377
+ module_root: module_root,
378
+ box_factory: -> { FakeBox.new }
379
+ )
380
+
381
+ assert_equal ["-rbundler/setup -w", "true"], container.invoke("Foo::EnvQuery", :call, constructor_args: [], constructor_kwargs: {}, method_args: [], method_kwargs: {})
382
+ ensure
383
+ restore_env("RUBYOPT", old_rubyopt)
384
+ restore_env("BUNDLER_SETUP", old_bundler_setup)
385
+ end
386
+ end
387
+
388
+ def test_bundler_env_stripping_is_serialized_across_containers
389
+ first_container = Torikago::EngineContainer.new(name: :foo, module_root: Dir.pwd)
390
+ second_container = Torikago::EngineContainer.new(name: :bar, module_root: Dir.pwd)
391
+ first_entered = Queue.new
392
+ release_first = Queue.new
393
+ second_ready = Queue.new
394
+ second_entered = Queue.new
395
+
396
+ old_rubyopt = ENV["RUBYOPT"]
397
+ old_bundler_setup = ENV["BUNDLER_SETUP"]
398
+ ENV["RUBYOPT"] = "-rbundler/setup"
399
+ ENV["BUNDLER_SETUP"] = "true"
400
+
401
+ first_thread = Thread.new do
402
+ first_container.send(:without_bundler_setup_env) do
403
+ first_entered << true
404
+ release_first.pop
405
+ end
406
+ end
407
+ first_entered.pop
408
+
409
+ second_thread = Thread.new do
410
+ second_ready << true
411
+ second_container.send(:without_bundler_setup_env) do
412
+ second_entered << true
413
+ end
414
+ end
415
+ second_ready.pop
416
+
417
+ sleep 0.05
418
+ assert_predicate second_entered, :empty?
419
+
420
+ release_first << true
421
+ first_thread.join
422
+ second_thread.join
423
+
424
+ refute_predicate second_entered, :empty?
425
+ ensure
426
+ first_thread&.kill if first_thread&.alive?
427
+ second_thread&.kill if second_thread&.alive?
428
+ restore_env("RUBYOPT", old_rubyopt)
429
+ restore_env("BUNDLER_SETUP", old_bundler_setup)
430
+ end
431
+
432
+ def test_invoke_uses_a_configured_entrypoint_directory_when_present
49
433
  with_custom_entrypoint_module_root do |module_root|
50
434
  container = Torikago::EngineContainer.new(
51
435
  name: :foo,
@@ -53,13 +437,13 @@ class TorikagoEngineContainerTest < Minitest::Test
53
437
  entrypoint: "components/public_api"
54
438
  )
55
439
 
56
- result = container.call("Foo::CustomEntryPointQuery")
440
+ result = container.invoke("Foo::CustomEntryPointQuery", :call, constructor_args: [], constructor_kwargs: {}, method_args: [], method_kwargs: {})
57
441
 
58
442
  assert_equal "custom entrypoint", result
59
443
  end
60
444
  end
61
445
 
62
- def test_call_does_not_load_parent_files_when_configured_entrypoint_directory_is_missing
446
+ def test_invoke_does_not_load_parent_files_when_configured_entrypoint_directory_is_missing
63
447
  Dir.mktmpdir("torikago-engine-container") do |module_root|
64
448
  FileUtils.mkdir_p(File.join(module_root, "app/controllers/foo"))
65
449
  File.write(
@@ -76,14 +460,213 @@ class TorikagoEngineContainerTest < Minitest::Test
76
460
  )
77
461
 
78
462
  error = assert_raises(NameError) do
79
- container.call("Foo::MissingQuery")
463
+ container.invoke("Foo::MissingQuery", :call, constructor_args: [], constructor_kwargs: {}, method_args: [], method_kwargs: {})
80
464
  end
81
465
 
82
466
  assert_match(/MissingQuery/, error.message)
83
467
  end
84
468
  end
85
469
 
86
- def test_call_prepends_explicit_gemfile_require_paths_before_loading_runtime
470
+ def test_invoke_loads_plain_lib_entrypoint_by_default
471
+ with_module_root do |module_root|
472
+ lib_dir = File.join(module_root, "lib")
473
+ FileUtils.mkdir_p(lib_dir)
474
+ File.write(
475
+ File.join(lib_dir, "foo.rb"),
476
+ <<~RUBY
477
+ module Foo
478
+ PLAIN_LIB_ENTRYPOINT = "loaded"
479
+ end
480
+ RUBY
481
+ )
482
+ File.write(
483
+ File.join(module_root, "app/package_api/foo/plain_lib_entrypoint_query.rb"),
484
+ <<~RUBY
485
+ class Foo::PlainLibEntrypointQuery
486
+ def call
487
+ Foo::PLAIN_LIB_ENTRYPOINT
488
+ end
489
+ end
490
+ RUBY
491
+ )
492
+
493
+ container = Torikago::EngineContainer.new(name: :foo, module_root: module_root)
494
+
495
+ assert_equal "loaded", container.invoke("Foo::PlainLibEntrypointQuery", :call, constructor_args: [], constructor_kwargs: {}, method_args: [], method_kwargs: {})
496
+ end
497
+ end
498
+
499
+ def test_invoke_loads_the_module_lib_entrypoint_when_rails_engine_is_enabled
500
+ with_module_root do |module_root|
501
+ lib_dir = File.join(module_root, "lib")
502
+ FileUtils.mkdir_p(File.join(lib_dir, "foo"))
503
+ File.write(
504
+ File.join(lib_dir, "foo.rb"),
505
+ <<~RUBY
506
+ module Foo
507
+ RAILS_ENGINE_ENTRYPOINT = "loaded in module runtime"
508
+ end
509
+ RUBY
510
+ )
511
+ File.write(
512
+ File.join(lib_dir, "foo/support.rb"),
513
+ <<~RUBY
514
+ module Foo
515
+ SUPPORT_VALUE = "ordinary lib file loaded"
516
+ end
517
+ RUBY
518
+ )
519
+ File.write(
520
+ File.join(module_root, "app/package_api/foo/rails_engine_support_query.rb"),
521
+ <<~RUBY
522
+ class Foo::RailsEngineSupportQuery
523
+ def call
524
+ [Foo::RAILS_ENGINE_ENTRYPOINT, Foo::SUPPORT_VALUE]
525
+ end
526
+ end
527
+ RUBY
528
+ )
529
+
530
+ container = Torikago::EngineContainer.new(
531
+ name: :foo,
532
+ module_root: module_root,
533
+ rails_engine: true
534
+ )
535
+
536
+ assert_equal(
537
+ ["loaded in module runtime", "ordinary lib file loaded"],
538
+ container.invoke("Foo::RailsEngineSupportQuery", :call, constructor_args: [], constructor_kwargs: {}, method_args: [], method_kwargs: {})
539
+ )
540
+ end
541
+ end
542
+
543
+ def test_call_loads_and_dispatches_to_a_top_level_rails_runtime_controller
544
+ Dir.mktmpdir("torikago-engine-container") do |module_root|
545
+ FileUtils.mkdir_p(File.join(module_root, "lib"))
546
+ FileUtils.mkdir_p(File.join(module_root, "app/controllers"))
547
+ FileUtils.mkdir_p(File.join(module_root, "config"))
548
+ File.write(
549
+ File.join(module_root, "lib/foo.rb"),
550
+ <<~RUBY
551
+ module Foo
552
+ class Routes
553
+ def recognize_path(path, method:)
554
+ raise "unexpected path" unless path == "/widgets/7"
555
+ raise "unexpected method" unless method == :get
556
+
557
+ { "controller" => "widgets", "action" => "show", "id" => "7" }
558
+ end
559
+ end
560
+
561
+ class Engine
562
+ def self.routes
563
+ @routes ||= Routes.new
564
+ end
565
+ end
566
+ end
567
+ RUBY
568
+ )
569
+ File.write(
570
+ File.join(module_root, "app/controllers/widgets_controller.rb"),
571
+ <<~RUBY
572
+ class WidgetsController
573
+ def self.action(action_name)
574
+ lambda do |env|
575
+ params = env.fetch("action_dispatch.request.path_parameters")
576
+ [
577
+ 200,
578
+ { "content-type" => "text/plain" },
579
+ ["\#{action_name}:\#{params.fetch(:id)}:\#{Torikago::CurrentExecution.current_box}"]
580
+ ]
581
+ end
582
+ end
583
+ end
584
+ RUBY
585
+ )
586
+
587
+ container = Torikago::EngineContainer.new(
588
+ name: :foo,
589
+ module_root: module_root,
590
+ rails_engine: true,
591
+ box_factory: -> { FakeBox.new }
592
+ )
593
+
594
+ response = container.call("PATH_INFO" => "/widgets/7", "REQUEST_METHOD" => "GET")
595
+
596
+ assert_equal [200, { "content-type" => "text/plain" }, ["show:7:foo"]], response
597
+ assert_nil Torikago::CurrentExecution.current_box
598
+ end
599
+ end
600
+
601
+ def test_dispatch_controller_loads_a_top_level_controller_without_a_rails_engine
602
+ with_module_root do |module_root|
603
+ controller_dir = File.join(module_root, "app/controllers")
604
+ FileUtils.mkdir_p(controller_dir)
605
+ File.write(
606
+ File.join(controller_dir, "widgets_controller.rb"),
607
+ <<~RUBY
608
+ class WidgetsController
609
+ def self.action(action_name)
610
+ lambda do |env|
611
+ params = env.fetch("action_dispatch.request.path_parameters")
612
+ [
613
+ 200,
614
+ { "content-type" => "text/plain" },
615
+ ["\#{action_name}:\#{params.fetch(:id)}:\#{Torikago::CurrentExecution.current_box}"]
616
+ ]
617
+ end
618
+ end
619
+ end
620
+ RUBY
621
+ )
622
+
623
+ container = Torikago::EngineContainer.new(
624
+ name: :foo,
625
+ module_root: module_root,
626
+ box_factory: -> { FakeBox.new }
627
+ )
628
+
629
+ # A prior Package API invocation must not prevent the later controller
630
+ # runtime from loading.
631
+ assert_equal(
632
+ ["coffee-beans", "drip-bag"],
633
+ container.invoke(
634
+ "Foo::ListProductsQuery",
635
+ :call,
636
+ constructor_args: [],
637
+ constructor_kwargs: {},
638
+ method_args: [],
639
+ method_kwargs: {}
640
+ )
641
+ )
642
+
643
+ response = container.dispatch_controller(
644
+ {
645
+ "PATH_INFO" => "/widgets/7",
646
+ "action_dispatch.request.path_parameters" => { id: "7" }
647
+ },
648
+ controller_name: "WidgetsController",
649
+ action_name: :show
650
+ )
651
+
652
+ assert_equal [200, { "content-type" => "text/plain" }, ["show:7:foo"]], response
653
+ assert_nil Torikago::CurrentExecution.current_box
654
+ end
655
+ end
656
+
657
+ def test_call_rejects_modules_without_a_rails_engine
658
+ with_module_root do |module_root|
659
+ container = Torikago::EngineContainer.new(name: :foo, module_root: module_root)
660
+
661
+ error = assert_raises(ArgumentError) do
662
+ container.call("PATH_INFO" => "/", "REQUEST_METHOD" => "GET")
663
+ end
664
+
665
+ assert_match(/rails_engine: true/, error.message)
666
+ end
667
+ end
668
+
669
+ def test_invoke_prepends_explicit_gemfile_require_paths_before_loading_runtime
87
670
  with_module_root do |module_root|
88
671
  dependency_lib = File.join(module_root, "vendor/example-gem-1.2.3/lib")
89
672
  FileUtils.mkdir_p(dependency_lib)
@@ -131,7 +714,7 @@ class TorikagoEngineContainerTest < Minitest::Test
131
714
  end
132
715
  )
133
716
 
134
- raise "unexpected result" unless container.call("Foo::DependencyVersionQuery") == "module-local"
717
+ raise "unexpected result" unless container.invoke("Foo::DependencyVersionQuery", :call, constructor_args: [], constructor_kwargs: {}, method_args: [], method_kwargs: {}) == "module-local"
135
718
 
136
719
  puts "ok"
137
720
  RUBY
@@ -141,7 +724,7 @@ class TorikagoEngineContainerTest < Minitest::Test
141
724
  end
142
725
  end
143
726
 
144
- def test_call_resolves_path_gem_require_paths_inside_ruby_box
727
+ def test_invoke_resolves_path_gem_require_paths_inside_ruby_box
145
728
  Dir.mktmpdir("torikago-engine-container") do |module_root|
146
729
  dependency_lib = File.join(module_root, "vendor/example-gem-1.0.0/lib")
147
730
  FileUtils.mkdir_p(dependency_lib)
@@ -203,7 +786,7 @@ class TorikagoEngineContainerTest < Minitest::Test
203
786
  gemfile: "Gemfile"
204
787
  )
205
788
 
206
- raise "unexpected result" unless container.call("Foo::DependencyVersionQuery") == "module-local"
789
+ raise "unexpected result" unless container.invoke("Foo::DependencyVersionQuery", :call, constructor_args: [], constructor_kwargs: {}, method_args: [], method_kwargs: {}) == "module-local"
207
790
 
208
791
  puts "ok"
209
792
  RUBY
@@ -212,7 +795,7 @@ class TorikagoEngineContainerTest < Minitest::Test
212
795
  end
213
796
  end
214
797
 
215
- def test_call_resolves_path_gem_require_paths_from_the_module_gemfile
798
+ def test_invoke_resolves_path_gem_require_paths_from_the_module_gemfile
216
799
  Dir.mktmpdir("torikago-engine-container") do |module_root|
217
800
  dependency_root = File.join(module_root, "vendor/versioned_formatter")
218
801
  dependency_lib = File.join(dependency_root, "lib")
@@ -275,7 +858,7 @@ class TorikagoEngineContainerTest < Minitest::Test
275
858
  gemfile: "Gemfile"
276
859
  )
277
860
 
278
- raise "unexpected result" unless container.call("Foo::GemfileDependencyQuery") == "1.0.0"
861
+ raise "unexpected result" unless container.invoke("Foo::GemfileDependencyQuery", :call, constructor_args: [], constructor_kwargs: {}, method_args: [], method_kwargs: {}) == "1.0.0"
279
862
 
280
863
  puts "ok"
281
864
  RUBY
@@ -284,7 +867,7 @@ class TorikagoEngineContainerTest < Minitest::Test
284
867
  end
285
868
  end
286
869
 
287
- def test_call_resolves_exact_installed_gem_require_paths_after_a_different_version_is_active
870
+ def test_invoke_resolves_exact_installed_gem_require_paths_after_a_different_version_is_active
288
871
  skip("requires rake 13.3.1 and 13.4.2") unless installed_gem?("rake", "= 13.3.1") && installed_gem?("rake", "= 13.4.2")
289
872
 
290
873
  Dir.mktmpdir("torikago-engine-container") do |module_root|
@@ -314,7 +897,7 @@ class TorikagoEngineContainerTest < Minitest::Test
314
897
  end
315
898
  end
316
899
 
317
- def test_call_reports_missing_installed_gemfile_dependencies_clearly
900
+ def test_invoke_reports_missing_installed_gemfile_dependencies_clearly
318
901
  with_module_root do |module_root|
319
902
  File.write(
320
903
  File.join(module_root, "Gemfile"),
@@ -332,7 +915,7 @@ class TorikagoEngineContainerTest < Minitest::Test
332
915
  )
333
916
 
334
917
  error = assert_raises(Torikago::GemfileOverrideError) do
335
- container.call("Foo::ListProductsQuery")
918
+ container.invoke("Foo::ListProductsQuery", :call, constructor_args: [], constructor_kwargs: {}, method_args: [], method_kwargs: {})
336
919
  end
337
920
 
338
921
  assert_match(/example-gem/, error.message)
@@ -340,7 +923,7 @@ class TorikagoEngineContainerTest < Minitest::Test
340
923
  end
341
924
  end
342
925
 
343
- def test_call_loads_configured_setup_before_public_api
926
+ def test_invoke_loads_configured_setup_before_public_api
344
927
  with_module_root do |module_root|
345
928
  setup_dir = File.join(module_root, "config")
346
929
  FileUtils.mkdir_p(setup_dir)
@@ -370,11 +953,11 @@ class TorikagoEngineContainerTest < Minitest::Test
370
953
  setup: "config/box_setup.rb"
371
954
  )
372
955
 
373
- assert_equal "patched", container.call("Foo::SetupAwareQuery")
956
+ assert_equal "patched", container.invoke("Foo::SetupAwareQuery", :call, constructor_args: [], constructor_kwargs: {}, method_args: [], method_kwargs: {})
374
957
  end
375
958
  end
376
959
 
377
- def test_call_loads_setup_only_once
960
+ def test_invoke_loads_setup_only_once
378
961
  with_module_root do |module_root|
379
962
  setup_dir = File.join(module_root, "config")
380
963
  FileUtils.mkdir_p(setup_dir)
@@ -404,12 +987,12 @@ class TorikagoEngineContainerTest < Minitest::Test
404
987
  setup: "config/box_setup.rb"
405
988
  )
406
989
 
407
- assert_equal 1, container.call("Foo::SetupCountQuery")
408
- assert_equal 1, container.call("Foo::SetupCountQuery")
990
+ assert_equal 1, container.invoke("Foo::SetupCountQuery", :call, constructor_args: [], constructor_kwargs: {}, method_args: [], method_kwargs: {})
991
+ assert_equal 1, container.invoke("Foo::SetupCountQuery", :call, constructor_args: [], constructor_kwargs: {}, method_args: [], method_kwargs: {})
409
992
  end
410
993
  end
411
994
 
412
- def test_call_raises_load_error_when_setup_is_missing
995
+ def test_invoke_raises_load_error_when_setup_is_missing
413
996
  with_module_root do |module_root|
414
997
  container = Torikago::EngineContainer.new(
415
998
  name: :foo,
@@ -418,7 +1001,7 @@ class TorikagoEngineContainerTest < Minitest::Test
418
1001
  )
419
1002
 
420
1003
  error = assert_raises(LoadError) do
421
- container.call("Foo::ListProductsQuery")
1004
+ container.invoke("Foo::ListProductsQuery", :call, constructor_args: [], constructor_kwargs: {}, method_args: [], method_kwargs: {})
422
1005
  end
423
1006
 
424
1007
  assert_match(/setup not found/, error.message)
@@ -426,7 +1009,7 @@ class TorikagoEngineContainerTest < Minitest::Test
426
1009
  end
427
1010
  end
428
1011
 
429
- def test_call_loads_plain_gateway_models_without_eager_loading_rails_models
1012
+ def test_invoke_loads_plain_gateway_models_without_eager_loading_rails_models
430
1013
  with_module_root do |module_root|
431
1014
  model_dir = File.join(module_root, "app/models/foo")
432
1015
  FileUtils.mkdir_p(model_dir)
@@ -462,12 +1045,218 @@ class TorikagoEngineContainerTest < Minitest::Test
462
1045
 
463
1046
  container = Torikago::EngineContainer.new(name: :foo, module_root: module_root)
464
1047
 
465
- assert_equal ["order"], container.call("Foo::ListOrdersQuery")
1048
+ assert_equal ["order"], container.invoke("Foo::ListOrdersQuery", :call, constructor_args: [], constructor_kwargs: {}, method_args: [], method_kwargs: {})
1049
+ end
1050
+ end
1051
+
1052
+ def test_invoke_keeps_constructor_and_method_arguments_separate
1053
+ with_module_root do |module_root|
1054
+ File.write(
1055
+ File.join(module_root, "app/package_api/foo/product_query.rb"),
1056
+ <<~RUBY
1057
+ class Foo::ProductQuery
1058
+ def initialize(prefix, page:)
1059
+ @prefix = prefix
1060
+ @page = page
1061
+ end
1062
+
1063
+ def execute!(suffix, force:)
1064
+ [@prefix, @page, suffix, force, Torikago::CurrentExecution.current_box]
1065
+ end
1066
+ end
1067
+ RUBY
1068
+ )
1069
+ container = Torikago::EngineContainer.new(name: :foo, module_root: module_root)
1070
+
1071
+ result = container.invoke(
1072
+ "Foo::ProductQuery",
1073
+ :execute!,
1074
+ constructor_args: ["products"],
1075
+ constructor_kwargs: { page: 2 },
1076
+ method_args: ["refresh"],
1077
+ method_kwargs: { force: true }
1078
+ )
1079
+
1080
+ assert_equal ["products", 2, "refresh", true, :foo], result
1081
+ assert_nil Torikago::CurrentExecution.current_box
1082
+ end
1083
+ end
1084
+
1085
+ def test_invoke_rejects_private_methods
1086
+ with_module_root do |module_root|
1087
+ File.write(
1088
+ File.join(module_root, "app/package_api/foo/private_query.rb"),
1089
+ <<~RUBY
1090
+ class Foo::PrivateQuery
1091
+ private
1092
+
1093
+ def execute!
1094
+ "should not run"
1095
+ end
1096
+ end
1097
+ RUBY
1098
+ )
1099
+ container = Torikago::EngineContainer.new(name: :foo, module_root: module_root)
1100
+
1101
+ assert_raises(NoMethodError) do
1102
+ container.invoke("Foo::PrivateQuery", :execute!, constructor_args: [], constructor_kwargs: {}, method_args: [], method_kwargs: {})
1103
+ end
1104
+ end
1105
+ end
1106
+
1107
+ def test_invoke_preserves_constructor_and_method_exceptions
1108
+ with_module_root do |module_root|
1109
+ File.write(
1110
+ File.join(module_root, "app/package_api/foo/failing_query.rb"),
1111
+ <<~RUBY
1112
+ class Foo::FailingQuery
1113
+ def initialize(fail_constructor: false)
1114
+ raise "constructor failed" if fail_constructor
1115
+ end
1116
+
1117
+ def execute!
1118
+ raise "method failed"
1119
+ end
1120
+ end
1121
+ RUBY
1122
+ )
1123
+ constructor_container = Torikago::EngineContainer.new(name: :foo, module_root: module_root)
1124
+ method_container = Torikago::EngineContainer.new(name: :foo, module_root: module_root)
1125
+
1126
+ constructor_error = assert_raises(RuntimeError) do
1127
+ constructor_container.invoke(
1128
+ "Foo::FailingQuery",
1129
+ :execute!,
1130
+ constructor_args: [],
1131
+ constructor_kwargs: { fail_constructor: true },
1132
+ method_args: [],
1133
+ method_kwargs: {}
1134
+ )
1135
+ end
1136
+ method_error = assert_raises(RuntimeError) do
1137
+ method_container.invoke(
1138
+ "Foo::FailingQuery",
1139
+ :execute!,
1140
+ constructor_args: [],
1141
+ constructor_kwargs: {},
1142
+ method_args: [],
1143
+ method_kwargs: {}
1144
+ )
1145
+ end
1146
+
1147
+ assert_equal "constructor failed", constructor_error.message
1148
+ assert_equal "method failed", method_error.message
1149
+ end
1150
+ end
1151
+
1152
+ def test_invoke_restores_current_execution_after_nested_execution
1153
+ with_module_root do |module_root|
1154
+ File.write(
1155
+ File.join(module_root, "app/package_api/foo/nested_query.rb"),
1156
+ <<~RUBY
1157
+ class Foo::NestedQuery
1158
+ def execute!
1159
+ Torikago::CurrentExecution.with_box(:bar) { :nested }
1160
+ Torikago::CurrentExecution.current_box
1161
+ end
1162
+ end
1163
+ RUBY
1164
+ )
1165
+ container = Torikago::EngineContainer.new(name: :foo, module_root: module_root)
1166
+
1167
+ result = Torikago::CurrentExecution.with_box(:caller) do
1168
+ current = container.invoke("Foo::NestedQuery", :execute!, constructor_args: [], constructor_kwargs: {}, method_args: [], method_kwargs: {})
1169
+ [current, Torikago::CurrentExecution.current_box]
1170
+ end
1171
+
1172
+ assert_equal [:foo, :caller], result
1173
+ assert_nil Torikago::CurrentExecution.current_box
1174
+ end
1175
+ end
1176
+
1177
+ def test_invoke_fails_closed_when_ruby_box_creation_fails
1178
+ with_module_root do |module_root|
1179
+ old_ruby_box = ENV["RUBY_BOX"]
1180
+ ENV["RUBY_BOX"] = "1"
1181
+ container = Torikago::EngineContainer.new(
1182
+ name: :foo,
1183
+ module_root: module_root,
1184
+ box_factory: -> { raise "box creation failed" }
1185
+ )
1186
+
1187
+ error = assert_raises(Torikago::BoxUnavailableError) do
1188
+ container.invoke("Foo::ListProductsQuery", :call, constructor_args: [], constructor_kwargs: {}, method_args: [], method_kwargs: {})
1189
+ end
1190
+
1191
+ assert_equal "Ruby::Box is unavailable for module foo: box creation failed", error.message
1192
+ assert_instance_of RuntimeError, error.cause
1193
+ refute Object.const_defined?(:Foo, false)
1194
+ ensure
1195
+ restore_env("RUBY_BOX", old_ruby_box)
1196
+ end
1197
+ end
1198
+
1199
+ def test_invoke_wraps_box_runtime_preparation_failures
1200
+ with_module_root do |module_root|
1201
+ old_ruby_box = ENV["RUBY_BOX"]
1202
+ ENV["RUBY_BOX"] = "1"
1203
+ failing_box = Object.new
1204
+ failing_box.define_singleton_method(:load_path) { raise "load path failed" }
1205
+ container = Torikago::EngineContainer.new(
1206
+ name: :foo,
1207
+ module_root: module_root,
1208
+ box_factory: -> { failing_box }
1209
+ )
1210
+
1211
+ error = assert_raises(Torikago::BoxUnavailableError) do
1212
+ container.invoke("Foo::ListProductsQuery", :call, constructor_args: [], constructor_kwargs: {}, method_args: [], method_kwargs: {})
1213
+ end
1214
+
1215
+ assert_match(/load path failed/, error.message)
1216
+ assert_instance_of RuntimeError, error.cause
1217
+ refute Object.const_defined?(:Foo, false)
1218
+ ensure
1219
+ restore_env("RUBY_BOX", old_ruby_box)
466
1220
  end
467
1221
  end
468
1222
 
469
1223
  private
470
1224
 
1225
+ class FakeBox
1226
+ attr_reader :load_path
1227
+
1228
+ def initialize
1229
+ @load_path = []
1230
+ end
1231
+
1232
+ def require(_name)
1233
+ end
1234
+
1235
+ def load(path)
1236
+ Kernel.load(path)
1237
+ end
1238
+
1239
+ def const_defined?(name, inherit = true)
1240
+ Object.const_defined?(name, inherit)
1241
+ end
1242
+
1243
+ def const_set(name, value)
1244
+ Object.const_set(name, value)
1245
+ end
1246
+
1247
+ def const_get(name)
1248
+ Object.const_get(name)
1249
+ end
1250
+ end
1251
+
1252
+ def restore_env(key, value)
1253
+ if value
1254
+ ENV[key] = value
1255
+ else
1256
+ ENV.delete(key)
1257
+ end
1258
+ end
1259
+
471
1260
  def with_module_root
472
1261
  Dir.mktmpdir("torikago-engine-container") do |module_root|
473
1262
  package_api_dir = File.join(module_root, "app/package_api/foo")
@@ -519,7 +1308,12 @@ class TorikagoEngineContainerTest < Minitest::Test
519
1308
 
520
1309
  def assert_ruby_box_child_process(script, *args)
521
1310
  stdout, stderr, status = Open3.capture3(
522
- { "RUBY_BOX" => "1" },
1311
+ {
1312
+ "RUBY_BOX" => "1",
1313
+ "RUBYOPT" => nil,
1314
+ "BUNDLER_SETUP" => nil,
1315
+ "BUNDLE_GEMFILE" => nil
1316
+ },
523
1317
  RbConfig.ruby,
524
1318
  "-e",
525
1319
  script,