ultra-smart-sys 0.0.1

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 (57) hide show
  1. checksums.yaml +7 -0
  2. data/state_machines-0.201.0/LICENSE.txt +23 -0
  3. data/state_machines-0.201.0/README.md +1024 -0
  4. data/state_machines-0.201.0/lib/state_machines/async_mode/async_event_extensions.rb +49 -0
  5. data/state_machines-0.201.0/lib/state_machines/async_mode/async_events.rb +282 -0
  6. data/state_machines-0.201.0/lib/state_machines/async_mode/async_machine.rb +60 -0
  7. data/state_machines-0.201.0/lib/state_machines/async_mode/async_transition_collection.rb +136 -0
  8. data/state_machines-0.201.0/lib/state_machines/async_mode/thread_safe_state.rb +47 -0
  9. data/state_machines-0.201.0/lib/state_machines/async_mode.rb +64 -0
  10. data/state_machines-0.201.0/lib/state_machines/branch.rb +246 -0
  11. data/state_machines-0.201.0/lib/state_machines/callback.rb +223 -0
  12. data/state_machines-0.201.0/lib/state_machines/core.rb +43 -0
  13. data/state_machines-0.201.0/lib/state_machines/core_ext/class/state_machine.rb +5 -0
  14. data/state_machines-0.201.0/lib/state_machines/core_ext.rb +4 -0
  15. data/state_machines-0.201.0/lib/state_machines/error.rb +115 -0
  16. data/state_machines-0.201.0/lib/state_machines/eval_helpers.rb +227 -0
  17. data/state_machines-0.201.0/lib/state_machines/event.rb +247 -0
  18. data/state_machines-0.201.0/lib/state_machines/event_collection.rb +149 -0
  19. data/state_machines-0.201.0/lib/state_machines/extensions.rb +150 -0
  20. data/state_machines-0.201.0/lib/state_machines/helper_module.rb +19 -0
  21. data/state_machines-0.201.0/lib/state_machines/integrations/base.rb +49 -0
  22. data/state_machines-0.201.0/lib/state_machines/integrations.rb +111 -0
  23. data/state_machines-0.201.0/lib/state_machines/machine/action_hooks.rb +53 -0
  24. data/state_machines-0.201.0/lib/state_machines/machine/async_extensions.rb +88 -0
  25. data/state_machines-0.201.0/lib/state_machines/machine/callbacks.rb +333 -0
  26. data/state_machines-0.201.0/lib/state_machines/machine/class_methods.rb +95 -0
  27. data/state_machines-0.201.0/lib/state_machines/machine/configuration.rb +128 -0
  28. data/state_machines-0.201.0/lib/state_machines/machine/event_methods.rb +436 -0
  29. data/state_machines-0.201.0/lib/state_machines/machine/helper_generators.rb +125 -0
  30. data/state_machines-0.201.0/lib/state_machines/machine/integration.rb +92 -0
  31. data/state_machines-0.201.0/lib/state_machines/machine/parsing.rb +77 -0
  32. data/state_machines-0.201.0/lib/state_machines/machine/rendering.rb +17 -0
  33. data/state_machines-0.201.0/lib/state_machines/machine/scoping.rb +44 -0
  34. data/state_machines-0.201.0/lib/state_machines/machine/state_methods.rb +398 -0
  35. data/state_machines-0.201.0/lib/state_machines/machine/utilities.rb +86 -0
  36. data/state_machines-0.201.0/lib/state_machines/machine/validation.rb +39 -0
  37. data/state_machines-0.201.0/lib/state_machines/machine.rb +615 -0
  38. data/state_machines-0.201.0/lib/state_machines/machine_collection.rb +105 -0
  39. data/state_machines-0.201.0/lib/state_machines/macro_methods.rb +522 -0
  40. data/state_machines-0.201.0/lib/state_machines/matcher.rb +124 -0
  41. data/state_machines-0.201.0/lib/state_machines/matcher_helpers.rb +56 -0
  42. data/state_machines-0.201.0/lib/state_machines/node_collection.rb +226 -0
  43. data/state_machines-0.201.0/lib/state_machines/options_validator.rb +72 -0
  44. data/state_machines-0.201.0/lib/state_machines/path.rb +123 -0
  45. data/state_machines-0.201.0/lib/state_machines/path_collection.rb +91 -0
  46. data/state_machines-0.201.0/lib/state_machines/state.rb +314 -0
  47. data/state_machines-0.201.0/lib/state_machines/state_collection.rb +113 -0
  48. data/state_machines-0.201.0/lib/state_machines/state_context.rb +134 -0
  49. data/state_machines-0.201.0/lib/state_machines/stdio_renderer.rb +74 -0
  50. data/state_machines-0.201.0/lib/state_machines/syntax_validator.rb +54 -0
  51. data/state_machines-0.201.0/lib/state_machines/test_helper.rb +775 -0
  52. data/state_machines-0.201.0/lib/state_machines/transition.rb +593 -0
  53. data/state_machines-0.201.0/lib/state_machines/transition_collection.rb +310 -0
  54. data/state_machines-0.201.0/lib/state_machines/version.rb +5 -0
  55. data/state_machines-0.201.0/lib/state_machines.rb +6 -0
  56. data/ultra-smart-sys.gemspec +12 -0
  57. metadata +96 -0
