trailblazer-macro 2.1.11 → 2.1.13

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,940 @@
1
+ require "test_helper"
2
+
3
+
4
+ # step Macro::Each(:report_templates, key: :report_template) {
5
+ # step Subprocess(ReportTemplate::Update), input: :input_report_template
6
+ # fail :set_report_template_errors
7
+ # }
8
+
9
+ # def report_templates(ctx, **) ctx["result.contract.default"].report_templates
10
+ # end
11
+
12
+ class EachTest < Minitest::Spec
13
+ class Composer < Struct.new(:full_name, :email)
14
+ end
15
+
16
+ class Mailer
17
+ def self.send(**options)
18
+ @send_options << options
19
+ end
20
+
21
+ class << self
22
+ attr_accessor :send_options
23
+ end
24
+ end
25
+
26
+ #@ operation has {#composers_for_each}
27
+ module B
28
+ class Song < Struct.new(:id, :title, :band, :composers)
29
+ def self.find_by(id:)
30
+ if id == 2
31
+ return Song.new(id, nil, nil, [Composer.new("Fat Mike", "mike@fat.wreck"), Composer.new("El Hefe")])
32
+ end
33
+
34
+ if id == 3
35
+ return Song.new(id, nil, nil, [Composer.new("Fat Mike", "mike@fat.wreck"), Composer.new("El Hefe", "scammer@spam")])
36
+ end
37
+
38
+ Song.new(id, nil, nil, [Composer.new("Fat Mike"), Composer.new("El Hefe")])
39
+ end
40
+ end
41
+
42
+ #:each
43
+ module Song::Activity
44
+ class Cover < Trailblazer::Activity::Railway
45
+ step :model
46
+ #:each-dataset
47
+ step Each(dataset_from: :composers_for_each, collect: true) {
48
+ step :notify_composers
49
+ }
50
+ #:each-dataset end
51
+ step :rearrange
52
+
53
+ # "decider interface"
54
+ def composers_for_each(ctx, model:, **)
55
+ model.composers
56
+ end
57
+
58
+ def notify_composers(ctx, index:, item:, **)
59
+ ctx[:value] = [index, item.full_name]
60
+ end
61
+
62
+ #~meths
63
+ def model(ctx, params:, **)
64
+ ctx[:model] = Song.find_by(id: params[:id])
65
+ end
66
+
67
+ include T.def_steps(:rearrange)
68
+ #~meths end
69
+ end
70
+ end
71
+ #:each end
72
+ end # B
73
+
74
+ it "allows a dataset compute in the hosting activity" do
75
+ #@ {:dataset} is not part of the {ctx}.
76
+ assert_invoke B::Song::Activity::Cover, params: {id: 1},
77
+ expected_ctx_variables: {
78
+ model: B::Song.find_by(id: 1),
79
+ collected_from_each: [[0, "Fat Mike"], [1, "El Hefe"],]
80
+ },
81
+ seq: "[:rearrange]"
82
+
83
+ =begin
84
+ #:collected_from_each
85
+ #~ctx_to_result
86
+ ctx = {params: {id: 1}} # Song 1 has two composers.
87
+
88
+ signal, (ctx, _) = Trailblazer::Activity.(Song::Activity::Cover, ctx)
89
+
90
+ puts ctx[:collected_from_each] #=> [[0, "Fat Mike"], [1, "El Hefe"]]
91
+ #~ctx_to_result end
92
+ #:collected_from_each end
93
+ =end
94
+ end
95
+
96
+ module CoverMethods
97
+ def notify_composers(ctx, index:, item:, **)
98
+ ctx[:value] = [index, item.full_name]
99
+ end
100
+
101
+ def model(ctx, params:, **)
102
+ ctx[:model] = EachTest::B::Song.find_by(id: params[:id])
103
+ end
104
+
105
+ include T.def_steps(:rearrange)
106
+ end
107
+
108
+ module ComposersForEach
109
+ def composers_for_each(ctx, model:, **)
110
+ model.composers
111
+ end
112
+ end
113
+
114
+ #@ operation has dedicated step {#find_composers}
115
+ module C
116
+ class Song < B::Song; end
117
+
118
+ module Song::Activity
119
+ class Cover < Trailblazer::Activity::Railway
120
+ step :model
121
+ step :find_composers
122
+ step Each(collect: true) {
123
+ step :notify_composers
124
+ }, In() => {:composers => :dataset}
125
+ step :rearrange
126
+
127
+ def find_composers(ctx, model:, **)
128
+ # You could also say {ctx[:dataset] = model.composers},
129
+ # and wouldn't need the In() mapping.
130
+ ctx[:composers] = model.composers
131
+ end
132
+ #~meths
133
+ include CoverMethods
134
+ #~meths end
135
+ end
136
+ end
137
+ end # C
138
+
139
+ it "dataset can come from the hosting activity" do
140
+ #@ {:dataset} is not part of the outgoing {ctx}.
141
+ assert_invoke B::Song::Activity::Cover, params: {id: 1},
142
+ expected_ctx_variables: {
143
+ model: B::Song.find_by(id: 1),
144
+ collected_from_each: [[0, "Fat Mike"], [1, "El Hefe"],]
145
+ }, seq: "[:rearrange]"
146
+ end
147
+
148
+ it "dataset coming via In() from the operation" do
149
+ #@ {:dataset} is not part of the {ctx}.
150
+ assert_invoke C::Song::Activity::Cover, params: {id: 1},
151
+ expected_ctx_variables: {
152
+ model: C::Song.find_by(id: 1),
153
+ composers: [Composer.new("Fat Mike"), Composer.new("El Hefe")],
154
+ collected_from_each: [[0, "Fat Mike"], [1, "El Hefe"],]
155
+ }, seq: "[:rearrange]"
156
+ end
157
+
158
+ #@ {:item_key}
159
+ module E
160
+ class Song < B::Song; end
161
+
162
+ Mailer = Class.new(EachTest::Mailer)
163
+
164
+ #:composer
165
+ module Song::Activity
166
+ class Cover < Trailblazer::Activity::Railway
167
+ #~meths
168
+ step :model
169
+ #:item_key
170
+ step Each(dataset_from: :composers_for_each, item_key: :composer) {
171
+ step :notify_composers
172
+ }
173
+ #:item_key end
174
+ step :rearrange
175
+
176
+
177
+ # circuit-step interface! "decider interface"
178
+ def composers_for_each(ctx, model:, **)
179
+ model.composers
180
+ end
181
+ include CoverMethods
182
+ #~meths end
183
+ def notify_composers(ctx, index:, composer:, **)
184
+ Mailer.send(to: composer.email, message: "#{index}) You, #{composer.full_name}, have been warned about your song being copied.")
185
+ end
186
+ end
187
+ end
188
+ #:composer end
189
+ end # E
190
+
191
+ it "{item_key: :composer}" do
192
+ E::Mailer.send_options = []
193
+ assert_invoke E::Song::Activity::Cover, params: {id: 1},
194
+ expected_ctx_variables: {
195
+ model: B::Song.find_by(id: 1),
196
+ # collected_from_each: ["Fat Mike", "El Hefe"]
197
+ },
198
+ seq: "[:rearrange]"
199
+ assert_equal E::Mailer.send_options, [{:to=>nil, :message=>"0) You, Fat Mike, have been warned about your song being copied."}, {:to=>nil, :message=>"1) You, El Hefe, have been warned about your song being copied."}]
200
+ end
201
+
202
+ #@ failure in Each
203
+ module F
204
+ class Song < B::Song; end
205
+
206
+ class Notify
207
+ def self.send_email(email)
208
+ return if email.nil?
209
+ true
210
+ end
211
+ end
212
+
213
+ module Song::Activity
214
+ class Cover < Trailblazer::Activity::Railway
215
+ step :model
216
+ step Each(dataset_from: :composers_for_each, collect: true) {
217
+ step :notify_composers
218
+ }
219
+ step :rearrange
220
+
221
+ def notify_composers(ctx, item:, **)
222
+ if Notify.send_email(item.email)
223
+ ctx[:value] = item.email # let's collect all emails that could be sent.
224
+ return true
225
+ else
226
+ return false
227
+ end
228
+ end
229
+ #~meths
230
+
231
+ # circuit-step interface! "decider interface"
232
+ def composers_for_each(ctx, model:, **)
233
+ model.composers
234
+ end
235
+ include CoverMethods
236
+ #~meths end
237
+ end
238
+ end
239
+ end # F
240
+
241
+ it "failure in Each" do
242
+ assert_invoke F::Song::Activity::Cover, params: {id: 2},
243
+ expected_ctx_variables: {
244
+ model: B::Song.find_by(id: 2),
245
+ collected_from_each: ["mike@fat.wreck", nil],
246
+ },
247
+ seq: "[]",
248
+ terminus: :failure
249
+
250
+ Trailblazer::Developer.wtf?(F::Song::Activity::Cover, [{params: {id: 2}, seq: []}])
251
+ end
252
+
253
+
254
+ #@ Each with operation
255
+ module D
256
+ class Song < B::Song; end
257
+ Mailer = Class.new(EachTest::Mailer)
258
+
259
+ #:operation-class
260
+ module Song::Activity
261
+ class Notify < Trailblazer::Activity::Railway
262
+ step :send_email
263
+
264
+ def send_email(ctx, index:, item:, **)
265
+ Mailer.send(to: item.email, message: "#{index}) You, #{item.full_name}, have been warned about your song being copied.")
266
+ end
267
+ end
268
+ end
269
+ #:operation-class end
270
+
271
+ #:operation
272
+ module Song::Activity
273
+ class Cover < Trailblazer::Activity::Railway
274
+ step :model
275
+ step Each(Notify, dataset_from: :composers_for_each)
276
+ step :rearrange
277
+ #~meths
278
+ def composers_for_each(ctx, model:, **)
279
+ model.composers
280
+ end
281
+ include CoverMethods
282
+ #~meths end
283
+ end
284
+ end
285
+ #:operation end
286
+ end
287
+
288
+ it "Each(Activity::Railway)" do
289
+ D::Mailer.send_options = []
290
+ assert_invoke D::Song::Activity::Cover, params: {id: 1},
291
+ seq: "[:rearrange]",
292
+ expected_ctx_variables: {
293
+ model: D::Song.find_by(id: 1),
294
+ # collected_from_each: [[0, "Fat Mike"], [1, "El Hefe"],]
295
+ }
296
+ assert_equal D::Mailer.send_options, [{:to=>nil, :message=>"0) You, Fat Mike, have been warned about your song being copied."}, {:to=>nil, :message=>"1) You, El Hefe, have been warned about your song being copied."}]
297
+ end
298
+
299
+ #@ Each with operation with three outcomes. Notify terminates on {End.spam_email},
300
+ # which is then routed to End.spam_alert in the hosting activity.
301
+ # NOTE: this is not documented, yet.
302
+ module G
303
+ class Song < B::Song; end
304
+
305
+ module Song::Activity
306
+ class Notify < Trailblazer::Activity::Railway
307
+ terminus :spam_email
308
+ # SpamEmail = Class.new(Trailblazer::Activity::Signal)
309
+
310
+ step :send_email, Output(:failure) => Track(:spam_email)
311
+
312
+ def send_email(ctx, index:, item:, **)
313
+ return false if item.email == "scammer@spam"
314
+ ctx[:value] = [index, item.full_name]
315
+ end
316
+ end
317
+ end
318
+
319
+ module Song::Activity
320
+ class Cover < Trailblazer::Activity::Railway
321
+ terminus :spam_alert
322
+
323
+ step :model
324
+ step Each(Notify, dataset_from: :composers_for_each, collect: true),
325
+ Output(:spam_email) => Track(:spam_alert)
326
+ step :rearrange
327
+ #~meths
328
+ def composers_for_each(ctx, model:, **)
329
+ model.composers
330
+ end
331
+ include CoverMethods
332
+ #~meths end
333
+ end
334
+ end
335
+ end
336
+
337
+ it "Each(Activity::Railway) with End.spam_email" do
338
+ Trailblazer::Developer.wtf?(G::Song::Activity::Cover, [{params: {id: 3}}, {}])
339
+
340
+ assert_invoke G::Song::Activity::Cover, params: {id: 3},
341
+ terminus: :spam_alert,
342
+ seq: "[]",
343
+ expected_ctx_variables: {
344
+ model: G::Song.find_by(id: 3),
345
+ collected_from_each: [[0, "Fat Mike"], nil,]
346
+ }
347
+ end
348
+ end
349
+
350
+ #@ Iteration doesn't add anything to ctx when {collect: false}.
351
+ class EachCtxDiscardedTest < Minitest::Spec
352
+ Composer = EachTest::Composer
353
+ Song = Class.new(EachTest::B::Song)
354
+
355
+ #@ iterated steps write to ctx, gets discarded.
356
+ module Song::Activity
357
+ class Cover < Trailblazer::Activity::Railway
358
+ step :model
359
+ #:write_to_ctx
360
+ step Each(dataset_from: :composers_for_each) {
361
+ step :notify_composers
362
+ step :write_to_ctx
363
+ }
364
+ #:write_to_ctx end
365
+ step :rearrange
366
+
367
+ #:write
368
+ def write_to_ctx(ctx, index:, seq:, **)
369
+ #~meths
370
+ seq << :write_to_ctx
371
+
372
+ #~meths end
373
+ ctx[:variable] = index # this is discarded!
374
+ end
375
+ #:write end
376
+
377
+ #~meths
378
+ include EachTest::CoverMethods
379
+ include EachTest::ComposersForEach
380
+ #~meths end
381
+ end
382
+ end
383
+
384
+ it "discards {ctx[:variable]}" do
385
+ assert_invoke Song::Activity::Cover, params: {id: 1},
386
+ expected_ctx_variables: {
387
+ model: Song.find_by(id: 1),
388
+ # collected_from_each: [[0, "Fat Mike"], [1, "El Hefe"],]
389
+ },
390
+ seq: "[:write_to_ctx, :write_to_ctx, :rearrange]"
391
+ end
392
+ end
393
+
394
+ # We add {:collected_from_each} ourselves.
395
+ class EachCtxAddsCollectedFromEachTest < Minitest::Spec
396
+ Composer = EachTest::Composer
397
+ Song = Class.new(EachTest::B::Song)
398
+
399
+ module Song::Activity
400
+ class Cover < Trailblazer::Activity::Railway
401
+ step :model
402
+ step Each(dataset_from: :composers_for_each,
403
+
404
+ # all filters called before/after each iteration!
405
+ Inject(:collected_from_each) => ->(ctx, **) { [] }, # this is called only once.
406
+ Out() => ->(ctx, collected_from_each:, **) { {collected_from_each: collected_from_each += [ctx[:value]] } }
407
+
408
+
409
+
410
+ ) {
411
+ step :notify_composers
412
+ step :write_to_ctx
413
+ }
414
+ step :rearrange
415
+
416
+ def write_to_ctx(ctx, index:, seq:, item:, **)
417
+ seq << :write_to_ctx
418
+
419
+ ctx[:value] = [index, item.full_name]
420
+ end
421
+
422
+ #~meths
423
+ include EachTest::CoverMethods
424
+ include EachTest::ComposersForEach
425
+ #~meths end
426
+ end
427
+ end
428
+
429
+ it "provides {:collected_from_each}" do
430
+ assert_invoke Song::Activity::Cover, params: {id: 1},
431
+ expected_ctx_variables: {
432
+ model: Song.find_by(id: 1),
433
+ collected_from_each: [[0, "Fat Mike"], [1, "El Hefe"],]
434
+ },
435
+ seq: "[:write_to_ctx, :write_to_ctx, :rearrange]"
436
+ end
437
+ end
438
+
439
+ #@ You can use Inject() to compute new variables.
440
+ #@ and Out() to compute what goes into the iterated {ctx}.
441
+ class EachCtxInOutTest < Minitest::Spec
442
+ Composer = EachTest::Composer
443
+ Song = Class.new(EachTest::B::Song)
444
+
445
+ module Song::Activity
446
+ class Cover < Trailblazer::Activity::Railway
447
+ step :model
448
+ step Each(dataset_from: :composers_for_each,
449
+ # Inject(always: true) => {
450
+ Inject(:composer_index) => ->(ctx, index:, **) { index },
451
+ # all filters called before/after each iteration!
452
+ Out() => ->(ctx, index:, variable:, **) { {:"composer-#{index}-value" => variable} }
453
+
454
+
455
+
456
+
457
+
458
+ ) {
459
+ step :notify_composers
460
+ step :write_to_ctx
461
+ }
462
+ step :rearrange
463
+
464
+ def write_to_ctx(ctx, composer_index:, model:, **)
465
+ ctx[:variable] = "#{composer_index} + #{model.class.name.split('::').last}"
466
+ end
467
+
468
+ #~meths
469
+ include EachTest::CoverMethods
470
+ include EachTest::ComposersForEach
471
+ #~meths end
472
+ end
473
+ end
474
+
475
+ it "discards {ctx[:variable]}" do
476
+ assert_invoke Song::Activity::Cover, params: {id: 1},
477
+ expected_ctx_variables: {
478
+ model: Song.find_by(id: 1),
479
+ :"composer-0-value" => "0 + Song",
480
+ :"composer-1-value" => "1 + Song",
481
+ },
482
+ seq: "[:rearrange]"
483
+ end
484
+ end
485
+
486
+ class EachOuterCtxTest < Minitest::Spec
487
+
488
+ end
489
+
490
+
491
+ #@ {:errors} is first initialized with a default injection,
492
+ #@ then passed across iterations.
493
+ # TODO: similar test above with {:collected_from_each}.
494
+ class EachSharedIterationVariableTest < Minitest::Spec
495
+ Song = Class.new(EachTest::B::Song)
496
+
497
+ #:inject
498
+ module Song::Activity
499
+ class Cover < Trailblazer::Activity::Railway
500
+ step :model
501
+ step Each(dataset_from: :composers_for_each,
502
+ Inject(:messages) => ->(*) { {} },
503
+
504
+ # all filters called before/after each iteration!
505
+ Out() => [:messages]
506
+ ) {
507
+ step :write_to_ctx
508
+ }
509
+ step :rearrange
510
+
511
+ def write_to_ctx(ctx, item:, messages:, index:, **)
512
+ ctx[:messages] = messages.merge(index => item.full_name)
513
+ end
514
+ #~meths
515
+ include EachTest::CoverMethods
516
+ include EachTest::ComposersForEach
517
+ #~meths end
518
+ end
519
+ end
520
+ #:inject end
521
+
522
+ it "passes {ctx[:messages]} across iterations and makes it grow" do
523
+ assert_invoke Song::Activity::Cover, params: {id: 1},
524
+ expected_ctx_variables: {
525
+ model: Song.find_by(id: 1),
526
+ messages: {0=>"Fat Mike", 1=>"El Hefe"}},
527
+ seq: "[:rearrange]"
528
+ end
529
+
530
+ end
531
+
532
+ #@ Each without any option
533
+ class EachPureTest < Minitest::Spec
534
+ Song = Class.new(EachTest::B::Song)
535
+
536
+ Mailer = Class.new(EachTest::Mailer)
537
+
538
+ #:each-pure
539
+ module Song::Activity
540
+ class Cover < Trailblazer::Activity::Railway
541
+ step :model
542
+ #:each-pure-macro
543
+ step Each(dataset_from: :composers_for_each) {
544
+ step :notify_composers
545
+ }
546
+ #:each-pure-macro end
547
+ step :rearrange
548
+
549
+ # "decider interface"
550
+ #:dataset_from
551
+ def composers_for_each(ctx, model:, **)
552
+ model.composers
553
+ end
554
+ #:dataset_from end
555
+
556
+ #:iterated
557
+ def notify_composers(ctx, index:, item:, **)
558
+ Mailer.send(to: item.email, message: "#{index}) You, #{item.full_name}, have been warned about your song being copied.")
559
+ end
560
+ #:iterated end
561
+ #~meths
562
+ def model(ctx, params:, **)
563
+ ctx[:model] = Song.find_by(id: params[:id])
564
+ end
565
+
566
+ include T.def_steps(:rearrange)
567
+ #~meths end
568
+ end
569
+ end
570
+ #:each-pure end
571
+
572
+ it "allows a dataset compute in the hosting activity" do
573
+ Mailer.send_options = []
574
+ #@ {:dataset} is not part of the {ctx}.
575
+ assert_invoke Song::Activity::Cover, params: {id: 1},
576
+ expected_ctx_variables: {
577
+ model: Song.find_by(id: 1),
578
+ },
579
+ seq: "[:rearrange]"
580
+
581
+ assert_equal Mailer.send_options, [{:to=>nil, :message=>"0) You, Fat Mike, have been warned about your song being copied."}, {:to=>nil, :message=>"1) You, El Hefe, have been warned about your song being copied."}]
582
+ end
583
+ end
584
+
585
+ #~ignore
586
+ class EachStrategyComplianceTest < Minitest::Spec
587
+ Song = EachPureTest::Song
588
+
589
+ it do
590
+ EachPureTest::Mailer.send_options = []
591
+
592
+ #:patch
593
+ cover_patched = Trailblazer::Activity::DSL::Linear.Patch(
594
+ Song::Activity::Cover,
595
+ ["Each/composers_for_each", "Each.iterate.block"] => -> { step :log_email }
596
+ )
597
+ #:patch end
598
+ cover_patched.include(T.def_steps(:log_email, :notify_composers))
599
+
600
+ #@ Original class isn't changed.
601
+ assert_invoke Song::Activity::Cover, params: {id: 1}, seq: [],
602
+ expected_ctx_variables: {
603
+ model: Song.find_by(id: 1),
604
+ },
605
+ seq: "[:rearrange]"
606
+
607
+ #@ Patched class runs
608
+ # Trailblazer::Developer.wtf?(cover_patched, [params: {id: 1}, seq: []])
609
+ assert_invoke cover_patched, params: {id: 1}, seq: [],
610
+ expected_ctx_variables: {
611
+ model: Song.find_by(id: 1),
612
+ },
613
+ seq: "[:notify_composers, :log_email, :notify_composers, :log_email, :rearrange]"
614
+ end
615
+
616
+
617
+ it "find_path" do
618
+ assert_equal Trailblazer::Developer::Introspect.find_path(Song::Activity::Cover,
619
+ ["Each/composers_for_each", "Each.iterate.block", "invoke_block_activity", :notify_composers])[0].task.inspect,
620
+ %{#<Trailblazer::Activity::TaskBuilder::Task user_proc=notify_composers>}
621
+
622
+ =begin
623
+ #:find_path
624
+ node, _ = Trailblazer::Developer::Introspect.find_path(
625
+ Song::Activity::Cover,
626
+ ["Each/composers_for_each", "Each.iterate.block", "invoke_block_activity", :notify_composers])
627
+ #=> #<Node ...>
628
+ #:find_path end
629
+ =end
630
+
631
+ end
632
+
633
+ it "{#find_path} for Each(Activity) with anonymous class" do
634
+ id = nil
635
+
636
+ activity = Class.new(Trailblazer::Activity::Railway) do
637
+ sub_activity = Class.new(Trailblazer::Activity::Railway) do
638
+ step :notify_composers
639
+ end
640
+ id = sub_activity.to_s
641
+
642
+ step :model
643
+ step Each(sub_activity, dataset_from: :composers_for_each)
644
+ step :rearrange
645
+
646
+ def composers_for_each(ctx, model:, **)
647
+ model.composers
648
+ end
649
+ # include CoverMethods
650
+ end
651
+
652
+ node, _activity = Trailblazer::Developer::Introspect.find_path(activity,
653
+ [%{Each/#{id}}, "Each.iterate.#{id}", "invoke_block_activity"])
654
+
655
+ assert_equal _activity.class.inspect, "Hash" # container_activity
656
+
657
+ #@ inside {invoke_block_activity}
658
+ node, _activity = Trailblazer::Developer::Introspect.find_path(activity,
659
+ [%{Each/#{id}}, "Each.iterate.#{id}", "invoke_block_activity", :notify_composers])
660
+
661
+ assert_equal node.task.inspect,
662
+ %{#<Trailblazer::Activity::TaskBuilder::Task user_proc=notify_composers>}
663
+ assert_equal _activity.class.inspect, "Trailblazer::Activity"
664
+ end
665
+
666
+
667
+ it "tracing" do
668
+ EachPureTest::Mailer.send_options = []
669
+ #:wtf
670
+ Trailblazer::Developer.wtf?(Song::Activity::Cover, [{
671
+ params: {id: 1},
672
+ #~meths
673
+ seq: []
674
+ #~meths end
675
+ }])
676
+ #:wtf end
677
+ end
678
+ end
679
+
680
+
681
+ #@ dataset: []
682
+ class EachEmptyDatasetTest < Minitest::Spec
683
+ it do
684
+ activity = Class.new(Trailblazer::Activity::Railway) do
685
+ step Each() {
686
+ step :raise
687
+ }
688
+ end
689
+
690
+ assert_invoke activity, dataset: []
691
+ end
692
+ end
693
+
694
+ class EachIDTest < Minitest::Spec
695
+ class Validate < Trailblazer::Activity::Railway
696
+ end
697
+
698
+ it "assigns IDs via {Macro.id_for}" do
699
+ activity = Class.new(Trailblazer::Activity::Railway) do
700
+ step Each() {}
701
+ step Each(Validate)
702
+ step Each() {}, id: "Each-1"
703
+ step Each(dataset_from: :composers_for_each) {}
704
+ end
705
+
706
+ assert_equal Trailblazer::Developer::Introspect.find_path(activity, ["Each/EachIDTest::Validate"])[0].id, "Each/EachIDTest::Validate"
707
+ assert_equal Trailblazer::Developer::Introspect.find_path(activity, ["Each-1"])[0].id, "Each-1"
708
+ assert_equal Trailblazer::Developer::Introspect.find_path(activity, ["Each/composers_for_each"])[0].id, "Each/composers_for_each"
709
+
710
+ assert_match /Each\/\w+/, Trailblazer::Activity::Introspect::TaskMap(activity).values[1].id
711
+ end
712
+ end
713
+
714
+ class DocsEachUnitTest < Minitest::Spec
715
+ module ComputeItem
716
+ def compute_item(ctx, item:, index:, **)
717
+ ctx[:value] = "#{item}-#{index.inspect}"
718
+ end
719
+ end
720
+
721
+ def self.block
722
+ -> (*){
723
+ step :compute_item
724
+ }
725
+ end
726
+
727
+ it "with Trace" do
728
+ activity = Class.new(Trailblazer::Activity::Railway) do
729
+ include T.def_steps(:a, :b)
730
+ include ComputeItem
731
+
732
+ step :a
733
+ step Each(&DocsEachUnitTest.block), id: "Each/1"
734
+ step :b
735
+ end
736
+
737
+ ctx = {seq: [], dataset: [3,2,1]}
738
+
739
+ stack, signal, (ctx, _) = Trailblazer::Developer::Trace.invoke(activity, [ctx, {}])
740
+
741
+ assert_equal Trailblazer::Developer::Trace::Present.(stack,
742
+ node_options: {stack.to_a[0] => {label: "<a-Each-b>"}}), %{<a-Each-b>
743
+ |-- Start.default
744
+ |-- a
745
+ |-- Each/1
746
+ | |-- Start.default
747
+ | |-- Each.iterate.block
748
+ | | |-- invoke_block_activity.0
749
+ | | | |-- Start.default
750
+ | | | |-- compute_item
751
+ | | | `-- End.success
752
+ | | |-- invoke_block_activity.1
753
+ | | | |-- Start.default
754
+ | | | |-- compute_item
755
+ | | | `-- End.success
756
+ | | `-- invoke_block_activity.2
757
+ | | |-- Start.default
758
+ | | |-- compute_item
759
+ | | `-- End.success
760
+ | `-- End.success
761
+ |-- b
762
+ `-- End.success}
763
+
764
+ #@ compile time
765
+ #@ make sure we can find tasks/compile-time artifacts in Each by using their {compile_id}.
766
+ assert_equal Trailblazer::Developer::Introspect.find_path(activity,
767
+ ["Each/1", "Each.iterate.block", "invoke_block_activity", :compute_item])[0].task.inspect,
768
+ %{#<Trailblazer::Activity::TaskBuilder::Task user_proc=compute_item>}
769
+ # puts Trailblazer::Developer::Render::TaskWrap.(activity, ["Each/1", "Each.iterate.block", "invoke_block_activity", :compute_item])
770
+
771
+ # TODO: grab runtime ctx for iteration 134
772
+ end
773
+
774
+ it "Each::Circuit" do
775
+ activity = Trailblazer::Macro.Each(collect: true, &DocsEachUnitTest.block)[:task]
776
+
777
+ my_exec_context = Class.new do
778
+ include ComputeItem
779
+ end.new
780
+
781
+ ctx = {
782
+ dataset: [1,2,3]
783
+ }
784
+
785
+ # signal, (_ctx, _) = Trailblazer::Activity::TaskWrap.invoke(activity, [ctx])
786
+ signal, (_ctx, _) = Trailblazer::Developer.wtf?(activity, [ctx], exec_context: my_exec_context)
787
+ assert_equal _ctx[:collected_from_each], ["1-0", "2-1", "3-2"]
788
+ end
789
+
790
+
791
+ it "accepts iterated {block}" do
792
+ activity = Class.new(Trailblazer::Activity::Railway) do
793
+ include ComputeItem
794
+
795
+ step Each(collect: true) { # expects {:dataset} # NOTE: use {} not {do ... end}
796
+ step :compute_item
797
+ }
798
+ end
799
+
800
+ Trailblazer::Developer.wtf?(activity, [{dataset: ["one", "two", "three"]}, {}])
801
+
802
+ assert_invoke activity, dataset: ["one", "two", "three"], expected_ctx_variables: {collected_from_each: ["one-0", "two-1", "three-2"]}
803
+ end
804
+
805
+ it "can see the entire ctx" do
806
+ activity = Class.new(Trailblazer::Activity::Railway) do
807
+ def compute_item_with_current_user(ctx, item:, index:, current_user:, **)
808
+ ctx[:value] = "#{item}-#{index.inspect}-#{current_user}"
809
+ end
810
+
811
+ step Each(collect: true) { # expects {:dataset}
812
+ step :compute_item_with_current_user
813
+ }
814
+ end
815
+
816
+ Trailblazer::Developer.wtf?(
817
+ activity,
818
+ [{
819
+ dataset: ["one", "two", "three"],
820
+ current_user: Object,
821
+ },
822
+ {}]
823
+ )
824
+ assert_invoke activity, dataset: ["one", "two", "three"], current_user: Object, expected_ctx_variables: {collected_from_each: ["one-0-Object", "two-1-Object", "three-2-Object"]}
825
+ end
826
+
827
+ it "allows taskWrap in Each" do
828
+ activity = Class.new(Trailblazer::Activity::Railway) do
829
+ step Each(collect: true) { # expects {:dataset} # NOTE: use {} not {do ... end}
830
+ step :compute_item, In() => {:current_user => :user}, In() => [:item, :index]
831
+ }
832
+
833
+ def compute_item(ctx, item:, index:, user:, **)
834
+ ctx[:value] = "#{item}-#{index.inspect}-#{user}"
835
+ end
836
+ end
837
+
838
+ assert_invoke activity, dataset: ["one", "two", "three"], current_user: "Yogi", expected_ctx_variables: {collected_from_each: ["one-0-Yogi", "two-1-Yogi", "three-2-Yogi"]}
839
+ end
840
+
841
+ it "accepts operation" do
842
+ nested_activity = Class.new(Trailblazer::Activity::Railway) do
843
+ step :compute_item
844
+
845
+ def compute_item(ctx, item:, index:, **)
846
+ ctx[:value] = "#{item}-#{index.inspect}"
847
+ end
848
+ end
849
+
850
+ activity = Class.new(Trailblazer::Activity::Railway) do
851
+ step Each(nested_activity, collect: true) # expects {:dataset}
852
+ end
853
+ # Trailblazer::Developer.wtf?(activity, [{dataset: ["one", "two", "three"]}, {}])
854
+
855
+ assert_invoke activity, dataset: ["one", "two", "three"], expected_ctx_variables: {collected_from_each: ["one-0", "two-1", "three-2"]}
856
+ end
857
+
858
+ it "doesn't override an existing ctx[:index]" do
859
+ activity = Class.new(Trailblazer::Activity::Railway) do
860
+ include T.def_steps(:a, :b)
861
+ include ComputeItem
862
+
863
+ step Each(collect: true, &DocsEachUnitTest.block), id: "Each/1"
864
+ step :b
865
+ def b(ctx, seq:, index:, **)
866
+ ctx[:seq] = seq + [index]
867
+ end
868
+
869
+ end
870
+
871
+ assert_invoke activity, dataset: [1,2,3], index: 9,
872
+ expected_ctx_variables: {collected_from_each: ["1-0", "2-1", "3-2"]},
873
+ seq: "[9]"
874
+ end
875
+
876
+ it "stops iterating when failure" do
877
+ activity = Class.new(Trailblazer::Activity::Railway) do
878
+ step Each(collect: true) {
879
+ step :check
880
+ }
881
+ step :a
882
+
883
+ include T.def_steps(:a)
884
+ def check(ctx, item:, **)
885
+ ctx[:value] = item.to_s #@ always collect the value, even in failure case.
886
+
887
+ return false if item >= 3
888
+ true
889
+ end
890
+ end
891
+
892
+ #@ all works
893
+ assert_invoke activity, dataset: [1,2],
894
+ expected_ctx_variables: {collected_from_each: ["1", "2"]},
895
+ seq: "[:a]"
896
+
897
+ #@ fail at 3 but still collect 3rd iteration!
898
+ Trailblazer::Developer.wtf?(activity, [{dataset: [1,2,3]}, {}])
899
+ assert_invoke activity, dataset: [1,2,3],
900
+ expected_ctx_variables: {collected_from_each: ["1", "2", "3"]},
901
+ seq: "[]",
902
+ terminus: :failure
903
+
904
+ #@ fail at 3, skip 4
905
+ assert_invoke activity, dataset: [1,2,3,4],
906
+ expected_ctx_variables: {collected_from_each: ["1", "2", "3"]},
907
+ seq: "[]",
908
+ terminus: :failure
909
+ end
910
+ end
911
+
912
+
913
+ class EachInEachTest < Minitest::Spec
914
+ it "what" do
915
+ activity = Class.new(Trailblazer::Activity::Railway) do
916
+ step Each(item_key: :outer) {
917
+ step :capture_outer
918
+
919
+ step Each(dataset_from: :inner_dataset, item_key: :inner) {
920
+ step :capture_inner
921
+ }
922
+ }
923
+
924
+ def capture_outer(ctx, outer:, **)
925
+ ctx[:seq] << outer
926
+ end
927
+
928
+ def capture_inner(ctx, inner:, **)
929
+ ctx[:seq] << inner
930
+ end
931
+
932
+ def inner_dataset(ctx, outer:, **)
933
+ outer.collect { |i| i * 10 }
934
+ end
935
+ end
936
+
937
+ assert_invoke activity, dataset: [[1,2],[3,4],[5,6]],
938
+ seq: %{[[1, 2], 10, 20, [3, 4], 30, 40, [5, 6], 50, 60]}
939
+ end
940
+ end