geneva_drive 0.6.0 → 0.8.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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +10 -1
  3. data/MANUAL.md +260 -1
  4. data/README.md +2 -0
  5. data/Rakefile +14 -1
  6. data/bin/matrix +76 -0
  7. data/bin/md2html +114 -0
  8. data/lib/generators/geneva_drive/install/install_generator.rb +5 -0
  9. data/lib/generators/geneva_drive/install/templates/add_resumable_step_support.rb +5 -9
  10. data/lib/generators/geneva_drive/install/templates/add_signals_support.rb +89 -0
  11. data/lib/geneva_drive/executor.rb +131 -10
  12. data/lib/geneva_drive/flow_control.rb +43 -3
  13. data/lib/geneva_drive/jobs/housekeeping_job.rb +83 -3
  14. data/lib/geneva_drive/json_column.rb +72 -0
  15. data/lib/geneva_drive/migration_helpers.rb +27 -0
  16. data/lib/geneva_drive/removed_step_definition.rb +34 -0
  17. data/lib/geneva_drive/signal.rb +245 -0
  18. data/lib/geneva_drive/signal_matcher.rb +60 -0
  19. data/lib/geneva_drive/step_collection.rb +31 -7
  20. data/lib/geneva_drive/step_definition.rb +91 -1
  21. data/lib/geneva_drive/step_execution.rb +39 -2
  22. data/lib/geneva_drive/test_helpers.rb +89 -2
  23. data/lib/geneva_drive/version.rb +1 -1
  24. data/lib/geneva_drive/workflow.rb +497 -24
  25. data/lib/geneva_drive.rb +24 -0
  26. data/test/dsl/signal_step_definition_test.rb +132 -0
  27. data/test/dummy/config/database.yml +40 -5
  28. data/test/dummy/config/initializers/geneva_drive.rb +46 -0
  29. data/test/dummy_install/db/migrate/20260217112949_create_geneva_drive_workflows.rb +76 -0
  30. data/test/dummy_install/db/migrate/20260217112950_create_geneva_drive_step_executions.rb +99 -0
  31. data/test/dummy_install/db/migrate/20260217112951_add_finished_at_to_geneva_drive_step_executions.rb +27 -0
  32. data/test/dummy_install/db/migrate/20260217112952_add_error_class_name_to_geneva_drive_step_executions.rb +9 -0
  33. data/test/dummy_install/db/migrate/20260217112953_add_resumable_step_support_to_geneva_drive_step_executions.rb +128 -0
  34. data/test/dummy_install/db/migrate/20260217112954_add_chained_step_executions_to_geneva_drive_step_executions.rb +136 -0
  35. data/test/dummy_install/db/schema.rb +75 -0
  36. data/test/dummy_install/log/test.log +1852 -0
  37. data/test/jobs/housekeeping_signals_test.rb +198 -0
  38. data/test/migration_helpers_test.rb +1 -1
  39. data/test/test_helper.rb +53 -13
  40. data/test/workflow/removed_step_test.rb +294 -0
  41. data/test/workflow/resume_at_test.rb +170 -0
  42. data/test/workflow/signal_consumption_test.rb +397 -0
  43. data/test/workflow/signal_delivery_test.rb +255 -0
  44. data/test/workflow/signal_durability_test.rb +164 -0
  45. data/test/workflow/signal_rendezvous_test.rb +581 -0
  46. data/test/workflow/signals_without_migration_test.rb +97 -0
  47. metadata +26 -2
@@ -46,6 +46,8 @@ module GenevaDrive::TestHelpers
46
46
  step_execution = workflow.current_execution
47
47
  break unless step_execution
48
48
 
49
+ raise_if_parked!(step_execution, "speedrun_workflow")
50
+
49
51
  # interruptible: false so resumable steps run to completion
50
52
  step_execution.execute!(interruptible: false)
51
53
  iterations += 1
@@ -79,6 +81,8 @@ module GenevaDrive::TestHelpers
79
81
  step_execution = workflow.current_execution
80
82
  return nil unless step_execution
81
83
 
84
+ raise_if_parked!(step_execution, "perform_next_step")
85
+
82
86
  step_execution.execute!
83
87
  workflow.reload
84
88
  step_execution
@@ -117,8 +121,9 @@ module GenevaDrive::TestHelpers
117
121
  "Available steps: #{available}"
118
122
  end
119
123
 
