rapidflow 0.1.0 → 0.2.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.
@@ -1,18 +1,19 @@
1
+ # frozen_string_literal: true
1
2
  require "test_helper"
2
3
 
3
- module Rapidflow
4
+ module RapidFlow
4
5
  class BatchTest < Minitest::Test
5
6
  def test_basic_functionality_with_arg_tasks
6
- belt = Batch.new(
7
+ batch = Batch.new(
7
8
  { fn: ->(data) { data.upcase }, workers: 4 },
8
9
  { fn: ->(data) { data + "!" }, workers: 4 }
9
10
  )
10
- belt.start
11
+ batch.start
11
12
 
12
- belt.push("hello")
13
- belt.push("world")
13
+ batch.push("hello")
14
+ batch.push("world")
14
15
 
15
- results = belt.results
16
+ results = batch.results
16
17
 
17
18
  assert_equal 2, results.length
18
19
  assert_equal ["HELLO!", nil], results[0]
@@ -20,7 +21,7 @@ module Rapidflow
20
21
  end
21
22
 
22
23
  def test_basic_functionality_with_build
23
- belt = Batch.build do
24
+ batch = Batch.build do
24
25
  # first stage to up case string
25
26
  stage ->(data) { data.upcase }
26
27
 
@@ -28,38 +29,23 @@ module Rapidflow
28
29
  stage ->(data) { data + "!" }
29
30
  end
30
31
 
31
- belt.push("hello")
32
- belt.push("world")
32
+ batch.push("hello")
33
+ batch.push("world")
33
34
 
34
- results = belt.results
35
+ results = batch.results
35
36
 
36
37
  assert_equal 2, results.length
37
38
  assert_equal ["HELLO!", nil], results[0]
38
39
  assert_equal ["WORLD!", nil], results[1]
39
40
  end
40
41
 
41
- def test_no_stages_with_build
42
- assert_raises(Batch::ConfigError, "Unable to start the belt without any stages") do
43
- Batch.build do
44
- # no stages
45
- end
46
- end
47
- end
48
-
49
- def test_no_stages_belt_start
50
- assert_raises(Batch::ConfigError, "Unable to start the belt without any stages") do
51
- belt = Batch.new
52
- belt.start
53
- end
54
- end
55
-
56
42
  def test_concurrent_execution_is_faster_than_sequential
57
43
  # Each lambda sleeps for 0.5 seconds
58
44
  # With 4 items and 2 stages:
59
45
  # - Sequential would take: 4 items * 0.5s * 2 stages = 4 seconds
60
46
  # - Concurrent (4 workers per stage) should take: max(0.5s, 0.5s) = ~0.5-1s
61
47
 
62
- belt = Batch.build do
48
+ batch = Batch.build do
63
49
  stage ->(data) {
64
50
  sleep(0.5)
65
51
  data
@@ -72,8 +58,8 @@ module Rapidflow
72
58
 
73
59
  start_time = Time.now
74
60
 
75
- 4.times { |i| belt.push(i) }
76
- results = belt.results
61
+ 4.times { |i| batch.push(i) }
62
+ results = batch.results
77
63
 
78
64
  elapsed = Time.now - start_time
79
65
 
@@ -89,7 +75,7 @@ module Rapidflow
89
75
  stage1_executing = []
90
76
  stage2_executing = []
91
77
 
92
- belt = Batch.build do
78
+ batch = Batch.build do
93
79
  stage ->(data) {
94
80
  execution_tracker.synchronize { stage1_executing << data }
95
81
  sleep(0.3)
@@ -105,7 +91,7 @@ module Rapidflow
105
91
  end
106
92
 
107
93
  # Push multiple items quickly
108
- 10.times { |i| belt.push(i) }
94
+ 10.times { |i| batch.push(i) }
109
95
 
110
96
  # Give threads time to start processing
111
97
  sleep(0.1)
@@ -117,7 +103,7 @@ module Rapidflow
117
103
  "Expected concurrent execution, but only #{stage1_executing.length} items processing"
118
104
  end
119
105
 
120
- results = belt.results
106
+ results = batch.results
121
107
  assert_equal 10, results.length
122
108
  end
123
109
 
@@ -125,7 +111,7 @@ module Rapidflow
125
111
  # Track execution order to verify pipeline behavior
126
112
  execution_log = Queue.new
127
113
 
128
- belt = Batch.build do
114
+ batch = Batch.build do
129
115
  stage ->(data) {
130
116
  execution_log.push("stage1_start_#{data}")
131
117
  sleep(0.2)
@@ -140,11 +126,11 @@ module Rapidflow
140
126
  }
141
127
  end
142
128
 
143
- belt.push("A")
129
+ batch.push("A")
144
130
  sleep(0.1) # Let A start processing
145
- belt.push("B")
131
+ batch.push("B")
146
132
 
147
- belt.results
133
+ batch.results
148
134
 
149
135
  # Convert log to array
150
136
  log = []
@@ -162,49 +148,23 @@ module Rapidflow
162
148
  assert stage1_start_b, "B should have started in stage1"
163
149
  end
164
150
 
165
- def test_error_handling_captures_exceptions
166
- belt = Batch.build do
167
- stage ->(data) {
168
- raise "Error in stage 1" if data == "bad"
169
- data
170
- }
171
- stage ->(data) { data.upcase }
172
- end
173
-
174
- belt.push("good")
175
- belt.push("bad")
176
-
177
- results = belt.results
178
-
179
- assert_equal 2, results.length
180
-
181
- # Good result should complete both stages
182
- assert_equal "GOOD", results[0][0]
183
- assert_nil results[0][1]
184
-
185
- # Bad result should have error from stage 1 and not be processed by stage 2
186
- assert_equal "bad", results[1][0] # Original data preserved
187
- assert_instance_of RuntimeError, results[1][1]
188
- assert_equal "Error in stage 1", results[1][1].message
189
- end
190
-
191
151
  def test_cannot_push_after_results_called
192
- belt = Batch.build do
152
+ batch = Batch.build do
193
153
  stage ->(data) { data }
194
154
  end
195
155
 
196
- belt.push("item1")
197
- belt.results
156
+ batch.push("item1")
157
+ batch.results
198
158
 
199
- assert_raises(Batch::RunError, "Cannot push to a locked belt when results are requested") do
200
- belt.push("item2")
201
- end
159
+ error = assert_raises(RapidFlow::RunError) { batch.push("item2") }
160
+
161
+ assert_equal "Cannot push to a locked batch when results are requested", error.message
202
162
  end
203
163
 
204
164
  def test_results_waits_for_all_processing_to_complete
205
165
  completion_times = Queue.new
206
166
 
207
- belt = Batch.build do
167
+ batch = Batch.build do
208
168
  stage ->(data) {
209
169
  sleep(0.5)
210
170
  data
@@ -215,11 +175,11 @@ module Rapidflow
215
175
  }
216
176
  end
217
177
 
218
- belt.push("item1")
219
- belt.push("item2")
178
+ batch.push("item1")
179
+ batch.push("item2")
220
180
 
221
181
  Time.now
222
- results = belt.results
182
+ results = batch.results
223
183
  results_end = Time.now
224
184
 
225
185
  # All items should have completed before results returns
@@ -237,7 +197,7 @@ module Rapidflow
237
197
  def test_high_throughput_with_many_items
238
198
  item_count = 100
239
199
 
240
- belt = Batch.build do
200
+ batch = Batch.build do
241
201
  stage ->(data) {
242
202
  sleep(0.01)
243
203
  data * 2
@@ -249,8 +209,8 @@ module Rapidflow
249
209
  end
250
210
 
251
211
  start_time = Time.now
252
- item_count.times { |i| belt.push(i) }
253
- results = belt.results
212
+ item_count.times { |i| batch.push(i) }
213
+ results = batch.results
254
214
  elapsed = Time.now - start_time
255
215
 
256
216
  assert_equal item_count, results.length
@@ -266,7 +226,7 @@ module Rapidflow
266
226
  end
267
227
 
268
228
  def test_three_stage_pipeline
269
- belt = Batch.build do
229
+ batch = Batch.build do
270
230
  stage ->(data) {
271
231
  sleep(0.1)
272
232
  data.upcase
@@ -281,10 +241,10 @@ module Rapidflow
281
241
  }
282
242
  end
283
243
 
284
- belt.push("hello")
285
- belt.push("world")
244
+ batch.push("hello")
245
+ batch.push("world")
286
246
 
287
- results = belt.results
247
+ results = batch.results
288
248
 
289
249
  assert_equal 2, results.length
290
250
  assert_equal ["HELLO!HELLO!", nil], results[0]
@@ -293,7 +253,7 @@ module Rapidflow
293
253
 
294
254
  def test_results_preserve_input_order
295
255
  # Even though items complete at different times, results should match push order
296
- belt = Batch.build do
256
+ batch = Batch.build do
297
257
  stage ->(data) {
298
258
  # Make later items finish faster
299
259
  sleep_time = (data[:id] == 0) ? 0.5 : 0.1
@@ -304,9 +264,9 @@ module Rapidflow
304
264
 
305
265
  # Push items in order 0, 1, 2, 3
306
266
  # But item 0 will take longer to complete
307
- 4.times { |i| belt.push({ id: i }) }
267
+ 4.times { |i| batch.push({ id: i }) }
308
268
 
309
- results = belt.results
269
+ results = batch.results
310
270
 
311
271
  # Results should still be in order 0, 1, 2, 3
312
272
  assert_equal 4, results.length
@@ -317,14 +277,14 @@ module Rapidflow
317
277
  end
318
278
 
319
279
  def test_single_stage_pipeline
320
- belt = Batch.build do
280
+ batch = Batch.build do
321
281
  stage ->(data) { data * 2 }
322
282
  end
323
283
 
324
- belt.push(5)
325
- belt.push(10)
284
+ batch.push(5)
285
+ batch.push(10)
326
286
 
327
- results = belt.results
287
+ results = batch.results
328
288
 
329
289
  assert_equal 2, results.length
330
290
  assert_equal [10, nil], results[0]
@@ -332,78 +292,13 @@ module Rapidflow
332
292
  end
333
293
 
334
294
  def test_empty_pipeline
335
- belt = Batch.build { stage ->(_data) { } }
295
+ batch = Batch.build { stage ->(_data) { } }
336
296
 
337
- results = belt.results
297
+ results = batch.results
338
298
 
339
299
  assert_equal 0, results.length
340
300
  end
341
301
 
342
- def test_error_in_middle_stage
343
- belt = Batch.build do
344
- stage ->(data) { data.upcase }
345
- stage ->(data) {
346
- raise "Error in stage 2" if data == "BAD"
347
- data
348
- }
349
- stage ->(data) { data + "!" }
350
- end
351
-
352
- belt.push("good")
353
- belt.push("bad")
354
- belt.push("also_good")
355
-
356
- results = belt.results
357
-
358
- assert_equal 3, results.length
359
- assert_equal ["GOOD!", nil], results[0]
360
- assert_equal ["BAD", results[1][1]], [results[1][0], results[1][1]]
361
- assert_equal "Error in stage 2", results[1][1].message
362
- assert_equal ["ALSO_GOOD!", nil], results[2]
363
- end
364
-
365
- def test_error_in_last_stage
366
- belt = Batch.build do
367
- stage ->(data) { data.upcase }
368
- stage ->(data) {
369
- raise "Error in final stage" if data == "BAD"
370
- data
371
- }
372
- end
373
-
374
- belt.push("good")
375
- belt.push("bad")
376
-
377
- results = belt.results
378
-
379
- assert_equal 2, results.length
380
- assert_equal ["GOOD", nil], results[0]
381
- assert_equal ["BAD", results[1][1]], [results[1][0], results[1][1]]
382
- assert_equal "Error in final stage", results[1][1].message
383
- end
384
-
385
- def test_multiple_errors_in_sequence
386
- belt = Batch.build do
387
- stage ->(data) {
388
- raise "Error at #{data}" if data.start_with?("bad")
389
- data
390
- }
391
- end
392
-
393
- belt.push("good1")
394
- belt.push("bad1")
395
- belt.push("bad2")
396
- belt.push("good2")
397
-
398
- results = belt.results
399
-
400
- assert_equal 4, results.length
401
- assert_equal ["good1", nil], results[0]
402
- assert_instance_of RuntimeError, results[1][1]
403
- assert_instance_of RuntimeError, results[2][1]
404
- assert_equal ["good2", nil], results[3]
405
- end
406
-
407
302
  def test_different_worker_counts
408
303
  # Test with 1 worker per stage (sequential at each stage)
409
304
  j1 = Batch.build do
@@ -431,15 +326,15 @@ module Rapidflow
431
326
  end
432
327
 
433
328
  def test_complex_data_types
434
- belt = Batch.build do
329
+ batch = Batch.build do
435
330
  stage ->(data) { { original: data, processed: true } }
436
331
  stage ->(data) { data.merge(stage2: Time.now.to_i) }
437
332
  end
438
333
 
439
- belt.push({ id: 1, name: "test" })
440
- belt.push([1, 2, 3])
334
+ batch.push({ id: 1, name: "test" })
335
+ batch.push([1, 2, 3])
441
336
 
442
- results = belt.results
337
+ results = batch.results
443
338
 
444
339
  assert_equal 2, results.length
445
340
  assert results[0][0].is_a?(Hash)
@@ -449,15 +344,15 @@ module Rapidflow
449
344
  end
450
345
 
451
346
  def test_nil_values
452
- belt = Batch.build do
347
+ batch = Batch.build do
453
348
  stage ->(data) { data.nil? ? "was_nil" : data }
454
349
  stage ->(data) { data.upcase }
455
350
  end
456
351
 
457
- belt.push(nil)
458
- belt.push("hello")
352
+ batch.push(nil)
353
+ batch.push("hello")
459
354
 
460
- results = belt.results
355
+ results = batch.results
461
356
 
462
357
  assert_equal 2, results.length
463
358
  assert_equal ["WAS_NIL", nil], results[0]
@@ -467,15 +362,15 @@ module Rapidflow
467
362
  def test_large_dataset_stress_test
468
363
  item_count = 500
469
364
 
470
- belt = Batch.build do
365
+ batch = Batch.build do
471
366
  stage ->(data) { data * 2 }, workers: 8
472
367
  stage ->(data) { data + 1 }, workers: 8
473
368
  stage ->(data) { data.to_s }, workers: 8
474
369
  end
475
370
 
476
- item_count.times { |i| belt.push(i) }
371
+ item_count.times { |i| batch.push(i) }
477
372
 
478
- results = belt.results
373
+ results = batch.results
479
374
 
480
375
  assert_equal item_count, results.length
481
376
 
@@ -488,7 +383,7 @@ module Rapidflow
488
383
 
489
384
  def test_varying_processing_times
490
385
  # Simulate real-world scenario with varying processing times
491
- belt = Batch.build do
386
+ batch = Batch.build do
492
387
  stage ->(data) {
493
388
  sleep(rand * 0.1) # Random 0-100ms
494
389
  data.upcase
@@ -500,9 +395,9 @@ module Rapidflow
500
395
  end
501
396
 
502
397
  words = %w[apple banana cherry date elderberry fig grape]
503
- words.each { |word| belt.push(word) }
398
+ words.each { |word| batch.push(word) }
504
399
 
505
- results = belt.results
400
+ results = batch.results
506
401
 
507
402
  assert_equal words.length, results.length
508
403
  words.each_with_index do |word, i|
@@ -511,61 +406,15 @@ module Rapidflow
511
406
  end
512
407
  end
513
408
 
514
- def test_exception_types_preserved
515
- belt = Batch.build do
516
- stage ->(data) {
517
- case data
518
- when "argument_error"
519
- raise ArgumentError, "Bad argument"
520
- when "runtime_error"
521
- raise "Runtime problem"
522
- when "custom_error"
523
- raise StandardError, "Custom error"
524
- else
525
- data
526
- end
527
- }
528
- end
529
-
530
- belt.push("good")
531
- belt.push("argument_error")
532
- belt.push("runtime_error")
533
- belt.push("custom_error")
534
-
535
- results = belt.results
536
-
537
- assert_equal 4, results.length
538
- assert_equal ["good", nil], results[0]
539
- assert_instance_of ArgumentError, results[1][1]
540
- assert_instance_of RuntimeError, results[2][1]
541
- assert_instance_of StandardError, results[3][1]
542
- end
543
-
544
- def test_all_items_fail
545
- belt = Batch.build do
546
- stage ->(data) { raise "Always fails" }
547
- end
548
-
549
- 5.times { |i| belt.push(i) }
550
-
551
- results = belt.results
552
-
553
- assert_equal 5, results.length
554
- results.each do |result, error|
555
- assert_instance_of RuntimeError, error
556
- assert_equal "Always fails", error.message
557
- end
558
- end
559
-
560
409
  def test_push_many_items_quickly
561
- belt = Batch.build do
410
+ batch = Batch.build do
562
411
  stage ->(data) { data }
563
412
  end
564
413
 
565
414
  # Push 1000 items as fast as possible
566
- 1000.times { |i| belt.push(i) }
415
+ 1000.times { |i| batch.push(i) }
567
416
 
568
- results = belt.results
417
+ results = batch.results
569
418
 
570
419
  assert_equal 1000, results.length
571
420
  # Verify order is maintained
@@ -575,22 +424,22 @@ module Rapidflow
575
424
  end
576
425
 
577
426
  def test_idempotent_results_calls_not_allowed
578
- belt = Batch.build do
427
+ batch = Batch.build do
579
428
  stage ->(data) { data }
580
429
  end
581
430
 
582
- belt.push(1)
583
- belt.results
431
+ batch.push(1)
432
+ batch.results
584
433
 
585
434
  # Can't call results again or push again
586
- assert_raises(RuntimeError) { belt.push(2) }
435
+ assert_raises(RuntimeError) { batch.push(2) }
587
436
  end
588
437
 
589
438
  def test_thread_safety_of_shared_state
590
439
  shared_counter = { count: 0 }
591
440
  mutex = Mutex.new
592
441
 
593
- belt = Batch.build do
442
+ batch = Batch.build do
594
443
  stage ->(data) {
595
444
  # Safely increment shared counter
596
445
  mutex.synchronize { shared_counter[:count] += 1 }
@@ -598,8 +447,8 @@ module Rapidflow
598
447
  }, workers: 10
599
448
  end
600
449
 
601
- 100.times { |i| belt.push(i) }
602
- results = belt.results
450
+ 100.times { |i| batch.push(i) }
451
+ results = batch.results
603
452
 
604
453
  assert_equal 100, results.length
605
454
  assert_equal 100, shared_counter[:count]
@@ -1,6 +1,6 @@
1
1
  require "test_helper"
2
2
 
3
- module Rapidflow
3
+ module RapidFlow
4
4
  class CounterTest < Minitest::Test
5
5
  def test_sequential_indices
6
6
  counter = Counter.new
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ module RapidFlow
6
+ class PipelineTest < Minitest::Test
7
+ def test_empty_pipeline
8
+ pipeline = Pipeline.new(0, 1)
9
+
10
+ pipeline.wait_for_completion
11
+ pipeline.shutdown
12
+
13
+ assert pipeline.results_empty?
14
+ end
15
+
16
+ def test_pipeline_with_single_stage
17
+ pipeline = Pipeline.new(1, 1)
18
+
19
+ pipeline.enqueue(0, "test_item")
20
+
21
+ # Simulate stage processing
22
+ item = pipeline.dequeue(0)
23
+ pipeline.enqueue(1, item.upcase)
24
+ pipeline.decrement_active_workers
25
+
26
+ result = pipeline.dequeue_result
27
+
28
+ assert_equal "TEST_ITEM", result
29
+
30
+ pipeline.shutdown
31
+ end
32
+
33
+ def test_pipeline_queues_created_correctly
34
+ pipeline = Pipeline.new(3, 2)
35
+
36
+ # Pipeline with 3 stages should have 4 queues (one per stage + results queue)
37
+ (0..3).each do |i|
38
+ pipeline.enqueue(i, "item_#{i}")
39
+
40
+ result = pipeline.dequeue(i)
41
+ assert_equal "item_#{i}", result
42
+ end
43
+
44
+ pipeline.shutdown
45
+ end
46
+
47
+ def test_active_workers_tracking
48
+ pipeline = Pipeline.new(1, 1)
49
+
50
+ pipeline.enqueue(0, "item1")
51
+ pipeline.enqueue(0, "item2")
52
+
53
+ # Simulate processing
54
+ pipeline.dequeue(0)
55
+ pipeline.decrement_active_workers
56
+
57
+ pipeline.dequeue(0)
58
+ pipeline.decrement_active_workers
59
+
60
+ pipeline.wait_for_completion
61
+
62
+ pipeline.shutdown
63
+
64
+ assert pipeline.results_empty?
65
+ end
66
+ end
67
+ end