trailblazer-macro 2.1.11 → 2.1.12

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,907 @@
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", :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", :notify_composers])
627
+ #=> #<Node ...>
628
+ #:find_path end
629
+ =end
630
+
631
+ end
632
+
633
+
634
+ it "tracing" do
635
+ EachPureTest::Mailer.send_options = []
636
+ #:wtf
637
+ Trailblazer::Developer.wtf?(Song::Activity::Cover, [{
638
+ params: {id: 1},
639
+ #~meths
640
+ seq: []
641
+ #~meths end
642
+ }])
643
+ #:wtf end
644
+ end
645
+ end
646
+
647
+
648
+ #@ dataset: []
649
+ class EachEmptyDatasetTest < Minitest::Spec
650
+ it do
651
+ activity = Class.new(Trailblazer::Activity::Railway) do
652
+ step Each() {
653
+ step :raise
654
+ }
655
+ end
656
+
657
+ assert_invoke activity, dataset: []
658
+ end
659
+ end
660
+
661
+ class EachIDTest < Minitest::Spec
662
+ class Validate < Trailblazer::Activity::Railway
663
+ end
664
+
665
+ it "assigns IDs via {Macro.id_for}" do
666
+ activity = Class.new(Trailblazer::Activity::Railway) do
667
+ step Each() {}
668
+ step Each(Validate)
669
+ step Each() {}, id: "Each-1"
670
+ step Each(dataset_from: :composers_for_each) {}
671
+ end
672
+
673
+ assert_equal Trailblazer::Developer::Introspect.find_path(activity, ["Each/EachIDTest::Validate"])[0].id, "Each/EachIDTest::Validate"
674
+ assert_equal Trailblazer::Developer::Introspect.find_path(activity, ["Each-1"])[0].id, "Each-1"
675
+ assert_equal Trailblazer::Developer::Introspect.find_path(activity, ["Each/composers_for_each"])[0].id, "Each/composers_for_each"
676
+
677
+ assert_match /Each\/\w+/, Trailblazer::Activity::Introspect::TaskMap(activity).values[1].id
678
+ end
679
+ end
680
+
681
+ class DocsEachUnitTest < Minitest::Spec
682
+ module ComputeItem
683
+ def compute_item(ctx, item:, index:, **)
684
+ ctx[:value] = "#{item}-#{index.inspect}"
685
+ end
686
+ end
687
+
688
+ def self.block
689
+ -> (*){
690
+ step :compute_item
691
+ }
692
+ end
693
+
694
+ it "with Trace" do
695
+ activity = Class.new(Trailblazer::Activity::Railway) do
696
+ include T.def_steps(:a, :b)
697
+ include ComputeItem
698
+
699
+ step :a
700
+ step Each(&DocsEachUnitTest.block), id: "Each/1"
701
+ step :b
702
+ end
703
+
704
+ ctx = {seq: [], dataset: [3,2,1]}
705
+
706
+ stack, signal, (ctx, _) = Trailblazer::Developer::Trace.invoke(activity, [ctx, {}])
707
+
708
+ assert_equal Trailblazer::Developer::Trace::Present.(stack,
709
+ node_options: {stack.to_a[0] => {label: "<a-Each-b>"}}), %{<a-Each-b>
710
+ |-- Start.default
711
+ |-- a
712
+ |-- Each/1
713
+ | |-- Start.default
714
+ | |-- Each.iterate.block
715
+ | | |-- invoke_block_activity.0
716
+ | | | |-- Start.default
717
+ | | | |-- compute_item
718
+ | | | `-- End.success
719
+ | | |-- invoke_block_activity.1
720
+ | | | |-- Start.default
721
+ | | | |-- compute_item
722
+ | | | `-- End.success
723
+ | | `-- invoke_block_activity.2
724
+ | | |-- Start.default
725
+ | | |-- compute_item
726
+ | | `-- End.success
727
+ | `-- End.success
728
+ |-- b
729
+ `-- End.success}
730
+
731
+ #@ compile time
732
+ #@ make sure we can find tasks/compile-time artifacts in Each by using their {compile_id}.
733
+ assert_equal Trailblazer::Developer::Introspect.find_path(activity,
734
+ ["Each/1", "Each.iterate.block", :compute_item])[0].task.inspect,
735
+ %{#<Trailblazer::Activity::TaskBuilder::Task user_proc=compute_item>}
736
+ # puts Trailblazer::Developer::Render::TaskWrap.(activity, ["Each/1", "Each.iterate.block", "invoke_block_activity", :compute_item])
737
+
738
+ # TODO: grab runtime ctx for iteration 134
739
+ end
740
+
741
+ it "Each::Circuit" do
742
+ activity = Trailblazer::Macro.Each(collect: true, &DocsEachUnitTest.block)[:task]
743
+
744
+ my_exec_context = Class.new do
745
+ include ComputeItem
746
+ end.new
747
+
748
+ ctx = {
749
+ dataset: [1,2,3]
750
+ }
751
+
752
+ # signal, (_ctx, _) = Trailblazer::Activity::TaskWrap.invoke(activity, [ctx])
753
+ signal, (_ctx, _) = Trailblazer::Developer.wtf?(activity, [ctx], exec_context: my_exec_context)
754
+ assert_equal _ctx[:collected_from_each], ["1-0", "2-1", "3-2"]
755
+ end
756
+
757
+
758
+ it "accepts iterated {block}" do
759
+ activity = Class.new(Trailblazer::Activity::Railway) do
760
+ include ComputeItem
761
+
762
+ step Each(collect: true) { # expects {:dataset} # NOTE: use {} not {do ... end}
763
+ step :compute_item
764
+ }
765
+ end
766
+
767
+ Trailblazer::Developer.wtf?(activity, [{dataset: ["one", "two", "three"]}, {}])
768
+
769
+ assert_invoke activity, dataset: ["one", "two", "three"], expected_ctx_variables: {collected_from_each: ["one-0", "two-1", "three-2"]}
770
+ end
771
+
772
+ it "can see the entire ctx" do
773
+ activity = Class.new(Trailblazer::Activity::Railway) do
774
+ def compute_item_with_current_user(ctx, item:, index:, current_user:, **)
775
+ ctx[:value] = "#{item}-#{index.inspect}-#{current_user}"
776
+ end
777
+
778
+ step Each(collect: true) { # expects {:dataset}
779
+ step :compute_item_with_current_user
780
+ }
781
+ end
782
+
783
+ Trailblazer::Developer.wtf?(
784
+ activity,
785
+ [{
786
+ dataset: ["one", "two", "three"],
787
+ current_user: Object,
788
+ },
789
+ {}]
790
+ )
791
+ 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"]}
792
+ end
793
+
794
+ it "allows taskWrap in Each" do
795
+ activity = Class.new(Trailblazer::Activity::Railway) do
796
+ step Each(collect: true) { # expects {:dataset} # NOTE: use {} not {do ... end}
797
+ step :compute_item, In() => {:current_user => :user}, In() => [:item, :index]
798
+ }
799
+
800
+ def compute_item(ctx, item:, index:, user:, **)
801
+ ctx[:value] = "#{item}-#{index.inspect}-#{user}"
802
+ end
803
+ end
804
+
805
+ 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"]}
806
+ end
807
+
808
+ it "accepts operation" do
809
+ nested_activity = Class.new(Trailblazer::Activity::Railway) do
810
+ step :compute_item
811
+
812
+ def compute_item(ctx, item:, index:, **)
813
+ ctx[:value] = "#{item}-#{index.inspect}"
814
+ end
815
+ end
816
+
817
+ activity = Class.new(Trailblazer::Activity::Railway) do
818
+ step Each(nested_activity, collect: true) # expects {:dataset}
819
+ end
820
+ # Trailblazer::Developer.wtf?(activity, [{dataset: ["one", "two", "three"]}, {}])
821
+
822
+ assert_invoke activity, dataset: ["one", "two", "three"], expected_ctx_variables: {collected_from_each: ["one-0", "two-1", "three-2"]}
823
+ end
824
+
825
+ it "doesn't override an existing ctx[:index]" do
826
+ activity = Class.new(Trailblazer::Activity::Railway) do
827
+ include T.def_steps(:a, :b)
828
+ include ComputeItem
829
+
830
+ step Each(collect: true, &DocsEachUnitTest.block), id: "Each/1"
831
+ step :b
832
+ def b(ctx, seq:, index:, **)
833
+ ctx[:seq] = seq + [index]
834
+ end
835
+
836
+ end
837
+
838
+ assert_invoke activity, dataset: [1,2,3], index: 9,
839
+ expected_ctx_variables: {collected_from_each: ["1-0", "2-1", "3-2"]},
840
+ seq: "[9]"
841
+ end
842
+
843
+ it "stops iterating when failure" do
844
+ activity = Class.new(Trailblazer::Activity::Railway) do
845
+ step Each(collect: true) {
846
+ step :check
847
+ }
848
+ step :a
849
+
850
+ include T.def_steps(:a)
851
+ def check(ctx, item:, **)
852
+ ctx[:value] = item.to_s #@ always collect the value, even in failure case.
853
+
854
+ return false if item >= 3
855
+ true
856
+ end
857
+ end
858
+
859
+ #@ all works
860
+ assert_invoke activity, dataset: [1,2],
861
+ expected_ctx_variables: {collected_from_each: ["1", "2"]},
862
+ seq: "[:a]"
863
+
864
+ #@ fail at 3 but still collect 3rd iteration!
865
+ Trailblazer::Developer.wtf?(activity, [{dataset: [1,2,3]}, {}])
866
+ assert_invoke activity, dataset: [1,2,3],
867
+ expected_ctx_variables: {collected_from_each: ["1", "2", "3"]},
868
+ seq: "[]",
869
+ terminus: :failure
870
+
871
+ #@ fail at 3, skip 4
872
+ assert_invoke activity, dataset: [1,2,3,4],
873
+ expected_ctx_variables: {collected_from_each: ["1", "2", "3"]},
874
+ seq: "[]",
875
+ terminus: :failure
876
+ end
877
+ end
878
+
879
+
880
+ class EachInEachTest < Minitest::Spec
881
+ it "what" do
882
+ activity = Class.new(Trailblazer::Activity::Railway) do
883
+ step Each(item_key: :outer) {
884
+ step :capture_outer
885
+
886
+ step Each(dataset_from: :inner_dataset, item_key: :inner) {
887
+ step :capture_inner
888
+ }
889
+ }
890
+
891
+ def capture_outer(ctx, outer:, **)
892
+ ctx[:seq] << outer
893
+ end
894
+
895
+ def capture_inner(ctx, inner:, **)
896
+ ctx[:seq] << inner
897
+ end
898
+
899
+ def inner_dataset(ctx, outer:, **)
900
+ outer.collect { |i| i * 10 }
901
+ end
902
+ end
903
+
904
+ assert_invoke activity, dataset: [[1,2],[3,4],[5,6]],
905
+ seq: %{[[1, 2], 10, 20, [3, 4], 30, 40, [5, 6], 50, 60]}
906
+ end
907
+ end