120
- # Cancel any existing scheduled step executions to satisfy uniqueness constraint
121
- workflow.step_executions.where(state: "scheduled").update_all(
124
+ # Cancel any existing scheduled or parked step executions to satisfy the
125
+ # uniqueness constraint
126
+ workflow.step_executions.where(state: %w[scheduled waiting]).update_all(
122
127
  state: "canceled",
123
128
  outcome: "canceled",
124
129
  canceled_at: Time.current
@@ -194,11 +199,14 @@ module GenevaDrive::TestHelpers
194
199
  step_execution = workflow.current_execution
195
200
  return nil unless step_execution
196
201
 
202
+ raise_if_parked!(step_execution, "speedrun_current_step")
203
+
197
204
  executions = 0
198
205
  original_step_name = step_execution.step_name
199
206
 
200
207
  loop do
201
208
  step_execution.reload
209
+ raise_if_parked!(step_execution, "speedrun_current_step")
202
210
 
203
211
  # Follow the execution chain: a completed execution that spawned a
204
212
  # successor means the step is still going (suspend! and skip_to! create
@@ -289,4 +297,83 @@ module GenevaDrive::TestHelpers
289
297
  assert successor, "Expected successor execution for step #{step_name}, but none found"
290
298
  assert successor.scheduled?, "Expected successor to be scheduled, but was #{successor.state}"
291
299
  end
300
+
301
+ # === Signal Test Helpers ===
302
+
303
+ # Asserts that the workflow is parked waiting for a signal.
304
+ #
305
+ # @param workflow [GenevaDrive::Workflow] the workflow to check
306
+ # @param signal_name [String, Symbol, nil] the signal name the step waits for
307
+ # @return [void]
308
+ #
309
+ # @example Check that the workflow is waiting
310
+ # assert_waiting_for_signal(workflow, :payment_confirmed)
311
+ #
312
+ def assert_waiting_for_signal(workflow, signal_name = nil)
313
+ workflow.reload
314
+ execution = workflow.step_executions.where(state: "waiting").first
315
+
316
+ assert execution,
317
+ "Expected #{workflow.class.name} to be waiting for a signal, but no parked step execution was found " \
318
+ "(current execution: #{workflow.current_execution&.state.inspect})"
319
+
320
+ return unless signal_name
321
+
322
+ matcher = execution.step_definition&.signal_matcher
323
+ assert_equal signal_name.to_s, matcher_name(matcher),
324
+ "Expected step '#{execution.step_name}' to be waiting for #{signal_name.inspect}, " \
325
+ "but it waits for #{matcher}"
326
+ end
327
+
328
+ # Asserts that a signal with the given name exists in the given state.
329
+ #
330
+ # @param workflow [GenevaDrive::Workflow] the workflow to check
331
+ # @param signal_name [String, Symbol] the signal name
332
+ # @param state [String, Symbol] the expected signal state
333
+ # @return [void]
334
+ #
335
+ # @example Check that the signal was consumed
336
+ # assert_signal_state(workflow, :payment_confirmed, :consumed)
337
+ #
338
+ def assert_signal_state(workflow, signal_name, state)
339
+ signal = workflow.signals.where(name: signal_name.to_s).order(:created_at, :id).last
340
+
341
+ assert signal, "Expected a #{signal_name.inspect} signal on #{workflow.class.name}, but none was found"
342
+ assert_equal state.to_s, signal.state,
343
+ "Expected signal #{signal_name.inspect} to be #{state}, but was #{signal.state}"
344
+ end
345
+
346
+ private
347
+
348
+ # The signal name a matcher accepts, when it has one to give. Custom
349
+ # matchers own their whole predicate and need not expose a name.
350
+ #
351
+ # @param matcher [Object, nil] anything responding to #matches?(signal)
352
+ # @return [String, nil]
353
+ def matcher_name(matcher)
354
+ matcher.name if matcher.respond_to?(:name)
355
+ end
356
+
357
+ # Raises a descriptive error when a step-driving helper runs into a parked
358
+ # execution. Without this a parked workflow would look like an infinite
359
+ # loop (or a silently passing assertion) instead of "you forgot to signal".
360
+ #
361
+ # @param step_execution [GenevaDrive::StepExecution] the execution about to be driven
362
+ # @param helper_name [String] the helper that was called
363
+ # @return [void]
364
+ def raise_if_parked!(step_execution, helper_name)
365
+ return unless step_execution.waiting?
366
+
367
+ matcher = begin
368
+ step_execution.step_definition&.signal_matcher
369
+ rescue
370
+ nil
371
+ end
372
+ waiting_for = matcher ? " waiting for #{matcher}" : ""
373
+ example_name = matcher_name(matcher)&.to_sym
374
+
375
+ raise "#{helper_name} cannot drive step '#{step_execution.step_name}': its execution is parked" \
376
+ "#{waiting_for}. Deliver the signal first, e.g. " \
377
+ "workflow.signal!(#{example_name.inspect}), then call #{helper_name} again."
378
+ end
292
379
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GenevaDrive
4
- VERSION = "0.6.0"
4
+ VERSION = "0.8.0"
5
5
  end