@@ -0,0 +1,775 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateMachines
4
+ # Test helper module providing assertion methods for state machine testing
5
+ # Designed to work with Minitest, RSpec, and future testing frameworks
6
+ #
7
+ # @example Basic usage with Minitest
8
+ # class MyModelTest < Minitest::Test
9
+ # include StateMachines::TestHelper
10
+ #
11
+ # def test_initial_state
12
+ # model = MyModel.new
13
+ # assert_state(model, :state_machine_name, :initial_state)
14
+ # end
15
+ # end
16
+ #
17
+ # @example Usage with RSpec
18
+ # RSpec.describe MyModel do
19
+ # include StateMachines::TestHelper
20
+ #
21
+ # it "starts in initial state" do
22
+ # model = MyModel.new
23
+ # assert_state(model, :state_machine_name, :initial_state)
24
+ # end
25
+ # end
26
+ #
27
+ # @since 0.10.0
28
+ module TestHelper
29
+ # Assert that an object is in a specific state for a given state machine
30
+ #
31
+ # @param object [Object] The object with state machines
32
+ # @param expected_state [Symbol] The expected state
33
+ # @param machine_name [Symbol] The name of the state machine (defaults to :state)
34
+ # @param message [String, nil] Custom failure message
35
+ # @return [void]
36
+ # @raise [AssertionError] If the state doesn't match
37
+ #
38
+ # @example
39
+ # user = User.new
40
+ # assert_sm_state(user, :active) # Uses default :state machine
41
+ # assert_sm_state(user, :active, machine_name: :status) # Uses :status machine
42
+ def assert_sm_state(object, expected_state, machine_name: :state, message: nil)
43
+ name_method = "#{machine_name}_name"
44
+
45
+ # Handle the case where machine_name doesn't have a corresponding _name method
46
+ unless object.respond_to?(name_method)
47
+ available_machines = begin
48
+ object.class.state_machines.keys
49
+ rescue StandardError
50
+ []
51
+ end
52
+ raise ArgumentError, "No state machine '#{machine_name}' found. Available machines: #{available_machines.inspect}"
53
+ end
54
+
55
+ actual = object.send(name_method)
56
+ default_message = "Expected #{object.class}##{machine_name} to be #{expected_state}, but was #{actual}"
57
+
58
+ _sm_assert_equal(expected_state.to_s, actual.to_s, message || default_message)
59
+ end
60
+
61
+ # Assert that an object can transition via a specific event
62
+ #
63
+ # @param object [Object] The object with state machines
64
+ # @param event [Symbol] The event name
65
+ # @param machine_name [Symbol] The name of the state machine (defaults to :state)
66
+ # @param message [String, nil] Custom failure message
67
+ # @return [void]
68
+ # @raise [AssertionError] If the transition is not available
69
+ #
70
+ # @example
71
+ # user = User.new
72
+ # assert_sm_can_transition(user, :activate) # Uses default :state machine
73
+ # assert_sm_can_transition(user, :activate, machine_name: :status) # Uses :status machine
74
+ def assert_sm_can_transition(object, event, machine_name: :state, message: nil)
75
+ can_method = _sm_find_can_method(object, event, machine_name)
76
+ default_message = "Expected to be able to trigger event :#{event} on #{machine_name}, but #{can_method} returned false"
77
+
78
+ _sm_assert(object.send(can_method), message || default_message)
79
+ end
80
+
81
+ # Assert that an object cannot transition via a specific event
82
+ #
83
+ # @param object [Object] The object with state machines
84
+ # @param event [Symbol] The event name
85
+ # @param machine_name [Symbol] The name of the state machine (defaults to :state)
86
+ # @param message [String, nil] Custom failure message
87
+ # @return [void]
88
+ # @raise [AssertionError] If the transition is available
89
+ #
90
+ # @example
91
+ # user = User.new
92
+ # assert_sm_cannot_transition(user, :delete) # Uses default :state machine
93
+ # assert_sm_cannot_transition(user, :delete, machine_name: :status) # Uses :status machine
94
+ def assert_sm_cannot_transition(object, event, machine_name: :state, message: nil)
95
+ can_method = _sm_find_can_method(object, event, machine_name)
96
+ default_message = "Expected not to be able to trigger event :#{event} on #{machine_name}, but #{can_method} returned true"
97
+
98
+ _sm_refute(object.send(can_method), message || default_message)
99
+ end
100
+
101
+ # Assert that triggering an event changes the object to the expected state
102
+ #
103
+ # @param object [Object] The object with state machines
104
+ # @param event [Symbol] The event to trigger
105
+ # @param expected_state [Symbol] The expected state after transition
106
+ # @param machine_name [Symbol] The name of the state machine (defaults to :state)
107
+ # @param message [String, nil] Custom failure message
108
+ # @return [void]
109
+ # @raise [AssertionError] If the transition fails or results in wrong state
110
+ #
111
+ # @example
112
+ # user = User.new
113
+ # assert_sm_transition(user, :activate, :active) # Uses default :state machine
114
+ # assert_sm_transition(user, :activate, :active, machine_name: :status) # Uses :status machine
115
+ def assert_sm_transition(object, event, expected_state, machine_name: :state, message: nil)
116
+ object.send("#{event}!")
117
+ assert_sm_state(object, expected_state, machine_name: machine_name, message: message)
118
+ end
119
+
120
+ # === Extended State Machine Assertions ===
121
+
122
+ def assert_sm_states_list(machine, expected_states, message = nil)
123
+ actual_states = machine.states.map(&:name).compact
124
+ default_message = "Expected states #{expected_states} but got #{actual_states}"
125
+
126
+ _sm_assert_equal(expected_states.sort, actual_states.sort, message || default_message)
127
+ end
128
+
129
+ def refute_sm_state_defined(machine, state, message = nil)
130
+ state_exists = machine.states.any? { |s| s.name == state }
131
+ default_message = "Expected state #{state} to not be defined in machine"
132
+
133
+ _sm_refute(state_exists, message || default_message)
134
+ end
135
+ alias assert_sm_state_not_defined refute_sm_state_defined
136
+
137
+ def assert_sm_initial_state(machine, expected_state, message = nil)
138
+ state_obj = machine.state(expected_state)
139
+ is_initial = state_obj&.initial?
140
+ default_message = "Expected state #{expected_state} to be the initial state"
141
+
142
+ _sm_assert(is_initial, message || default_message)
143
+ end
144
+
145
+ def assert_sm_final_state(machine, state, message = nil)
146
+ state_obj = machine.states[state]
147
+ is_final = state_obj&.final?
148
+ default_message = "Expected state #{state} to be final"
149
+
150
+ _sm_assert(is_final, message || default_message)
151
+ end
152
+
153
+ def assert_sm_possible_transitions(machine, from:, expected_to_states:, message: nil)
154
+ actual_transitions = machine.events.flat_map do |event|
155
+ event.branches.select { |branch| branch.known_states.include?(from) }
156
+ .map(&:to)
157
+ end.uniq
158
+ default_message = "Expected transitions from #{from} to #{expected_to_states} but got #{actual_transitions}"
159
+
160
+ _sm_assert_equal(expected_to_states.sort, actual_transitions.sort, message || default_message)
161
+ end
162
+
163
+ def refute_sm_transition_allowed(machine, from:, to:, on:, message: nil)
164
+ event = machine.events[on]
165
+ is_allowed = event&.branches&.any? { |branch| branch.known_states.include?(from) && branch.to == to }
166
+ default_message = "Expected transition from #{from} to #{to} on #{on} to not be allowed"
167
+
168
+ _sm_refute(is_allowed, message || default_message)
169
+ end
170
+ alias assert_sm_transition_not_allowed refute_sm_transition_allowed
171
+
172
+ def assert_sm_event_triggers(object, event, machine_name = :state, message = nil)
173
+ initial_state = object.send(machine_name)
174
+ object.send("#{event}!")
175
+ state_changed = initial_state != object.send(machine_name)
176
+ default_message = "Expected event #{event} to trigger state change on #{machine_name}"
177
+
178
+ _sm_assert(state_changed, message || default_message)
179
+ end
180
+
181
+ def refute_sm_event_triggers(object, event, machine_name = :state, message = nil)
182
+ initial_state = object.send(machine_name)
183
+ begin
184
+ object.send("#{event}!")
185
+ state_unchanged = initial_state == object.send(machine_name)
186
+ default_message = "Expected event #{event} to not trigger state change on #{machine_name}"
187
+
188
+ _sm_assert(state_unchanged, message || default_message)
189
+ rescue StateMachines::InvalidTransition
190
+ # Expected behavior - transition was blocked
191
+ end
192
+ end
193
+ alias assert_sm_event_not_triggers refute_sm_event_triggers
194
+
195
+ def assert_sm_event_raises_error(object, event, error_class, message = nil)
196
+ default_message = "Expected event #{event} to raise #{error_class}"
197
+
198
+ if defined?(::Minitest)
199
+ assert_raises(error_class, message || default_message) do
200
+ object.send("#{event}!")
201
+ end
202
+ elsif defined?(::RSpec)
203
+ expect { object.send("#{event}!") }.to raise_error(error_class), message || default_message
204
+ else
205
+ begin
206
+ object.send("#{event}!")
207
+ raise default_message
208
+ rescue error_class
209
+ # Expected behavior
210
+ end
211
+ end
212
+ end
213
+
214
+ def assert_sm_callback_executed(object, callback_name, message = nil)
215
+ callbacks_executed = object.instance_variable_get(:@_sm_callbacks_executed) || []
216
+ callback_was_executed = callbacks_executed.include?(callback_name)
217
+ default_message = "Expected callback #{callback_name} to be executed"
218
+
219
+ _sm_assert(callback_was_executed, message || default_message)
220
+ end
221
+
222
+ def refute_sm_callback_executed(object, callback_name, message = nil)
223
+ callbacks_executed = object.instance_variable_get(:@_sm_callbacks_executed) || []
224
+ callback_was_executed = callbacks_executed.include?(callback_name)
225
+ default_message = "Expected callback #{callback_name} to not be executed"
226
+
227
+ _sm_refute(callback_was_executed, message || default_message)
228
+ end
229
+ alias assert_sm_callback_not_executed refute_sm_callback_executed
230
+
231
+ # Assert that a record's state is persisted correctly for a specific state machine
232
+ #
233
+ # @param record [Object] The record to check (should respond to reload)
234
+ # @param expected [String, Symbol] The expected persisted state
235
+ # @param machine_name [Symbol] The name of the state machine (defaults to :state)
236
+ # @param message [String, nil] Custom failure message
237
+ # @return [void]
238
+ # @raise [AssertionError] If the persisted state doesn't match
239
+ #
240
+ # @example
241
+ # # Default state machine
242
+ # assert_sm_state_persisted(user, "active")
243
+ #
244
+ # # Specific state machine
245
+ # assert_sm_state_persisted(ship, "up", :shields)
246
+ # assert_sm_state_persisted(ship, "armed", :weapons)
247
+ def assert_sm_state_persisted(record, expected, machine_name = :state, message = nil)
248
+ record.reload if record.respond_to?(:reload)
249
+ actual_state = record.send(machine_name)
250
+ default_message = "Expected persisted state #{expected} for #{machine_name} but got #{actual_state}"
251
+
252
+ _sm_assert_equal(expected, actual_state, message || default_message)
253
+ end
254
+
255
+ # Assert that executing a block triggers one or more expected events
256
+ #
257
+ # @param object [Object] The object with state machines
258
+ # @param expected_events [Symbol, Array<Symbol>] The event(s) expected to be triggered
259
+ # @param machine_name [Symbol] The name of the state machine (defaults to :state)
260
+ # @param message [String, nil] Custom failure message
261
+ # @return [void]
262
+ # @raise [AssertionError] If the expected events were not triggered
263
+ #
264
+ # @example
265
+ # # Single event
266
+ # assert_sm_triggers_event(vehicle, :crash) { vehicle.redline }
267
+ #
268
+ # # Multiple events
269
+ # assert_sm_triggers_event(vehicle, [:crash, :emergency]) { vehicle.emergency_stop }
270
+ #
271
+ # # Specific machine
272
+ # assert_sm_triggers_event(vehicle, :disable, machine_name: :alarm) { vehicle.turn_off_alarm }
273
+ def assert_sm_triggers_event(object, expected_events, machine_name: :state, message: nil)
274
+ expected_events = Array(expected_events)
275
+ triggered_events = []
276
+
277
+ # Get the state machine
278
+ machine = object.class.state_machines[machine_name]
279
+ raise ArgumentError, "No state machine found for #{machine_name}" unless machine
280
+
281
+ # Save original callbacks to restore later
282
+ machine.callbacks[:before].dup
283
+
284
+ # Add a temporary callback to track triggered events
285
+ temp_callback = machine.before_transition do |_obj, transition|
286
+ triggered_events << transition.event if transition.event
287
+ end
288
+
289
+ begin
290
+ # Execute the block
291
+ yield
292
+
293
+ # Check if expected events were triggered
294
+ missing_events = expected_events - triggered_events
295
+ extra_events = triggered_events - expected_events
296
+
297
+ unless missing_events.empty? && extra_events.empty?
298
+ default_message = "Expected events #{expected_events.inspect} to be triggered, but got #{triggered_events.inspect}"
299
+ default_message += ". Missing: #{missing_events.inspect}" if missing_events.any?
300
+ default_message += ". Extra: #{extra_events.inspect}" if extra_events.any?
301
+
302
+ _sm_flunk(message || default_message)
303
+ end
304
+ ensure
305
+ # Restore original callbacks by removing the temporary one
306
+ machine.callbacks[:before].delete(temp_callback)
307
+ end
308
+ end
309
+
310
+ # Assert that a before_transition callback is defined with expected arguments
311
+ #
312
+ # @param machine_or_class [StateMachines::Machine, Class] The machine or class to check
313
+ # @param options [Hash] Expected callback options (on:, from:, to:, do:, if:, unless:)
314
+ # @param message [String, nil] Custom failure message
315
+ # @return [void]
316
+ # @raise [AssertionError] If the callback is not defined
317
+ #
318
+ # @example
319
+ # # Check for specific transition callback
320
+ # assert_before_transition(Vehicle, on: :crash, do: :emergency_stop)
321
+ #
322
+ # # Check with from/to states
323
+ # assert_before_transition(Vehicle.state_machine, from: :parked, to: :idling, do: :start_engine)
324
+ #
325
+ # # Check with conditions
326
+ # assert_before_transition(Vehicle, on: :ignite, if: :seatbelt_on?)
327
+ def assert_before_transition(machine_or_class, options = {}, message = nil)
328
+ _assert_transition_callback(:before, machine_or_class, options, message)
329
+ end
330
+
331
+ # Assert that an after_transition callback is defined with expected arguments
332
+ #
333
+ # @param machine_or_class [StateMachines::Machine, Class] The machine or class to check
334
+ # @param options [Hash] Expected callback options (on:, from:, to:, do:, if:, unless:)
335
+ # @param message [String, nil] Custom failure message
336
+ # @return [void]
337
+ # @raise [AssertionError] If the callback is not defined
338
+ #
339
+ # @example
340
+ # # Check for specific transition callback
341
+ # assert_after_transition(Vehicle, on: :crash, do: :tow)
342
+ #
343
+ # # Check with from/to states
344
+ # assert_after_transition(Vehicle.state_machine, from: :stalled, to: :parked, do: :log_repair)
345
+ def assert_after_transition(machine_or_class, options = {}, message = nil)
346
+ _assert_transition_callback(:after, machine_or_class, options, message)
347
+ end
348
+
349
+ # === Sync Mode Assertions ===
350
+
351
+ # Assert that a state machine is operating in synchronous mode
352
+ #
353
+ # @param object [Object] The object with state machines
354
+ # @param machine_name [Symbol] The name of the state machine (defaults to :state)
355
+ # @param message [String, nil] Custom failure message
356
+ # @return [void]
357
+ # @raise [AssertionError] If the machine has async mode enabled
358
+ #
359
+ # @example
360
+ # user = User.new
361
+ # assert_sm_sync_mode(user) # Uses default :state machine
362
+ # assert_sm_sync_mode(user, :status) # Uses :status machine
363
+ def assert_sm_sync_mode(object, machine_name = :state, message = nil)
364
+ machine = object.class.state_machines[machine_name]
365
+ raise ArgumentError, "No state machine '#{machine_name}' found" unless machine
366
+
367
+ async_enabled = machine.respond_to?(:async_mode_enabled?) && machine.async_mode_enabled?
368
+ default_message = "Expected state machine '#{machine_name}' to be in sync mode, but async mode is enabled"
369
+
370
+ _sm_refute(async_enabled, message || default_message)
371
+ end
372
+
373
+ # Assert that async methods are not available on a sync-only object
374
+ #
375
+ # @param object [Object] The object with state machines
376
+ # @param message [String, nil] Custom failure message
377
+ # @return [void]
378
+ # @raise [AssertionError] If async methods are available
379
+ #
380
+ # @example
381
+ # sync_only_car = Car.new # Car has no async: true machines
382
+ # assert_sm_no_async_methods(sync_only_car)
383
+ def assert_sm_no_async_methods(object, message = nil)
384
+ async_methods = %i[fire_event_async fire_events_async fire_event_async! async_fire_event]
385
+ available_async_methods = async_methods.select { |method| object.respond_to?(method) }
386
+
387
+ default_message = "Expected no async methods to be available, but found: #{available_async_methods.inspect}"
388
+
389
+ _sm_assert_empty(available_async_methods, message || default_message)
390
+ end
391
+
392
+ # Assert that an object has no async-enabled state machines
393
+ #
394
+ # @param object [Object] The object with state machines
395
+ # @param message [String, nil] Custom failure message
396
+ # @return [void]
397
+ # @raise [AssertionError] If any machine has async mode enabled
398
+ #
399
+ # @example
400
+ # sync_only_vehicle = Vehicle.new # All machines are sync-only
401
+ # assert_sm_all_sync(sync_only_vehicle)
402
+ def assert_sm_all_sync(object, message = nil)
403
+ async_machines = []
404
+
405
+ object.class.state_machines.each do |name, machine|
406
+ if machine.respond_to?(:async_mode_enabled?) && machine.async_mode_enabled?
407
+ async_machines << name
408
+ end
409
+ end
410
+
411
+ default_message = "Expected all state machines to be sync-only, but these have async enabled: #{async_machines.inspect}"
412
+
413
+ _sm_assert_empty(async_machines, message || default_message)
414
+ end
415
+
416
+ # Assert that synchronous event execution works correctly
417
+ #
418
+ # @param object [Object] The object with state machines
419
+ # @param event [Symbol] The event to trigger
420
+ # @param expected_state [Symbol] The expected state after transition
421
+ # @param machine_name [Symbol] The name of the state machine (defaults to :state)
422
+ # @param message [String, nil] Custom failure message
423
+ # @return [void]
424
+ # @raise [AssertionError] If sync execution fails
425
+ #
426
+ # @example
427
+ # car = Car.new
428
+ # assert_sm_sync_execution(car, :start, :running)
429
+ # assert_sm_sync_execution(car, :turn_on, :active, :alarm)
430
+ def assert_sm_sync_execution(object, event, expected_state, machine_name = :state, message = nil)
431
+ # Store initial state
432
+ initial_state = object.send(machine_name)
433
+
434
+ # Execute event synchronously
435
+ result = object.send("#{event}!")
436
+
437
+ # Verify immediate state change (no async delay)
438
+ final_state = object.send(machine_name)
439
+
440
+ # Check that transition succeeded
441
+ state_changed = initial_state != final_state
442
+ correct_final_state = final_state.to_s == expected_state.to_s
443
+
444
+ default_message = "Expected sync execution of '#{event}' to change #{machine_name} from '#{initial_state}' to '#{expected_state}', but got '#{final_state}'"
445
+
446
+ _sm_assert(result, "Event #{event} should return true on success")
447
+ _sm_assert(state_changed, "State should change from #{initial_state}")
448
+ _sm_assert(correct_final_state, message || default_message)
449
+ end
450
+
451
+ # Assert that event execution is immediate (no async delay)
452
+ #
453
+ # @param object [Object] The object with state machines
454
+ # @param event [Symbol] The event to trigger
455
+ # @param machine_name [Symbol] The name of the state machine (defaults to :state)
456
+ # @param message [String, nil] Custom failure message
457
+ # @return [void]
458
+ # @raise [AssertionError] If execution appears to be async
459
+ #
460
+ # @example
461
+ # car = Car.new
462
+ # assert_sm_immediate_execution(car, :start)
463
+ def assert_sm_immediate_execution(object, event, machine_name = :state, message = nil)
464
+ initial_state = object.send(machine_name)
465
+
466
+ # Record start time and execute
467
+ start_time = Time.now
468
+ object.send("#{event}!")
469
+ execution_time = Time.now - start_time
470
+
471
+ final_state = object.send(machine_name)
472
+ state_changed = initial_state != final_state
473
+
474
+ # Should complete very quickly (under 10ms for sync operations)
475
+ is_immediate = execution_time < 0.01
476
+
477
+ default_message = "Expected immediate sync execution of '#{event}', but took #{execution_time}s (likely async)"
478
+
479
+ _sm_assert(state_changed, 'Event should trigger state change')
480
+ _sm_assert(is_immediate, message || default_message)
481
+ end
482
+
483
+ # === Async Mode Assertions ===
484
+
485
+ # Assert that a state machine is operating in asynchronous mode
486
+ #
487
+ # @param object [Object] The object with state machines
488
+ # @param machine_name [Symbol] The name of the state machine (defaults to :state)
489
+ # @param message [String, nil] Custom failure message
490
+ # @return [void]
491
+ # @raise [AssertionError] If the machine doesn't have async mode enabled
492
+ #
493
+ # @example
494
+ # drone = AutonomousDrone.new
495
+ # assert_sm_async_mode(drone) # Uses default :state machine
496
+ # assert_sm_async_mode(drone, :teleporter_status) # Uses :teleporter_status machine
497
+ def assert_sm_async_mode(object, machine_name = :state, message = nil)
498
+ machine = object.class.state_machines[machine_name]
499
+ raise ArgumentError, "No state machine '#{machine_name}' found" unless machine
500
+
501
+ async_enabled = machine.respond_to?(:async_mode_enabled?) && machine.async_mode_enabled?
502
+ default_message = "Expected state machine '#{machine_name}' to have async mode enabled, but it's in sync mode"
503
+
504
+ _sm_assert(async_enabled, message || default_message)
505
+ end
506
+
507
+ # Assert that async methods are available on an async-enabled object
508
+ #
509
+ # @param object [Object] The object with state machines
510
+ # @param message [String, nil] Custom failure message
511
+ # @return [void]
512
+ # @raise [AssertionError] If async methods are not available
513
+ #
514
+ # @example
515
+ # drone = AutonomousDrone.new # Has async: true machines
516
+ # assert_sm_async_methods(drone)
517
+ def assert_sm_async_methods(object, message = nil)
518
+ async_methods = %i[fire_event_async fire_events_async fire_event_async! async_fire_event]
519
+ available_async_methods = async_methods.select { |method| object.respond_to?(method) }
520
+
521
+ default_message = "Expected async methods to be available, but found none"
522
+
523
+ _sm_refute_empty(available_async_methods, message || default_message)
524
+ end
525
+
526
+ # Assert that an object has async-enabled state machines
527
+ #
528
+ # @param object [Object] The object with state machines
529
+ # @param machine_names [Array<Symbol>] Expected async machine names
530
+ # @param message [String, nil] Custom failure message
531
+ # @return [void]
532
+ # @raise [AssertionError] If expected machines don't have async mode
533
+ #
534
+ # @example
535
+ # drone = AutonomousDrone.new
536
+ # assert_sm_has_async(drone, [:status, :teleporter_status, :shields])
537
+ def assert_sm_has_async(object, machine_names = nil, message = nil)
538
+ if machine_names
539
+ # Check specific machines
540
+ non_async_machines = machine_names.reject do |name|
541
+ machine = object.class.state_machines[name]
542
+ machine&.respond_to?(:async_mode_enabled?) && machine.async_mode_enabled?
543
+ end
544
+
545
+ default_message = "Expected machines #{machine_names.inspect} to have async enabled, but these don't: #{non_async_machines.inspect}"
546
+
547
+ _sm_assert_empty(non_async_machines, message || default_message)
548
+ else
549
+ # Check that at least one machine has async
550
+ async_machines = object.class.state_machines.select do |name, machine|
551
+ machine.respond_to?(:async_mode_enabled?) && machine.async_mode_enabled?
552
+ end
553
+
554
+ default_message = "Expected at least one state machine to have async enabled, but none found"
555
+
556
+ _sm_refute_empty(async_machines, message || default_message)
557
+ end
558
+ end
559
+
560
+ # Assert that individual async event methods are available
561
+ #
562
+ # @param object [Object] The object with state machines
563
+ # @param event [Symbol] The event name
564
+ # @param message [String, nil] Custom failure message
565
+ # @return [void]
566
+ # @raise [AssertionError] If async event methods are not available
567
+ #
568
+ # @example
569
+ # drone = AutonomousDrone.new
570
+ # assert_sm_async_event_methods(drone, :launch) # Checks launch_async and launch_async!
571
+ def assert_sm_async_event_methods(object, event, message = nil)
572
+ async_method = "#{event}_async".to_sym
573
+ async_bang_method = "#{event}_async!".to_sym
574
+
575
+ has_async = object.respond_to?(async_method)
576
+ has_async_bang = object.respond_to?(async_bang_method)
577
+
578
+
579
+ _sm_assert(has_async, "Missing #{async_method} method")
580
+ _sm_assert(has_async_bang, "Missing #{async_bang_method} method")
581
+ end
582
+
583
+ # Assert that an object has thread-safe state methods when async is enabled
584
+ #
585
+ # @param object [Object] The object with state machines
586
+ # @param message [String, nil] Custom failure message
587
+ # @return [void]
588
+ # @raise [AssertionError] If thread-safe methods are not available
589
+ #
590
+ # @example
591
+ # drone = AutonomousDrone.new
592
+ # assert_sm_thread_safe_methods(drone)
593
+ def assert_sm_thread_safe_methods(object, message = nil)
594
+ thread_safe_methods = %i[state_machine_mutex read_state_safely write_state_safely]
595
+ missing_methods = thread_safe_methods.reject { |method| object.respond_to?(method) }
596
+
597
+ default_message = "Expected thread-safe methods to be available, but missing: #{missing_methods.inspect}"
598
+
599
+ _sm_assert_empty(missing_methods, message || default_message)
600
+ end
601
+
602
+ # RSpec-style aliases for event triggering (for consistency with RSpec expectations)
603
+ alias expect_to_trigger_event assert_sm_triggers_event
604
+ alias have_triggered_event assert_sm_triggers_event
605
+
606
+ private
607
+
608
+ # Finds the can_<event>? method for the event, trying both the default and
609
+ # namespaced naming patterns, and raising when neither exists
610
+ def _sm_find_can_method(object, event, machine_name)
611
+ possible_methods = [
612
+ "can_#{event}?", # Default state machine or non-namespaced
613
+ "can_#{event}_#{machine_name}?" # Namespaced events
614
+ ]
615
+
616
+ can_method = possible_methods.find { |method| object.respond_to?(method) }
617
+ return can_method if can_method
618
+
619
+ available_methods = object.methods.grep(/^can_.*\?$/).sort
620
+ raise ArgumentError, "No transition method found for event :#{event} on machine :#{machine_name}. Available methods: #{available_methods.first(10).inspect}"
621
+ end
622
+
623
+ # Framework dispatch helpers - route assertions to whichever test
624
+ # framework is loaded (Minitest, RSpec, or none)
625
+
626
+ def _sm_assert(condition, message)
627
+ if defined?(::Minitest)
628
+ assert condition, message
629
+ elsif defined?(::RSpec)
630
+ expect(condition).to be_truthy, message
631
+ else
632
+ raise message unless condition
633
+ end
634
+ end
635
+
636
+ def _sm_refute(condition, message)
637
+ if defined?(::Minitest)
638
+ refute condition, message
639
+ elsif defined?(::RSpec)
640
+ expect(condition).to be_falsy, message
641
+ elsif condition
642
+ raise message
643
+ end
644
+ end
645
+
646
+ def _sm_assert_equal(expected, actual, message)
647
+ if defined?(::Minitest)
648
+ assert_equal expected, actual, message
649
+ elsif defined?(::RSpec)
650
+ expect(actual).to eq(expected), message
651
+ else
652
+ raise message unless expected == actual
653
+ end
654
+ end
655
+
656
+ def _sm_assert_empty(collection, message)
657
+ if defined?(::Minitest)
658
+ assert_empty collection, message
659
+ elsif defined?(::RSpec)
660
+ expect(collection).to be_empty, message
661
+ elsif collection.any?
662
+ raise message
663
+ end
664
+ end
665
+
666
+ def _sm_refute_empty(collection, message)
667
+ if defined?(::Minitest)
668
+ refute_empty collection, message
669
+ elsif defined?(::RSpec)
670
+ expect(collection).not_to be_empty, message
671
+ elsif collection.empty?
672
+ raise message
673
+ end
674
+ end
675
+
676
+ def _sm_flunk(message)
677
+ raise message unless defined?(::Minitest)
678
+
679
+ assert false, message
680
+ end
681
+
682
+ # Internal helper for checking transition callbacks
683
+ def _assert_transition_callback(callback_type, machine_or_class, options, message)
684
+ # Get the machine
685
+ machine = machine_or_class.is_a?(StateMachines::Machine) ? machine_or_class : machine_or_class.state_machine
686
+ raise ArgumentError, 'No state machine found' unless machine
687
+
688
+ callbacks = machine.callbacks[callback_type] || []
689
+
690
+ # Extract expected conditions
691
+ expected_event = options[:on]
692
+ expected_from = options[:from]
693
+ expected_to = options[:to]
694
+ expected_method = options[:do]
695
+ expected_if = options[:if]
696
+ expected_unless = options[:unless]
697
+
698
+ # Find matching callback
699
+ matching_callback = callbacks.find do |callback|
700
+ branch = callback.branch
701
+
702
+ # Check event requirement
703
+ if expected_event
704
+ event_requirement = branch.event_requirement
705
+ event_matches = if event_requirement && event_requirement.respond_to?(:values)
706
+ event_requirement.values.include?(expected_event)
707
+ else
708
+ false
709
+ end
710
+ next false unless event_matches
711
+ end
712
+
713
+ # Check state requirements (from/to)
714
+ if expected_from || expected_to
715
+ state_matches = false
716
+ branch.state_requirements.each do |req|
717
+ from_matches = !expected_from || (req[:from] && req[:from].respond_to?(:values) && req[:from].values.include?(expected_from))
718
+ to_matches = !expected_to || (req[:to] && req[:to].respond_to?(:values) && req[:to].values.include?(expected_to))
719
+
720
+ if from_matches && to_matches
721
+ state_matches = true
722
+ break
723
+ end
724
+ end
725
+ next false unless state_matches
726
+ end
727
+
728
+ # Check method requirement
729
+ if expected_method
730
+ methods = callback.instance_variable_get(:@methods) || []
731
+ method_matches = methods.any? do |method|
732
+ (method.is_a?(Symbol) && method == expected_method) ||
733
+ (method.is_a?(String) && method.to_sym == expected_method) ||
734
+ (method.respond_to?(:call) && method.respond_to?(:source_location))
735
+ end
736
+ next false unless method_matches
737
+ end
738
+
739
+ # Check if condition
740
+ if expected_if
741
+ if_condition = branch.if_condition
742
+ if_matches = (if_condition.is_a?(Symbol) && if_condition == expected_if) ||
743
+ (if_condition.is_a?(String) && if_condition.to_sym == expected_if) ||
744
+ if_condition.respond_to?(:call)
745
+ next false unless if_matches
746
+ end
747
+
748
+ # Check unless condition
749
+ if expected_unless
750
+ unless_condition = branch.unless_condition
751
+ unless_matches = (unless_condition.is_a?(Symbol) && unless_condition == expected_unless) ||
752
+ (unless_condition.is_a?(String) && unless_condition.to_sym == expected_unless) ||
753
+ unless_condition.respond_to?(:call)
754
+ next false unless unless_matches
755
+ end
756
+
757
+ true
758
+ end
759
+
760
+ return if matching_callback
761
+
762
+ expected_parts = []
763
+ expected_parts << "on: #{expected_event.inspect}" if expected_event
764
+ expected_parts << "from: #{expected_from.inspect}" if expected_from
765
+ expected_parts << "to: #{expected_to.inspect}" if expected_to
766
+ expected_parts << "do: #{expected_method.inspect}" if expected_method
767
+ expected_parts << "if: #{expected_if.inspect}" if expected_if
768
+ expected_parts << "unless: #{expected_unless.inspect}" if expected_unless
769
+
770
+ default_message = "Expected #{callback_type}_transition callback with #{expected_parts.join(', ')} to be defined, but it was not found"
771
+
772
+ _sm_flunk(message || default_message)
773
+ end
774
+ end
775
+ end