simple_flow 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.
- checksums.yaml +7 -0
- data/.envrc +1 -0
- data/.github/workflows/deploy-github-pages.yml +52 -0
- data/.rubocop.yml +57 -0
- data/CHANGELOG.md +4 -0
- data/COMMITS.md +196 -0
- data/LICENSE +21 -0
- data/README.md +481 -0
- data/Rakefile +15 -0
- data/benchmarks/parallel_vs_sequential.rb +98 -0
- data/benchmarks/pipeline_overhead.rb +130 -0
- data/docs/api/middleware.md +468 -0
- data/docs/api/parallel-step.md +363 -0
- data/docs/api/pipeline.md +382 -0
- data/docs/api/result.md +375 -0
- data/docs/concurrent/best-practices.md +687 -0
- data/docs/concurrent/introduction.md +246 -0
- data/docs/concurrent/parallel-steps.md +418 -0
- data/docs/concurrent/performance.md +481 -0
- data/docs/core-concepts/flow-control.md +452 -0
- data/docs/core-concepts/middleware.md +389 -0
- data/docs/core-concepts/overview.md +219 -0
- data/docs/core-concepts/pipeline.md +315 -0
- data/docs/core-concepts/result.md +168 -0
- data/docs/core-concepts/steps.md +391 -0
- data/docs/development/benchmarking.md +443 -0
- data/docs/development/contributing.md +380 -0
- data/docs/development/dagwood-concepts.md +435 -0
- data/docs/development/testing.md +514 -0
- data/docs/getting-started/examples.md +197 -0
- data/docs/getting-started/installation.md +62 -0
- data/docs/getting-started/quick-start.md +218 -0
- data/docs/guides/choosing-concurrency-model.md +441 -0
- data/docs/guides/complex-workflows.md +440 -0
- data/docs/guides/data-fetching.md +478 -0
- data/docs/guides/error-handling.md +635 -0
- data/docs/guides/file-processing.md +505 -0
- data/docs/guides/validation-patterns.md +496 -0
- data/docs/index.md +169 -0
- data/examples/.gitignore +3 -0
- data/examples/01_basic_pipeline.rb +112 -0
- data/examples/02_error_handling.rb +178 -0
- data/examples/03_middleware.rb +186 -0
- data/examples/04_parallel_automatic.rb +221 -0
- data/examples/05_parallel_explicit.rb +279 -0
- data/examples/06_real_world_ecommerce.rb +288 -0
- data/examples/07_real_world_etl.rb +277 -0
- data/examples/08_graph_visualization.rb +246 -0
- data/examples/09_pipeline_visualization.rb +266 -0
- data/examples/10_concurrency_control.rb +235 -0
- data/examples/11_sequential_dependencies.rb +243 -0
- data/examples/12_none_constant.rb +161 -0
- data/examples/README.md +374 -0
- data/examples/regression_test/01_basic_pipeline.txt +38 -0
- data/examples/regression_test/02_error_handling.txt +92 -0
- data/examples/regression_test/03_middleware.txt +61 -0
- data/examples/regression_test/04_parallel_automatic.txt +86 -0
- data/examples/regression_test/05_parallel_explicit.txt +80 -0
- data/examples/regression_test/06_real_world_ecommerce.txt +53 -0
- data/examples/regression_test/07_real_world_etl.txt +58 -0
- data/examples/regression_test/08_graph_visualization.txt +429 -0
- data/examples/regression_test/09_pipeline_visualization.txt +305 -0
- data/examples/regression_test/10_concurrency_control.txt +96 -0
- data/examples/regression_test/11_sequential_dependencies.txt +86 -0
- data/examples/regression_test/12_none_constant.txt +64 -0
- data/examples/regression_test.rb +105 -0
- data/lib/simple_flow/dependency_graph.rb +120 -0
- data/lib/simple_flow/dependency_graph_visualizer.rb +326 -0
- data/lib/simple_flow/middleware.rb +36 -0
- data/lib/simple_flow/parallel_executor.rb +80 -0
- data/lib/simple_flow/pipeline.rb +405 -0
- data/lib/simple_flow/result.rb +88 -0
- data/lib/simple_flow/step_tracker.rb +58 -0
- data/lib/simple_flow/version.rb +5 -0
- data/lib/simple_flow.rb +41 -0
- data/mkdocs.yml +146 -0
- data/pipeline_graph.dot +51 -0
- data/pipeline_graph.html +60 -0
- data/pipeline_graph.mmd +19 -0
- metadata +127 -0
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
# Testing Guide
|
|
2
|
+
|
|
3
|
+
This guide covers testing strategies, patterns, and best practices for SimpleFlow.
|
|
4
|
+
|
|
5
|
+
## Test Suite Overview
|
|
6
|
+
|
|
7
|
+
SimpleFlow uses Minitest for its test suite.
|
|
8
|
+
|
|
9
|
+
**Current Coverage:**
|
|
10
|
+
- **121 tests**
|
|
11
|
+
- **296 assertions**
|
|
12
|
+
- **96.61% code coverage**
|
|
13
|
+
- **All tests passing**
|
|
14
|
+
|
|
15
|
+
## Running Tests
|
|
16
|
+
|
|
17
|
+
### Run All Tests
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bundle exec rake test
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Expected output:
|
|
24
|
+
```
|
|
25
|
+
Run options: --seed 12345
|
|
26
|
+
|
|
27
|
+
# Running:
|
|
28
|
+
|
|
29
|
+
.............................................................................
|
|
30
|
+
|
|
31
|
+
Finished in 0.123456s, 987.65 runs/s, 2345.67 assertions/s.
|
|
32
|
+
|
|
33
|
+
121 tests, 296 assertions, 0 failures, 0 errors, 0 skips
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Run Specific Test File
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
ruby -Ilib:test test/pipeline_test.rb
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Run Specific Test
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
ruby -Ilib:test test/pipeline_test.rb -n test_basic_pipeline
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Run Tests with Verbose Output
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
ruby -Ilib:test test/pipeline_test.rb --verbose
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Test Organization
|
|
55
|
+
|
|
56
|
+
Tests are organized by component:
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
test/
|
|
60
|
+
├── test_helper.rb # Test configuration and helpers
|
|
61
|
+
├── result_test.rb # Result class tests
|
|
62
|
+
├── pipeline_test.rb # Pipeline class tests
|
|
63
|
+
├── middleware_test.rb # Middleware tests
|
|
64
|
+
├── parallel_execution_test.rb # Parallel execution tests
|
|
65
|
+
├── dependency_graph_test.rb # Dependency graph tests
|
|
66
|
+
├── dependency_graph_visualizer_test.rb # Visualization tests
|
|
67
|
+
├── pipeline_visualization_test.rb # Pipeline visualization tests
|
|
68
|
+
└── step_tracker_test.rb # StepTracker tests
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Writing Tests
|
|
72
|
+
|
|
73
|
+
### Basic Test Structure
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
require 'test_helper'
|
|
77
|
+
|
|
78
|
+
class MyFeatureTest < Minitest::Test
|
|
79
|
+
def setup
|
|
80
|
+
# Runs before each test
|
|
81
|
+
@pipeline = SimpleFlow::Pipeline.new
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def test_feature_description
|
|
85
|
+
# Arrange
|
|
86
|
+
initial = SimpleFlow::Result.new(42)
|
|
87
|
+
|
|
88
|
+
# Act
|
|
89
|
+
result = @pipeline.call(initial)
|
|
90
|
+
|
|
91
|
+
# Assert
|
|
92
|
+
assert result.continue?
|
|
93
|
+
assert_equal 42, result.value
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def teardown
|
|
97
|
+
# Runs after each test (if needed)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Testing Result Objects
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
class ResultTest < Minitest::Test
|
|
106
|
+
def test_new_result_has_default_values
|
|
107
|
+
result = SimpleFlow::Result.new(42)
|
|
108
|
+
|
|
109
|
+
assert_equal 42, result.value
|
|
110
|
+
assert_equal({}, result.context)
|
|
111
|
+
assert_equal({}, result.errors)
|
|
112
|
+
assert result.continue?
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def test_with_context_adds_context
|
|
116
|
+
result = SimpleFlow::Result.new(42)
|
|
117
|
+
.with_context(:user_id, 123)
|
|
118
|
+
.with_context(:timestamp, Time.now)
|
|
119
|
+
|
|
120
|
+
assert_equal 123, result.context[:user_id]
|
|
121
|
+
assert result.context[:timestamp]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def test_with_error_accumulates_errors
|
|
125
|
+
result = SimpleFlow::Result.new(nil)
|
|
126
|
+
.with_error(:validation, "Error 1")
|
|
127
|
+
.with_error(:validation, "Error 2")
|
|
128
|
+
|
|
129
|
+
assert_equal 2, result.errors[:validation].size
|
|
130
|
+
assert_includes result.errors[:validation], "Error 1"
|
|
131
|
+
assert_includes result.errors[:validation], "Error 2"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def test_halt_stops_continuation
|
|
135
|
+
result = SimpleFlow::Result.new(42).halt
|
|
136
|
+
|
|
137
|
+
refute result.continue?
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def test_immutability
|
|
141
|
+
original = SimpleFlow::Result.new(42)
|
|
142
|
+
modified = original.with_context(:key, "value")
|
|
143
|
+
|
|
144
|
+
assert_equal({}, original.context)
|
|
145
|
+
assert_equal({ key: "value" }, modified.context)
|
|
146
|
+
refute_equal original.object_id, modified.object_id
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Testing Pipelines
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
class PipelineTest < Minitest::Test
|
|
155
|
+
def test_basic_sequential_execution
|
|
156
|
+
pipeline = SimpleFlow::Pipeline.new do
|
|
157
|
+
step ->(result) { result.continue(result.value + 1) }
|
|
158
|
+
step ->(result) { result.continue(result.value * 2) }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
result = pipeline.call(SimpleFlow::Result.new(5))
|
|
162
|
+
|
|
163
|
+
assert_equal 12, result.value # (5 + 1) * 2
|
|
164
|
+
assert result.continue?
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def test_pipeline_halts_on_error
|
|
168
|
+
pipeline = SimpleFlow::Pipeline.new do
|
|
169
|
+
step ->(result) { result.continue(result.value + 1) }
|
|
170
|
+
step ->(result) { result.halt.with_error(:error, "Failed") }
|
|
171
|
+
step ->(result) { result.continue(result.value * 2) } # Should not execute
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
result = pipeline.call(SimpleFlow::Result.new(5))
|
|
175
|
+
|
|
176
|
+
assert_equal 6, result.value # Only first step executed
|
|
177
|
+
refute result.continue?
|
|
178
|
+
assert_includes result.errors[:error], "Failed"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def test_pipeline_with_middleware
|
|
182
|
+
executed = []
|
|
183
|
+
|
|
184
|
+
logging_middleware = ->(callable) {
|
|
185
|
+
->(result) {
|
|
186
|
+
executed << :before
|
|
187
|
+
output = callable.call(result)
|
|
188
|
+
executed << :after
|
|
189
|
+
output
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
pipeline = SimpleFlow::Pipeline.new do
|
|
194
|
+
use_middleware logging_middleware
|
|
195
|
+
|
|
196
|
+
step ->(result) {
|
|
197
|
+
executed << :step
|
|
198
|
+
result.continue(result.value)
|
|
199
|
+
}
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
pipeline.call(SimpleFlow::Result.new(nil))
|
|
203
|
+
|
|
204
|
+
assert_equal [:before, :step, :after], executed
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Testing Parallel Execution
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
class ParallelExecutionTest < Minitest::Test
|
|
213
|
+
def test_parallel_steps_execute_concurrently
|
|
214
|
+
skip unless SimpleFlow::Pipeline.new.async_available?
|
|
215
|
+
|
|
216
|
+
execution_order = []
|
|
217
|
+
mutex = Mutex.new
|
|
218
|
+
|
|
219
|
+
pipeline = SimpleFlow::Pipeline.new do
|
|
220
|
+
step :step_a, ->(result) {
|
|
221
|
+
mutex.synchronize { execution_order << :a_start }
|
|
222
|
+
sleep 0.1
|
|
223
|
+
mutex.synchronize { execution_order << :a_end }
|
|
224
|
+
result.with_context(:a, true).continue(result.value)
|
|
225
|
+
}, depends_on: []
|
|
226
|
+
|
|
227
|
+
step :step_b, ->(result) {
|
|
228
|
+
mutex.synchronize { execution_order << :b_start }
|
|
229
|
+
sleep 0.1
|
|
230
|
+
mutex.synchronize { execution_order << :b_end }
|
|
231
|
+
result.with_context(:b, true).continue(result.value)
|
|
232
|
+
}, depends_on: []
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
result = pipeline.call_parallel(SimpleFlow::Result.new(nil))
|
|
236
|
+
|
|
237
|
+
assert result.context[:a]
|
|
238
|
+
assert result.context[:b]
|
|
239
|
+
|
|
240
|
+
# Both steps started before either finished
|
|
241
|
+
a_start_index = execution_order.index(:a_start)
|
|
242
|
+
b_start_index = execution_order.index(:b_start)
|
|
243
|
+
a_end_index = execution_order.index(:a_end)
|
|
244
|
+
b_end_index = execution_order.index(:b_end)
|
|
245
|
+
|
|
246
|
+
assert a_start_index < a_end_index
|
|
247
|
+
assert b_start_index < b_end_index
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def test_parallel_execution_merges_contexts
|
|
251
|
+
pipeline = SimpleFlow::Pipeline.new do
|
|
252
|
+
step :step_a, ->(result) {
|
|
253
|
+
result.with_context(:data_a, "from A").continue(result.value)
|
|
254
|
+
}, depends_on: []
|
|
255
|
+
|
|
256
|
+
step :step_b, ->(result) {
|
|
257
|
+
result.with_context(:data_b, "from B").continue(result.value)
|
|
258
|
+
}, depends_on: []
|
|
259
|
+
|
|
260
|
+
step :combine, ->(result) {
|
|
261
|
+
assert_equal "from A", result.context[:data_a]
|
|
262
|
+
assert_equal "from B", result.context[:data_b]
|
|
263
|
+
result.continue(result.value)
|
|
264
|
+
}, depends_on: [:step_a, :step_b]
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
result = pipeline.call_parallel(SimpleFlow::Result.new(nil))
|
|
268
|
+
assert result.continue?
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Testing Middleware
|
|
274
|
+
|
|
275
|
+
```ruby
|
|
276
|
+
class MiddlewareTest < Minitest::Test
|
|
277
|
+
def test_logging_middleware_logs_execution
|
|
278
|
+
output = StringIO.new
|
|
279
|
+
logger = Logger.new(output)
|
|
280
|
+
|
|
281
|
+
pipeline = SimpleFlow::Pipeline.new do
|
|
282
|
+
use_middleware SimpleFlow::MiddleWare::Logging, logger: logger
|
|
283
|
+
|
|
284
|
+
step ->(result) { result.continue(result.value) }
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
pipeline.call(SimpleFlow::Result.new(42))
|
|
288
|
+
|
|
289
|
+
log_output = output.string
|
|
290
|
+
assert_match(/Before call/, log_output)
|
|
291
|
+
assert_match(/After call/, log_output)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def test_instrumentation_middleware_measures_time
|
|
295
|
+
output = StringIO.new
|
|
296
|
+
$stdout = output
|
|
297
|
+
|
|
298
|
+
pipeline = SimpleFlow::Pipeline.new do
|
|
299
|
+
use_middleware SimpleFlow::MiddleWare::Instrumentation, api_key: 'test'
|
|
300
|
+
|
|
301
|
+
step ->(result) {
|
|
302
|
+
sleep 0.01
|
|
303
|
+
result.continue(result.value)
|
|
304
|
+
}
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
pipeline.call(SimpleFlow::Result.new(nil))
|
|
308
|
+
|
|
309
|
+
$stdout = STDOUT
|
|
310
|
+
assert_match(/Instrumentation: test took/, output.string)
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
## Testing Patterns
|
|
316
|
+
|
|
317
|
+
### Testing Step Classes
|
|
318
|
+
|
|
319
|
+
```ruby
|
|
320
|
+
class FetchUserStep
|
|
321
|
+
def call(result)
|
|
322
|
+
user = User.find(result.value)
|
|
323
|
+
result.with_context(:user, user).continue(result.value)
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
class FetchUserStepTest < Minitest::Test
|
|
328
|
+
def test_fetches_user_and_adds_to_context
|
|
329
|
+
# Mock User.find
|
|
330
|
+
user = { id: 123, name: "John" }
|
|
331
|
+
User.stub :find, user do
|
|
332
|
+
step = FetchUserStep.new
|
|
333
|
+
result = step.call(SimpleFlow::Result.new(123))
|
|
334
|
+
|
|
335
|
+
assert_equal user, result.context[:user]
|
|
336
|
+
assert result.continue?
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def test_handles_user_not_found
|
|
341
|
+
User.stub :find, nil do
|
|
342
|
+
step = FetchUserStep.new
|
|
343
|
+
result = step.call(SimpleFlow::Result.new(999))
|
|
344
|
+
|
|
345
|
+
assert_nil result.context[:user]
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### Testing Error Handling
|
|
352
|
+
|
|
353
|
+
```ruby
|
|
354
|
+
def test_validation_errors
|
|
355
|
+
pipeline = SimpleFlow::Pipeline.new do
|
|
356
|
+
step ->(result) {
|
|
357
|
+
if result.value[:email].nil?
|
|
358
|
+
result.with_error(:validation, "Email required")
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
if result.value[:password].nil?
|
|
362
|
+
result.with_error(:validation, "Password required")
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
if result.errors.any?
|
|
366
|
+
result.halt(result.value)
|
|
367
|
+
else
|
|
368
|
+
result.continue(result.value)
|
|
369
|
+
end
|
|
370
|
+
}
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
result = pipeline.call(SimpleFlow::Result.new({ email: nil, password: nil }))
|
|
374
|
+
|
|
375
|
+
refute result.continue?
|
|
376
|
+
assert_equal 2, result.errors[:validation].size
|
|
377
|
+
assert_includes result.errors[:validation], "Email required"
|
|
378
|
+
assert_includes result.errors[:validation], "Password required"
|
|
379
|
+
end
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### Testing with Mocks and Stubs
|
|
383
|
+
|
|
384
|
+
```ruby
|
|
385
|
+
def test_external_api_call
|
|
386
|
+
# Stub HTTP client
|
|
387
|
+
mock_response = { status: "ok", data: [1, 2, 3] }
|
|
388
|
+
|
|
389
|
+
HTTP.stub :get, mock_response do
|
|
390
|
+
pipeline = SimpleFlow::Pipeline.new do
|
|
391
|
+
step ->(result) {
|
|
392
|
+
response = HTTP.get("https://api.example.com")
|
|
393
|
+
result.continue(response[:data])
|
|
394
|
+
}
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
result = pipeline.call(SimpleFlow::Result.new(nil))
|
|
398
|
+
|
|
399
|
+
assert_equal [1, 2, 3], result.value
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
## Best Practices
|
|
405
|
+
|
|
406
|
+
### 1. Test Public Interfaces
|
|
407
|
+
|
|
408
|
+
Focus on testing public methods and behaviors:
|
|
409
|
+
|
|
410
|
+
```ruby
|
|
411
|
+
# GOOD: Tests public interface
|
|
412
|
+
def test_pipeline_processes_data
|
|
413
|
+
result = pipeline.call(initial_data)
|
|
414
|
+
assert_equal expected_output, result.value
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# AVOID: Testing internal implementation
|
|
418
|
+
def test_internal_step_processing
|
|
419
|
+
# Don't test private methods directly
|
|
420
|
+
end
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
### 2. Use Descriptive Test Names
|
|
424
|
+
|
|
425
|
+
```ruby
|
|
426
|
+
# GOOD: Clear what is being tested
|
|
427
|
+
def test_pipeline_halts_when_validation_fails
|
|
428
|
+
def test_parallel_steps_merge_contexts
|
|
429
|
+
def test_middleware_wraps_steps_in_correct_order
|
|
430
|
+
|
|
431
|
+
# BAD: Vague test names
|
|
432
|
+
def test_pipeline
|
|
433
|
+
def test_it_works
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### 3. Test Edge Cases
|
|
437
|
+
|
|
438
|
+
```ruby
|
|
439
|
+
def test_handles_nil_value
|
|
440
|
+
def test_handles_empty_array
|
|
441
|
+
def test_handles_large_dataset
|
|
442
|
+
def test_handles_unicode_characters
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### 4. Keep Tests Focused
|
|
446
|
+
|
|
447
|
+
```ruby
|
|
448
|
+
# GOOD: Tests one thing
|
|
449
|
+
def test_with_context_adds_context
|
|
450
|
+
result = SimpleFlow::Result.new(42).with_context(:key, "value")
|
|
451
|
+
assert_equal "value", result.context[:key]
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
# BAD: Tests multiple things
|
|
455
|
+
def test_result_functionality
|
|
456
|
+
# Tests context, errors, halt, continue all in one test
|
|
457
|
+
end
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
### 5. Use Setup and Teardown
|
|
461
|
+
|
|
462
|
+
```ruby
|
|
463
|
+
class PipelineTest < Minitest::Test
|
|
464
|
+
def setup
|
|
465
|
+
@pipeline = create_test_pipeline
|
|
466
|
+
@initial_data = SimpleFlow::Result.new(test_data)
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def teardown
|
|
470
|
+
cleanup_test_data if needed
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def test_something
|
|
474
|
+
result = @pipeline.call(@initial_data)
|
|
475
|
+
# Test assertions
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
## Running Tests in CI
|
|
481
|
+
|
|
482
|
+
SimpleFlow uses GitHub Actions for continuous integration. Tests run automatically on:
|
|
483
|
+
|
|
484
|
+
- Every push to any branch
|
|
485
|
+
- Every pull request
|
|
486
|
+
- Multiple Ruby versions (2.7, 3.0, 3.1, 3.2, 3.3)
|
|
487
|
+
|
|
488
|
+
## Coverage Reports
|
|
489
|
+
|
|
490
|
+
To generate coverage reports locally:
|
|
491
|
+
|
|
492
|
+
```ruby
|
|
493
|
+
# Add to test_helper.rb
|
|
494
|
+
require 'simplecov'
|
|
495
|
+
SimpleCov.start do
|
|
496
|
+
add_filter '/test/'
|
|
497
|
+
end
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
Run tests:
|
|
501
|
+
```bash
|
|
502
|
+
bundle exec rake test
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
View coverage:
|
|
506
|
+
```bash
|
|
507
|
+
open coverage/index.html
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
## Related Documentation
|
|
511
|
+
|
|
512
|
+
- [Contributing Guide](contributing.md) - How to contribute
|
|
513
|
+
- [Benchmarking Guide](benchmarking.md) - Performance testing
|
|
514
|
+
- [Examples](/Users/dewayne/sandbox/git_repos/madbomber/simple_flow/examples/) - Working examples to test against
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# Examples
|
|
2
|
+
|
|
3
|
+
Explore real-world examples demonstrating SimpleFlow's capabilities.
|
|
4
|
+
|
|
5
|
+
## Running Examples
|
|
6
|
+
|
|
7
|
+
All examples are located in the `examples/` directory of the repository. Run them with:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
ruby examples/example_name.rb
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Available Examples
|
|
14
|
+
|
|
15
|
+
### Parallel Data Fetching
|
|
16
|
+
|
|
17
|
+
**File:** `examples/parallel_data_fetching.rb`
|
|
18
|
+
|
|
19
|
+
Demonstrates fetching data from multiple APIs concurrently for improved performance.
|
|
20
|
+
|
|
21
|
+
**Key Features:**
|
|
22
|
+
- Concurrent API calls
|
|
23
|
+
- 4x performance improvement (0.4s → 0.1s)
|
|
24
|
+
- Result merging and aggregation
|
|
25
|
+
|
|
26
|
+
**Run:**
|
|
27
|
+
```bash
|
|
28
|
+
ruby examples/parallel_data_fetching.rb
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Learn More:** [Data Fetching Guide](../guides/data-fetching.md)
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
### Parallel Validation
|
|
36
|
+
|
|
37
|
+
**File:** `examples/parallel_validation.rb`
|
|
38
|
+
|
|
39
|
+
Shows how to run multiple validation checks concurrently to quickly identify all errors.
|
|
40
|
+
|
|
41
|
+
**Key Features:**
|
|
42
|
+
- Concurrent validation checks
|
|
43
|
+
- Error accumulation
|
|
44
|
+
- Fast feedback on multiple validation failures
|
|
45
|
+
|
|
46
|
+
**Run:**
|
|
47
|
+
```bash
|
|
48
|
+
ruby examples/parallel_validation.rb
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Learn More:** [Validation Patterns Guide](../guides/validation-patterns.md)
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
### Error Handling
|
|
56
|
+
|
|
57
|
+
**File:** `examples/error_handling.rb`
|
|
58
|
+
|
|
59
|
+
Demonstrates various error handling patterns including validation, graceful degradation, and retry logic.
|
|
60
|
+
|
|
61
|
+
**Key Features:**
|
|
62
|
+
- Validation with error accumulation
|
|
63
|
+
- Graceful degradation with optional services
|
|
64
|
+
- Retry logic with exponential backoff
|
|
65
|
+
|
|
66
|
+
**Run:**
|
|
67
|
+
```bash
|
|
68
|
+
ruby examples/error_handling.rb
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Learn More:** [Error Handling Guide](../guides/error-handling.md)
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
### File Processing
|
|
76
|
+
|
|
77
|
+
**File:** `examples/file_processing.rb`
|
|
78
|
+
|
|
79
|
+
Shows how to process multiple files in parallel with validation, conversion, and summarization.
|
|
80
|
+
|
|
81
|
+
**Key Features:**
|
|
82
|
+
- Parallel file processing
|
|
83
|
+
- Format conversion (JSON to CSV)
|
|
84
|
+
- Data validation and summarization
|
|
85
|
+
|
|
86
|
+
**Run:**
|
|
87
|
+
```bash
|
|
88
|
+
ruby examples/file_processing.rb
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Learn More:** [File Processing Guide](../guides/file-processing.md)
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
### Complex Workflow
|
|
96
|
+
|
|
97
|
+
**File:** `examples/complex_workflow.rb`
|
|
98
|
+
|
|
99
|
+
A realistic e-commerce order processing pipeline with multiple stages and parallel execution blocks.
|
|
100
|
+
|
|
101
|
+
**Key Features:**
|
|
102
|
+
- 6-stage workflow
|
|
103
|
+
- 4 parallel execution blocks
|
|
104
|
+
- 15+ steps
|
|
105
|
+
- User validation, inventory checks, payment processing
|
|
106
|
+
- Context accumulation across stages
|
|
107
|
+
|
|
108
|
+
**Run:**
|
|
109
|
+
```bash
|
|
110
|
+
ruby examples/complex_workflow.rb
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Learn More:** [Complex Workflows Guide](../guides/complex-workflows.md)
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Code Snippets
|
|
118
|
+
|
|
119
|
+
### Basic Pipeline
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
require 'simple_flow'
|
|
123
|
+
|
|
124
|
+
pipeline = SimpleFlow::Pipeline.new do
|
|
125
|
+
step ->(result) { result.continue(result.value.strip) }
|
|
126
|
+
step ->(result) { result.continue(result.value.downcase) }
|
|
127
|
+
step ->(result) { result.continue("Hello, #{result.value}!") }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
result = pipeline.call(SimpleFlow::Result.new(" WORLD "))
|
|
131
|
+
puts result.value # => "Hello, world!"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### With Middleware
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
pipeline = SimpleFlow::Pipeline.new do
|
|
138
|
+
use_middleware SimpleFlow::MiddleWare::Logging
|
|
139
|
+
use_middleware SimpleFlow::MiddleWare::Instrumentation, api_key: 'abc123'
|
|
140
|
+
|
|
141
|
+
step ->(result) { result.continue(result.value + 10) }
|
|
142
|
+
step ->(result) { result.continue(result.value * 2) }
|
|
143
|
+
end
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Concurrent Execution
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
pipeline = SimpleFlow::Pipeline.new do
|
|
150
|
+
step ->(result) { fetch_user(result) }
|
|
151
|
+
|
|
152
|
+
parallel do
|
|
153
|
+
step ->(result) { fetch_orders(result) }
|
|
154
|
+
step ->(result) { fetch_preferences(result) }
|
|
155
|
+
step ->(result) { fetch_analytics(result) }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
step ->(result) { aggregate_data(result) }
|
|
159
|
+
end
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Error Handling
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
pipeline = SimpleFlow::Pipeline.new do
|
|
166
|
+
step ->(result) {
|
|
167
|
+
if result.value < 0
|
|
168
|
+
result.halt.with_error(:validation, "Value must be positive")
|
|
169
|
+
else
|
|
170
|
+
result.continue(result.value)
|
|
171
|
+
end
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
step ->(result) {
|
|
175
|
+
# Only runs if validation passed
|
|
176
|
+
result.continue(result.value * 2)
|
|
177
|
+
}
|
|
178
|
+
end
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Performance Benchmarks
|
|
182
|
+
|
|
183
|
+
Run benchmarks to see SimpleFlow's performance:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
# Compare parallel vs sequential execution
|
|
187
|
+
ruby benchmarks/parallel_vs_sequential.rb
|
|
188
|
+
|
|
189
|
+
# Measure pipeline overhead
|
|
190
|
+
ruby benchmarks/pipeline_overhead.rb
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Next Steps
|
|
194
|
+
|
|
195
|
+
- [Core Concepts](../core-concepts/overview.md) - Understand the fundamentals
|
|
196
|
+
- [Concurrent Execution](../concurrent/introduction.md) - Deep dive into parallelism
|
|
197
|
+
- [API Reference](../api/pipeline.md) - Complete API documentation
|