[DRE-commits] [ruby-state-machines] 01/02: Imported Upstream version 0.4.0
Abhijith PA
abhijithpa-guest at moszumanska.debian.org
Tue Nov 10 17:33:15 UTC 2015
This is an automated email from the git hooks/post-receive script.
abhijithpa-guest pushed a commit to branch master
in repository ruby-state-machines.
commit c26d885bb2e3d7b8c5b70cd4fae49f126c8a17a4
Author: Abhijith PA <abhijith at openmailbox.org>
Date: Tue Nov 10 22:58:06 2015 +0530
Imported Upstream version 0.4.0
---
.gitignore | 21 +
.rspec | 3 +
.travis.yml | 14 +
Changelog.md | 16 +
Contributors.md | 39 +
Gemfile | 8 +
LICENSE.txt | 23 +
README.md | 601 ++++++
Rakefile | 12 +
Testing.md | 0
lib/state_machines.rb | 3 +
lib/state_machines/assertions.rb | 40 +
lib/state_machines/branch.rb | 183 ++
lib/state_machines/callback.rb | 220 ++
lib/state_machines/core.rb | 43 +
lib/state_machines/core_ext.rb | 2 +
lib/state_machines/core_ext/class/state_machine.rb | 3 +
lib/state_machines/error.rb | 112 +
lib/state_machines/eval_helpers.rb | 87 +
lib/state_machines/event.rb | 228 ++
lib/state_machines/event_collection.rb | 139 ++
lib/state_machines/extensions.rb | 148 ++
lib/state_machines/helper_module.rb | 17 +
lib/state_machines/integrations.rb | 116 +
lib/state_machines/integrations/base.rb | 44 +
lib/state_machines/machine.rb | 2230 ++++++++++++++++++++
lib/state_machines/machine_collection.rb | 96 +
lib/state_machines/macro_methods.rb | 520 +++++
lib/state_machines/matcher.rb | 121 ++
lib/state_machines/matcher_helpers.rb | 54 +
lib/state_machines/node_collection.rb | 219 ++
lib/state_machines/path.rb | 120 ++
lib/state_machines/path_collection.rb | 88 +
lib/state_machines/state.rb | 272 +++
lib/state_machines/state_collection.rb | 110 +
lib/state_machines/state_context.rb | 133 ++
lib/state_machines/transition.rb | 414 ++++
lib/state_machines/transition_collection.rb | 246 +++
lib/state_machines/version.rb | 3 +
metadata.yml | 965 +++++++++
state_machines.gemspec | 23 +
.../integrations/event_on_failure_integration.rb | 10 +
test/files/integrations/vehicle.rb | 7 +
test/files/models/auto_shop.rb | 31 +
test/files/models/car.rb | 21 +
test/files/models/model_base.rb | 6 +
test/files/models/motorcycle.rb | 11 +
test/files/models/traffic_light.rb | 47 +
test/files/models/vehicle.rb | 127 ++
test/files/node.rb | 5 +
test/files/switch.rb | 15 +
test/functional/auto_shop_available_test.rb | 20 +
test/functional/auto_shop_busy_test.rb | 25 +
test/functional/car_backing_up_test.rb | 45 +
test/functional/car_test.rb | 49 +
test/functional/motorcycle_test.rb | 46 +
test/functional/traffic_light_caution_test.rb | 17 +
test/functional/traffic_light_proceed_test.rb | 17 +
test/functional/traffic_light_stop_test.rb | 26 +
test/functional/vehicle_first_gear_test.rb | 42 +
test/functional/vehicle_idling_test.rb | 59 +
test/functional/vehicle_locked_test.rb | 29 +
test/functional/vehicle_parked_test.rb | 53 +
test/functional/vehicle_repaired_test.rb | 20 +
test/functional/vehicle_second_gear_test.rb | 42 +
test/functional/vehicle_stalled_test.rb | 65 +
test/functional/vehicle_test.rb | 20 +
test/functional/vehicle_third_gear_test.rb | 42 +
test/functional/vehicle_unsaved_test.rb | 181 ++
.../vehicle_with_event_attributes_test.rb | 30 +
.../vehicle_with_parallel_events_test.rb | 36 +
test/test_helper.rb | 15 +
test/unit/assertions/assert_exclusive_keys_test.rb | 22 +
test/unit/assertions/assert_valid_key_test.rb | 12 +
test/unit/branch/branch_test.rb | 28 +
.../branch_with_conflicting_conditionals_test.rb | 27 +
...anch_with_conflicting_from_requirements_test.rb | 8 +
...branch_with_conflicting_on_requirements_test.rb | 8 +
...branch_with_conflicting_to_requirements_test.rb | 8 +
.../branch_with_different_requirements_test.rb | 41 +
...ch_with_except_from_matcher_requirement_test.rb | 8 +
.../branch_with_except_from_requirement_test.rb | 36 +
...anch_with_except_on_matcher_requirement_test.rb | 8 +
.../branch_with_except_on_requirement_test.rb | 36 +
...anch_with_except_to_matcher_requirement_test.rb | 8 +
.../branch_with_except_to_requirement_test.rb | 36 +
.../branch_with_from_matcher_requirement_test.rb | 20 +
.../branch/branch_with_from_requirement_test.rb | 45 +
.../unit/branch/branch_with_if_conditional_test.rb | 27 +
...with_implicit_and_explicit_requirements_test.rb | 23 +
..._with_implicit_from_requirement_matcher_test.rb | 20 +
.../branch_with_implicit_requirement_test.rb | 20 +
...ch_with_implicit_to_requirement_matcher_test.rb | 16 +
..._with_multiple_except_from_requirements_test.rb | 20 +
...ch_with_multiple_except_on_requirements_test.rb | 16 +
...ch_with_multiple_except_to_requirements_test.rb | 20 +
.../branch_with_multiple_from_requirements_test.rb | 16 +
.../branch_with_multiple_if_conditionals_test.rb | 20 +
...nch_with_multiple_implicit_requirements_test.rb | 53 +
.../branch_with_multiple_on_requirements_test.rb | 20 +
.../branch_with_multiple_to_requirements_test.rb | 20 +
...ranch_with_multiple_unless_conditionals_test.rb | 20 +
.../branch/branch_with_nil_requirements_test.rb | 28 +
.../branch/branch_with_no_requirements_test.rb | 36 +
.../branch_with_on_matcher_requirement_test.rb | 16 +
.../unit/branch/branch_with_on_requirement_test.rb | 45 +
.../branch_with_to_matcher_requirement_test.rb | 20 +
.../unit/branch/branch_with_to_requirement_test.rb | 45 +
.../branch/branch_with_unless_conditional_test.rb | 27 +
test/unit/branch/branch_without_guards_test.rb | 27 +
test/unit/callback/callback_by_default_test.rb | 25 +
test/unit/callback/callback_test.rb | 53 +
.../callback_with_application_bound_object_test.rb | 23 +
.../callback_with_application_terminator_test.rb | 24 +
test/unit/callback/callback_with_arguments_test.rb | 14 +
...callback_with_around_type_and_arguments_test.rb | 25 +
.../callback_with_around_type_and_block_test.rb | 44 +
...lback_with_around_type_and_bound_method_test.rb | 23 +
...k_with_around_type_and_multiple_methods_test.rb | 93 +
...allback_with_around_type_and_terminator_test.rb | 17 +
test/unit/callback/callback_with_block_test.rb | 20 +
...allback_with_bound_method_and_arguments_test.rb | 28 +
.../callback/callback_with_bound_method_test.rb | 35 +
test/unit/callback/callback_with_do_method_test.rb | 18 +
.../callback_with_explicit_requirements_test.rb | 32 +
.../callback/callback_with_if_condition_test.rb | 17 +
.../callback_with_implicit_requirements_test.rb | 32 +
.../callback/callback_with_method_argument_test.rb | 18 +
.../callback/callback_with_mixed_methods_test.rb | 31 +
.../callback_with_multiple_bound_methods_test.rb | 21 +
.../callback_with_multiple_do_methods_test.rb | 29 +
...callback_with_multiple_method_arguments_test.rb | 29 +
.../unit/callback/callback_with_terminator_test.rb | 22 +
.../callback/callback_with_unbound_method_test.rb | 14 +
.../callback_with_unless_condition_test.rb | 17 +
.../callback/callback_without_arguments_test.rb | 14 +
.../callback/callback_without_terminator_test.rb | 12 +
test/unit/error/error_by_default_test.rb | 21 +
test/unit/error/error_with_message_test.rb | 23 +
test/unit/eval_helper/eval_helpers_base_test.rb | 8 +
...lpers_proc_block_and_explicit_arguments_test.rb | 14 +
...lpers_proc_block_and_implicit_arguments_test.rb | 14 +
test/unit/eval_helper/eval_helpers_proc_test.rb | 13 +
.../eval_helpers_proc_with_arguments_test.rb | 13 +
.../eval_helpers_proc_with_block_test.rb | 13 +
...lpers_proc_with_block_without_arguments_test.rb | 18 +
..._helpers_proc_with_block_without_object_test.rb | 14 +
.../eval_helpers_proc_without_arguments_test.rb | 19 +
test/unit/eval_helper/eval_helpers_string_test.rb | 25 +
.../eval_helpers_string_with_block_test.rb | 12 +
.../eval_helpers_symbol_method_missing_test.rb | 20 +
.../eval_helpers_symbol_private_test.rb | 17 +
.../eval_helpers_symbol_protected_test.rb | 17 +
.../eval_helpers_symbol_tainted_method_test.rb | 18 +
test/unit/eval_helper/eval_helpers_symbol_test.rb | 16 +
...helpers_symbol_with_arguments_and_block_test.rb | 16 +
.../eval_helpers_symbol_with_arguments_test.rb | 16 +
.../eval_helpers_symbol_with_block_test.rb | 16 +
test/unit/eval_helper/eval_helpers_test.rb | 13 +
test/unit/event/event_after_being_copied_test.rb | 17 +
test/unit/event/event_by_default_test.rb | 60 +
test/unit/event/event_context_test.rb | 16 +
test/unit/event/event_on_failure_test.rb | 44 +
test/unit/event/event_test.rb | 34 +
test/unit/event/event_transitions_test.rb | 62 +
...th_conflicting_helpers_after_definition_test.rb | 79 +
...h_conflicting_helpers_before_definition_test.rb | 58 +
.../event/event_with_conflicting_machine_test.rb | 48 +
.../event/event_with_dynamic_human_name_test.rb | 26 +
test/unit/event/event_with_human_name_test.rb | 13 +
.../event/event_with_invalid_current_state_test.rb | 30 +
test/unit/event/event_with_machine_action_test.rb | 33 +
test/unit/event/event_with_marshalling_test.rb | 47 +
...vent_with_matching_disabled_transitions_test.rb | 115 +
...event_with_matching_enabled_transitions_test.rb | 75 +
.../event/event_with_multiple_transitions_test.rb | 61 +
test/unit/event/event_with_namespace_test.rb | 34 +
...th_transition_with_blacklisted_to_state_test.rb | 60 +
...ent_with_transition_with_loopback_state_test.rb | 36 +
...event_with_transition_with_nil_to_state_test.rb | 36 +
...th_transition_with_whitelisted_to_state_test.rb | 51 +
.../event_with_transition_without_to_state_test.rb | 36 +
test/unit/event/event_with_transitions_test.rb | 32 +
.../event_without_matching_transitions_test.rb | 41 +
test/unit/event/event_without_transitions_test.rb | 28 +
test/unit/event/invalid_event_test.rb | 20 +
...ollection_attribute_with_machine_action_test.rb | 62 +
...ction_attribute_with_namespaced_machine_test.rb | 36 +
.../event_collection_by_default_test.rb | 26 +
.../unit/event_collection/event_collection_test.rb | 39 +
...ollection_with_custom_machine_attribute_test.rb | 31 +
...collection_with_events_with_transitions_test.rb | 76 +
.../event_collection_with_multiple_events_test.rb | 27 +
.../event_collection_with_validations_test.rb | 74 +
...event_collection_without_machine_action_test.rb | 18 +
.../event_string_collection_test.rb | 31 +
test/unit/helper_module_test.rb | 17 +
test/unit/integrations/integration_finder_test.rb | 16 +
test/unit/integrations/integration_matcher_test.rb | 27 +
.../invalid_parallel_transition_test.rb | 18 +
.../invalid_transition/invalid_transition_test.rb | 47 +
.../invalid_transition_with_integration_test.rb | 45 +
.../invalid_transition_with_namespace_test.rb | 32 +
.../machine/machine_after_being_copied_test.rb | 62 +
.../machine_after_changing_initial_state.rb | 28 +
.../machine_after_changing_owner_class_test.rb | 31 +
test/unit/machine/machine_by_default_test.rb | 160 ++
.../machine/machine_finder_custom_options_test.rb | 17 +
...der_with_existing_machine_on_superclass_test.rb | 85 +
...hine_finder_with_existing_on_same_class_test.rb | 23 +
...machine_finder_without_existing_machine_test.rb | 25 +
test/unit/machine/machine_persistence_test.rb | 52 +
.../machine/machine_state_initialization_test.rb | 56 +
test/unit/machine/machine_test.rb | 30 +
.../machine_with_action_already_overridden_test.rb | 23 +
.../machine_with_action_defined_in_class_test.rb | 37 +
..._with_action_defined_in_included_module_test.rb | 46 +
...chine_with_action_defined_in_superclass_test.rb | 43 +
.../machine/machine_with_action_undefined_test.rb | 33 +
.../unit/machine/machine_with_cached_state_test.rb | 20 +
.../machine/machine_with_class_helpers_test.rb | 179 ++
...th_conflicting_helpers_after_definition_test.rb | 244 +++
...h_conflicting_helpers_before_definition_test.rb | 175 ++
.../machine/machine_with_custom_action_test.rb | 11 +
.../machine/machine_with_custom_attribute_test.rb | 103 +
.../machine/machine_with_custom_initialize_test.rb | 24 +
.../machine_with_custom_integration_test.rb | 72 +
.../machine_with_custom_invalidation_test.rb | 39 +
test/unit/machine/machine_with_custom_name_test.rb | 57 +
.../machine/machine_with_custom_plural_test.rb | 52 +
.../machine_with_dynamic_initial_state_test.rb | 65 +
.../machine/machine_with_event_matchers_test.rb | 41 +
test/unit/machine/machine_with_events_test.rb | 52 +
...ine_with_events_with_custom_human_names_test.rb | 18 +
.../machine_with_events_with_transitions_test.rb | 37 +
.../machine/machine_with_existing_event_test.rb | 17 +
...e_with_existing_machines_on_owner_class_test.rb | 20 +
...nes_with_same_attributes_on_owner_class_test.rb | 71 +
..._with_same_attributes_on_owner_subclass_test.rb | 31 +
.../machine/machine_with_existing_state_test.rb | 27 +
.../machine/machine_with_failure_callbacks_test.rb | 48 +
test/unit/machine/machine_with_helpers_test.rb | 14 +
...h_initial_state_with_value_and_owner_default.rb | 25 +
.../machine_with_initialize_and_super_test.rb | 17 +
...ine_with_initialize_arguments_and_block_test.rb | 31 +
.../machine_with_initialize_without_super_test.rb | 17 +
.../machine/machine_with_instance_helpers_test.rb | 179 ++
test/unit/machine/machine_with_integration_test.rb | 72 +
.../machine/machine_with_multiple_events_test.rb | 32 +
test/unit/machine/machine_with_namespace_test.rb | 48 +
test/unit/machine/machine_with_nil_action_test.rb | 27 +
test/unit/machine/machine_with_other_states.rb | 22 +
.../machine/machine_with_owner_subclass_test.rb | 18 +
test/unit/machine/machine_with_paths_test.rb | 25 +
.../machine/machine_with_private_action_test.rb | 43 +
.../machine/machine_with_state_matchers_test.rb | 41 +
.../machine_with_state_with_matchers_test.rb | 19 +
test/unit/machine/machine_with_states_test.rb | 55 +
.../machine_with_states_with_behaviors_test.rb | 23 +
...ine_with_states_with_custom_human_names_test.rb | 18 +
.../machine_with_states_with_custom_values_test.rb | 21 +
...e_with_states_with_runtime_dependencies_test.rb | 19 +
.../machine_with_static_initial_state_test.rb | 49 +
...ss_conflicting_helpers_after_definition_test.rb | 36 +
.../machine_with_transition_callbacks_test.rb | 144 ++
test/unit/machine/machine_with_transitions_test.rb | 87 +
.../machine/machine_without_initialization_test.rb | 31 +
.../machine/machine_without_initialize_test.rb | 14 +
.../machine/machine_without_integration_test.rb | 31 +
.../machine_collection_by_default_test.rb | 11 +
...ection_fire_attributes_with_validations_test.rb | 72 +
.../machine_collection_fire_test.rb | 80 +
...chine_collection_fire_with_transactions_test.rb | 54 +
...achine_collection_fire_with_validations_test.rb | 76 +
...machine_collection_state_initialization_test.rb | 111 +
...ollection_transitions_with_blank_events_test.rb | 25 +
...lection_transitions_with_custom_options_test.rb | 20 +
...tion_transitions_with_different_actions_test.rb | 26 +
..._transitions_with_exisiting_transitions_test.rb | 25 +
...lection_transitions_with_invalid_events_test.rb | 25 +
...ollection_transitions_with_same_actions_test.rb | 31 +
..._collection_transitions_with_transition_test.rb | 26 +
...e_collection_transitions_without_events_test.rb | 25 +
...llection_transitions_without_transition_test.rb | 27 +
test/unit/matcher/all_matcher_test.rb | 29 +
test/unit/matcher/blacklist_matcher_test.rb | 30 +
test/unit/matcher/loopback_matcher_test.rb | 27 +
test/unit/matcher/matcher_by_default_test.rb | 15 +
.../matcher/matcher_with_multiple_values_test.rb | 15 +
test/unit/matcher/matcher_with_value_test.rb | 15 +
test/unit/matcher/whitelist_matcher_test.rb | 30 +
.../matcher_helpers/matcher_helpers_all_test.rb | 14 +
.../matcher_helpers/matcher_helpers_any_test.rb | 14 +
.../matcher_helpers/matcher_helpers_same_test.rb | 13 +
.../node_collection_after_being_copied_test.rb | 46 +
.../node_collection_after_update_test.rb | 36 +
.../node_collection_by_default_test.rb | 22 +
test/unit/node_collection/node_collection_test.rb | 23 +
.../node_collection_with_indices_test.rb | 42 +
.../node_collection_with_matcher_contexts_test.rb | 25 +
.../node_collection_with_nodes_test.rb | 46 +
.../node_collection_with_numeric_index_test.rb | 24 +
...de_collection_with_postdefined_contexts_test.rb | 22 +
...ode_collection_with_predefined_contexts_test.rb | 23 +
.../node_collection_with_string_index_test.rb | 20 +
.../node_collection_with_symbol_index_test.rb | 20 +
.../node_collection_without_indices_test.rb | 30 +
test/unit/path/path_by_default_test.rb | 54 +
test/unit/path/path_test.rb | 14 +
...lable_transitions_after_reaching_target_test.rb | 40 +
.../path/path_with_available_transitions_test.rb | 54 +
.../path/path_with_deep_target_reached_test.rb | 50 +
test/unit/path/path_with_deep_target_test.rb | 40 +
test/unit/path/path_with_duplicates_test.rb | 32 +
.../path/path_with_encountered_transitions_test.rb | 34 +
.../path/path_with_guarded_transitions_test.rb | 42 +
test/unit/path/path_with_reached_target_test.rb | 35 +
test/unit/path/path_with_transitions_test.rb | 54 +
test/unit/path/path_with_unreached_target_test.rb | 31 +
test/unit/path/path_without_transitions_test.rb | 24 +
.../path_collection_by_default_test.rb | 46 +
test/unit/path_collection/path_collection_test.rb | 24 +
.../path_collection_with_deep_paths_test.rb | 43 +
.../path_collection_with_duplicate_nodes_test.rb | 31 +
.../path_collection_with_from_state_test.rb | 27 +
.../path_collection_with_paths_test.rb | 47 +
.../path_collection_with_to_state_test.rb | 29 +
.../path_with_guarded_paths_test.rb | 25 +
test/unit/state/state_after_being_copied_test.rb | 19 +
test/unit/state/state_by_default_test.rb | 41 +
test/unit/state/state_final_test.rb | 28 +
test/unit/state/state_initial_test.rb | 13 +
test/unit/state/state_not_final_test.rb | 32 +
test/unit/state/state_not_initial_test.rb | 13 +
test/unit/state/state_test.rb | 44 +
.../state/state_with_cached_lambda_value_test.rb | 29 +
...th_conflicting_helpers_after_definition_test.rb | 38 +
...h_conflicting_helpers_before_definition_test.rb | 29 +
.../state_with_conflicting_machine_name_test.rb | 20 +
.../state/state_with_conflicting_machine_test.rb | 37 +
test/unit/state/state_with_context_test.rb | 60 +
.../state/state_with_dynamic_human_name_test.rb | 25 +
.../state_with_existing_context_method_test.rb | 24 +
test/unit/state/state_with_human_name_test.rb | 13 +
test/unit/state/state_with_integer_value_test.rb | 32 +
.../state/state_with_invalid_method_call_test.rb | 21 +
test/unit/state/state_with_lambda_value_test.rb | 37 +
test/unit/state/state_with_matcher_test.rb | 18 +
.../state/state_with_multiple_contexts_test.rb | 57 +
test/unit/state/state_with_name_test.rb | 43 +
test/unit/state/state_with_namespace_test.rb | 22 +
test/unit/state/state_with_nil_value_test.rb | 35 +
.../state_with_redefined_context_method_test.rb | 45 +
test/unit/state/state_with_symbolic_value_test.rb | 32 +
...inherited_method_call_for_current_state_test.rb | 40 +
...ith_valid_method_call_for_current_state_test.rb | 33 +
...h_valid_method_call_for_different_state_test.rb | 41 +
.../state_without_cached_lambda_value_test.rb | 25 +
test/unit/state/state_without_name_test.rb | 39 +
.../state_collection_by_default_test.rb | 21 +
.../state_collection_string_test.rb | 35 +
.../unit/state_collection/state_collection_test.rb | 74 +
...ate_collection_with_custom_state_values_test.rb | 29 +
...state_collection_with_event_transitions_test.rb | 39 +
.../state_collection_with_initial_state_test.rb | 40 +
.../state_collection_with_namespace_test.rb | 21 +
.../state_collection_with_state_behaviors_test.rb | 40 +
.../state_collection_with_state_matchers_test.rb | 29 +
...te_collection_with_transition_callbacks_test.rb | 40 +
.../unit/state_context/state_context_proxy_test.rb | 26 +
...ext_proxy_with_if_and_unless_conditions_test.rb | 42 +
.../state_context_proxy_with_if_condition_test.rb | 64 +
...ntext_proxy_with_multiple_if_conditions_test.rb | 32 +
...t_proxy_with_multiple_unless_conditions_test.rb | 32 +
...ate_context_proxy_with_unless_condition_test.rb | 64 +
.../state_context_proxy_without_conditions_test.rb | 31 +
test/unit/state_context/state_context_test.rb | 28 +
.../state_context/state_context_transition_test.rb | 104 +
.../state_context_with_matching_transition_test.rb | 27 +
.../state_machine/state_machine_by_default_test.rb | 12 +
test/unit/state_machine/state_machine_test.rb | 20 +
.../transition_after_being_performed_test.rb | 48 +
.../transition_after_being_persisted_test.rb | 46 +
.../transition_after_being_rolled_back_test.rb | 35 +
test/unit/transition/transition_equality_test.rb | 52 +
test/unit/transition/transition_loopback_test.rb | 18 +
test/unit/transition/transition_test.rb | 96 +
test/unit/transition/transition_transient_test.rb | 20 +
.../unit/transition/transition_with_action_test.rb | 27 +
...transition_with_after_callbacks_skipped_test.rb | 127 ++
.../transition_with_after_callbacks_test.rb | 93 +
.../transition_with_around_callbacks_test.rb | 141 ++
...ransition_with_before_callbacks_skipped_test.rb | 30 +
.../transition_with_before_callbacks_test.rb | 104 +
...ransition_with_custom_machine_attribute_test.rb | 28 +
.../transition_with_different_states_test.rb | 18 +
.../transition_with_dynamic_to_value_test.rb | 19 +
.../transition_with_failure_callbacks_test.rb | 84 +
.../transition_with_invalid_nodes_test.rb | 29 +
.../transition_with_mixed_callbacks_test.rb | 105 +
...ransition_with_multiple_after_callbacks_test.rb | 40 +
...ansition_with_multiple_around_callbacks_test.rb | 114 +
...ansition_with_multiple_before_callbacks_test.rb | 40 +
...nsition_with_multiple_failure_callbacks_test.rb | 40 +
.../transition/transition_with_namespace_test.rb | 47 +
.../transition_with_perform_arguments_test.rb | 35 +
.../transition_with_transactions_test.rb | 42 +
.../transition_without_callbacks_test.rb | 33 +
.../transition_without_reading_state_test.rb | 22 +
.../transition_without_running_action_test.rb | 47 +
...ribute_transition_collection_by_default_test.rb | 23 +
...ibute_transition_collection_marshalling_test.rb | 64 +
...transition_collection_with_action_error_test.rb | 44 +
...ransition_collection_with_action_failed_test.rb | 44 +
...on_collection_with_after_callback_error_test.rb | 32 +
...ion_collection_with_after_callback_halt_test.rb | 33 +
..._with_around_after_yield_callback_error_test.rb | 32 +
..._with_around_callback_after_yield_error_test.rb | 32 +
...n_with_around_callback_after_yield_halt_test.rb | 33 +
..._with_around_callback_before_yield_halt_test.rb | 33 +
...n_collection_with_before_callback_error_test.rb | 32 +
...on_collection_with_before_callback_halt_test.rb | 33 +
...te_transition_collection_with_callbacks_test.rb | 68 +
...ition_collection_with_event_transitions_test.rb | 41 +
...ibute_transition_collection_with_events_test.rb | 44 +
...collection_with_skipped_after_callbacks_test.rb | 42 +
.../transition_collection_by_default_test.rb | 23 +
.../transition_collection_empty_with_block_test.rb | 23 +
...ansition_collection_empty_without_block_test.rb | 12 +
.../transition_collection_invalid_test.rb | 21 +
.../transition_collection_partial_invalid_test.rb | 69 +
.../transition_collection_test.rb | 26 +
.../transition_collection_valid_test.rb | 57 +
...transition_collection_with_action_error_test.rb | 66 +
...ransition_collection_with_action_failed_test.rb | 60 +
...n_collection_with_action_hook_and_block_test.rb | 17 +
...ion_with_action_hook_and_skipped_action_test.rb | 17 +
...action_hook_and_skipped_after_callbacks_test.rb | 37 +
...sition_collection_with_action_hook_base_test.rb | 34 +
...ition_collection_with_action_hook_error_test.rb | 29 +
...ion_collection_with_action_hook_invalid_test.rb | 17 +
...on_collection_with_action_hook_multiple_test.rb | 79 +
.../transition_collection_with_action_hook_test.rb | 45 +
...with_action_hook_with_different_actions_test.rb | 48 +
...ection_with_action_hook_with_nil_action_test.rb | 42 +
...ion_collection_with_after_callback_halt_test.rb | 51 +
...on_collection_with_before_callback_halt_test.rb | 47 +
.../transition_collection_with_block_test.rb | 46 +
.../transition_collection_with_callbacks_test.rb | 135 ++
...ition_collection_with_different_actions_test.rb | 189 ++
...ition_collection_with_duplicate_actions_test.rb | 48 +
...ransition_collection_with_empty_actions_test.rb | 41 +
...ransition_collection_with_mixed_actions_test.rb | 41 +
...llection_with_skipped_actions_and_block_test.rb | 34 +
...nsition_collection_with_skipped_actions_test.rb | 69 +
...ed_after_callbacks_and_around_callbacks_test.rb | 53 +
...collection_with_skipped_after_callbacks_test.rb | 34 +
...transition_collection_with_transactions_test.rb | 65 +
...nsition_collection_without_transactions_test.rb | 29 +
459 files changed, 23653 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..27474cf
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,21 @@
+*.gem
+*.rbc
+.bundle
+.config
+.yardoc
+Gemfile.lock
+InstalledFiles
+_yardoc
+coverage
+doc/
+lib/bundler/man
+pkg
+rdoc
+spec/reports
+tmp
+*.bundle
+*.so
+*.o
+*.a
+mkmf.log
+.idea/
diff --git a/.rspec b/.rspec
new file mode 100644
index 0000000..0d786ba
--- /dev/null
+++ b/.rspec
@@ -0,0 +1,3 @@
+--color
+--warnings
+--require spec_helper
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..b24e118
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,14 @@
+language: ruby
+sudo: false
+script: "bundle exec rake"
+cache: bundler
+rvm:
+ - 2.1
+ - 2.0.0
+ - 2.2
+ - jruby
+ - rbx-2
+matrix:
+ allow_failures:
+ - rvm: rbx-2
+ - rvm: jruby
diff --git a/Changelog.md b/Changelog.md
new file mode 100644
index 0000000..7f54fa6
--- /dev/null
+++ b/Changelog.md
@@ -0,0 +1,16 @@
+* Fixed inconsistent use of :use_transactions
+
+* Namespaced integrations are not registered by default anymore
+
+* Pass `static: false` in case you don't want initial states to be forced. e.g.
+
+ ```ruby
+ # will set the initial machine state
+ @machines.initialize_states(@object)
+
+ # optionally you can pass the attributes to have that as the initial state
+ @machines.initialize_states(@object, {}, { state: 'finished' })
+
+ # or pass set `static` to false if you want to keep the `object.state` current value
+ @machines.initialize_states(@object, { static: false })
+ ```
diff --git a/Contributors.md b/Contributors.md
new file mode 100644
index 0000000..b84d93e
--- /dev/null
+++ b/Contributors.md
@@ -0,0 +1,39 @@
+- Aaron Gibralter
+- Aaron Pfeifer
+- Abdelkader Boudih
+- Akira Matsuda
+- Andrea Longhi
+- Brad Heller
+- Brandon Dimcheff
+- Casey Howard
+- Chinasaur
+- Daniel Huckstep
+- Durran Jordan
+- Gareth Adams
+- Jahangir Zinedine
+- Jeremy Wells
+- Joe Lind
+- Jon Evans
+- Markus Schirp
+- Michael Klishin
+- Mikhail Shirkov
+- Mohamed Alouane
+- Nate Murray
+- Nathan Long
+- Nicolas Blanco
+- Pawel Pierzchala
+- Pete Forde
+- Peter Lampesberger
+- Rin Raeuber
+- Robert Poor
+- Rustam Zagirov
+- Sandro Turriate and Tim Pope
+- Sean O'Brien
+- Stefan Penner
+- Steve Richert
+- Wojciech Wnętrzak
+- @chris
+- @gmitrev
+- @nblumoe
+- @reiner
+- @sanemat
\ No newline at end of file
diff --git a/Gemfile b/Gemfile
new file mode 100644
index 0000000..497b9a6
--- /dev/null
+++ b/Gemfile
@@ -0,0 +1,8 @@
+source 'https://rubygems.org'
+gemspec
+
+platform :mri_20, :mri_21 do
+ gem 'pry-byebug'
+end
+
+gem 'minitest-reporters'
\ No newline at end of file
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..a671699
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,23 @@
+Copyright (c) 2006-2012 Aaron Pfeifer
+Copyright (c) 2014-2015 Abdelkader Boudih
+
+MIT License
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..8875009
--- /dev/null
+++ b/README.md
@@ -0,0 +1,601 @@
+[](https://travis-ci.org/state-machines/state_machines)
+[](https://codeclimate.com/github/state-machines/state_machines)
+# State Machines
+
+State Machines adds support for creating state machines for attributes on any Ruby class.
+
+## Installation
+
+Add this line to your application's Gemfile:
+
+ gem 'state_machines'
+
+And then execute:
+
+ $ bundle
+
+Or install it yourself as:
+
+ $ gem install state_machines
+
+## Usage
+
+### Example
+
+Below is an example of many of the features offered by this plugin, including:
+
+* Initial states
+* Namespaced states
+* Transition callbacks
+* Conditional transitions
+* State-driven instance behavior
+* Customized state values
+* Parallel events
+* Path analysis
+
+Class definition:
+
+```ruby
+class Vehicle
+ attr_accessor :seatbelt_on, :time_used, :auto_shop_busy
+
+ state_machine :state, initial: :parked do
+ before_transition parked: :any - :parked, do: :put_on_seatbelt
+
+ after_transition on: :crash, do: :tow
+ after_transition on: :repair, :do: :fix
+ after_transition any => :parked do |vehicle, transition|
+ vehicle.seatbelt_on = false
+ end
+
+ after_failure on: :ignite, do: :log_start_failure
+
+ around_transition do |vehicle, transition, block|
+ start = Time.now
+ block.call
+ vehicle.time_used += Time.now - start
+ end
+
+ event :park do
+ transition [:idling, :first_gear] => :parked
+ end
+
+ event :ignite do
+ transition stalled: same, parked: :idling
+ end
+
+ event :idle do
+ transition first_gear: :idling
+ end
+
+ event :shift_up do
+ transition idling: :first_gear, first_gear: :second_gear, second_gear: :third_gear
+ end
+
+ event :shift_down do
+ transition third_gear: :second_gear, second_gear: :first_gear
+ end
+
+ event :crash do
+ transition all - [:parked, :stalled] => :stalled, if: ->(vehicle) {!vehicle.passed_inspection?}
+ end
+
+ event :repair do
+ # The first transition that matches the state and passes its conditions
+ # will be used
+ transition stalled: parked, unless: :auto_shop_busy
+ transition stalled: same
+ end
+
+ state :parked do
+ def speed
+ 0
+ end
+ end
+
+ state :idling, :first_gear do
+ def speed
+ 10
+ end
+ end
+
+ state all - [:parked, :stalled, :idling] do
+ def moving?
+ true
+ end
+ end
+
+ state :parked, :stalled, :idling do
+ def moving?
+ false
+ end
+ end
+ end
+
+ state_machine :alarm_state, initial: :active, namespace: :'alarm' do
+ event :enable do
+ transition all => :active
+ end
+
+ event :disable do
+ transition all => :off
+ end
+
+ state :active, :value => 1
+ state :off, :value => 0
+ end
+
+ def initialize
+ @seatbelt_on = false
+ @time_used = 0
+ @auto_shop_busy = true
+ super() # NOTE: This *must* be called, otherwise states won't get initialized
+ end
+
+ def put_on_seatbelt
+ @seatbelt_on = true
+ end
+
+ def passed_inspection?
+ false
+ end
+
+ def tow
+ # tow the vehicle
+ end
+
+ def fix
+ # get the vehicle fixed by a mechanic
+ end
+
+ def log_start_failure
+ # log a failed attempt to start the vehicle
+ end
+end
+```
+
+**Note** the comment made on the `initialize` method in the class. In order for
+state machine attributes to be properly initialized, `super()` must be called.
+See `StateMachines:MacroMethods` for more information about this.
+
+Using the above class as an example, you can interact with the state machine
+like so:
+
+```ruby
+vehicle = Vehicle.new # => #<Vehicle:0xb7cf4eac @state="parked", @seatbelt_on=false>
+vehicle.state # => "parked"
+vehicle.state_name # => :parked
+vehicle.human_state_name # => "parked"
+vehicle.parked? # => true
+vehicle.can_ignite? # => true
+vehicle.ignite_transition # => #<StateMachines:Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>
+vehicle.state_events # => [:ignite]
+vehicle.state_transitions # => [#<StateMachines:Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
+vehicle.speed # => 0
+vehicle.moving? # => false
+
+vehicle.ignite # => true
+vehicle.parked? # => false
+vehicle.idling? # => true
+vehicle.speed # => 10
+vehicle # => #<Vehicle:0xb7cf4eac @state="idling", @seatbelt_on=true>
+
+vehicle.shift_up # => true
+vehicle.speed # => 10
+vehicle.moving? # => true
+vehicle # => #<Vehicle:0xb7cf4eac @state="first_gear", @seatbelt_on=true>
+
+# A generic event helper is available to fire without going through the event's instance method
+vehicle.fire_state_event(:shift_up) # => true
+
+# Call state-driven behavior that's undefined for the state raises a NoMethodError
+vehicle.speed # => NoMethodError: super: no superclass method `speed' for #<Vehicle:0xb7cf4eac>
+vehicle # => #<Vehicle:0xb7cf4eac @state="second_gear", @seatbelt_on=true>
+
+# The bang (!) operator can raise exceptions if the event fails
+vehicle.park! # => StateMachines:InvalidTransition: Cannot transition state via :park from :second_gear
+
+# Generic state predicates can raise exceptions if the value does not exist
+vehicle.state?(:parked) # => false
+vehicle.state?(:invalid) # => IndexError: :invalid is an invalid name
+
+# Namespaced machines have uniquely-generated methods
+vehicle.alarm_state # => 1
+vehicle.alarm_state_name # => :active
+
+vehicle.can_disable_alarm? # => true
+vehicle.disable_alarm # => true
+vehicle.alarm_state # => 0
+vehicle.alarm_state_name # => :off
+vehicle.can_enable_alarm? # => true
+
+vehicle.alarm_off? # => true
+vehicle.alarm_active? # => false
+
+# Events can be fired in parallel
+vehicle.fire_events(:shift_down, :enable_alarm) # => true
+vehicle.state_name # => :first_gear
+vehicle.alarm_state_name # => :active
+
+vehicle.fire_events!(:ignite, :enable_alarm) # => StateMachines:InvalidTransition: Cannot run events in parallel: ignite, enable_alarm
+
+# Human-friendly names can be accessed for states/events
+Vehicle.human_state_name(:first_gear) # => "first gear"
+Vehicle.human_alarm_state_name(:active) # => "active"
+
+Vehicle.human_state_event_name(:shift_down) # => "shift down"
+Vehicle.human_alarm_state_event_name(:enable) # => "enable"
+
+# States / events can also be references by the string version of their name
+Vehicle.human_state_name('first_gear') # => "first gear"
+Vehicle.human_state_event_name('shift_down') # => "shift down"
+
+# Available transition paths can be analyzed for an object
+vehicle.state_paths # => [[#<StateMachines:Transition ...], [#<StateMachines:Transition ...], ...]
+vehicle.state_paths.to_states # => [:parked, :idling, :first_gear, :stalled, :second_gear, :third_gear]
+vehicle.state_paths.events # => [:park, :ignite, :shift_up, :idle, :crash, :repair, :shift_down]
+
+# Find all paths that start and end on certain states
+vehicle.state_paths(:from => :parked, :to => :first_gear) # => [[
+ # #<StateMachines:Transition attribute=:state event=:ignite from="parked" ...>,
+ # #<StateMachines:Transition attribute=:state event=:shift_up from="idling" ...>
+ # ]]
+# Skipping state_machine and writing to attributes directly
+vehicle.state = "parked"
+vehicle.state # => "parked"
+vehicle.state_name # => :parked
+
+# *Note* that the following is not supported (see StateMachines:MacroMethods#state_machine):
+# vehicle.state = :parked
+```
+
+## Additional Topics
+
+### Explicit vs. Implicit Event Transitions
+
+Every event defined for a state machine generates an instance method on the
+class that allows the event to be explicitly triggered. Most of the examples in
+the state_machine documentation use this technique. However, with some types of
+integrations, like ActiveRecord, you can also *implicitly* fire events by
+setting a special attribute on the instance.
+
+Suppose you're using the ActiveRecord integration and the following model is
+defined:
+
+```ruby
+class Vehicle < ActiveRecord::Base
+ state_machine initial: :parked do
+ event :ignite do
+ transition parked: :idling
+ end
+ end
+end
+```
+
+To trigger the `ignite` event, you would typically call the `Vehicle#ignite`
+method like so:
+
+```ruby
+vehicle = Vehicle.create # => #<Vehicle id=1 state="parked">
+vehicle.ignite # => true
+vehicle.state # => "idling"
+```
+
+This is referred to as an *explicit* event transition. The same behavior can
+also be achieved *implicitly* by setting the state event attribute and invoking
+the action associated with the state machine. For example:
+
+```ruby
+vehicle = Vehicle.create # => #<Vehicle id=1 state="parked">
+vehicle.state_event = 'ignite' # => 'ignite'
+vehicle.save # => true
+vehicle.state # => 'idling'
+vehicle.state_event # => nil
+```
+
+As you can see, the `ignite` event was automatically triggered when the `save`
+action was called. This is particularly useful if you want to allow users to
+drive the state transitions from a web API.
+
+See each integration's API documentation for more information on the implicit
+approach.
+
+### Symbols vs. Strings
+
+In all of the examples used throughout the documentation, you'll notice that
+states and events are almost always referenced as symbols. This isn't a
+requirement, but rather a suggested best practice.
+
+You can very well define your state machine with Strings like so:
+
+```ruby
+class Vehicle
+ state_machine initial: 'parked' do
+ event 'ignite' do
+ transition 'parked' => 'idling'
+ end
+
+ # ...
+ end
+end
+```
+
+You could even use numbers as your state / event names. The **important** thing
+to keep in mind is that the type being used for referencing states / events in
+your machine definition must be **consistent**. If you're using Symbols, then
+all states / events must use Symbols. Otherwise you'll encounter the following
+error:
+
+```ruby
+class Vehicle
+ state_machine do
+ event :ignite do
+ transition parked: 'idling'
+ end
+ end
+end
+
+# => ArgumentError: "idling" state defined as String, :parked defined as Symbol; all states must be consistent
+```
+
+There **is** an exception to this rule. The consistency is only required within
+the definition itself. However, when the machine's helper methods are called
+with input from external sources, such as a web form, state_machine will map
+that input to a String / Symbol. For example:
+
+```ruby
+class Vehicle
+ state_machine initial: :parked do
+ event :ignite do
+ transition parked: :idling
+ end
+ end
+end
+
+v = Vehicle.new # => #<Vehicle:0xb71da5f8 @state="parked">
+v.state?('parked') # => true
+v.state?(:parked) # => true
+```
+
+**Note** that none of this actually has to do with the type of the value that
+gets stored. By default, all state values are assumed to be string -- regardless
+of whether the state names are symbols or strings. If you want to store states
+as symbols instead you'll have to be explicit about it:
+
+```ruby
+class Vehicle
+ state_machine initial: :parked do
+ event :ignite do
+ transition parked: :idling
+ end
+
+ states.each do |state|
+ self.state(state.name, :value => state.name.to_sym)
+ end
+ end
+end
+
+v = Vehicle.new # => #<Vehicle:0xb71da5f8 @state=:parked>
+v.state?('parked') # => true
+v.state?(:parked) # => true
+```
+
+### Syntax flexibility
+
+Although state_machine introduces a simplified syntax, it still remains
+backwards compatible with previous versions and other state-related libraries by
+providing some flexibility around how transitions are defined. See below for an
+overview of these syntaxes.
+
+#### Verbose syntax
+
+In general, it's recommended that state machines use the implicit syntax for
+transitions. However, you can be a little more explicit and verbose about
+transitions by using the `:from`, `:except_from`, `:to`,
+and `:except_to` options.
+
+For example, transitions and callbacks can be defined like so:
+
+```ruby
+class Vehicle
+ state_machine initial: :parked do
+ before_transition from: :parked, except_to: :parked, do: :put_on_seatbelt
+ after_transition to: :parked do |transition|
+ self.seatbelt = 'off' # self is the record
+ end
+
+ event :ignite do
+ transition from: :parked, to: :idling
+ end
+ end
+end
+```
+
+#### Transition context
+
+Some flexibility is provided around the context in which transitions can be
+defined. In almost all examples throughout the documentation, transitions are
+defined within the context of an event. If you prefer to have state machines
+defined in the context of a **state** either out of preference or in order to
+easily migrate from a different library, you can do so as shown below:
+
+```ruby
+class Vehicle
+ state_machine initial: :parked do
+ ...
+
+ state :parked do
+ transition to::idling, :on => [:ignite, :shift_up], if: :seatbelt_on?
+
+ def speed
+ 0
+ end
+ end
+
+ state :first_gear do
+ transition to: :second_gear, on: :shift_up
+
+ def speed
+ 10
+ end
+ end
+
+ state :idling, :first_gear do
+ transition to: :parked, on: :park
+ end
+ end
+end
+```
+
+In the above example, there's no need to specify the `from` state for each
+transition since it's inferred from the context.
+
+You can also define transitions completely outside the context of a particular
+state / event. This may be useful in cases where you're building a state
+machine from a data store instead of part of the class definition. See the
+example below:
+
+```ruby
+class Vehicle
+ state_machine initial: :parked do
+ ...
+
+ transition parked: :idling, :on => [:ignite, :shift_up]
+ transition first_gear: :second_gear, second_gear: :third_gear, on: :shift_up
+ transition [:idling, :first_gear] => :parked, on: :park
+ transition [:idling, :first_gear] => :parked, on: :park
+ transition all - [:parked, :stalled]: :stalled, unless: :auto_shop_busy?
+ end
+end
+```
+
+Notice that in these alternative syntaxes:
+
+* You can continue to configure `:if` and `:unless` conditions
+* You can continue to define `from` states (when in the machine context) using
+the `all`, `any`, and `same` helper methods
+
+### Static / Dynamic definitions
+
+In most cases, the definition of a state machine is **static**. That is to say,
+the states, events and possible transitions are known ahead of time even though
+they may depend on data that's only known at runtime. For example, certain
+transitions may only be available depending on an attribute on that object it's
+being run on. All of the documentation in this library define static machines
+like so:
+
+```ruby
+class Vehicle
+ state_machine :state, initial: :parked do
+ event :park do
+ transition [:idling, :first_gear] => :parked
+ end
+
+ ...
+ end
+end
+```
+
+However, there may be cases where the definition of a state machine is **dynamic**.
+This means that you don't know the possible states or events for a machine until
+runtime. For example, you may allow users in your application to manage the
+state machine of a project or task in your system. This means that the list of
+transitions (and their associated states / events) could be stored externally,
+such as in a database. In a case like this, you can define dynamically-generated
+state machines like so:
+
+```ruby
+class Vehicle
+ attr_accessor :state
+
+ # Make sure the machine gets initialized so the initial state gets set properly
+ def initialize(*)
+ super
+ machine
+ end
+
+ # Replace this with an external source (like a db)
+ def transitions
+ [
+ {parked: :idling, on: :ignite},
+ {idling: :first_gear, first_gear: :second_gear, on: :shift_up}
+ # ...
+ ]
+ end
+
+ # Create a state machine for this vehicle instance dynamically based on the
+ # transitions defined from the source above
+ def machine
+ vehicle = self
+ @machine ||= Machine.new(vehicle, initial: :parked, action: :save) do
+ vehicle.transitions.each {|attrs| transition(attrs)}
+ end
+ end
+
+ def save
+ # Save the state change...
+ true
+ end
+end
+
+# Generic class for building machines
+class Machine
+ def self.new(object, *args, &block)
+ machine_class = Class.new
+ machine = machine_class.state_machine(*args, &block)
+ attribute = machine.attribute
+ action = machine.action
+
+ # Delegate attributes
+ machine_class.class_eval do
+ define_method(:definition) { machine }
+ define_method(attribute) { object.send(attribute) }
+ define_method("#{attribute}=") {|value| object.send("#{attribute}=", value) }
+ define_method(action) { object.send(action) } if action
+ end
+
+ machine_class.new
+ end
+end
+
+vehicle = Vehicle.new # => #<Vehicle:0xb708412c @state="parked" ...>
+vehicle.state # => "parked"
+vehicle.machine.ignite # => true
+vehicle.machine.state # => "idling
+vehicle.state # => "idling"
+vehicle.machine.state_transitions # => [#<StateMachines:Transition ...>]
+vehicle.machine.definition.states.keys # => :first_gear, :second_gear, :parked, :idling
+```
+
+As you can see, state_machine provides enough flexibility for you to be able
+to create new machine definitions on the fly based on an external source of
+transitions.
+
+## Dependencies
+
+Ruby versions officially supported and tested:
+
+* Ruby (MRI) 2.0.0+
+* JRuby
+* Rubinius
+
+For graphing state machine:
+
+* [state_machines-graphviz](http://github.com/state-machines/state_machines-graphviz)
+
+For documenting state machines:
+
+* [state_machines-yard](http://github.com/state-machines/state_machines-yard)
+
+
+## TODO
+
+* Add matchers/assertions for rspec and minitest
+
+## Contributing
+
+1. Fork it ( https://github.com/state-machines/state_machines/fork )
+2. Create your feature branch (`git checkout -b my-new-feature`)
+3. Commit your changes (`git commit -am 'Add some feature'`)
+4. Push to the branch (`git push origin my-new-feature`)
+5. Create a new Pull Request
diff --git a/Rakefile b/Rakefile
new file mode 100644
index 0000000..85e08ab
--- /dev/null
+++ b/Rakefile
@@ -0,0 +1,12 @@
+require 'bundler/gem_tasks'
+require 'rake/testtask'
+Rake::TestTask.new(:functional) do |t|
+ t.test_files = FileList['test/functional/*_test.rb']
+end
+
+Rake::TestTask.new(:unit) do |t|
+ t.test_files = FileList['test/unit/**/*_test.rb']
+end
+
+desc 'Default: run all tests.'
+task default: [:unit, :functional]
\ No newline at end of file
diff --git a/Testing.md b/Testing.md
new file mode 100644
index 0000000..e69de29
diff --git a/lib/state_machines.rb b/lib/state_machines.rb
new file mode 100644
index 0000000..2fcff97
--- /dev/null
+++ b/lib/state_machines.rb
@@ -0,0 +1,3 @@
+require 'state_machines/version'
+require 'state_machines/core'
+require 'state_machines/core_ext'
\ No newline at end of file
diff --git a/lib/state_machines/assertions.rb b/lib/state_machines/assertions.rb
new file mode 100644
index 0000000..044a550
--- /dev/null
+++ b/lib/state_machines/assertions.rb
@@ -0,0 +1,40 @@
+class Hash
+ # Provides a set of helper methods for making assertions about the content
+ # of various objects
+
+ unless respond_to?(:assert_valid_keys)
+ # Validate all keys in a hash match <tt>*valid_keys</tt>, raising ArgumentError
+ # on a mismatch. Note that keys are NOT treated indifferently, meaning if you
+ # use strings for keys but assert symbols as keys, this will fail.
+ #
+ # { name: 'Rob', years: '28' }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key: :years. Valid keys are: :name, :age"
+ # { name: 'Rob', age: '28' }.assert_valid_keys('name', 'age') # => raises "ArgumentError: Unknown key: :name. Valid keys are: 'name', 'age'"
+ # { name: 'Rob', age: '28' }.assert_valid_keys(:name, :age) # => passes, raises nothing
+ # Code from ActiveSupport
+ def assert_valid_keys(*valid_keys)
+ valid_keys.flatten!
+ each_key do |k|
+ unless valid_keys.include?(k)
+ raise ArgumentError.new("Unknown key: #{k.inspect}. Valid keys are: #{valid_keys.map(&:inspect).join(', ')}")
+ end
+ end
+ end
+ end
+
+ # Validates that the given hash only includes at *most* one of a set of
+ # exclusive keys. If more than one key is found, an ArgumentError will be
+ # raised.
+ #
+ # == Examples
+ #
+ # options = {:only => :on, :except => :off}
+ # options.assert_exclusive_keys(:only) # => nil
+ # options.assert_exclusive_keys(:except) # => nil
+ # options.assert_exclusive_keys(:only, :except) # => ArgumentError: Conflicting keys: only, except
+ # options.assert_exclusive_keys(:only, :except, :with) # => ArgumentError: Conflicting keys: only, except
+ def assert_exclusive_keys(*exclusive_keys)
+ conflicting_keys = exclusive_keys & keys
+ raise ArgumentError, "Conflicting keys: #{conflicting_keys.join(', ')}" unless conflicting_keys.length <= 1
+ end
+end
+
diff --git a/lib/state_machines/branch.rb b/lib/state_machines/branch.rb
new file mode 100644
index 0000000..1ab4eb6
--- /dev/null
+++ b/lib/state_machines/branch.rb
@@ -0,0 +1,183 @@
+module StateMachines
+ # Represents a set of requirements that must be met in order for a transition
+ # or callback to occur. Branches verify that the event, from state, and to
+ # state of the transition match, in addition to if/unless conditionals for
+ # an object's state.
+ class Branch
+
+ include EvalHelpers
+
+ # The condition that must be met on an object
+ attr_reader :if_condition
+
+ # The condition that must *not* be met on an object
+ attr_reader :unless_condition
+
+ # The requirement for verifying the event being matched
+ attr_reader :event_requirement
+
+ # One or more requirements for verifying the states being matched. All
+ # requirements contain a mapping of {:from => matcher, :to => matcher}.
+ attr_reader :state_requirements
+
+ # A list of all of the states known to this branch. This will pull states
+ # from the following options (in the same order):
+ # * +from+ / +except_from+
+ # * +to+ / +except_to+
+ attr_reader :known_states
+
+ # Creates a new branch
+ def initialize(options = {}) #:nodoc:
+ # Build conditionals
+ @if_condition = options.delete(:if)
+ @unless_condition = options.delete(:unless)
+
+ # Build event requirement
+ @event_requirement = build_matcher(options, :on, :except_on)
+
+ if (options.keys - [:from, :to, :on, :except_from, :except_to, :except_on]).empty?
+ # Explicit from/to requirements specified
+ @state_requirements = [{:from => build_matcher(options, :from, :except_from), :to => build_matcher(options, :to, :except_to)}]
+ else
+ # Separate out the event requirement
+ options.delete(:on)
+ options.delete(:except_on)
+
+ # Implicit from/to requirements specified
+ @state_requirements = options.collect do |from, to|
+ from = WhitelistMatcher.new(from) unless from.is_a?(Matcher)
+ to = WhitelistMatcher.new(to) unless to.is_a?(Matcher)
+ {:from => from, :to => to}
+ end
+ end
+
+ # Track known states. The order that requirements are iterated is based
+ # on the priority in which tracked states should be added.
+ @known_states = []
+ @state_requirements.each do |state_requirement|
+ [:from, :to].each {|option| @known_states |= state_requirement[option].values}
+ end
+ end
+
+ # Determines whether the given object / query matches the requirements
+ # configured for this branch. In addition to matching the event, from state,
+ # and to state, this will also check whether the configured :if/:unless
+ # conditions pass on the given object.
+ #
+ # == Examples
+ #
+ # branch = StateMachines::Branch.new(:parked => :idling, :on => :ignite)
+ #
+ # # Successful
+ # branch.matches?(object, :on => :ignite) # => true
+ # branch.matches?(object, :from => nil) # => true
+ # branch.matches?(object, :from => :parked) # => true
+ # branch.matches?(object, :to => :idling) # => true
+ # branch.matches?(object, :from => :parked, :to => :idling) # => true
+ # branch.matches?(object, :on => :ignite, :from => :parked, :to => :idling) # => true
+ #
+ # # Unsuccessful
+ # branch.matches?(object, :on => :park) # => false
+ # branch.matches?(object, :from => :idling) # => false
+ # branch.matches?(object, :to => :first_gear) # => false
+ # branch.matches?(object, :from => :parked, :to => :first_gear) # => false
+ # branch.matches?(object, :on => :park, :from => :parked, :to => :idling) # => false
+ def matches?(object, query = {})
+ !match(object, query).nil?
+ end
+
+ # Attempts to match the given object / query against the set of requirements
+ # configured for this branch. In addition to matching the event, from state,
+ # and to state, this will also check whether the configured :if/:unless
+ # conditions pass on the given object.
+ #
+ # If a match is found, then the event/state requirements that the query
+ # passed successfully will be returned. Otherwise, nil is returned if there
+ # was no match.
+ #
+ # Query options:
+ # * <tt>:from</tt> - One or more states being transitioned from. If none
+ # are specified, then this will always match.
+ # * <tt>:to</tt> - One or more states being transitioned to. If none are
+ # specified, then this will always match.
+ # * <tt>:on</tt> - One or more events that fired the transition. If none
+ # are specified, then this will always match.
+ # * <tt>:guard</tt> - Whether to guard matches with the if/unless
+ # conditionals defined for this branch. Default is true.
+ #
+ # == Examples
+ #
+ # branch = StateMachines::Branch.new(:parked => :idling, :on => :ignite)
+ #
+ # branch.match(object, :on => :ignite) # => {:to => ..., :from => ..., :on => ...}
+ # branch.match(object, :on => :park) # => nil
+ def match(object, query = {})
+ query.assert_valid_keys(:from, :to, :on, :guard)
+
+ if (match = match_query(query)) && matches_conditions?(object, query)
+ match
+ end
+ end
+
+ def draw(graph, event, valid_states)
+ fail NotImplementedError
+ end
+
+ protected
+ # Builds a matcher strategy to use for the given options. If neither a
+ # whitelist nor a blacklist option is specified, then an AllMatcher is
+ # built.
+ def build_matcher(options, whitelist_option, blacklist_option)
+ options.assert_exclusive_keys(whitelist_option, blacklist_option)
+
+ if options.include?(whitelist_option)
+ value = options[whitelist_option]
+ value.is_a?(Matcher) ? value : WhitelistMatcher.new(options[whitelist_option])
+ elsif options.include?(blacklist_option)
+ value = options[blacklist_option]
+ raise ArgumentError, ":#{blacklist_option} option cannot use matchers; use :#{whitelist_option} instead" if value.is_a?(Matcher)
+ BlacklistMatcher.new(value)
+ else
+ AllMatcher.instance
+ end
+ end
+
+ # Verifies that all configured requirements (event and state) match the
+ # given query. If a match is found, then a hash containing the
+ # event/state requirements that passed will be returned; otherwise, nil.
+ def match_query(query)
+ query ||= {}
+
+ if match_event(query) && (state_requirement = match_states(query))
+ state_requirement.merge(:on => event_requirement)
+ end
+ end
+
+ # Verifies that the event requirement matches the given query
+ def match_event(query)
+ matches_requirement?(query, :on, event_requirement)
+ end
+
+ # Verifies that the state requirements match the given query. If a
+ # matching requirement is found, then it is returned.
+ def match_states(query)
+ state_requirements.detect do |state_requirement|
+ [:from, :to].all? {|option| matches_requirement?(query, option, state_requirement[option])}
+ end
+ end
+
+ # Verifies that an option in the given query matches the values required
+ # for that option
+ def matches_requirement?(query, option, requirement)
+ !query.include?(option) || requirement.matches?(query[option], query)
+ end
+
+ # Verifies that the conditionals for this branch evaluate to true for the
+ # given object
+ def matches_conditions?(object, query)
+ query[:guard] == false ||
+ Array(if_condition).all? {|condition| evaluate_method(object, condition)} &&
+ !Array(unless_condition).any? {|condition| evaluate_method(object, condition)}
+ end
+ end
+end
diff --git a/lib/state_machines/callback.rb b/lib/state_machines/callback.rb
new file mode 100644
index 0000000..baefc62
--- /dev/null
+++ b/lib/state_machines/callback.rb
@@ -0,0 +1,220 @@
+require 'state_machines/branch'
+require 'state_machines/eval_helpers'
+
+module StateMachines
+ # Callbacks represent hooks into objects that allow logic to be triggered
+ # before, after, or around a specific set of transitions.
+ class Callback
+ include EvalHelpers
+
+ class << self
+ # Determines whether to automatically bind the callback to the object
+ # being transitioned. This only applies to callbacks that are defined as
+ # lambda blocks (or Procs). Some integrations, such as DataMapper, handle
+ # callbacks by executing them bound to the object involved, while other
+ # integrations, such as ActiveRecord, pass the object as an argument to
+ # the callback. This can be configured on an application-wide basis by
+ # setting this configuration to +true+ or +false+. The default value
+ # is +false+.
+ #
+ # *Note* that the DataMapper and Sequel integrations automatically
+ # configure this value on a per-callback basis, so it does not have to
+ # be enabled application-wide.
+ #
+ # == Examples
+ #
+ # When not bound to the object:
+ #
+ # class Vehicle
+ # state_machine do
+ # before_transition do |vehicle|
+ # vehicle.set_alarm
+ # end
+ # end
+ #
+ # def set_alarm
+ # ...
+ # end
+ # end
+ #
+ # When bound to the object:
+ #
+ # StateMachines::Callback.bind_to_object = true
+ #
+ # class Vehicle
+ # state_machine do
+ # before_transition do
+ # self.set_alarm
+ # end
+ # end
+ #
+ # def set_alarm
+ # ...
+ # end
+ # end
+ attr_accessor :bind_to_object
+
+ # The application-wide terminator to use for callbacks when not
+ # explicitly defined. Terminators determine whether to cancel a
+ # callback chain based on the return value of the callback.
+ #
+ # See StateMachines::Callback#terminator for more information.
+ attr_accessor :terminator
+ end
+
+ # The type of callback chain this callback is for. This can be one of the
+ # following:
+ # * +before+
+ # * +after+
+ # * +around+
+ # * +failure+
+ attr_accessor :type
+
+ # An optional block for determining whether to cancel the callback chain
+ # based on the return value of the callback. By default, the callback
+ # chain never cancels based on the return value (i.e. there is no implicit
+ # terminator). Certain integrations, such as ActiveRecord and Sequel,
+ # change this default value.
+ #
+ # == Examples
+ #
+ # Canceling the callback chain without a terminator:
+ #
+ # class Vehicle
+ # state_machine do
+ # before_transition do |vehicle|
+ # throw :halt
+ # end
+ # end
+ # end
+ #
+ # Canceling the callback chain with a terminator value of +false+:
+ #
+ # class Vehicle
+ # state_machine do
+ # before_transition do |vehicle|
+ # false
+ # end
+ # end
+ # end
+ attr_reader :terminator
+
+ # The branch that determines whether or not this callback can be invoked
+ # based on the context of the transition. The event, from state, and
+ # to state must all match in order for the branch to pass.
+ #
+ # See StateMachines::Branch for more information.
+ attr_reader :branch
+
+ # Creates a new callback that can get called based on the configured
+ # options.
+ #
+ # In addition to the possible configuration options for branches, the
+ # following options can be configured:
+ # * <tt>:bind_to_object</tt> - Whether to bind the callback to the object involved.
+ # If set to false, the object will be passed as a parameter instead.
+ # Default is integration-specific or set to the application default.
+ # * <tt>:terminator</tt> - A block/proc that determines what callback
+ # results should cause the callback chain to halt (if not using the
+ # default <tt>throw :halt</tt> technique).
+ #
+ # More information about how those options affect the behavior of the
+ # callback can be found in their attribute definitions.
+ def initialize(type, *args, &block)
+ @type = type
+ raise ArgumentError, 'Type must be :before, :after, :around, or :failure' unless [:before, :after, :around, :failure].include?(type)
+
+ options = args.last.is_a?(Hash) ? args.pop : {}
+ @methods = args
+ @methods.concat(Array(options.delete(:do)))
+ @methods << block if block_given?
+ raise ArgumentError, 'Method(s) for callback must be specified' unless @methods.any?
+
+ options = {:bind_to_object => self.class.bind_to_object, :terminator => self.class.terminator}.merge(options)
+
+ # Proxy lambda blocks so that they're bound to the object
+ bind_to_object = options.delete(:bind_to_object)
+ @methods.map! do |method|
+ bind_to_object && method.is_a?(Proc) ? bound_method(method) : method
+ end
+
+ @terminator = options.delete(:terminator)
+ @branch = Branch.new(options)
+ end
+
+ # Gets a list of the states known to this callback by looking at the
+ # branch's known states
+ def known_states
+ branch.known_states
+ end
+
+ # Runs the callback as long as the transition context matches the branch
+ # requirements configured for this callback. If a block is provided, it
+ # will be called when the last method has run.
+ #
+ # If a terminator has been configured and it matches the result from the
+ # evaluated method, then the callback chain should be halted.
+ def call(object, context = {}, *args, &block)
+ if @branch.matches?(object, context)
+ run_methods(object, context, 0, *args, &block)
+ true
+ else
+ false
+ end
+ end
+
+ private
+ # Runs all of the methods configured for this callback.
+ #
+ # When running +around+ callbacks, this will evaluate each method and
+ # yield when the last method has yielded. The callback will only halt if
+ # one of the methods does not yield.
+ #
+ # For all other types of callbacks, this will evaluate each method in
+ # order. The callback will only halt if the resulting value from the
+ # method passes the terminator.
+ def run_methods(object, context = {}, index = 0, *args, &block)
+ if type == :around
+ current_method = @methods[index]
+ if current_method
+ yielded = false
+ evaluate_method(object, current_method, *args) do
+ yielded = true
+ run_methods(object, context, index + 1, *args, &block)
+ end
+
+ throw :halt unless yielded
+ else
+ yield if block_given?
+ end
+ else
+ @methods.each do |method|
+ result = evaluate_method(object, method, *args)
+ throw :halt if @terminator && @terminator.call(result)
+ end
+ end
+ end
+
+ # Generates a method that can be bound to the object being transitioned
+ # when the callback is invoked
+ def bound_method(block)
+ type = self.type
+ arity = block.arity
+ arity += 1 if arity >= 0 # Make sure the object gets passed
+ arity += 1 if arity == 1 && type == :around # Make sure the block gets passed
+
+ method = lambda { |object, *args| object.instance_exec(*args, &block) }
+
+
+ # Proxy arity to the original block
+ (
+ class << method;
+ self;
+ end).class_eval do
+ define_method(:arity) { arity }
+ end
+
+ method
+ end
+ end
+end
diff --git a/lib/state_machines/core.rb b/lib/state_machines/core.rb
new file mode 100644
index 0000000..1bca6a1
--- /dev/null
+++ b/lib/state_machines/core.rb
@@ -0,0 +1,43 @@
+# Load all of the core implementation required to use state_machine. This
+# includes:
+# * StateMachines::MacroMethods which adds the state_machine DSL to your class
+# * A set of initializers for setting state_machine defaults based on the current
+# running environment (such as within Rails)
+require 'state_machines/assertions'
+require 'state_machines/error'
+
+require 'state_machines/extensions'
+
+require 'state_machines/integrations'
+require 'state_machines/integrations/base'
+
+require 'state_machines/eval_helpers'
+
+require 'singleton'
+require 'state_machines/matcher'
+require 'state_machines/matcher_helpers'
+
+require 'state_machines/transition'
+require 'state_machines/transition_collection'
+
+require 'state_machines/branch'
+
+require 'state_machines/helper_module'
+require 'state_machines/state'
+require 'state_machines/callback'
+require 'state_machines/node_collection'
+
+require 'state_machines/state_context'
+require 'state_machines/state'
+require 'state_machines/state_collection'
+
+require 'state_machines/event'
+require 'state_machines/event_collection'
+
+require 'state_machines/path'
+require 'state_machines/path_collection'
+
+require 'state_machines/machine'
+require 'state_machines/machine_collection'
+
+require 'state_machines/macro_methods'
\ No newline at end of file
diff --git a/lib/state_machines/core_ext.rb b/lib/state_machines/core_ext.rb
new file mode 100644
index 0000000..82a1ffe
--- /dev/null
+++ b/lib/state_machines/core_ext.rb
@@ -0,0 +1,2 @@
+# Loads all of the extensions to be made to Ruby core classes
+require 'state_machines/core_ext/class/state_machine'
diff --git a/lib/state_machines/core_ext/class/state_machine.rb b/lib/state_machines/core_ext/class/state_machine.rb
new file mode 100644
index 0000000..9afb3e7
--- /dev/null
+++ b/lib/state_machines/core_ext/class/state_machine.rb
@@ -0,0 +1,3 @@
+Class.class_eval do
+ include StateMachines::MacroMethods
+end
diff --git a/lib/state_machines/error.rb b/lib/state_machines/error.rb
new file mode 100644
index 0000000..e2c5cb7
--- /dev/null
+++ b/lib/state_machines/error.rb
@@ -0,0 +1,112 @@
+module StateMachines
+ # An error occurred during a state machine invocation
+ class Error < StandardError
+ # The object that failed
+ attr_reader :object
+
+ def initialize(object, message = nil) #:nodoc:
+ @object = object
+
+ super(message)
+ end
+ end
+
+ # An invalid integration was specified
+ class IntegrationNotFound < Error
+ def initialize(name)
+ super(nil, "#{name.inspect} is an invalid integration. #{error_message}")
+ end
+
+ def valid_integrations
+ "Valid integrations are: #{valid_integrations_name}"
+ end
+
+ def valid_integrations_name
+ Integrations.list.collect(&:integration_name)
+ end
+
+ def no_integrations
+ 'No integrations registered'
+ end
+
+ def error_message
+ if Integrations.list.size.zero?
+ no_integrations
+ else
+ valid_integrations
+ end
+ end
+ end
+
+ # An invalid integration was registered
+ class IntegrationError < StandardError
+ end
+
+ # An invalid event was specified
+ class InvalidEvent < Error
+ # The event that was attempted to be run
+ attr_reader :event
+
+ def initialize(object, event_name) #:nodoc:
+ @event = event_name
+
+ super(object, "#{event.inspect} is an unknown state machine event")
+ end
+ end
+ # An invalid transition was attempted
+ class InvalidTransition < Error
+ # The machine attempting to be transitioned
+ attr_reader :machine
+
+ # The current state value for the machine
+ attr_reader :from
+
+ def initialize(object, machine, event) #:nodoc:
+ @machine = machine
+ @from_state = machine.states.match!(object)
+ @from = machine.read(object, :state)
+ @event = machine.events.fetch(event)
+ errors = machine.errors_for(object)
+
+ message = "Cannot transition #{machine.name} via :#{self.event} from #{from_name.inspect}"
+ message << " (Reason(s): #{errors})" unless errors.empty?
+ super(object, message)
+ end
+
+ # The event that triggered the failed transition
+ def event
+ @event.name
+ end
+
+ # The fully-qualified name of the event that triggered the failed transition
+ def qualified_event
+ @event.qualified_name
+ end
+
+ # The name for the current state
+ def from_name
+ @from_state.name
+ end
+
+ # The fully-qualified name for the current state
+ def qualified_from_name
+ @from_state.qualified_name
+ end
+ end
+
+ # A set of transition failed to run in parallel
+ class InvalidParallelTransition < Error
+ # The set of events that failed the transition(s)
+ attr_reader :events
+
+ def initialize(object, events) #:nodoc:
+ @events = events
+
+ super(object, "Cannot run events in parallel: #{events * ', '}")
+ end
+ end
+
+ # A method was called in an invalid state context
+ class InvalidContext < Error
+ end
+end
diff --git a/lib/state_machines/eval_helpers.rb b/lib/state_machines/eval_helpers.rb
new file mode 100644
index 0000000..feb0ced
--- /dev/null
+++ b/lib/state_machines/eval_helpers.rb
@@ -0,0 +1,87 @@
+module StateMachines
+ # Provides a set of helper methods for evaluating methods within the context
+ # of an object.
+ module EvalHelpers
+ # Evaluates one of several different types of methods within the context
+ # of the given object. Methods can be one of the following types:
+ # * Symbol
+ # * Method / Proc
+ # * String
+ #
+ # == Examples
+ #
+ # Below are examples of the various ways that a method can be evaluated
+ # on an object:
+ #
+ # class Person
+ # def initialize(name)
+ # @name = name
+ # end
+ #
+ # def name
+ # @name
+ # end
+ # end
+ #
+ # class PersonCallback
+ # def self.run(person)
+ # person.name
+ # end
+ # end
+ #
+ # person = Person.new('John Smith')
+ #
+ # evaluate_method(person, :name) # => "John Smith"
+ # evaluate_method(person, PersonCallback.method(:run)) # => "John Smith"
+ # evaluate_method(person, Proc.new {|person| person.name}) # => "John Smith"
+ # evaluate_method(person, lambda {|person| person.name}) # => "John Smith"
+ # evaluate_method(person, '@name') # => "John Smith"
+ #
+ # == Additional arguments
+ #
+ # Additional arguments can be passed to the methods being evaluated. If
+ # the method defines additional arguments other than the object context,
+ # then all arguments are required.
+ #
+ # For example,
+ #
+ # person = Person.new('John Smith')
+ #
+ # evaluate_method(person, lambda {|person| person.name}, 21) # => "John Smith"
+ # evaluate_method(person, lambda {|person, age| "#{person.name} is #{age}"}, 21) # => "John Smith is 21"
+ # evaluate_method(person, lambda {|person, age| "#{person.name} is #{age}"}, 21, 'male') # => ArgumentError: wrong number of arguments (3 for 2)
+ def evaluate_method(object, method, *args, &block)
+ case method
+ when Symbol
+ klass = (class << object; self; end)
+ args = [] if (klass.method_defined?(method) || klass.private_method_defined?(method)) && object.method(method).arity == 0
+ object.send(method, *args, &block)
+ when Proc, Method
+ args.unshift(object)
+ arity = method.arity
+
+ # Procs don't support blocks in < Ruby 1.9, so it's tacked on as an
+ # argument for consistency across versions of Ruby
+ if block_given? && Proc === method && arity != 0
+ if [1, 2].include?(arity)
+ # Force the block to be either the only argument or the 2nd one
+ # after the object (may mean additional arguments get discarded)
+ args = args[0, arity - 1] + [block]
+ else
+ # Tack the block to the end of the args
+ args << block
+ end
+ else
+ # These method types are only called with 0, 1, or n arguments
+ args = args[0, arity] if [0, 1].include?(arity)
+ end
+
+ method.is_a?(Proc) ? method.call(*args) : method.call(*args, &block)
+ when String
+ eval(method, object.instance_eval {binding}, &block)
+ else
+ raise ArgumentError, 'Methods must be a symbol denoting the method to call, a block to be invoked, or a string to be evaluated'
+ end
+ end
+ end
+end
diff --git a/lib/state_machines/event.rb b/lib/state_machines/event.rb
new file mode 100644
index 0000000..6f9b27f
--- /dev/null
+++ b/lib/state_machines/event.rb
@@ -0,0 +1,228 @@
+module StateMachines
+ # An event defines an action that transitions an attribute from one state to
+ # another. The state that an attribute is transitioned to depends on the
+ # branches configured for the event.
+ class Event
+
+ include MatcherHelpers
+
+ # The state machine for which this event is defined
+ attr_accessor :machine
+
+ # The name of the event
+ attr_reader :name
+
+ # The fully-qualified name of the event, scoped by the machine's namespace
+ attr_reader :qualified_name
+
+ # The human-readable name for the event
+ attr_writer :human_name
+
+ # The list of branches that determine what state this event transitions
+ # objects to when fired
+ attr_reader :branches
+
+ # A list of all of the states known to this event using the configured
+ # branches/transitions as the source
+ attr_reader :known_states
+
+ # Creates a new event within the context of the given machine
+ #
+ # Configuration options:
+ # * <tt>:human_name</tt> - The human-readable version of this event's name
+ def initialize(machine, name, options = {}) #:nodoc:
+ options.assert_valid_keys(:human_name)
+
+ @machine = machine
+ @name = name
+ @qualified_name = machine.namespace ? :"#{name}_#{machine.namespace}" : name
+ @human_name = options[:human_name] || @name.to_s.tr('_', ' ')
+ reset
+
+ # Output a warning if another event has a conflicting qualified name
+ if conflict = machine.owner_class.state_machines.detect { |_other_name, other_machine| other_machine != @machine && other_machine.events[qualified_name, :qualified_name] }
+ _name, other_machine = conflict
+ warn "Event #{qualified_name.inspect} for #{machine.name.inspect} is already defined in #{other_machine.name.inspect}"
+ else
+ add_actions
+ end
+ end
+
+ # Creates a copy of this event in addition to the list of associated
+ # branches to prevent conflicts across events within a class hierarchy.
+ def initialize_copy(orig) #:nodoc:
+ super
+ @branches = @branches.dup
+ @known_states = @known_states.dup
+ end
+
+ # Transforms the event name into a more human-readable format, such as
+ # "turn on" instead of "turn_on"
+ def human_name(klass = @machine.owner_class)
+ @human_name.is_a?(Proc) ? @human_name.call(self, klass) : @human_name
+ end
+
+ # Evaluates the given block within the context of this event. This simply
+ # provides a DSL-like syntax for defining transitions.
+ def context(&block)
+ instance_eval(&block)
+ end
+
+ # Creates a new transition that determines what to change the current state
+ # to when this event fires.
+ #
+ # Since this transition is being defined within an event context, you do
+ # *not* need to specify the <tt>:on</tt> option for the transition. For
+ # example:
+ #
+ # state_machine do
+ # event :ignite do
+ # transition :parked => :idling, :idling => same, :if => :seatbelt_on? # Transitions to :idling if seatbelt is on
+ # transition all => :parked, :unless => :seatbelt_on? # Transitions to :parked if seatbelt is off
+ # end
+ # end
+ #
+ # See StateMachines::Machine#transition for a description of the possible
+ # configurations for defining transitions.
+ def transition(options)
+ raise ArgumentError, 'Must specify as least one transition requirement' if options.empty?
+
+ # Only a certain subset of explicit options are allowed for transition
+ # requirements
+ options.assert_valid_keys(:from, :to, :except_from, :except_to, :if, :unless) if (options.keys - [:from, :to, :on, :except_from, :except_to, :except_on, :if, :unless]).empty?
+
+ branches << branch = Branch.new(options.merge(:on => name))
+ @known_states |= branch.known_states
+ branch
+ end
+
+ # Determines whether any transitions can be performed for this event based
+ # on the current state of the given object.
+ #
+ # If the event can't be fired, then this will return false, otherwise true.
+ #
+ # *Note* that this will not take the object context into account. Although
+ # a transition may be possible based on the state machine definition,
+ # object-specific behaviors (like validations) may prevent it from firing.
+ def can_fire?(object, requirements = {})
+ !transition_for(object, requirements).nil?
+ end
+
+ # Finds and builds the next transition that can be performed on the given
+ # object. If no transitions can be made, then this will return nil.
+ #
+ # Valid requirement options:
+ # * <tt>:from</tt> - One or more states being transitioned from. If none
+ # are specified, then this will be the object's current state.
+ # * <tt>:to</tt> - One or more states being transitioned to. If none are
+ # specified, then this will match any to state.
+ # * <tt>:guard</tt> - Whether to guard transitions with the if/unless
+ # conditionals defined for each one. Default is true.
+ def transition_for(object, requirements = {})
+ requirements.assert_valid_keys(:from, :to, :guard)
+ requirements[:from] = machine.states.match!(object).name unless custom_from_state = requirements.include?(:from)
+
+ branches.each do |branch|
+ if match = branch.match(object, requirements)
+ # Branch allows for the transition to occur
+ from = requirements[:from]
+ to = if match[:to].is_a?(LoopbackMatcher)
+ from
+ else
+ values = requirements.include?(:to) ? [requirements[:to]].flatten : [from] | machine.states.map { |state| state.name }
+
+ match[:to].filter(values).first
+ end
+
+ return Transition.new(object, machine, name, from, to, !custom_from_state)
+ end
+ end
+
+ # No transition matched
+ nil
+ end
+
+ # Attempts to perform the next available transition on the given object.
+ # If no transitions can be made, then this will return false, otherwise
+ # true.
+ #
+ # Any additional arguments are passed to the StateMachines::Transition#perform
+ # instance method.
+ def fire(object, *args)
+ machine.reset(object)
+
+ if transition = transition_for(object)
+ transition.perform(*args)
+ else
+ on_failure(object)
+ false
+ end
+ end
+
+ # Marks the object as invalid and runs any failure callbacks associated with
+ # this event. This should get called anytime this event fails to transition.
+ def on_failure(object)
+ state = machine.states.match!(object)
+ machine.invalidate(object, :state, :invalid_transition, [[:event, human_name(object.class)], [:state, state.human_name(object.class)]])
+
+ Transition.new(object, machine, name, state.name, state.name).run_callbacks(:before => false)
+ end
+
+ # Resets back to the initial state of the event, with no branches / known
+ # states associated. This allows you to redefine an event in situations
+ # where you either are re-using an existing state machine implementation
+ # or are subclassing machines.
+ def reset
+ @branches = []
+ @known_states = []
+ end
+
+
+ def draw(graph, options = {})
+ fail NotImplementedError
+ end
+
+ # Generates a nicely formatted description of this event's contents.
+ #
+ # For example,
+ #
+ # event = StateMachines::Event.new(machine, :park)
+ # event.transition all - :idling => :parked, :idling => same
+ # event # => #<StateMachines::Event name=:park transitions=[all - :idling => :parked, :idling => same]>
+ def inspect
+ transitions = branches.map do |branch|
+ branch.state_requirements.map do |state_requirement|
+ "#{state_requirement[:from].description} => #{state_requirement[:to].description}"
+ end * ', '
+ end
+
+ "#<#{self.class} name=#{name.inspect} transitions=[#{transitions * ', '}]>"
+ end
+
+ protected
+ # Add the various instance methods that can transition the object using
+ # the current event
+ def add_actions
+ # Checks whether the event can be fired on the current object
+ machine.define_helper(:instance, "can_#{qualified_name}?") do |machine, object, *args|
+ machine.event(name).can_fire?(object, *args)
+ end
+
+ # Gets the next transition that would be performed if the event were
+ # fired now
+ machine.define_helper(:instance, "#{qualified_name}_transition") do |machine, object, *args|
+ machine.event(name).transition_for(object, *args)
+ end
+
+ # Fires the event
+ machine.define_helper(:instance, qualified_name) do |machine, object, *args|
+ machine.event(name).fire(object, *args)
+ end
+
+ # Fires the event, raising an exception if it fails
+ machine.define_helper(:instance, "#{qualified_name}!") do |machine, object, *args|
+ object.send(qualified_name, *args) || raise(StateMachines::InvalidTransition.new(object, machine, name))
+ end
+ end
+ end
+end
diff --git a/lib/state_machines/event_collection.rb b/lib/state_machines/event_collection.rb
new file mode 100644
index 0000000..90b5cb2
--- /dev/null
+++ b/lib/state_machines/event_collection.rb
@@ -0,0 +1,139 @@
+module StateMachines
+ # Represents a collection of events in a state machine
+ class EventCollection < NodeCollection
+ def initialize(machine) #:nodoc:
+ super(machine, :index => [:name, :qualified_name])
+ end
+
+ # Gets the list of events that can be fired on the given object.
+ #
+ # Valid requirement options:
+ # * <tt>:from</tt> - One or more states being transitioned from. If none
+ # are specified, then this will be the object's current state.
+ # * <tt>:to</tt> - One or more states being transitioned to. If none are
+ # specified, then this will match any to state.
+ # * <tt>:on</tt> - One or more events that fire the transition. If none
+ # are specified, then this will match any event.
+ # * <tt>:guard</tt> - Whether to guard transitions with the if/unless
+ # conditionals defined for each one. Default is true.
+ #
+ # == Examples
+ #
+ # class Vehicle
+ # state_machine :initial => :parked do
+ # event :park do
+ # transition :idling => :parked
+ # end
+ #
+ # event :ignite do
+ # transition :parked => :idling
+ # end
+ # end
+ # end
+ #
+ # events = Vehicle.state_machine(:state).events
+ #
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
+ # events.valid_for(vehicle) # => [#<StateMachines::Event name=:ignite transitions=[:parked => :idling]>]
+ #
+ # vehicle.state = 'idling'
+ # events.valid_for(vehicle) # => [#<StateMachines::Event name=:park transitions=[:idling => :parked]>]
+ def valid_for(object, requirements = {})
+ match(requirements).select { |event| event.can_fire?(object, requirements) }
+ end
+
+ # Gets the list of transitions that can be run on the given object.
+ #
+ # Valid requirement options:
+ # * <tt>:from</tt> - One or more states being transitioned from. If none
+ # are specified, then this will be the object's current state.
+ # * <tt>:to</tt> - One or more states being transitioned to. If none are
+ # specified, then this will match any to state.
+ # * <tt>:on</tt> - One or more events that fire the transition. If none
+ # are specified, then this will match any event.
+ # * <tt>:guard</tt> - Whether to guard transitions with the if/unless
+ # conditionals defined for each one. Default is true.
+ #
+ # == Examples
+ #
+ # class Vehicle
+ # state_machine :initial => :parked do
+ # event :park do
+ # transition :idling => :parked
+ # end
+ #
+ # event :ignite do
+ # transition :parked => :idling
+ # end
+ # end
+ # end
+ #
+ # events = Vehicle.state_machine.events
+ #
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
+ # events.transitions_for(vehicle) # => [#<StateMachines::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
+ #
+ # vehicle.state = 'idling'
+ # events.transitions_for(vehicle) # => [#<StateMachines::Transition attribute=:state event=:park from="idling" from_name=:idling to="parked" to_name=:parked>]
+ #
+ # # Search for explicit transitions regardless of the current state
+ # events.transitions_for(vehicle, :from => :parked) # => [#<StateMachines::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
+ def transitions_for(object, requirements = {})
+ match(requirements).map { |event| event.transition_for(object, requirements) }.compact
+ end
+
+ # Gets the transition that should be performed for the event stored in the
+ # given object's event attribute. This also takes an additional parameter
+ # for automatically invalidating the object if the event or transition are
+ # invalid. By default, this is turned off.
+ #
+ # *Note* that if a transition has already been generated for the event, then
+ # that transition will be used.
+ #
+ # == Examples
+ #
+ # class Vehicle < ActiveRecord::Base
+ # state_machine :initial => :parked do
+ # event :ignite do
+ # transition :parked => :idling
+ # end
+ # end
+ # end
+ #
+ # vehicle = Vehicle.new # => #<Vehicle id: nil, state: "parked">
+ # events = Vehicle.state_machine.events
+ #
+ # vehicle.state_event = nil
+ # events.attribute_transition_for(vehicle) # => nil # Event isn't defined
+ #
+ # vehicle.state_event = 'invalid'
+ # events.attribute_transition_for(vehicle) # => false # Event is invalid
+ #
+ # vehicle.state_event = 'ignite'
+ # events.attribute_transition_for(vehicle) # => #<StateMachines::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>
+ def attribute_transition_for(object, invalidate = false)
+ return unless machine.action
+
+ # TODO, simplify
+ machine.read(object, :event_transition) || if event_name = machine.read(object, :event)
+ if event = self[event_name.to_sym, :name]
+ event.transition_for(object) || begin
+ # No valid transition: invalidate
+ machine.invalidate(object, :event, :invalid_event, [[:state, machine.states.match!(object).human_name(object.class)]]) if invalidate
+ false
+ end
+ else
+ # Event is unknown: invalidate
+ machine.invalidate(object, :event, :invalid) if invalidate
+ false
+ end
+ end
+
+ end
+
+ private
+ def match(requirements) #:nodoc:
+ requirements && requirements[:on] ? [fetch(requirements.delete(:on))] : self
+ end
+ end
+end
diff --git a/lib/state_machines/extensions.rb b/lib/state_machines/extensions.rb
new file mode 100644
index 0000000..08f8c85
--- /dev/null
+++ b/lib/state_machines/extensions.rb
@@ -0,0 +1,148 @@
+module StateMachines
+ module ClassMethods
+ def self.extended(base) #:nodoc:
+ base.class_eval do
+ @state_machines = MachineCollection.new
+ end
+ end
+
+ # Gets the current list of state machines defined for this class. This
+ # class-level attribute acts like an inheritable attribute. The attribute
+ # is available to each subclass, each having a copy of its superclass's
+ # attribute.
+ #
+ # The hash of state machines maps <tt>:attribute</tt> => +machine+, e.g.
+ #
+ # Vehicle.state_machines # => {:state => #<StateMachines::Machine:0xb6f6e4a4 ...>}
+ def state_machines
+ @state_machines ||= superclass.state_machines.dup
+ end
+ end
+
+ module InstanceMethods
+ # Runs one or more events in parallel. All events will run through the
+ # following steps:
+ # * Before callbacks
+ # * Persist state
+ # * Invoke action
+ # * After callbacks
+ #
+ # For example, if two events (for state machines A and B) are run in
+ # parallel, the order in which steps are run is:
+ # * A - Before transition callbacks
+ # * B - Before transition callbacks
+ # * A - Persist new state
+ # * B - Persist new state
+ # * A - Invoke action
+ # * B - Invoke action (only if different than A's action)
+ # * A - After transition callbacks
+ # * B - After transition callbacks
+ #
+ # *Note* that multiple events on the same state machine / attribute cannot
+ # be run in parallel. If this is attempted, an ArgumentError will be
+ # raised.
+ #
+ # == Halting callbacks
+ #
+ # When running multiple events in parallel, special consideration should
+ # be taken with regard to how halting within callbacks affects the flow.
+ #
+ # For *before* callbacks, any <tt>:halt</tt> error that's thrown will
+ # immediately cancel the perform for all transitions. As a result, it's
+ # possible for one event's transition to affect the continuation of
+ # another.
+ #
+ # On the other hand, any <tt>:halt</tt> error that's thrown within an
+ # *after* callback with only affect that event's transition. Other
+ # transitions will continue to run their own callbacks.
+ #
+ # == Example
+ #
+ # class Vehicle
+ # state_machine :initial => :parked do
+ # event :ignite do
+ # transition :parked => :idling
+ # end
+ #
+ # event :park do
+ # transition :idling => :parked
+ # end
+ # end
+ #
+ # state_machine :alarm_state, :namespace => 'alarm', :initial => :on do
+ # event :enable do
+ # transition all => :active
+ # end
+ #
+ # event :disable do
+ # transition all => :off
+ # end
+ # end
+ # end
+ #
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c02850 @state="parked", @alarm_state="active">
+ # vehicle.state # => "parked"
+ # vehicle.alarm_state # => "active"
+ #
+ # vehicle.fire_events(:ignite, :disable_alarm) # => true
+ # vehicle.state # => "idling"
+ # vehicle.alarm_state # => "off"
+ #
+ # # If any event fails, the entire event chain fails
+ # vehicle.fire_events(:ignite, :enable_alarm) # => false
+ # vehicle.state # => "idling"
+ # vehicle.alarm_state # => "off"
+ #
+ # # Exception raised on invalid event
+ # vehicle.fire_events(:park, :invalid) # => StateMachines::InvalidEvent: :invalid is an unknown event
+ # vehicle.state # => "idling"
+ # vehicle.alarm_state # => "off"
+ def fire_events(*events)
+ self.class.state_machines.fire_events(self, *events)
+ end
+
+ # Run one or more events in parallel. If any event fails to run, then
+ # a StateMachines::InvalidTransition exception will be raised.
+ #
+ # See StateMachines::InstanceMethods#fire_events for more information.
+ #
+ # == Example
+ #
+ # class Vehicle
+ # state_machine :initial => :parked do
+ # event :ignite do
+ # transition :parked => :idling
+ # end
+ #
+ # event :park do
+ # transition :idling => :parked
+ # end
+ # end
+ #
+ # state_machine :alarm_state, :namespace => 'alarm', :initial => :active do
+ # event :enable do
+ # transition all => :active
+ # end
+ #
+ # event :disable do
+ # transition all => :off
+ # end
+ # end
+ # end
+ #
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c02850 @state="parked", @alarm_state="active">
+ # vehicle.fire_events(:ignite, :disable_alarm) # => true
+ #
+ # vehicle.fire_events!(:ignite, :disable_alarm) # => StateMachines::InvalidTranstion: Cannot run events in parallel: ignite, disable_alarm
+ def fire_events!(*events)
+ run_action = [true, false].include?(events.last) ? events.pop : true
+ fire_events(*(events + [run_action])) || fail(StateMachines::InvalidParallelTransition.new(self, events))
+ end
+
+ protected
+
+ def initialize_state_machines(options = {}, &block) #:nodoc:
+ self.class.state_machines.initialize_states(self, options, &block)
+ end
+ end
+end
diff --git a/lib/state_machines/helper_module.rb b/lib/state_machines/helper_module.rb
new file mode 100644
index 0000000..e7c3e89
--- /dev/null
+++ b/lib/state_machines/helper_module.rb
@@ -0,0 +1,17 @@
+module StateMachines
+ # Represents a type of module that defines instance / class methods for a
+ # state machine
+ class HelperModule < Module #:nodoc:
+ def initialize(machine, kind)
+ @machine = machine
+ @kind = kind
+ end
+
+ # Provides a human-readable description of the module
+ def to_s
+ owner_class = @machine.owner_class
+ owner_class_name = owner_class.name && !owner_class.name.empty? ? owner_class.name : owner_class.to_s
+ "#{owner_class_name} #{@machine.name.inspect} #{@kind} helpers"
+ end
+ end
+end
diff --git a/lib/state_machines/integrations.rb b/lib/state_machines/integrations.rb
new file mode 100644
index 0000000..e271bc8
--- /dev/null
+++ b/lib/state_machines/integrations.rb
@@ -0,0 +1,116 @@
+require 'set'
+
+module StateMachines
+ # Integrations allow state machines to take advantage of features within the
+ # context of a particular library. This is currently most useful with
+ # database libraries. For example, the various database integrations allow
+ # state machines to hook into features like:
+ # * Saving
+ # * Transactions
+ # * Observers
+ # * Scopes
+ # * Callbacks
+ # * Validation errors
+ #
+ # This type of integration allows the user to work with state machines in a
+ # fashion similar to other object models in their application.
+ #
+ # The integration interface is loosely defined by various unimplemented
+ # methods in the StateMachines::Machine class. See that class or the various
+ # built-in integrations for more information about how to define additional
+ # integrations.
+ module Integrations
+ @integrations = []
+
+ class << self
+ # Register integration
+ def register(name_or_module)
+ case name_or_module.class.to_s
+ when 'Module'
+ add(name_or_module)
+ else
+ fail IntegrationError
+ end
+ true
+ end
+
+ def reset #:nodoc:#
+ @integrations = []
+ end
+
+ # Gets a list of all of the available integrations for use.
+ #
+ # == Example
+ #
+ # StateMachines::Integrations.integrations
+ # # => []
+ # StateMachines::Integrations.register(StateMachines::Integrations::ActiveModel)
+ # StateMachines::Integrations.integrations
+ # # => [StateMachines::Integrations::ActiveModel]
+ def integrations
+ # Register all namespaced integrations
+ @integrations
+ end
+
+ alias_method :list, :integrations
+
+
+ # Attempts to find an integration that matches the given class. This will
+ # look through all of the built-in integrations under the StateMachines::Integrations
+ # namespace and find one that successfully matches the class.
+ #
+ # == Examples
+ #
+ # class Vehicle
+ # end
+ #
+ # class ActiveModelVehicle
+ # include ActiveModel::Observing
+ # include ActiveModel::Validations
+ # end
+ #
+ # class ActiveRecordVehicle < ActiveRecord::Base
+ # end
+ #
+ # StateMachines::Integrations.match(Vehicle) # => nil
+ # StateMachines::Integrations.match(ActiveModelVehicle) # => StateMachines::Integrations::ActiveModel
+ # StateMachines::Integrations.match(ActiveRecordVehicle) # => StateMachines::Integrations::ActiveRecord
+ def match(klass)
+ integrations.detect { |integration| integration.matches?(klass) }
+ end
+
+ # Attempts to find an integration that matches the given list of ancestors.
+ # This will look through all of the built-in integrations under the StateMachines::Integrations
+ # namespace and find one that successfully matches one of the ancestors.
+ #
+ # == Examples
+ #
+ # StateMachines::Integrations.match_ancestors([]) # => nil
+ # StateMachines::Integrations.match_ancestors(['ActiveRecord::Base']) # => StateMachines::Integrations::ActiveModel
+ def match_ancestors(ancestors)
+ integrations.detect { |integration| integration.matches_ancestors?(ancestors) }
+ end
+
+ # Finds an integration with the given name. If the integration cannot be
+ # found, then a NameError exception will be raised.
+ #
+ # == Examples
+ #
+ # StateMachines::Integrations.find_by_name(:active_model) # => StateMachines::Integrations::ActiveModel
+ # StateMachines::Integrations.find_by_name(:active_record) # => StateMachines::Integrations::ActiveRecord
+ # StateMachines::Integrations.find_by_name(:invalid) # => StateMachines::IntegrationNotFound: :invalid is an invalid integration
+ def find_by_name(name)
+ integrations.detect { |integration| integration.integration_name == name } || raise(IntegrationNotFound.new(name))
+ end
+
+
+ private
+
+ def add(integration)
+ if integration.respond_to?(:integration_name)
+ @integrations.insert(0, integration) unless @integrations.include?(integration)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/state_machines/integrations/base.rb b/lib/state_machines/integrations/base.rb
new file mode 100644
index 0000000..dbe0324
--- /dev/null
+++ b/lib/state_machines/integrations/base.rb
@@ -0,0 +1,44 @@
+module StateMachines
+ module Integrations
+ # Provides a set of base helpers for managing individual integrations
+ module Base
+ module ClassMethods
+ # The default options to use for state machines using this integration
+ attr_reader :defaults
+
+ # The name of the integration
+ def integration_name
+ @integration_name ||= begin
+ name = self.name.split('::').last
+ name.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
+ name.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
+ name.downcase!
+ name.to_sym
+ end
+ end
+
+ # The list of ancestor names that cause this integration to matched.
+ def matching_ancestors
+ []
+ end
+
+ # Whether the integration should be used for the given class.
+ def matches?(klass)
+ matches_ancestors?(klass.ancestors.map { |ancestor| ancestor.name })
+ end
+
+ # Whether the integration should be used for the given list of ancestors.
+ def matches_ancestors?(ancestors)
+ (ancestors & matching_ancestors).any?
+ end
+
+ end
+
+ extend ClassMethods
+
+ def self.included(base) #:nodoc:
+ base.class_eval { extend ClassMethods }
+ end
+ end
+ end
+end
diff --git a/lib/state_machines/machine.rb b/lib/state_machines/machine.rb
new file mode 100644
index 0000000..44648c6
--- /dev/null
+++ b/lib/state_machines/machine.rb
@@ -0,0 +1,2230 @@
+module StateMachines
+ # Represents a state machine for a particular attribute. State machines
+ # consist of states, events and a set of transitions that define how the
+ # state changes after a particular event is fired.
+ #
+ # A state machine will not know all of the possible states for an object
+ # unless they are referenced *somewhere* in the state machine definition.
+ # As a result, any unused states should be defined with the +other_states+
+ # or +state+ helper.
+ #
+ # == Actions
+ #
+ # When an action is configured for a state machine, it is invoked when an
+ # object transitions via an event. The success of the event becomes
+ # dependent on the success of the action. If the action is successful, then
+ # the transitioned state remains persisted. However, if the action fails
+ # (by returning false), the transitioned state will be rolled back.
+ #
+ # For example,
+ #
+ # class Vehicle
+ # attr_accessor :fail, :saving_state
+ #
+ # state_machine :initial => :parked, :action => :save do
+ # event :ignite do
+ # transition :parked => :idling
+ # end
+ #
+ # event :park do
+ # transition :idling => :parked
+ # end
+ # end
+ #
+ # def save
+ # @saving_state = state
+ # fail != true
+ # end
+ # end
+ #
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c27024 @state="parked">
+ # vehicle.save # => true
+ # vehicle.saving_state # => "parked" # The state was "parked" was save was called
+ #
+ # # Successful event
+ # vehicle.ignite # => true
+ # vehicle.saving_state # => "idling" # The state was "idling" when save was called
+ # vehicle.state # => "idling"
+ #
+ # # Failed event
+ # vehicle.fail = true
+ # vehicle.park # => false
+ # vehicle.saving_state # => "parked"
+ # vehicle.state # => "idling"
+ #
+ # As shown, even though the state is set prior to calling the +save+ action
+ # on the object, it will be rolled back to the original state if the action
+ # fails. *Note* that this will also be the case if an exception is raised
+ # while calling the action.
+ #
+ # === Indirect transitions
+ #
+ # In addition to the action being run as the _result_ of an event, the action
+ # can also be used to run events itself. For example, using the above as an
+ # example:
+ #
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c27024 @state="parked">
+ #
+ # vehicle.state_event = 'ignite'
+ # vehicle.save # => true
+ # vehicle.state # => "idling"
+ # vehicle.state_event # => nil
+ #
+ # As can be seen, the +save+ action automatically invokes the event stored in
+ # the +state_event+ attribute (<tt>:ignite</tt> in this case).
+ #
+ # One important note about using this technique for running transitions is
+ # that if the class in which the state machine is defined *also* defines the
+ # action being invoked (and not a superclass), then it must manually run the
+ # StateMachine hook that checks for event attributes.
+ #
+ # For example, in ActiveRecord, DataMapper, Mongoid, MongoMapper, and Sequel,
+ # the default action (+save+) is already defined in a base class. As a result,
+ # when a state machine is defined in a model / resource, StateMachine can
+ # automatically hook into the +save+ action.
+ #
+ # On the other hand, the Vehicle class from above defined its own +save+
+ # method (and there is no +save+ method in its superclass). As a result, it
+ # must be modified like so:
+ #
+ # def save
+ # self.class.state_machines.transitions(self, :save).perform do
+ # @saving_state = state
+ # fail != true
+ # end
+ # end
+ #
+ # This will add in the functionality for firing the event stored in the
+ # +state_event+ attribute.
+ #
+ # == Callbacks
+ #
+ # Callbacks are supported for hooking before and after every possible
+ # transition in the machine. Each callback is invoked in the order in which
+ # it was defined. See StateMachines::Machine#before_transition and
+ # StateMachines::Machine#after_transition for documentation on how to define
+ # new callbacks.
+ #
+ # *Note* that callbacks only get executed within the context of an event. As
+ # a result, if a class has an initial state when it's created, any callbacks
+ # that would normally get executed when the object enters that state will
+ # *not* get triggered.
+ #
+ # For example,
+ #
+ # class Vehicle
+ # state_machine :initial => :parked do
+ # after_transition all => :parked do
+ # raise ArgumentError
+ # end
+ # ...
+ # end
+ # end
+ #
+ # vehicle = Vehicle.new # => #<Vehicle id: 1, state: "parked">
+ # vehicle.save # => true (no exception raised)
+ #
+ # If you need callbacks to get triggered when an object is created, this
+ # should be done by one of the following techniques:
+ # * Use a <tt>before :create</tt> or equivalent hook:
+ #
+ # class Vehicle
+ # before :create, :track_initial_transition
+ #
+ # state_machine do
+ # ...
+ # end
+ # end
+ #
+ # * Set an initial state and use the correct event to create the
+ # object with the proper state, resulting in callbacks being triggered and
+ # the object getting persisted (note that the <tt>:pending</tt> state is
+ # actually stored as nil):
+ #
+ # class Vehicle
+ # state_machine :initial => :pending
+ # after_transition :pending => :parked, :do => :track_initial_transition
+ #
+ # event :park do
+ # transition :pending => :parked
+ # end
+ #
+ # state :pending, :value => nil
+ # end
+ # end
+ #
+ # vehicle = Vehicle.new
+ # vehicle.park
+ #
+ # * Use a default event attribute that will automatically trigger when the
+ # configured action gets run (note that the <tt>:pending</tt> state is
+ # actually stored as nil):
+ #
+ # class Vehicle < ActiveRecord::Base
+ # state_machine :initial => :pending
+ # after_transition :pending => :parked, :do => :track_initial_transition
+ #
+ # event :park do
+ # transition :pending => :parked
+ # end
+ #
+ # state :pending, :value => nil
+ # end
+ #
+ # def initialize(*)
+ # super
+ # self.state_event = 'park'
+ # end
+ # end
+ #
+ # vehicle = Vehicle.new
+ # vehicle.save
+ #
+ # === Canceling callbacks
+ #
+ # Callbacks can be canceled by throwing :halt at any point during the
+ # callback. For example,
+ #
+ # ...
+ # throw :halt
+ # ...
+ #
+ # If a +before+ callback halts the chain, the associated transition and all
+ # later callbacks are canceled. If an +after+ callback halts the chain,
+ # the later callbacks are canceled, but the transition is still successful.
+ #
+ # These same rules apply to +around+ callbacks with the exception that any
+ # +around+ callback that doesn't yield will essentially result in :halt being
+ # thrown. Any code executed after the yield will behave in the same way as
+ # +after+ callbacks.
+ #
+ # *Note* that if a +before+ callback fails and the bang version of an event
+ # was invoked, an exception will be raised instead of returning false. For
+ # example,
+ #
+ # class Vehicle
+ # state_machine :initial => :parked do
+ # before_transition any => :idling, :do => lambda {|vehicle| throw :halt}
+ # ...
+ # end
+ # end
+ #
+ # vehicle = Vehicle.new
+ # vehicle.park # => false
+ # vehicle.park! # => StateMachines::InvalidTransition: Cannot transition state via :park from "idling"
+ #
+ # == Observers
+ #
+ # Observers, in the sense of external classes and *not* Ruby's Observable
+ # mechanism, can hook into state machines as well. Such observers use the
+ # same callback api that's used internally.
+ #
+ # Below are examples of defining observers for the following state machine:
+ #
+ # class Vehicle
+ # state_machine do
+ # event :park do
+ # transition :idling => :parked
+ # end
+ # ...
+ # end
+ # ...
+ # end
+ #
+ # Event/Transition behaviors:
+ #
+ # class VehicleObserver
+ # def self.before_park(vehicle, transition)
+ # logger.info "#{vehicle} instructed to park... state is: #{transition.from}, state will be: #{transition.to}"
+ # end
+ #
+ # def self.after_park(vehicle, transition, result)
+ # logger.info "#{vehicle} instructed to park... state was: #{transition.from}, state is: #{transition.to}"
+ # end
+ #
+ # def self.before_transition(vehicle, transition)
+ # logger.info "#{vehicle} instructed to #{transition.event}... #{transition.attribute} is: #{transition.from}, #{transition.attribute} will be: #{transition.to}"
+ # end
+ #
+ # def self.after_transition(vehicle, transition)
+ # logger.info "#{vehicle} instructed to #{transition.event}... #{transition.attribute} was: #{transition.from}, #{transition.attribute} is: #{transition.to}"
+ # end
+ #
+ # def self.around_transition(vehicle, transition)
+ # logger.info Benchmark.measure { yield }
+ # end
+ # end
+ #
+ # Vehicle.state_machine do
+ # before_transition :on => :park, :do => VehicleObserver.method(:before_park)
+ # before_transition VehicleObserver.method(:before_transition)
+ #
+ # after_transition :on => :park, :do => VehicleObserver.method(:after_park)
+ # after_transition VehicleObserver.method(:after_transition)
+ #
+ # around_transition VehicleObserver.method(:around_transition)
+ # end
+ #
+ # One common callback is to record transitions for all models in the system
+ # for auditing/debugging purposes. Below is an example of an observer that
+ # can easily automate this process for all models:
+ #
+ # class StateMachineObserver
+ # def self.before_transition(object, transition)
+ # Audit.log_transition(object.attributes)
+ # end
+ # end
+ #
+ # [Vehicle, Switch, Project].each do |klass|
+ # klass.state_machines.each do |attribute, machine|
+ # machine.before_transition StateMachineObserver.method(:before_transition)
+ # end
+ # end
+ #
+ # Additional observer-like behavior may be exposed by the various integrations
+ # available. See below for more information on integrations.
+ #
+ # == Overriding instance / class methods
+ #
+ # Hooking in behavior to the generated instance / class methods from the
+ # state machine, events, and states is very simple because of the way these
+ # methods are generated on the class. Using the class's ancestors, the
+ # original generated method can be referred to via +super+. For example,
+ #
+ # class Vehicle
+ # state_machine do
+ # event :park do
+ # ...
+ # end
+ # end
+ #
+ # def park(*args)
+ # logger.info "..."
+ # super
+ # end
+ # end
+ #
+ # In the above example, the +park+ instance method that's generated on the
+ # Vehicle class (by the associated event) is overridden with custom behavior.
+ # Once this behavior is complete, the original method from the state machine
+ # is invoked by simply calling +super+.
+ #
+ # The same technique can be used for +state+, +state_name+, and all other
+ # instance *and* class methods on the Vehicle class.
+ #
+ # == Method conflicts
+ #
+ # By default state_machine does not redefine methods that exist on
+ # superclasses (*including* Object) or any modules (*including* Kernel) that
+ # were included before it was defined. This is in order to ensure that
+ # existing behavior on the class is not broken by the inclusion of
+ # state_machine.
+ #
+ # If a conflicting method is detected, state_machine will generate a warning.
+ # For example, consider the following class:
+ #
+ # class Vehicle
+ # state_machine do
+ # event :open do
+ # ...
+ # end
+ # end
+ # end
+ #
+ # In the above class, an event named "open" is defined for its state machine.
+ # However, "open" is already defined as an instance method in Ruby's Kernel
+ # module that gets included in every Object. As a result, state_machine will
+ # generate the following warning:
+ #
+ # Instance method "open" is already defined in Object, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true.
+ #
+ # Even though you may not be using Kernel's implementation of the "open"
+ # instance method, state_machine isn't aware of this and, as a result, stays
+ # safe and just skips redefining the method.
+ #
+ # As with almost all helpers methods defined by state_machine in your class,
+ # there are generic methods available for working around this method conflict.
+ # In the example above, you can invoke the "open" event like so:
+ #
+ # vehicle = Vehicle.new # => #<Vehicle:0xb72686b4 @state=nil>
+ # vehicle.fire_events(:open) # => true
+ #
+ # # This will not work
+ # vehicle.open # => NoMethodError: private method `open' called for #<Vehicle:0xb72686b4 @state=nil>
+ #
+ # If you want to take on the risk of overriding existing methods and just
+ # ignore method conflicts altogether, you can do so by setting the following
+ # configuration:
+ #
+ # StateMachines::Machine.ignore_method_conflicts = true
+ #
+ # This will allow you to define events like "open" as described above and
+ # still generate the "open" instance helper method. For example:
+ #
+ # StateMachines::Machine.ignore_method_conflicts = true
+ #
+ # class Vehicle
+ # state_machine do
+ # event :open do
+ # ...
+ # end
+ # end
+ #
+ # vehicle = Vehicle.new # => #<Vehicle:0xb72686b4 @state=nil>
+ # vehicle.open # => true
+ #
+ # By default, state_machine helps prevent you from making mistakes and
+ # accidentally overriding methods that you didn't intend to. Once you
+ # understand this and what the consequences are, setting the
+ # +ignore_method_conflicts+ option is a perfectly reasonable workaround.
+ #
+ # == Integrations
+ #
+ # By default, state machines are library-agnostic, meaning that they work
+ # on any Ruby class and have no external dependencies. However, there are
+ # certain libraries which expose additional behavior that can be taken
+ # advantage of by state machines.
+ #
+ # This library is built to work out of the box with a few popular Ruby
+ # libraries that allow for additional behavior to provide a cleaner and
+ # smoother experience. This is especially the case for objects backed by a
+ # database that may allow for transactions, persistent storage,
+ # search/filters, callbacks, etc.
+ #
+ # When a state machine is defined for classes using any of the above libraries,
+ # it will try to automatically determine the integration to use (Agnostic,
+ # ActiveModel, ActiveRecord, DataMapper, Mongoid, MongoMapper, or Sequel)
+ # based on the class definition. To see how each integration affects the
+ # machine's behavior, refer to all constants defined under the
+ # StateMachines::Integrations namespace.
+ class Machine
+
+ include EvalHelpers
+ include MatcherHelpers
+
+ class << self
+ # Attempts to find or create a state machine for the given class. For
+ # example,
+ #
+ # StateMachines::Machine.find_or_create(Vehicle)
+ # StateMachines::Machine.find_or_create(Vehicle, :initial => :parked)
+ # StateMachines::Machine.find_or_create(Vehicle, :status)
+ # StateMachines::Machine.find_or_create(Vehicle, :status, :initial => :parked)
+ #
+ # If a machine of the given name already exists in one of the class's
+ # superclasses, then a copy of that machine will be created and stored
+ # in the new owner class (the original will remain unchanged).
+ def find_or_create(owner_class, *args, &block)
+ options = args.last.is_a?(Hash) ? args.pop : {}
+ name = args.first || :state
+
+ # Find an existing machine
+ if owner_class.respond_to?(:state_machines) && machine = owner_class.state_machines[name]
+ # Only create a new copy if changes are being made to the machine in
+ # a subclass
+ if machine.owner_class != owner_class && (options.any? || block_given?)
+ machine = machine.clone
+ machine.initial_state = options[:initial] if options.include?(:initial)
+ machine.owner_class = owner_class
+ end
+
+ # Evaluate DSL
+ machine.instance_eval(&block) if block_given?
+ else
+ # No existing machine: create a new one
+ machine = new(owner_class, name, options, &block)
+ end
+
+ machine
+ end
+
+
+ def draw(*)
+ fail NotImplementedError
+ end
+
+ # Default messages to use for validation errors in ORM integrations
+ attr_accessor :default_messages
+ attr_accessor :ignore_method_conflicts
+ end
+ @default_messages = {
+ :invalid => 'is invalid',
+ :invalid_event => 'cannot transition when %s',
+ :invalid_transition => 'cannot transition via "%1$s"'
+ }
+
+ # Whether to ignore any conflicts that are detected for helper methods that
+ # get generated for a machine's owner class. Default is false.
+ @ignore_method_conflicts = false
+
+ # The class that the machine is defined in
+ attr_reader :owner_class
+
+ # The name of the machine, used for scoping methods generated for the
+ # machine as a whole (not states or events)
+ attr_reader :name
+
+ # The events that trigger transitions. These are sorted, by default, in
+ # the order in which they were defined.
+ attr_reader :events
+
+ # A list of all of the states known to this state machine. This will pull
+ # states from the following sources:
+ # * Initial state
+ # * State behaviors
+ # * Event transitions (:to, :from, and :except_from options)
+ # * Transition callbacks (:to, :from, :except_to, and :except_from options)
+ # * Unreferenced states (using +other_states+ helper)
+ #
+ # These are sorted, by default, in the order in which they were referenced.
+ attr_reader :states
+
+ # The callbacks to invoke before/after a transition is performed
+ #
+ # Maps :before => callbacks and :after => callbacks
+ attr_reader :callbacks
+
+ # The action to invoke when an object transitions
+ attr_reader :action
+
+ # An identifier that forces all methods (including state predicates and
+ # event methods) to be generated with the value prefixed or suffixed,
+ # depending on the context.
+ attr_reader :namespace
+
+ # Whether the machine will use transactions when firing events
+ attr_reader :use_transactions
+
+ # Creates a new state machine for the given attribute
+ def initialize(owner_class, *args, &block)
+ options = args.last.is_a?(Hash) ? args.pop : {}
+ options.assert_valid_keys(:attribute, :initial, :initialize, :action, :plural, :namespace, :integration, :messages, :use_transactions)
+
+ # Find an integration that matches this machine's owner class
+ if options.include?(:integration)
+ @integration = options[:integration] && StateMachines::Integrations.find_by_name(options[:integration])
+ else
+ @integration = StateMachines::Integrations.match(owner_class)
+ end
+
+ if @integration
+ extend @integration
+ options = (@integration.defaults || {}).merge(options)
+ end
+
+ # Add machine-wide defaults
+ options = {:use_transactions => true, :initialize => true}.merge(options)
+
+ # Set machine configuration
+ @name = args.first || :state
+ @attribute = options[:attribute] || @name
+ @events = EventCollection.new(self)
+ @states = StateCollection.new(self)
+ @callbacks = {:before => [], :after => [], :failure => []}
+ @namespace = options[:namespace]
+ @messages = options[:messages] || {}
+ @action = options[:action]
+ @use_transactions = options[:use_transactions]
+ @initialize_state = options[:initialize]
+ @action_hook_defined = false
+ self.owner_class = owner_class
+
+ # Merge with sibling machine configurations
+ add_sibling_machine_configs
+
+ # Define class integration
+ define_helpers
+ define_scopes(options[:plural])
+ after_initialize
+
+ # Evaluate DSL
+ instance_eval(&block) if block_given?
+ self.initial_state = options[:initial] unless sibling_machines.any?
+ end
+
+ # Creates a copy of this machine in addition to copies of each associated
+ # event/states/callback, so that the modifications to those collections do
+ # not affect the original machine.
+ def initialize_copy(orig) #:nodoc:
+ super
+
+ @events = @events.dup
+ @events.machine = self
+ @states = @states.dup
+ @states.machine = self
+ @callbacks = {:before => @callbacks[:before].dup, :after => @callbacks[:after].dup, :failure => @callbacks[:failure].dup}
+ end
+
+ # Sets the class which is the owner of this state machine. Any methods
+ # generated by states, events, or other parts of the machine will be defined
+ # on the given owner class.
+ def owner_class=(klass)
+ @owner_class = klass
+
+ # Create modules for extending the class with state/event-specific methods
+ @helper_modules = helper_modules = {:instance => HelperModule.new(self, :instance), :class => HelperModule.new(self, :class)}
+ owner_class.class_eval do
+ extend helper_modules[:class]
+ include helper_modules[:instance]
+ end
+
+ # Add class-/instance-level methods to the owner class for state initialization
+ unless owner_class < StateMachines::InstanceMethods
+ owner_class.class_eval do
+ extend StateMachines::ClassMethods
+ include StateMachines::InstanceMethods
+ end
+
+ define_state_initializer if @initialize_state
+ end
+
+ # Record this machine as matched to the name in the current owner class.
+ # This will override any machines mapped to the same name in any superclasses.
+ owner_class.state_machines[name] = self
+ end
+
+ # Sets the initial state of the machine. This can be either the static name
+ # of a state or a lambda block which determines the initial state at
+ # creation time.
+ def initial_state=(new_initial_state)
+ @initial_state = new_initial_state
+ add_states([@initial_state]) unless dynamic_initial_state?
+
+ # Update all states to reflect the new initial state
+ states.each { |state| state.initial = (state.name == @initial_state) }
+
+ # Output a warning if there are conflicting initial states for the machine's
+ # attribute
+ initial_state = states.detect { |state| state.initial }
+ if !owner_class_attribute_default.nil? && (dynamic_initial_state? || !owner_class_attribute_default_matches?(initial_state))
+ warn(
+ "Both #{owner_class.name} and its #{name.inspect} machine have defined "\
+ "a different default for \"#{attribute}\". Use only one or the other for "\
+ "defining defaults to avoid unexpected behaviors."
+ )
+ end
+ end
+
+ # Gets the initial state of the machine for the given object. If a dynamic
+ # initial state was configured for this machine, then the object will be
+ # passed into the lambda block to help determine the actual state.
+ #
+ # == Examples
+ #
+ # With a static initial state:
+ #
+ # class Vehicle
+ # state_machine :initial => :parked do
+ # ...
+ # end
+ # end
+ #
+ # vehicle = Vehicle.new
+ # Vehicle.state_machine.initial_state(vehicle) # => #<StateMachines::State name=:parked value="parked" initial=true>
+ #
+ # With a dynamic initial state:
+ #
+ # class Vehicle
+ # attr_accessor :force_idle
+ #
+ # state_machine :initial => lambda {|vehicle| vehicle.force_idle ? :idling : :parked} do
+ # ...
+ # end
+ # end
+ #
+ # vehicle = Vehicle.new
+ #
+ # vehicle.force_idle = true
+ # Vehicle.state_machine.initial_state(vehicle) # => #<StateMachines::State name=:idling value="idling" initial=false>
+ #
+ # vehicle.force_idle = false
+ # Vehicle.state_machine.initial_state(vehicle) # => #<StateMachines::State name=:parked value="parked" initial=false>
+ def initial_state(object)
+ states.fetch(dynamic_initial_state? ? evaluate_method(object, @initial_state) : @initial_state) if instance_variable_defined?('@initial_state')
+ end
+
+ # Whether a dynamic initial state is being used in the machine
+ def dynamic_initial_state?
+ instance_variable_defined?('@initial_state') && @initial_state.is_a?(Proc)
+ end
+
+ # Initializes the state on the given object. Initial values are only set if
+ # the machine's attribute hasn't been previously initialized.
+ #
+ # Configuration options:
+ # * <tt>:force</tt> - Whether to initialize the state regardless of its
+ # current value
+ # * <tt>:to</tt> - A hash to set the initial value in instead of writing
+ # directly to the object
+ def initialize_state(object, options = {})
+ state = initial_state(object)
+ if state && (options[:force] || initialize_state?(object))
+ value = state.value
+
+ if hash = options[:to]
+ hash[attribute.to_s] = value
+ else
+ write(object, :state, value)
+ end
+ end
+ end
+
+ # Gets the actual name of the attribute on the machine's owner class that
+ # stores data with the given name.
+ def attribute(name = :state)
+ name == :state ? @attribute : :"#{self.name}_#{name}"
+ end
+
+ # Defines a new helper method in an instance or class scope with the given
+ # name. If the method is already defined in the scope, then this will not
+ # override it.
+ #
+ # If passing in a block, there are two side effects to be aware of
+ # 1. The method cannot be chained, meaning that the block cannot call +super+
+ # 2. If the method is already defined in an ancestor, then it will not get
+ # overridden and a warning will be output.
+ #
+ # Example:
+ #
+ # # Instance helper
+ # machine.define_helper(:instance, :state_name) do |machine, object|
+ # machine.states.match(object).name
+ # end
+ #
+ # # Class helper
+ # machine.define_helper(:class, :state_machine_name) do |machine, klass|
+ # "State"
+ # end
+ #
+ # You can also define helpers using string evaluation like so:
+ #
+ # # Instance helper
+ # machine.define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
+ # def state_name
+ # self.class.state_machine(:state).states.match(self).name
+ # end
+ # end_eval
+ #
+ # # Class helper
+ # machine.define_helper :class, <<-end_eval, __FILE__, __LINE__ + 1
+ # def state_machine_name
+ # "State"
+ # end
+ # end_eval
+ def define_helper(scope, method, *args, &block)
+ helper_module = @helper_modules.fetch(scope)
+
+ if block_given?
+ if !self.class.ignore_method_conflicts && conflicting_ancestor = owner_class_ancestor_has_method?(scope, method)
+ ancestor_name = conflicting_ancestor.name && !conflicting_ancestor.name.empty? ? conflicting_ancestor.name : conflicting_ancestor.to_s
+ warn "#{scope == :class ? 'Class' : 'Instance'} method \"#{method}\" is already defined in #{ancestor_name}, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true."
+ else
+ name = self.name
+ helper_module.class_eval do
+ define_method(method) do |*block_args|
+ block.call((scope == :instance ? self.class : self).state_machine(name), self, *block_args)
+ end
+ end
+ end
+ else
+ helper_module.class_eval(method, *args)
+ end
+ end
+
+ # Customizes the definition of one or more states in the machine.
+ #
+ # Configuration options:
+ # * <tt>:value</tt> - The actual value to store when an object transitions
+ # to the state. Default is the name (stringified).
+ # * <tt>:cache</tt> - If a dynamic value (via a lambda block) is being used,
+ # then setting this to true will cache the evaluated result
+ # * <tt>:if</tt> - Determines whether an object's value matches the state
+ # (e.g. :value => lambda {Time.now}, :if => lambda {|state| !state.nil?}).
+ # By default, the configured value is matched.
+ # * <tt>:human_name</tt> - The human-readable version of this state's name.
+ # By default, this is either defined by the integration or stringifies the
+ # name and converts underscores to spaces.
+ #
+ # == Customizing the stored value
+ #
+ # Whenever a state is automatically discovered in the state machine, its
+ # default value is assumed to be the stringified version of the name. For
+ # example,
+ #
+ # class Vehicle
+ # state_machine :initial => :parked do
+ # event :ignite do
+ # transition :parked => :idling
+ # end
+ # end
+ # end
+ #
+ # In the above state machine, there are two states automatically discovered:
+ # :parked and :idling. These states, by default, will store their stringified
+ # equivalents when an object moves into that state (e.g. "parked" / "idling").
+ #
+ # For legacy systems or when tying state machines into existing frameworks,
+ # it's oftentimes necessary to need to store a different value for a state
+ # than the default. In order to continue taking advantage of an expressive
+ # state machine and helper methods, every defined state can be re-configured
+ # with a custom stored value. For example,
+ #
+ # class Vehicle
+ # state_machine :initial => :parked do
+ # event :ignite do
+ # transition :parked => :idling
+ # end
+ #
+ # state :idling, :value => 'IDLING'
+ # state :parked, :value => 'PARKED
+ # end
+ # end
+ #
+ # This is also useful if being used in association with a database and,
+ # instead of storing the state name in a column, you want to store the
+ # state's foreign key:
+ #
+ # class VehicleState < ActiveRecord::Base
+ # end
+ #
+ # class Vehicle < ActiveRecord::Base
+ # state_machine :attribute => :state_id, :initial => :parked do
+ # event :ignite do
+ # transition :parked => :idling
+ # end
+ #
+ # states.each do |state|
+ # self.state(state.name, :value => lambda { VehicleState.find_by_name(state.name.to_s).id }, :cache => true)
+ # end
+ # end
+ # end
+ #
+ # In the above example, each known state is configured to store it's
+ # associated database id in the +state_id+ attribute. Also, notice that a
+ # lambda block is used to define the state's value. This is required in
+ # situations (like testing) where the model is loaded without any existing
+ # data (i.e. no VehicleState records available).
+ #
+ # One caveat to the above example is to keep performance in mind. To avoid
+ # constant db hits for looking up the VehicleState ids, the value is cached
+ # by specifying the <tt>:cache</tt> option. Alternatively, a custom
+ # caching strategy can be used like so:
+ #
+ # class VehicleState < ActiveRecord::Base
+ # cattr_accessor :cache_store
+ # self.cache_store = ActiveSupport::Cache::MemoryStore.new
+ #
+ # def self.find_by_name(name)
+ # cache_store.fetch(name) { find(:first, :conditions => {:name => name}) }
+ # end
+ # end
+ #
+ # === Dynamic values
+ #
+ # In addition to customizing states with other value types, lambda blocks
+ # can also be specified to allow for a state's value to be determined
+ # dynamically at runtime. For example,
+ #
+ # class Vehicle
+ # state_machine :purchased_at, :initial => :available do
+ # event :purchase do
+ # transition all => :purchased
+ # end
+ #
+ # event :restock do
+ # transition all => :available
+ # end
+ #
+ # state :available, :value => nil
+ # state :purchased, :if => lambda {|value| !value.nil?}, :value => lambda {Time.now}
+ # end
+ # end
+ #
+ # In the above definition, the <tt>:purchased</tt> state is customized with
+ # both a dynamic value *and* a value matcher.
+ #
+ # When an object transitions to the purchased state, the value's lambda
+ # block will be called. This will get the current time and store it in the
+ # object's +purchased_at+ attribute.
+ #
+ # *Note* that the custom matcher is very important here. Since there's no
+ # way for the state machine to figure out an object's state when it's set to
+ # a runtime value, it must be explicitly defined. If the <tt>:if</tt> option
+ # were not configured for the state, then an ArgumentError exception would
+ # be raised at runtime, indicating that the state machine could not figure
+ # out what the current state of the object was.
+ #
+ # == Behaviors
+ #
+ # Behaviors define a series of methods to mixin with objects when the current
+ # state matches the given one(s). This allows instance methods to behave
+ # a specific way depending on what the value of the object's state is.
+ #
+ # For example,
+ #
+ # class Vehicle
+ # attr_accessor :driver
+ # attr_accessor :passenger
+ #
+ # state_machine :initial => :parked do
+ # event :ignite do
+ # transition :parked => :idling
+ # end
+ #
+ # state :parked do
+ # def speed
+ # 0
+ # end
+ #
+ # def rotate_driver
+ # driver = self.driver
+ # self.driver = passenger
+ # self.passenger = driver
+ # true
+ # end
+ # end
+ #
+ # state :idling, :first_gear do
+ # def speed
+ # 20
+ # end
+ #
+ # def rotate_driver
+ # self.state = 'parked'
+ # rotate_driver
+ # end
+ # end
+ #
+ # other_states :backing_up
+ # end
+ # end
+ #
+ # In the above example, there are two dynamic behaviors defined for the
+ # class:
+ # * +speed+
+ # * +rotate_driver+
+ #
+ # Each of these behaviors are instance methods on the Vehicle class. However,
+ # which method actually gets invoked is based on the current state of the
+ # object. Using the above class as the example:
+ #
+ # vehicle = Vehicle.new
+ # vehicle.driver = 'John'
+ # vehicle.passenger = 'Jane'
+ #
+ # # Behaviors in the "parked" state
+ # vehicle.state # => "parked"
+ # vehicle.speed # => 0
+ # vehicle.rotate_driver # => true
+ # vehicle.driver # => "Jane"
+ # vehicle.passenger # => "John"
+ #
+ # vehicle.ignite # => true
+ #
+ # # Behaviors in the "idling" state
+ # vehicle.state # => "idling"
+ # vehicle.speed # => 20
+ # vehicle.rotate_driver # => true
+ # vehicle.driver # => "John"
+ # vehicle.passenger # => "Jane"
+ #
+ # As can be seen, both the +speed+ and +rotate_driver+ instance method
+ # implementations changed how they behave based on what the current state
+ # of the vehicle was.
+ #
+ # === Invalid behaviors
+ #
+ # If a specific behavior has not been defined for a state, then a
+ # NoMethodError exception will be raised, indicating that that method would
+ # not normally exist for an object with that state.
+ #
+ # Using the example from before:
+ #
+ # vehicle = Vehicle.new
+ # vehicle.state = 'backing_up'
+ # vehicle.speed # => NoMethodError: undefined method 'speed' for #<Vehicle:0xb7d296ac> in state "backing_up"
+ #
+ # === Using matchers
+ #
+ # The +all+ / +any+ matchers can be used to easily define behaviors for a
+ # group of states. Note, however, that you cannot use these matchers to
+ # set configurations for states. Behaviors using these matchers can be
+ # defined at any point in the state machine and will always get applied to
+ # the proper states.
+ #
+ # For example:
+ #
+ # state_machine :initial => :parked do
+ # ...
+ #
+ # state all - [:parked, :idling, :stalled] do
+ # validates_presence_of :speed
+ #
+ # def speed
+ # gear * 10
+ # end
+ # end
+ # end
+ #
+ # == State-aware class methods
+ #
+ # In addition to defining scopes for instance methods that are state-aware,
+ # the same can be done for certain types of class methods.
+ #
+ # Some libraries have support for class-level methods that only run certain
+ # behaviors based on a conditions hash passed in. For example:
+ #
+ # class Vehicle < ActiveRecord::Base
+ # state_machine do
+ # ...
+ # state :first_gear, :second_gear, :third_gear do
+ # validates_presence_of :speed
+ # validates_inclusion_of :speed, :in => 0..25, :if => :in_school_zone?
+ # end
+ # end
+ # end
+ #
+ # In the above ActiveRecord model, two validations have been defined which
+ # will *only* run when the Vehicle object is in one of the three states:
+ # +first_gear+, +second_gear+, or +third_gear. Notice, also, that if/unless
+ # conditions can continue to be used.
+ #
+ # This functionality is not library-specific and can work for any class-level
+ # method that is defined like so:
+ #
+ # def validates_presence_of(attribute, options = {})
+ # ...
+ # end
+ #
+ # The minimum requirement is that the last argument in the method be an
+ # options hash which contains at least <tt>:if</tt> condition support.
+ def state(*names, &block)
+ options = names.last.is_a?(Hash) ? names.pop : {}
+ options.assert_valid_keys(:value, :cache, :if, :human_name)
+
+ # Store the context so that it can be used for / matched against any state
+ # that gets added
+ @states.context(names, &block) if block_given?
+
+ if names.first.is_a?(Matcher)
+ # Add any states referenced in the matcher. When matchers are used,
+ # states are not allowed to be configured.
+ raise ArgumentError, "Cannot configure states when using matchers (using #{options.inspect})" if options.any?
+ states = add_states(names.first.values)
+ else
+ states = add_states(names)
+
+ # Update the configuration for the state(s)
+ states.each do |state|
+ if options.include?(:value)
+ state.value = options[:value]
+ self.states.update(state)
+ end
+
+ state.human_name = options[:human_name] if options.include?(:human_name)
+ state.cache = options[:cache] if options.include?(:cache)
+ state.matcher = options[:if] if options.include?(:if)
+ end
+ end
+
+ states.length == 1 ? states.first : states
+ end
+
+ alias_method :other_states, :state
+
+ # Gets the current value stored in the given object's attribute.
+ #
+ # For example,
+ #
+ # class Vehicle
+ # state_machine :initial => :parked do
+ # ...
+ # end
+ # end
+ #
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7d94ab0 @state="parked">
+ # Vehicle.state_machine.read(vehicle, :state) # => "parked" # Equivalent to vehicle.state
+ # Vehicle.state_machine.read(vehicle, :event) # => nil # Equivalent to vehicle.state_event
+ def read(object, attribute, ivar = false)
+ attribute = self.attribute(attribute)
+ if ivar
+ object.instance_variable_defined?("@#{attribute}") ? object.instance_variable_get("@#{attribute}") : nil
+ else
+ object.send(attribute)
+ end
+ end
+
+ # Sets a new value in the given object's attribute.
+ #
+ # For example,
+ #
+ # class Vehicle
+ # state_machine :initial => :parked do
+ # ...
+ # end
+ # end
+ #
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7d94ab0 @state="parked">
+ # Vehicle.state_machine.write(vehicle, :state, 'idling') # => Equivalent to vehicle.state = 'idling'
+ # Vehicle.state_machine.write(vehicle, :event, 'park') # => Equivalent to vehicle.state_event = 'park'
+ # vehicle.state # => "idling"
+ # vehicle.event # => "park"
+ def write(object, attribute, value, ivar = false)
+ attribute = self.attribute(attribute)
+ ivar ? object.instance_variable_set("@#{attribute}", value) : object.send("#{attribute}=", value)
+ end
+
+ # Defines one or more events for the machine and the transitions that can
+ # be performed when those events are run.
+ #
+ # This method is also aliased as +on+ for improved compatibility with
+ # using a domain-specific language.
+ #
+ # Configuration options:
+ # * <tt>:human_name</tt> - The human-readable version of this event's name.
+ # By default, this is either defined by the integration or stringifies the
+ # name and converts underscores to spaces.
+ #
+ # == Instance methods
+ #
+ # The following instance methods are generated when a new event is defined
+ # (the "park" event is used as an example):
+ # * <tt>park(..., run_action = true)</tt> - Fires the "park" event,
+ # transitioning from the current state to the next valid state. If the
+ # last argument is a boolean, it will control whether the machine's action
+ # gets run.
+ # * <tt>park!(..., run_action = true)</tt> - Fires the "park" event,
+ # transitioning from the current state to the next valid state. If the
+ # transition fails, then a StateMachines::InvalidTransition error will be
+ # raised. If the last argument is a boolean, it will control whether the
+ # machine's action gets run.
+ # * <tt>can_park?(requirements = {})</tt> - Checks whether the "park" event
+ # can be fired given the current state of the object. This will *not* run
+ # validations or callbacks in ORM integrations. It will only determine if
+ # the state machine defines a valid transition for the event. To check
+ # whether an event can fire *and* passes validations, use event attributes
+ # (e.g. state_event) as described in the "Events" documentation of each
+ # ORM integration.
+ # * <tt>park_transition(requirements = {})</tt> - Gets the next transition
+ # that would be performed if the "park" event were to be fired now on the
+ # object or nil if no transitions can be performed. Like <tt>can_park?</tt>
+ # this will also *not* run validations or callbacks. It will only
+ # determine if the state machine defines a valid transition for the event.
+ #
+ # With a namespace of "car", the above names map to the following methods:
+ # * <tt>can_park_car?</tt>
+ # * <tt>park_car_transition</tt>
+ # * <tt>park_car</tt>
+ # * <tt>park_car!</tt>
+ #
+ # The <tt>can_park?</tt> and <tt>park_transition</tt> helpers both take an
+ # optional set of requirements for determining what transitions are available
+ # for the current object. These requirements include:
+ # * <tt>:from</tt> - One or more states to transition from. If none are
+ # specified, then this will be the object's current state.
+ # * <tt>:to</tt> - One or more states to transition to. If none are
+ # specified, then this will match any to state.
+ # * <tt>:guard</tt> - Whether to guard transitions with the if/unless
+ # conditionals defined for each one. Default is true.
+ #
+ # == Defining transitions
+ #
+ # +event+ requires a block which allows you to define the possible
+ # transitions that can happen as a result of that event. For example,
+ #
+ # event :park, :stop do
+ # transition :idling => :parked
+ # end
+ #
+ # event :first_gear do
+ # transition :parked => :first_gear, :if => :seatbelt_on?
+ # transition :parked => same # Allow to loopback if seatbelt is off
+ # end
+ #
+ # See StateMachines::Event#transition for more information on
+ # the possible options that can be passed in.
+ #
+ # *Note* that this block is executed within the context of the actual event
+ # object. As a result, you will not be able to reference any class methods
+ # on the model without referencing the class itself. For example,
+ #
+ # class Vehicle
+ # def self.safe_states
+ # [:parked, :idling, :stalled]
+ # end
+ #
+ # state_machine do
+ # event :park do
+ # transition Vehicle.safe_states => :parked
+ # end
+ # end
+ # end
+ #
+ # == Overriding the event method
+ #
+ # By default, this will define an instance method (with the same name as the
+ # event) that will fire the next possible transition for that. Although the
+ # +before_transition+, +after_transition+, and +around_transition+ hooks
+ # allow you to define behavior that gets executed as a result of the event's
+ # transition, you can also override the event method in order to have a
+ # little more fine-grained control.
+ #
+ # For example:
+ #
+ # class Vehicle
+ # state_machine do
+ # event :park do
+ # ...
+ # end
+ # end
+ #
+ # def park(*)
+ # take_deep_breath # Executes before the transition (and before_transition hooks) even if no transition is possible
+ # if result = super # Runs the transition and all before/after/around hooks
+ # applaud # Executes after the transition (and after_transition hooks)
+ # end
+ # result
+ # end
+ # end
+ #
+ # There are a few important things to note here. First, the method
+ # signature is defined with an unlimited argument list in order to allow
+ # callers to continue passing arguments that are expected by state_machine.
+ # For example, it will still allow calls to +park+ with a single parameter
+ # for skipping the configured action.
+ #
+ # Second, the overridden event method must call +super+ in order to run the
+ # logic for running the next possible transition. In order to remain
+ # consistent with other events, the result of +super+ is returned.
+ #
+ # Third, any behavior defined in this method will *not* get executed if
+ # you're taking advantage of attribute-based event transitions. For example:
+ #
+ # vehicle = Vehicle.new
+ # vehicle.state_event = 'park'
+ # vehicle.save
+ #
+ # In this case, the +park+ event will run the before/after/around transition
+ # hooks and transition the state, but the behavior defined in the overriden
+ # +park+ method will *not* be executed.
+ #
+ # == Defining additional arguments
+ #
+ # Additional arguments can be passed into events and accessed by transition
+ # hooks like so:
+ #
+ # class Vehicle
+ # state_machine do
+ # after_transition :on => :park do |vehicle, transition|
+ # kind = *transition.args # :parallel
+ # ...
+ # end
+ # after_transition :on => :park, :do => :take_deep_breath
+ #
+ # event :park do
+ # ...
+ # end
+ #
+ # def take_deep_breath(transition)
+ # kind = *transition.args # :parallel
+ # ...
+ # end
+ # end
+ # end
+ #
+ # vehicle = Vehicle.new
+ # vehicle.park(:parallel)
+ #
+ # *Remember* that if the last argument is a boolean, it will be used as the
+ # +run_action+ parameter to the event action. Using the +park+ action
+ # example from above, you can might call it like so:
+ #
+ # vehicle.park # => Uses default args and runs machine action
+ # vehicle.park(:parallel) # => Specifies the +kind+ argument and runs the machine action
+ # vehicle.park(:parallel, false) # => Specifies the +kind+ argument and *skips* the machine action
+ #
+ # If you decide to override the +park+ event method *and* define additional
+ # arguments, you can do so as shown below:
+ #
+ # class Vehicle
+ # state_machine do
+ # event :park do
+ # ...
+ # end
+ # end
+ #
+ # def park(kind = :parallel, *args)
+ # take_deep_breath if kind == :parallel
+ # super
+ # end
+ # end
+ #
+ # Note that +super+ is called instead of <tt>super(*args)</tt>. This allow
+ # the entire arguments list to be accessed by transition callbacks through
+ # StateMachines::Transition#args.
+ #
+ # === Using matchers
+ #
+ # The +all+ / +any+ matchers can be used to easily execute blocks for a
+ # group of events. Note, however, that you cannot use these matchers to
+ # set configurations for events. Blocks using these matchers can be
+ # defined at any point in the state machine and will always get applied to
+ # the proper events.
+ #
+ # For example:
+ #
+ # state_machine :initial => :parked do
+ # ...
+ #
+ # event all - [:crash] do
+ # transition :stalled => :parked
+ # end
+ # end
+ #
+ # == Example
+ #
+ # class Vehicle
+ # state_machine do
+ # # The park, stop, and halt events will all share the given transitions
+ # event :park, :stop, :halt do
+ # transition [:idling, :backing_up] => :parked
+ # end
+ #
+ # event :stop do
+ # transition :first_gear => :idling
+ # end
+ #
+ # event :ignite do
+ # transition :parked => :idling
+ # transition :idling => same # Allow ignite while still idling
+ # end
+ # end
+ # end
+ def event(*names, &block)
+ options = names.last.is_a?(Hash) ? names.pop : {}
+ options.assert_valid_keys(:human_name)
+
+ # Store the context so that it can be used for / matched against any event
+ # that gets added
+ @events.context(names, &block) if block_given?
+
+ if names.first.is_a?(Matcher)
+ # Add any events referenced in the matcher. When matchers are used,
+ # events are not allowed to be configured.
+ raise ArgumentError, "Cannot configure events when using matchers (using #{options.inspect})" if options.any?
+ events = add_events(names.first.values)
+ else
+ events = add_events(names)
+
+ # Update the configuration for the event(s)
+ events.each do |event|
+ event.human_name = options[:human_name] if options.include?(:human_name)
+
+ # Add any states that may have been referenced within the event
+ add_states(event.known_states)
+ end
+ end
+
+ events.length == 1 ? events.first : events
+ end
+
+ alias_method :on, :event
+
+ # Creates a new transition that determines what to change the current state
+ # to when an event fires.
+ #
+ # == Defining transitions
+ #
+ # The options for a new transition uses the Hash syntax to map beginning
+ # states to ending states. For example,
+ #
+ # transition :parked => :idling, :idling => :first_gear, :on => :ignite
+ #
+ # In this case, when the +ignite+ event is fired, this transition will cause
+ # the state to be +idling+ if it's current state is +parked+ or +first_gear+
+ # if it's current state is +idling+.
+ #
+ # To help define these implicit transitions, a set of helpers are available
+ # for slightly more complex matching:
+ # * <tt>all</tt> - Matches every state in the machine
+ # * <tt>all - [:parked, :idling, ...]</tt> - Matches every state except those specified
+ # * <tt>any</tt> - An alias for +all+ (matches every state in the machine)
+ # * <tt>same</tt> - Matches the same state being transitioned from
+ #
+ # See StateMachines::MatcherHelpers for more information.
+ #
+ # Examples:
+ #
+ # transition all => nil, :on => :ignite # Transitions to nil regardless of the current state
+ # transition all => :idling, :on => :ignite # Transitions to :idling regardless of the current state
+ # transition all - [:idling, :first_gear] => :idling, :on => :ignite # Transitions every state but :idling and :first_gear to :idling
+ # transition nil => :idling, :on => :ignite # Transitions to :idling from the nil state
+ # transition :parked => :idling, :on => :ignite # Transitions to :idling if :parked
+ # transition [:parked, :stalled] => :idling, :on => :ignite # Transitions to :idling if :parked or :stalled
+ #
+ # transition :parked => same, :on => :park # Loops :parked back to :parked
+ # transition [:parked, :stalled] => same, :on => [:park, :stall] # Loops either :parked or :stalled back to the same state on the park and stall events
+ # transition all - :parked => same, :on => :noop # Loops every state but :parked back to the same state
+ #
+ # # Transitions to :idling if :parked, :first_gear if :idling, or :second_gear if :first_gear
+ # transition :parked => :idling, :idling => :first_gear, :first_gear => :second_gear, :on => :shift_up
+ #
+ # == Verbose transitions
+ #
+ # Transitions can also be defined use an explicit set of configuration
+ # options:
+ # * <tt>:from</tt> - A state or array of states that can be transitioned from.
+ # If not specified, then the transition can occur for *any* state.
+ # * <tt>:to</tt> - The state that's being transitioned to. If not specified,
+ # then the transition will simply loop back (i.e. the state will not change).
+ # * <tt>:except_from</tt> - A state or array of states that *cannot* be
+ # transitioned from.
+ #
+ # These options must be used when defining transitions within the context
+ # of a state.
+ #
+ # Examples:
+ #
+ # transition :to => nil, :on => :park
+ # transition :to => :idling, :on => :ignite
+ # transition :except_from => [:idling, :first_gear], :to => :idling, :on => :ignite
+ # transition :from => nil, :to => :idling, :on => :ignite
+ # transition :from => [:parked, :stalled], :to => :idling, :on => :ignite
+ #
+ # == Conditions
+ #
+ # In addition to the state requirements for each transition, a condition
+ # can also be defined to help determine whether that transition is
+ # available. These options will work on both the normal and verbose syntax.
+ #
+ # Configuration options:
+ # * <tt>:if</tt> - A method, proc or string to call to determine if the
+ # transition should occur (e.g. :if => :moving?, or :if => lambda {|vehicle| vehicle.speed > 60}).
+ # The condition should return or evaluate to true or false.
+ # * <tt>:unless</tt> - A method, proc or string to call to determine if the
+ # transition should not occur (e.g. :unless => :stopped?, or :unless => lambda {|vehicle| vehicle.speed <= 60}).
+ # The condition should return or evaluate to true or false.
+ #
+ # Examples:
+ #
+ # transition :parked => :idling, :on => :ignite, :if => :moving?
+ # transition :parked => :idling, :on => :ignite, :unless => :stopped?
+ # transition :idling => :first_gear, :first_gear => :second_gear, :on => :shift_up, :if => :seatbelt_on?
+ #
+ # transition :from => :parked, :to => :idling, :on => ignite, :if => :moving?
+ # transition :from => :parked, :to => :idling, :on => ignite, :unless => :stopped?
+ #
+ # == Order of operations
+ #
+ # Transitions are evaluated in the order in which they're defined. As a
+ # result, if more than one transition applies to a given object, then the
+ # first transition that matches will be performed.
+ def transition(options)
+ raise ArgumentError, 'Must specify :on event' unless options[:on]
+
+ branches = []
+ options = options.dup
+ event(*Array(options.delete(:on))) { branches << transition(options) }
+
+ branches.length == 1 ? branches.first : branches
+ end
+
+ # Creates a callback that will be invoked *before* a transition is
+ # performed so long as the given requirements match the transition.
+ #
+ # == The callback
+ #
+ # Callbacks must be defined as either an argument, in the :do option, or
+ # as a block. For example,
+ #
+ # class Vehicle
+ # state_machine do
+ # before_transition :set_alarm
+ # before_transition :set_alarm, all => :parked
+ # before_transition all => :parked, :do => :set_alarm
+ # before_transition all => :parked do |vehicle, transition|
+ # vehicle.set_alarm
+ # end
+ # ...
+ # end
+ # end
+ #
+ # Notice that the first three callbacks are the same in terms of how the
+ # methods to invoke are defined. However, using the <tt>:do</tt> can
+ # provide for a more fluid DSL.
+ #
+ # In addition, multiple callbacks can be defined like so:
+ #
+ # class Vehicle
+ # state_machine do
+ # before_transition :set_alarm, :lock_doors, all => :parked
+ # before_transition all => :parked, :do => [:set_alarm, :lock_doors]
+ # before_transition :set_alarm do |vehicle, transition|
+ # vehicle.lock_doors
+ # end
+ # end
+ # end
+ #
+ # Notice that the different ways of configuring methods can be mixed.
+ #
+ # == State requirements
+ #
+ # Callbacks can require that the machine be transitioning from and to
+ # specific states. These requirements use a Hash syntax to map beginning
+ # states to ending states. For example,
+ #
+ # before_transition :parked => :idling, :idling => :first_gear, :do => :set_alarm
+ #
+ # In this case, the +set_alarm+ callback will only be called if the machine
+ # is transitioning from +parked+ to +idling+ or from +idling+ to +parked+.
+ #
+ # To help define state requirements, a set of helpers are available for
+ # slightly more complex matching:
+ # * <tt>all</tt> - Matches every state/event in the machine
+ # * <tt>all - [:parked, :idling, ...]</tt> - Matches every state/event except those specified
+ # * <tt>any</tt> - An alias for +all+ (matches every state/event in the machine)
+ # * <tt>same</tt> - Matches the same state being transitioned from
+ #
+ # See StateMachines::MatcherHelpers for more information.
+ #
+ # Examples:
+ #
+ # before_transition :parked => [:idling, :first_gear], :do => ... # Matches from parked to idling or first_gear
+ # before_transition all - [:parked, :idling] => :idling, :do => ... # Matches from every state except parked and idling to idling
+ # before_transition all => :parked, :do => ... # Matches all states to parked
+ # before_transition any => same, :do => ... # Matches every loopback
+ #
+ # == Event requirements
+ #
+ # In addition to state requirements, an event requirement can be defined so
+ # that the callback is only invoked on specific events using the +on+
+ # option. This can also use the same matcher helpers as the state
+ # requirements.
+ #
+ # Examples:
+ #
+ # before_transition :on => :ignite, :do => ... # Matches only on ignite
+ # before_transition :on => all - :ignite, :do => ... # Matches on every event except ignite
+ # before_transition :parked => :idling, :on => :ignite, :do => ... # Matches from parked to idling on ignite
+ #
+ # == Verbose Requirements
+ #
+ # Requirements can also be defined using verbose options rather than the
+ # implicit Hash syntax and helper methods described above.
+ #
+ # Configuration options:
+ # * <tt>:from</tt> - One or more states being transitioned from. If none
+ # are specified, then all states will match.
+ # * <tt>:to</tt> - One or more states being transitioned to. If none are
+ # specified, then all states will match.
+ # * <tt>:on</tt> - One or more events that fired the transition. If none
+ # are specified, then all events will match.
+ # * <tt>:except_from</tt> - One or more states *not* being transitioned from
+ # * <tt>:except_to</tt> - One more states *not* being transitioned to
+ # * <tt>:except_on</tt> - One or more events that *did not* fire the transition
+ #
+ # Examples:
+ #
+ # before_transition :from => :ignite, :to => :idling, :on => :park, :do => ...
+ # before_transition :except_from => :ignite, :except_to => :idling, :except_on => :park, :do => ...
+ #
+ # == Conditions
+ #
+ # In addition to the state/event requirements, a condition can also be
+ # defined to help determine whether the callback should be invoked.
+ #
+ # Configuration options:
+ # * <tt>:if</tt> - A method, proc or string to call to determine if the
+ # callback should occur (e.g. :if => :allow_callbacks, or
+ # :if => lambda {|user| user.signup_step > 2}). The method, proc or string
+ # should return or evaluate to a true or false value.
+ # * <tt>:unless</tt> - A method, proc or string to call to determine if the
+ # callback should not occur (e.g. :unless => :skip_callbacks, or
+ # :unless => lambda {|user| user.signup_step <= 2}). The method, proc or
+ # string should return or evaluate to a true or false value.
+ #
+ # Examples:
+ #
+ # before_transition :parked => :idling, :if => :moving?, :do => ...
+ # before_transition :on => :ignite, :unless => :seatbelt_on?, :do => ...
+ #
+ # == Accessing the transition
+ #
+ # In addition to passing the object being transitioned, the actual
+ # transition describing the context (e.g. event, from, to) can be accessed
+ # as well. This additional argument is only passed if the callback allows
+ # for it.
+ #
+ # For example,
+ #
+ # class Vehicle
+ # # Only specifies one parameter (the object being transitioned)
+ # before_transition all => :parked do |vehicle|
+ # vehicle.set_alarm
+ # end
+ #
+ # # Specifies 2 parameters (object being transitioned and actual transition)
+ # before_transition all => :parked do |vehicle, transition|
+ # vehicle.set_alarm(transition)
+ # end
+ # end
+ #
+ # *Note* that the object in the callback will only be passed in as an
+ # argument if callbacks are configured to *not* be bound to the object
+ # involved. This is the default and may change on a per-integration basis.
+ #
+ # See StateMachines::Transition for more information about the
+ # attributes available on the transition.
+ #
+ # == Usage with delegates
+ #
+ # As noted above, state_machine uses the callback method's argument list
+ # arity to determine whether to include the transition in the method call.
+ # If you're using delegates, such as those defined in ActiveSupport or
+ # Forwardable, the actual arity of the delegated method gets masked. This
+ # means that callbacks which reference delegates will always get passed the
+ # transition as an argument. For example:
+ #
+ # class Vehicle
+ # extend Forwardable
+ # delegate :refresh => :dashboard
+ #
+ # state_machine do
+ # before_transition :refresh
+ # ...
+ # end
+ #
+ # def dashboard
+ # @dashboard ||= Dashboard.new
+ # end
+ # end
+ #
+ # class Dashboard
+ # def refresh(transition)
+ # # ...
+ # end
+ # end
+ #
+ # In the above example, <tt>Dashboard#refresh</tt> *must* defined a
+ # +transition+ argument. Otherwise, an +ArgumentError+ exception will get
+ # raised. The only way around this is to avoid the use of delegates and
+ # manually define the delegate method so that the correct arity is used.
+ #
+ # == Examples
+ #
+ # Below is an example of a class with one state machine and various types
+ # of +before+ transitions defined for it:
+ #
+ # class Vehicle
+ # state_machine do
+ # # Before all transitions
+ # before_transition :update_dashboard
+ #
+ # # Before specific transition:
+ # before_transition [:first_gear, :idling] => :parked, :on => :park, :do => :take_off_seatbelt
+ #
+ # # With conditional callback:
+ # before_transition all => :parked, :do => :take_off_seatbelt, :if => :seatbelt_on?
+ #
+ # # Using helpers:
+ # before_transition all - :stalled => same, :on => any - :crash, :do => :update_dashboard
+ # ...
+ # end
+ # end
+ #
+ # As can be seen, any number of transitions can be created using various
+ # combinations of configuration options.
+ def before_transition(*args, &block)
+ options = (args.last.is_a?(Hash) ? args.pop : {})
+ options[:do] = args if args.any?
+ add_callback(:before, options, &block)
+ end
+
+ # Creates a callback that will be invoked *after* a transition is
+ # performed so long as the given requirements match the transition.
+ #
+ # See +before_transition+ for a description of the possible configurations
+ # for defining callbacks.
+ def after_transition(*args, &block)
+ options = (args.last.is_a?(Hash) ? args.pop : {})
+ options[:do] = args if args.any?
+ add_callback(:after, options, &block)
+ end
+
+ # Creates a callback that will be invoked *around* a transition so long as
+ # the given requirements match the transition.
+ #
+ # == The callback
+ #
+ # Around callbacks wrap transitions, executing code both before and after.
+ # These callbacks are defined in the exact same manner as before / after
+ # callbacks with the exception that the transition must be yielded to in
+ # order to finish running it.
+ #
+ # If defining +around+ callbacks using blocks, you must yield within the
+ # transition by directly calling the block (since yielding is not allowed
+ # within blocks).
+ #
+ # For example,
+ #
+ # class Vehicle
+ # state_machine do
+ # around_transition do |block|
+ # Benchmark.measure { block.call }
+ # end
+ #
+ # around_transition do |vehicle, block|
+ # logger.info "vehicle was #{state}..."
+ # block.call
+ # logger.info "...and is now #{state}"
+ # end
+ #
+ # around_transition do |vehicle, transition, block|
+ # logger.info "before #{transition.event}: #{vehicle.state}"
+ # block.call
+ # logger.info "after #{transition.event}: #{vehicle.state}"
+ # end
+ # end
+ # end
+ #
+ # Notice that referencing the block is similar to doing so within an
+ # actual method definition in that it is always the last argument.
+ #
+ # On the other hand, if you're defining +around+ callbacks using method
+ # references, you can yield like normal:
+ #
+ # class Vehicle
+ # state_machine do
+ # around_transition :benchmark
+ # ...
+ # end
+ #
+ # def benchmark
+ # Benchmark.measure { yield }
+ # end
+ # end
+ #
+ # See +before_transition+ for a description of the possible configurations
+ # for defining callbacks.
+ def around_transition(*args, &block)
+ options = (args.last.is_a?(Hash) ? args.pop : {})
+ options[:do] = args if args.any?
+ add_callback(:around, options, &block)
+ end
+
+ # Creates a callback that will be invoked *after* a transition failures to
+ # be performed so long as the given requirements match the transition.
+ #
+ # See +before_transition+ for a description of the possible configurations
+ # for defining callbacks. *Note* however that you cannot define the state
+ # requirements in these callbacks. You may only define event requirements.
+ #
+ # = The callback
+ #
+ # Failure callbacks get invoked whenever an event fails to execute. This
+ # can happen when no transition is available, a +before+ callback halts
+ # execution, or the action associated with this machine fails to succeed.
+ # In any of these cases, any failure callback that matches the attempted
+ # transition will be run.
+ #
+ # For example,
+ #
+ # class Vehicle
+ # state_machine do
+ # after_failure do |vehicle, transition|
+ # logger.error "vehicle #{vehicle} failed to transition on #{transition.event}"
+ # end
+ #
+ # after_failure :on => :ignite, :do => :log_ignition_failure
+ #
+ # ...
+ # end
+ # end
+ def after_failure(*args, &block)
+ options = (args.last.is_a?(Hash) ? args.pop : {})
+ options[:do] = args if args.any?
+ options.assert_valid_keys(:on, :do, :if, :unless)
+
+ add_callback(:failure, options, &block)
+ end
+
+ # Generates a list of the possible transition sequences that can be run on
+ # the given object. These paths can reveal all of the possible states and
+ # events that can be encountered in the object's state machine based on the
+ # object's current state.
+ #
+ # Configuration options:
+ # * +from+ - The initial state to start all paths from. By default, this
+ # is the object's current state.
+ # * +to+ - The target state to end all paths on. By default, paths will
+ # end when they loop back to the first transition on the path.
+ # * +deep+ - Whether to allow the target state to be crossed more than once
+ # in a path. By default, paths will immediately stop when the target
+ # state (if specified) is reached. If this is enabled, then paths can
+ # continue even after reaching the target state; they will stop when
+ # reaching the target state a second time.
+ #
+ # *Note* that the object is never modified when the list of paths is
+ # generated.
+ #
+ # == Examples
+ #
+ # class Vehicle
+ # state_machine :initial => :parked do
+ # event :ignite do
+ # transition :parked => :idling
+ # end
+ #
+ # event :shift_up do
+ # transition :idling => :first_gear, :first_gear => :second_gear
+ # end
+ #
+ # event :shift_down do
+ # transition :second_gear => :first_gear, :first_gear => :idling
+ # end
+ # end
+ # end
+ #
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c27024 @state="parked">
+ # vehicle.state # => "parked"
+ #
+ # vehicle.state_paths
+ # # => [
+ # # [#<StateMachines::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>,
+ # # #<StateMachines::Transition attribute=:state event=:shift_up from="idling" from_name=:idling to="first_gear" to_name=:first_gear>,
+ # # #<StateMachines::Transition attribute=:state event=:shift_up from="first_gear" from_name=:first_gear to="second_gear" to_name=:second_gear>,
+ # # #<StateMachines::Transition attribute=:state event=:shift_down from="second_gear" from_name=:second_gear to="first_gear" to_name=:first_gear>,
+ # # #<StateMachines::Transition attribute=:state event=:shift_down from="first_gear" from_name=:first_gear to="idling" to_name=:idling>],
+ # #
+ # # [#<StateMachines::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>,
+ # # #<StateMachines::Transition attribute=:state event=:shift_up from="idling" from_name=:idling to="first_gear" to_name=:first_gear>,
+ # # #<StateMachines::Transition attribute=:state event=:shift_down from="first_gear" from_name=:first_gear to="idling" to_name=:idling>]
+ # # ]
+ #
+ # vehicle.state_paths(:from => :parked, :to => :second_gear)
+ # # => [
+ # # [#<StateMachines::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>,
+ # # #<StateMachines::Transition attribute=:state event=:shift_up from="idling" from_name=:idling to="first_gear" to_name=:first_gear>,
+ # # #<StateMachines::Transition attribute=:state event=:shift_up from="first_gear" from_name=:first_gear to="second_gear" to_name=:second_gear>]
+ # # ]
+ #
+ # In addition to getting the possible paths that can be accessed, you can
+ # also get summary information about the states / events that can be
+ # accessed at some point along one of the paths. For example:
+ #
+ # # Get the list of states that can be accessed from the current state
+ # vehicle.state_paths.to_states # => [:idling, :first_gear, :second_gear]
+ #
+ # # Get the list of events that can be accessed from the current state
+ # vehicle.state_paths.events # => [:ignite, :shift_up, :shift_down]
+ def paths_for(object, requirements = {})
+ PathCollection.new(object, self, requirements)
+ end
+
+ # Marks the given object as invalid with the given message.
+ #
+ # By default, this is a no-op.
+ def invalidate(_object, _attribute, _message, _values = [])
+ end
+
+ # Gets a description of the errors for the given object. This is used to
+ # provide more detailed information when an InvalidTransition exception is
+ # raised.
+ def errors_for(_object)
+ ''
+ end
+
+ # Resets any errors previously added when invalidating the given object.
+ #
+ # By default, this is a no-op.
+ def reset(_object)
+ end
+
+ # Generates the message to use when invalidating the given object after
+ # failing to transition on a specific event
+ def generate_message(name, values = [])
+ message = (@messages[name] || self.class.default_messages[name])
+
+ # Check whether there are actually any values to interpolate to avoid
+ # any warnings
+ if message.scan(/%./).any? { |match| match != '%%' }
+ message % values.map { |value| value.last }
+ else
+ message
+ end
+ end
+
+ # Runs a transaction, rolling back any changes if the yielded block fails.
+ #
+ # This is only applicable to integrations that involve databases. By
+ # default, this will not run any transactions since the changes aren't
+ # taking place within the context of a database.
+ def within_transaction(object)
+ if use_transactions
+ transaction(object) { yield }
+ else
+ yield
+ end
+ end
+
+
+ def draw(*)
+ fail NotImplementedError
+ end
+
+ # Determines whether an action hook was defined for firing attribute-based
+ # event transitions when the configured action gets called.
+ def action_hook?(self_only = false)
+ @action_hook_defined || !self_only && owner_class.state_machines.any? { |name, machine| machine.action == action && machine != self && machine.action_hook?(true) }
+ end
+
+ protected
+ # Runs additional initialization hooks. By default, this is a no-op.
+ def after_initialize
+ end
+
+ # Looks up other machines that have been defined in the owner class and
+ # are targeting the same attribute as this machine. When accessing
+ # sibling machines, they will be automatically copied for the current
+ # class if they haven't been already. This ensures that any configuration
+ # changes made to the sibling machines only affect this class and not any
+ # base class that may have originally defined the machine.
+ def sibling_machines
+ owner_class.state_machines.inject([]) do |machines, (name, machine)|
+ if machine.attribute == attribute && machine != self
+ machines << (owner_class.state_machine(name) {})
+ end
+ machines
+ end
+ end
+
+ # Determines if the machine's attribute needs to be initialized. This
+ # will only be true if the machine's attribute is blank.
+ def initialize_state?(object)
+ value = read(object, :state)
+ (value.nil? || value.respond_to?(:empty?) && value.empty?) && !states[value, :value]
+ end
+
+ # Adds helper methods for interacting with the state machine, including
+ # for states, events, and transitions
+ def define_helpers
+ define_state_accessor
+ define_state_predicate
+ define_event_helpers
+ define_path_helpers
+ define_action_helpers if define_action_helpers?
+ define_name_helpers
+ end
+
+ # Defines the initial values for state machine attributes. Static values
+ # are set prior to the original initialize method and dynamic values are
+ # set *after* the initialize method in case it is dependent on it.
+ def define_state_initializer
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
+ def initialize(*)
+ self.class.state_machines.initialize_states(self) { super }
+ end
+ end_eval
+ end
+
+ # Adds reader/writer methods for accessing the state attribute
+ def define_state_accessor
+ attribute = self.attribute
+
+ @helper_modules[:instance].class_eval { attr_reader attribute } unless owner_class_ancestor_has_method?(:instance, attribute)
+ @helper_modules[:instance].class_eval { attr_writer attribute } unless owner_class_ancestor_has_method?(:instance, "#{attribute}=")
+ end
+
+ # Adds predicate method to the owner class for determining the name of the
+ # current state
+ def define_state_predicate
+ call_super = !!owner_class_ancestor_has_method?(:instance, "#{name}?")
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
+ def #{name}?(*args)
+ args.empty? && (#{call_super} || defined?(super)) ? super : self.class.state_machine(#{name.inspect}).states.matches?(self, *args)
+ end
+ end_eval
+ end
+
+ # Adds helper methods for getting information about this state machine's
+ # events
+ def define_event_helpers
+ # Gets the events that are allowed to fire on the current object
+ define_helper(:instance, attribute(:events)) do |machine, object, *args|
+ machine.events.valid_for(object, *args).map { |event| event.name }
+ end
+
+ # Gets the next possible transitions that can be run on the current
+ # object
+ define_helper(:instance, attribute(:transitions)) do |machine, object, *args|
+ machine.events.transitions_for(object, *args)
+ end
+
+ # Fire an arbitrary event for this machine
+ define_helper(:instance, "fire_#{attribute(:event)}") do |machine, object, event, *args|
+ machine.events.fetch(event).fire(object, *args)
+ end
+
+ # Add helpers for tracking the event / transition to invoke when the
+ # action is called
+ if action
+ event_attribute = attribute(:event)
+ define_helper(:instance, event_attribute) do |machine, object|
+ # Interpret non-blank events as present
+ event = machine.read(object, :event, true)
+ event && !(event.respond_to?(:empty?) && event.empty?) ? event.to_sym : nil
+ end
+
+ # A roundabout way of writing the attribute is used here so that
+ # integrations can hook into this modification
+ define_helper(:instance, "#{event_attribute}=") do |machine, object, value|
+ machine.write(object, :event, value, true)
+ end
+
+ event_transition_attribute = attribute(:event_transition)
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
+ protected; attr_accessor #{event_transition_attribute.inspect}
+ end_eval
+ end
+ end
+
+ # Adds helper methods for getting information about this state machine's
+ # available transition paths
+ def define_path_helpers
+ # Gets the paths of transitions available to the current object
+ define_helper(:instance, attribute(:paths)) do |machine, object, *args|
+ machine.paths_for(object, *args)
+ end
+ end
+
+ # Determines whether action helpers should be defined for this machine.
+ # This is only true if there is an action configured and no other machines
+ # have process this same configuration already.
+ def define_action_helpers?
+ action && !owner_class.state_machines.any? { |name, machine| machine.action == action && machine != self }
+ end
+
+ # Adds helper methods for automatically firing events when an action
+ # is invoked
+ def define_action_helpers
+ if action_hook
+ @action_hook_defined = true
+ define_action_hook
+ end
+ end
+
+ # Hooks directly into actions by defining the same method in an included
+ # module. As a result, when the action gets invoked, any state events
+ # defined for the object will get run. Method visibility is preserved.
+ def define_action_hook
+ action_hook = self.action_hook
+ action = self.action
+ private_action_hook = owner_class.private_method_defined?(action_hook)
+
+ # Only define helper if it hasn't
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
+ def #{action_hook}(*)
+ self.class.state_machines.transitions(self, #{action.inspect}).perform { super }
+ end
+
+ private #{action_hook.inspect} if #{private_action_hook}
+ end_eval
+ end
+
+ # The method to hook into for triggering transitions when invoked. By
+ # default, this is the action configured for the machine.
+ #
+ # Since the default hook technique relies on module inheritance, the
+ # action must be defined in an ancestor of the owner classs in order for
+ # it to be the action hook.
+ def action_hook
+ action && owner_class_ancestor_has_method?(:instance, action) ? action : nil
+ end
+
+ # Determines whether there's already a helper method defined within the
+ # given scope. This is true only if one of the owner's ancestors defines
+ # the method and is further along in the ancestor chain than this
+ # machine's helper module.
+ def owner_class_ancestor_has_method?(scope, method)
+ superclasses = owner_class.ancestors[1..-1].select { |ancestor| ancestor.is_a?(Class) }
+
+ if scope == :class
+ # Use singleton classes
+ current = (
+ class << owner_class;
+ self;
+ end)
+ superclass = superclasses.first
+ else
+ current = owner_class
+ superclass = owner_class.superclass
+ end
+
+ # Generate the list of modules that *only* occur in the owner class, but
+ # were included *prior* to the helper modules, in addition to the
+ # superclasses
+ ancestors = current.ancestors - superclass.ancestors + superclasses
+ ancestors = ancestors[ancestors.index(@helper_modules[scope])..-1].reverse
+
+ # Search for for the first ancestor that defined this method
+ ancestors.detect do |ancestor|
+ ancestor = (
+ class << ancestor;
+ self;
+ end) if scope == :class && ancestor.is_a?(Class)
+ ancestor.method_defined?(method) || ancestor.private_method_defined?(method)
+ end
+ end
+
+ # Adds helper methods for accessing naming information about states and
+ # events on the owner class
+ def define_name_helpers
+ # Gets the humanized version of a state
+ define_helper(:class, "human_#{attribute(:name)}") do |machine, klass, state|
+ machine.states.fetch(state).human_name(klass)
+ end
+
+ # Gets the humanized version of an event
+ define_helper(:class, "human_#{attribute(:event_name)}") do |machine, klass, event|
+ machine.events.fetch(event).human_name(klass)
+ end
+
+ # Gets the state name for the current value
+ define_helper(:instance, attribute(:name)) do |machine, object|
+ machine.states.match!(object).name
+ end
+
+ # Gets the human state name for the current value
+ define_helper(:instance, "human_#{attribute(:name)}") do |machine, object|
+ machine.states.match!(object).human_name(object.class)
+ end
+ end
+
+ # Defines the with/without scope helpers for this attribute. Both the
+ # singular and plural versions of the attribute are defined for each
+ # scope helper. A custom plural can be specified if it cannot be
+ # automatically determined by either calling +pluralize+ on the attribute
+ # name or adding an "s" to the end of the name.
+ def define_scopes(custom_plural = nil)
+ plural = custom_plural || pluralize(name)
+
+ [:with, :without].each do |kind|
+ [name, plural].map { |s| s.to_s }.uniq.each do |suffix|
+ method = "#{kind}_#{suffix}"
+
+ if scope = send("create_#{kind}_scope", method)
+ # Converts state names to their corresponding values so that they
+ # can be looked up properly
+ define_helper(:class, method) do |machine, klass, *states|
+ run_scope(scope, machine, klass, states)
+ end
+ end
+ end
+ end
+ end
+
+ # Generates the results for the given scope based on one or more states to
+ # filter by
+ def run_scope(scope, machine, klass, states)
+ values = states.flatten.map { |state| machine.states.fetch(state).value }
+ scope.call(klass, values)
+ end
+
+ # Pluralizes the given word using #pluralize (if available) or simply
+ # adding an "s" to the end of the word
+ def pluralize(word)
+ word = word.to_s
+ if word.respond_to?(:pluralize)
+ word.pluralize
+ else
+ "#{name}s"
+ end
+ end
+
+ # Creates a scope for finding objects *with* a particular value or values
+ # for the attribute.
+ #
+ # By default, this is a no-op.
+ def create_with_scope(name)
+ end
+
+ # Creates a scope for finding objects *without* a particular value or
+ # values for the attribute.
+ #
+ # By default, this is a no-op.
+ def create_without_scope(name)
+ end
+
+ # Always yields
+ def transaction(object)
+ yield
+ end
+
+ # Gets the initial attribute value defined by the owner class (outside of
+ # the machine's definition). By default, this is always nil.
+ def owner_class_attribute_default
+ nil
+ end
+
+ # Checks whether the given state matches the attribute default specified
+ # by the owner class
+ def owner_class_attribute_default_matches?(state)
+ state.matches?(owner_class_attribute_default)
+ end
+
+ # Updates this machine based on the configuration of other machines in the
+ # owner class that share the same target attribute.
+ def add_sibling_machine_configs
+ # Add existing states
+ sibling_machines.each do |machine|
+ machine.states.each { |state| states << state unless states[state.name] }
+ end
+ end
+
+ # Adds a new transition callback of the given type.
+ def add_callback(type, options, &block)
+ callbacks[type == :around ? :before : type] << callback = Callback.new(type, options, &block)
+ add_states(callback.known_states)
+ callback
+ end
+
+ # Tracks the given set of states in the list of all known states for
+ # this machine
+ def add_states(new_states)
+ new_states.map do |new_state|
+ # Check for other states that use a different class type for their name.
+ # This typically prevents string / symbol misuse.
+ if new_state && conflict = states.detect { |state| state.name && state.name.class != new_state.class }
+ raise ArgumentError, "#{new_state.inspect} state defined as #{new_state.class}, #{conflict.name.inspect} defined as #{conflict.name.class}; all states must be consistent"
+ end
+
+ unless state = states[new_state]
+ states << state = State.new(self, new_state)
+
+ # Copy states over to sibling machines
+ sibling_machines.each { |machine| machine.states << state }
+ end
+
+ state
+ end
+ end
+
+ # Tracks the given set of events in the list of all known events for
+ # this machine
+ def add_events(new_events)
+ new_events.map do |new_event|
+ # Check for other states that use a different class type for their name.
+ # This typically prevents string / symbol misuse.
+ if conflict = events.detect { |event| event.name.class != new_event.class }
+ raise ArgumentError, "#{new_event.inspect} event defined as #{new_event.class}, #{conflict.name.inspect} defined as #{conflict.name.class}; all events must be consistent"
+ end
+
+ unless event = events[new_event]
+ events << event = Event.new(self, new_event)
+ end
+
+ event
+ end
+ end
+ end
+end
diff --git a/lib/state_machines/machine_collection.rb b/lib/state_machines/machine_collection.rb
new file mode 100644
index 0000000..a301aaa
--- /dev/null
+++ b/lib/state_machines/machine_collection.rb
@@ -0,0 +1,96 @@
+module StateMachines
+ # Represents a collection of state machines for a class
+ class MachineCollection < Hash
+ # Initializes the state of each machine in the given object. This can allow
+ # states to be initialized in two groups: static and dynamic. For example:
+ #
+ # machines.initialize_states(object) do
+ # # After static state initialization, before dynamic state initialization
+ # end
+ #
+ # If no block is provided, then all states will still be initialized.
+ #
+ # Valid configuration options:
+ # * <tt>:static</tt> - Whether to initialize static states. Unless set to
+ # false, the state will be initialized regardless of its current value.
+ # Default is true.
+ # * <tt>:dynamic</tt> - Whether to initialize dynamic states. If set to
+ # :force, the state will be initialized regardless of its current value.
+ # Default is true.
+ # * <tt>:to</tt> - A hash to write the initialized state to instead of
+ # writing to the object. Default is to write directly to the object.
+ def initialize_states(object, options = {}, attributes = {})
+ options.assert_valid_keys( :static, :dynamic, :to)
+ options = {:static => true, :dynamic => true}.merge(options)
+
+ result = yield if block_given?
+
+ each_value do |machine|
+ unless machine.dynamic_initial_state?
+ force = options[:static] == :force || !attributes.keys.map(&:to_sym).include?(machine.attribute)
+ machine.initialize_state(object, force: force, :to => options[:to])
+ end
+ end if options[:static]
+
+ each_value do |machine|
+ machine.initialize_state(object, :force => options[:dynamic] == :force, :to => options[:to]) if machine.dynamic_initial_state?
+ end if options[:dynamic]
+
+ result
+ end
+
+ # Runs one or more events in parallel on the given object. See
+ # StateMachines::InstanceMethods#fire_events for more information.
+ def fire_events(object, *events)
+ run_action = [true, false].include?(events.last) ? events.pop : true
+
+ # Generate the transitions to run for each event
+ transitions = events.collect do |event_name|
+ # Find the actual event being run
+ event = nil
+ detect {|name, machine| event = machine.events[event_name, :qualified_name]}
+
+ raise(InvalidEvent.new(object, event_name)) unless event
+
+ # Get the transition that will be performed for the event
+ unless transition = event.transition_for(object)
+ event.on_failure(object)
+ end
+ transition
+ end.compact
+
+ # Run the events in parallel only if valid transitions were found for
+ # all of them
+ if events.length == transitions.length
+ TransitionCollection.new(transitions, {use_transactions: resolve_use_transactions, actions: run_action}).perform
+ else
+ false
+ end
+ end
+
+ # Builds the collection of transitions for all event attributes defined on
+ # the given object. This will only include events whose machine actions
+ # match the one specified.
+ #
+ # These should only be fired as a result of the action being run.
+ def transitions(object, action, options = {})
+ transitions = map do |name, machine|
+ machine.events.attribute_transition_for(object, true) if machine.action == action
+ end
+
+ AttributeTransitionCollection.new(transitions.compact, {use_transactions: resolve_use_transactions}.merge(options))
+ end
+
+ protected
+
+ def resolve_use_transactions
+ use_transactions = nil
+ each_value do |machine|
+ # Determine use_transactions setting for this set of transitions. If from multiple state_machines, the settings must match.
+ raise 'Encountered mismatched use_transactions configurations for multiple state_machines' if !use_transactions.nil? && use_transactions != machine.use_transactions
+ use_transactions = machine.use_transactions
+ end
+ use_transactions
+ end
+ end
+end
diff --git a/lib/state_machines/macro_methods.rb b/lib/state_machines/macro_methods.rb
new file mode 100644
index 0000000..d6b05fe
--- /dev/null
+++ b/lib/state_machines/macro_methods.rb
@@ -0,0 +1,520 @@
+# A state machine is a model of behavior composed of states, events, and
+# transitions. This helper adds support for defining this type of
+# functionality on any Ruby class.
+module StateMachines
+ module MacroMethods
+ # Creates a new state machine with the given name. The default name, if not
+ # specified, is <tt>:state</tt>.
+ #
+ # Configuration options:
+ # * <tt>:attribute</tt> - The name of the attribute to store the state value
+ # in. By default, this is the same as the name of the machine.
+ # * <tt>:initial</tt> - The initial state of the attribute. This can be a
+ # static state or a lambda block which will be evaluated at runtime
+ # (e.g. lambda {|vehicle| vehicle.speed == 0 ? :parked : :idling}).
+ # Default is nil.
+ # * <tt>:initialize</tt> - Whether to automatically initialize the attribute
+ # by hooking into #initialize on the owner class. Default is true.
+ # * <tt>:action</tt> - The instance method to invoke when an object
+ # transitions. Default is nil unless otherwise specified by the
+ # configured integration.
+ # * <tt>:namespace</tt> - The name to use for namespacing all generated
+ # state / event instance methods (e.g. "heater" would generate
+ # :turn_on_heater and :turn_off_heater for the :turn_on/:turn_off events).
+ # Default is nil.
+ # * <tt>:integration</tt> - The name of the integration to use for adding
+ # library-specific behavior to the machine. Built-in integrations
+ # include :active_model, :active_record, :data_mapper, :mongo_mapper, and
+ # :sequel. By default, this is determined automatically.
+ #
+ # Configuration options relevant to ORM integrations:
+ # * <tt>:plural</tt> - The pluralized version of the name. By default, this
+ # will attempt to call +pluralize+ on the name. If this method is not
+ # available, an "s" is appended. This is used for generating scopes.
+ # * <tt>:messages</tt> - The error messages to use when invalidating
+ # objects due to failed transitions. Messages include:
+ # * <tt>:invalid</tt>
+ # * <tt>:invalid_event</tt>
+ # * <tt>:invalid_transition</tt>
+ # * <tt>:use_transactions</tt> - Whether transactions should be used when
+ # firing events. Default is true unless otherwise specified by the
+ # configured integration.
+ #
+ # This also expects a block which will be used to actually configure the
+ # states, events and transitions for the state machine. *Note* that this
+ # block will be executed within the context of the state machine. As a
+ # result, you will not be able to access any class methods unless you refer
+ # to them directly (i.e. specifying the class name).
+ #
+ # For examples on the types of state machine configurations and blocks, see
+ # the section below.
+ #
+ # == Examples
+ #
+ # With the default name/attribute and no configuration:
+ #
+ # class Vehicle
+ # state_machine do
+ # event :park do
+ # ...
+ # end
+ # end
+ # end
+ #
+ # The above example will define a state machine named "state" that will
+ # store the value in the +state+ attribute. Every vehicle will start
+ # without an initial state.
+ #
+ # With a custom name / attribute:
+ #
+ # class Vehicle
+ # state_machine :status, :attribute => :status_value do
+ # ...
+ # end
+ # end
+ #
+ # With a static initial state:
+ #
+ # class Vehicle
+ # state_machine :status, :initial => :parked do
+ # ...
+ # end
+ # end
+ #
+ # With a dynamic initial state:
+ #
+ # class Vehicle
+ # state_machine :status, :initial => lambda {|vehicle| vehicle.speed == 0 ? :parked : :idling} do
+ # ...
+ # end
+ # end
+ #
+ # == Class Methods
+ #
+ # The following class methods will be automatically generated by the
+ # state machine based on the *name* of the machine. Any existing methods
+ # will not be overwritten.
+ # * <tt>human_state_name(state)</tt> - Gets the humanized value for the
+ # given state. This may be generated by internationalization libraries if
+ # supported by the integration.
+ # * <tt>human_state_event_name(event)</tt> - Gets the humanized value for
+ # the given event. This may be generated by internationalization
+ # libraries if supported by the integration.
+ #
+ # For example,
+ #
+ # class Vehicle
+ # state_machine :state, :initial => :parked do
+ # event :ignite do
+ # transition :parked => :idling
+ # end
+ #
+ # event :shift_up do
+ # transition :idling => :first_gear
+ # end
+ # end
+ # end
+ #
+ # Vehicle.human_state_name(:parked) # => "parked"
+ # Vehicle.human_state_name(:first_gear) # => "first gear"
+ # Vehicle.human_state_event_name(:park) # => "park"
+ # Vehicle.human_state_event_name(:shift_up) # => "shift up"
+ #
+ # == Instance Methods
+ #
+ # The following instance methods will be automatically generated by the
+ # state machine based on the *name* of the machine. Any existing methods
+ # will not be overwritten.
+ # * <tt>state</tt> - Gets the current value for the attribute
+ # * <tt>state=(value)</tt> - Sets the current value for the attribute
+ # * <tt>state?(name)</tt> - Checks the given state name against the current
+ # state. If the name is not a known state, then an ArgumentError is raised.
+ # * <tt>state_name</tt> - Gets the name of the state for the current value
+ # * <tt>human_state_name</tt> - Gets the human-readable name of the state
+ # for the current value
+ # * <tt>state_events(requirements = {})</tt> - Gets the list of events that
+ # can be fired on the current object's state (uses the *unqualified* event
+ # names)
+ # * <tt>state_transitions(requirements = {})</tt> - Gets the list of
+ # transitions that can be made on the current object's state
+ # * <tt>state_paths(requirements = {})</tt> - Gets the list of sequences of
+ # transitions that can be run from the current object's state
+ # * <tt>fire_state_event(name, *args)</tt> - Fires an arbitrary event with
+ # the given argument list. This is essentially the same as calling the
+ # actual event method itself.
+ #
+ # The <tt>state_events</tt>, <tt>state_transitions</tt>, and <tt>state_paths</tt>
+ # helpers all take an optional set of requirements for determining what's
+ # available for the current object. These requirements include:
+ # * <tt>:from</tt> - One or more states to transition from. If none are
+ # specified, then this will be the object's current state.
+ # * <tt>:to</tt> - One or more states to transition to. If none are
+ # specified, then this will match any to state.
+ # * <tt>:on</tt> - One or more events to transition on. If none are
+ # specified, then this will match any event.
+ # * <tt>:guard</tt> - Whether to guard transitions with the if/unless
+ # conditionals defined for each one. Default is true.
+ #
+ # For example,
+ #
+ # class Vehicle
+ # state_machine :state, :initial => :parked do
+ # event :ignite do
+ # transition :parked => :idling
+ # end
+ #
+ # event :park do
+ # transition :idling => :parked
+ # end
+ # end
+ # end
+ #
+ # vehicle = Vehicle.new
+ # vehicle.state # => "parked"
+ # vehicle.state_name # => :parked
+ # vehicle.human_state_name # => "parked"
+ # vehicle.state?(:parked) # => true
+ #
+ # # Changing state
+ # vehicle.state = 'idling'
+ # vehicle.state # => "idling"
+ # vehicle.state_name # => :idling
+ # vehicle.state?(:parked) # => false
+ #
+ # # Getting current event / transition availability
+ # vehicle.state_events # => [:park]
+ # vehicle.park # => true
+ # vehicle.state_events # => [:ignite]
+ # vehicle.state_events(:from => :idling) # => [:park]
+ # vehicle.state_events(:to => :parked) # => []
+ #
+ # vehicle.state_transitions # => [#<StateMachines::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
+ # vehicle.ignite # => true
+ # vehicle.state_transitions # => [#<StateMachines::Transition attribute=:state event=:park from="idling" from_name=:idling to="parked" to_name=:parked>]
+ #
+ # vehicle.state_transitions(:on => :ignite) # => []
+ #
+ # # Getting current path availability
+ # vehicle.state_paths # => [
+ # # [#<StateMachines::Transition attribute=:state event=:park from="idling" from_name=:idling to="parked" to_name=:parked>,
+ # # #<StateMachines::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
+ # # ]
+ # vehicle.state_paths(:guard => false) # =>
+ # # [#<StateMachines::Transition attribute=:state event=:park from="idling" from_name=:idling to="parked" to_name=:parked>,
+ # # #<StateMachines::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
+ # # ]
+ #
+ # # Fire arbitrary events
+ # vehicle.fire_state_event(:park) # => true
+ #
+ # == Attribute initialization
+ #
+ # For most classes, the initial values for state machine attributes are
+ # automatically assigned when a new object is created. However, this
+ # behavior will *not* work if the class defines an +initialize+ method
+ # without properly calling +super+.
+ #
+ # For example,
+ #
+ # class Vehicle
+ # state_machine :state, :initial => :parked do
+ # ...
+ # end
+ # end
+ #
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c8dbf8 @state="parked">
+ # vehicle.state # => "parked"
+ #
+ # In the above example, no +initialize+ method is defined. As a result,
+ # the default behavior of initializing the state machine attributes is used.
+ #
+ # In the following example, a custom +initialize+ method is defined:
+ #
+ # class Vehicle
+ # state_machine :state, :initial => :parked do
+ # ...
+ # end
+ #
+ # def initialize
+ # end
+ # end
+ #
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c77678>
+ # vehicle.state # => nil
+ #
+ # Since the +initialize+ method is defined, the state machine attributes
+ # never get initialized. In order to ensure that all initialization hooks
+ # are called, the custom method *must* call +super+ without any arguments
+ # like so:
+ #
+ # class Vehicle
+ # state_machine :state, :initial => :parked do
+ # ...
+ # end
+ #
+ # def initialize(attributes = {})
+ # ...
+ # super()
+ # end
+ # end
+ #
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c8dbf8 @state="parked">
+ # vehicle.state # => "parked"
+ #
+ # Because of the way the inclusion of modules works in Ruby, calling
+ # <tt>super()</tt> will not only call the superclass's +initialize+, but
+ # also +initialize+ on all included modules. This allows the original state
+ # machine hook to get called properly.
+ #
+ # If you want to avoid calling the superclass's constructor, but still want
+ # to initialize the state machine attributes:
+ #
+ # class Vehicle
+ # state_machine :state, :initial => :parked do
+ # ...
+ # end
+ #
+ # def initialize(attributes = {})
+ # ...
+ # initialize_state_machines
+ # end
+ # end
+ #
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c8dbf8 @state="parked">
+ # vehicle.state # => "parked"
+ #
+ # You may also need to call the +initialize_state_machines+ helper manually
+ # in cases where you want to change how static / dynamic initial states get
+ # set. For example, the following example forces the initialization of
+ # static states regardless of their current value:
+ #
+ # class Vehicle
+ # state_machine :state, :initial => :parked do
+ # state nil, :idling
+ # ...
+ # end
+ #
+ # def initialize(attributes = {})
+ # @state = 'idling'
+ # initialize_state_machines(:static => :force) do
+ # ...
+ # end
+ # end
+ # end
+ #
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c8dbf8 @state="parked">
+ # vehicle.state # => "parked"
+ #
+ # The above example is also noteworthy because it demonstrates how to avoid
+ # initialization issues when +nil+ is a valid state. Without passing in
+ # <tt>:static => :force</tt>, state_machine would never have initialized
+ # the state because +nil+ (the default attribute value) would have been
+ # interpreted as a valid current state. As a result, state_machine would
+ # have simply skipped initialization.
+ #
+ # == States
+ #
+ # All of the valid states for the machine are automatically tracked based
+ # on the events, transitions, and callbacks defined for the machine. If
+ # there are additional states that are never referenced, these should be
+ # explicitly added using the StateMachines::Machine#state or
+ # StateMachines::Machine#other_states helpers.
+ #
+ # When a new state is defined, a predicate method for that state is
+ # generated on the class. For example,
+ #
+ # class Vehicle
+ # state_machine :initial => :parked do
+ # event :ignite do
+ # transition all => :idling
+ # end
+ # end
+ # end
+ #
+ # ...will generate the following instance methods (assuming they're not
+ # already defined in the class):
+ # * <tt>parked?</tt>
+ # * <tt>idling?</tt>
+ #
+ # Each predicate method will return true if it matches the object's
+ # current state. Otherwise, it will return false.
+ #
+ # == Attribute access
+ #
+ # The actual value for a state is stored in the attribute configured for the
+ # state machine. In most cases, this is the same as the name of the state
+ # machine. For example:
+ #
+ # class Vehicle
+ # attr_accessor :state
+ #
+ # state_machine :state, :initial => :parked do
+ # ...
+ # state :parked, :value => 0
+ # start :idling, :value => 1
+ # end
+ # end
+ #
+ # vehicle = Vehicle.new # => #<Vehicle:0xb712da60 @state=0>
+ # vehicle.state # => 0
+ # vehicle.parked? # => true
+ # vehicle.state = 1
+ # vehicle.idling? # => true
+ #
+ # The most important thing to note from the example above is what it means
+ # to read from and write to the state machine's attribute. In particular,
+ # state_machine treats the attribute (+state+ in this case) like a basic
+ # attr_accessor that's been defined on the class. There are no special
+ # behaviors added, such as allowing the attribute to be written to based on
+ # the name of a state in the machine. This is the case for a few reasons:
+ # * Setting the attribute directly is an edge case that is meant to only be
+ # used when you want to skip state_machine altogether. This means that
+ # state_machine shouldn't have any effect on the attribute accessor
+ # methods. If you want to change the state, you should be using one of
+ # the events defined in the state machine.
+ # * Many ORMs provide custom behavior for the attribute reader / writer - it
+ # may even be defined by your own framework / method implementation just
+ # the example above showed. In order to avoid having to worry about the
+ # different ways an attribute can get written, state_machine just makes
+ # sure that the configured value for a state is always used when writing
+ # to the attribute.
+ #
+ # If you were interested in accessing the name of a state (instead of its
+ # actual value through the attribute), you could do the following:
+ #
+ # vehicle.state_name # => :idling
+ #
+ # == Events and Transitions
+ #
+ # Events defined on the machine are the interface to transitioning states
+ # for an object. Events can be fired either directly (through the method
+ # generated for the event) or indirectly (through attributes defined on
+ # the machine).
+ #
+ # For example,
+ #
+ # class Vehicle
+ # include DataMapper::Resource
+ # property :id, Serial
+ #
+ # state_machine :initial => :parked do
+ # event :ignite do
+ # transition :parked => :idling
+ # end
+ # end
+ #
+ # state_machine :alarm_state, :initial => :active do
+ # event :disable do
+ # transition all => :off
+ # end
+ # end
+ # end
+ #
+ # # Fire +ignite+ event directly
+ # vehicle = Vehicle.create # => #<Vehicle id=1 state="parked" alarm_state="active">
+ # vehicle.ignite # => true
+ # vehicle.state # => "idling"
+ # vehicle.alarm_state # => "active"
+ #
+ # # Fire +disable+ event automatically
+ # vehicle.alarm_state_event = 'disable'
+ # vehicle.save # => true
+ # vehicle.alarm_state # => "off"
+ #
+ # In the above example, the +state+ attribute is transitioned using the
+ # +ignite+ action that's generated from the state machine. On the other
+ # hand, the +alarm_state+ attribute is transitioned using the +alarm_state_event+
+ # attribute that automatically gets fired when the machine's action (+save+)
+ # is invoked.
+ #
+ # For more information about how to configure an event and its associated
+ # transitions, see StateMachines::Machine#event.
+ #
+ # == Defining callbacks
+ #
+ # Within the +state_machine+ block, you can also define callbacks for
+ # transitions. For more information about defining these callbacks,
+ # see StateMachines::Machine#before_transition, StateMachines::Machine#after_transition,
+ # and StateMachines::Machine#around_transition, and StateMachines::Machine#after_failure.
+ #
+ # == Namespaces
+ #
+ # When a namespace is configured for a state machine, the name provided
+ # will be used in generating the instance methods for interacting with
+ # states/events in the machine. This is particularly useful when a class
+ # has multiple state machines and it would be difficult to differentiate
+ # between the various states / events.
+ #
+ # For example,
+ #
+ # class Vehicle
+ # state_machine :heater_state, :initial => :off, :namespace => 'heater' do
+ # event :turn_on do
+ # transition all => :on
+ # end
+ #
+ # event :turn_off do
+ # transition all => :off
+ # end
+ # end
+ #
+ # state_machine :alarm_state, :initial => :active, :namespace => 'alarm' do
+ # event :turn_on do
+ # transition all => :active
+ # end
+ #
+ # event :turn_off do
+ # transition all => :off
+ # end
+ # end
+ # end
+ #
+ # The above class defines two state machines: +heater_state+ and +alarm_state+.
+ # For the +heater_state+ machine, the following methods are generated since
+ # it's namespaced by "heater":
+ # * <tt>can_turn_on_heater?</tt>
+ # * <tt>turn_on_heater</tt>
+ # * ...
+ # * <tt>can_turn_off_heater?</tt>
+ # * <tt>turn_off_heater</tt>
+ # * ..
+ # * <tt>heater_off?</tt>
+ # * <tt>heater_on?</tt>
+ #
+ # As shown, each method is unique to the state machine so that the states
+ # and events don't conflict. The same goes for the +alarm_state+ machine:
+ # * <tt>can_turn_on_alarm?</tt>
+ # * <tt>turn_on_alarm</tt>
+ # * ...
+ # * <tt>can_turn_off_alarm?</tt>
+ # * <tt>turn_off_alarm</tt>
+ # * ..
+ # * <tt>alarm_active?</tt>
+ # * <tt>alarm_off?</tt>
+ #
+ # == Scopes
+ #
+ # For integrations that support it, a group of default scope filters will
+ # be automatically created for assisting in finding objects that have the
+ # attribute set to one of a given set of states.
+ #
+ # For example,
+ #
+ # Vehicle.with_state(:parked) # => All vehicles where the state is parked
+ # Vehicle.with_states(:parked, :idling) # => All vehicles where the state is either parked or idling
+ #
+ # Vehicle.without_state(:parked) # => All vehicles where the state is *not* parked
+ # Vehicle.without_states(:parked, :idling) # => All vehicles where the state is *not* parked or idling
+ #
+ # *Note* that if class methods already exist with those names (i.e.
+ # :with_state, :with_states, :without_state, or :without_states), then a
+ # scope will not be defined for that name.
+ #
+ # See StateMachines::Machine for more information about using integrations
+ # and the individual integration docs for information about the actual
+ # scopes that are generated.
+ def state_machine(*args, &block)
+ StateMachines::Machine.find_or_create(self, *args, &block)
+ end
+ end
+end
diff --git a/lib/state_machines/matcher.rb b/lib/state_machines/matcher.rb
new file mode 100644
index 0000000..01e5f4d
--- /dev/null
+++ b/lib/state_machines/matcher.rb
@@ -0,0 +1,121 @@
+module StateMachines
+ # Provides a general strategy pattern for determining whether a match is found
+ # for a value. The algorithm that actually determines the match depends on
+ # the matcher in use.
+ class Matcher
+ # The list of values against which queries are matched
+ attr_reader :values
+
+ # Creates a new matcher for querying against the given set of values
+ def initialize(values = [])
+ @values = values.is_a?(Array) ? values : [values]
+ end
+
+ # Generates a subset of values that exists in both the set of values being
+ # filtered and the values configured for the matcher
+ def filter(values)
+ self.values & values
+ end
+ end
+
+ # Matches any given value. Since there is no configuration for this type of
+ # matcher, it must be used as a singleton.
+ class AllMatcher < Matcher
+ include Singleton
+
+ # Generates a blacklist matcher based on the given set of values
+ #
+ # == Examples
+ #
+ # matcher = StateMachines::AllMatcher.instance - [:parked, :idling]
+ # matcher.matches?(:parked) # => false
+ # matcher.matches?(:first_gear) # => true
+ def -(blacklist)
+ BlacklistMatcher.new(blacklist)
+ end
+
+ # Always returns true
+ def matches?(value, context = {})
+ true
+ end
+
+ # Always returns the given set of values
+ def filter(values)
+ values
+ end
+
+ # A human-readable description of this matcher. Always "all".
+ def description
+ 'all'
+ end
+ end
+
+ # Matches a specific set of values
+ class WhitelistMatcher < Matcher
+ # Checks whether the given value exists within the whitelist configured
+ # for this matcher.
+ #
+ # == Examples
+ #
+ # matcher = StateMachines::WhitelistMatcher.new([:parked, :idling])
+ # matcher.matches?(:parked) # => true
+ # matcher.matches?(:first_gear) # => false
+ def matches?(value, context = {})
+ values.include?(value)
+ end
+
+ # A human-readable description of this matcher
+ def description
+ values.length == 1 ? values.first.inspect : values.inspect
+ end
+ end
+
+ # Matches everything but a specific set of values
+ class BlacklistMatcher < Matcher
+ # Checks whether the given value exists outside the blacklist configured
+ # for this matcher.
+ #
+ # == Examples
+ #
+ # matcher = StateMachines::BlacklistMatcher.new([:parked, :idling])
+ # matcher.matches?(:parked) # => false
+ # matcher.matches?(:first_gear) # => true
+ def matches?(value, context = {})
+ !values.include?(value)
+ end
+
+ # Finds all values that are *not* within the blacklist configured for this
+ # matcher
+ def filter(values)
+ values - self.values
+ end
+
+ # A human-readable description of this matcher
+ def description
+ "all - #{values.length == 1 ? values.first.inspect : values.inspect}"
+ end
+ end
+
+ # Matches a loopback of two values within a context. Since there is no
+ # configuration for this type of matcher, it must be used as a singleton.
+ class LoopbackMatcher < Matcher
+ include Singleton
+
+ # Checks whether the given value matches what the value originally was.
+ # This value should be defined in the context.
+ #
+ # == Examples
+ #
+ # matcher = StateMachines::LoopbackMatcher.instance
+ # matcher.matches?(:parked, :from => :parked) # => true
+ # matcher.matches?(:parked, :from => :idling) # => false
+ def matches?(value, context)
+ context[:from] == value
+ end
+
+ # A human-readable description of this matcher. Always "same".
+ def description
+ 'same'
+ end
+ end
+end
diff --git a/lib/state_machines/matcher_helpers.rb b/lib/state_machines/matcher_helpers.rb
new file mode 100644
index 0000000..056879e
--- /dev/null
+++ b/lib/state_machines/matcher_helpers.rb
@@ -0,0 +1,54 @@
+module StateMachines
+ # Provides a set of helper methods for generating matchers
+ module MatcherHelpers
+ # Represents a state that matches all known states in a machine.
+ #
+ # == Examples
+ #
+ # class Vehicle
+ # state_machine do
+ # before_transition any => :parked, :do => lambda {...}
+ # before_transition all - :parked => all - :idling, :do => lambda {}
+ #
+ # event :park
+ # transition all => :parked
+ # end
+ #
+ # event :crash
+ # transition all - :parked => :stalled
+ # end
+ # end
+ # end
+ #
+ # In the above example, +all+ will match the following states since they
+ # are known:
+ # * +parked+
+ # * +stalled+
+ # * +idling+
+ def all
+ AllMatcher.instance
+ end
+ alias_method :any, :all
+
+ # Represents a state that matches the original +from+ state. This is useful
+ # for defining transitions which are loopbacks.
+ #
+ # == Examples
+ #
+ # class Vehicle
+ # state_machine do
+ # event :ignite
+ # transition [:idling, :first_gear] => same
+ # end
+ # end
+ # end
+ #
+ # In the above example, +same+ will match whichever the from state is. In
+ # the case of the +ignite+ event, it is essential the same as the following:
+ #
+ # transition :parked => :parked, :first_gear => :first_gear
+ def same
+ LoopbackMatcher.instance
+ end
+ end
+end
diff --git a/lib/state_machines/node_collection.rb b/lib/state_machines/node_collection.rb
new file mode 100644
index 0000000..9ed3c44
--- /dev/null
+++ b/lib/state_machines/node_collection.rb
@@ -0,0 +1,219 @@
+module StateMachines
+ # Represents a collection of nodes in a state machine, be it events or states.
+ # Nodes will not differentiate between the String and Symbol versions of the
+ # values being indexed.
+ class NodeCollection
+ include Enumerable
+
+ # The machine associated with the nodes
+ attr_reader :machine
+
+ # Creates a new collection of nodes for the given state machine. By default,
+ # the collection is empty.
+ #
+ # Configuration options:
+ # * <tt>:index</tt> - One or more attributes to automatically generate
+ # hashed indices for in order to perform quick lookups. Default is to
+ # index by the :name attribute
+ def initialize(machine, options = {})
+ options.assert_valid_keys(:index)
+ options = { index: :name }.merge(options)
+
+ @machine = machine
+ @nodes = []
+ @index_names = Array(options[:index])
+ @indices = @index_names.reduce({}) do |indices, name|
+ indices[name] = {}
+ indices[:"#{name}_to_s"] = {}
+ indices[:"#{name}_to_sym"] = {}
+ indices
+ end
+ @default_index = Array(options[:index]).first
+ @contexts = []
+ end
+
+ # Creates a copy of this collection such that modifications don't affect
+ # the original collection
+ def initialize_copy(orig) #:nodoc:
+ super
+
+ nodes = @nodes
+ contexts = @contexts
+ @nodes = []
+ @contexts = []
+ @indices = @indices.reduce({}) { |indices, (name, *)| indices[name] = {}; indices }
+
+ # Add nodes *prior* to copying over the contexts so that they don't get
+ # evaluated multiple times
+ concat(nodes.map { |n| n.dup })
+ @contexts = contexts.dup
+ end
+
+ # Changes the current machine associated with the collection. In turn, this
+ # will change the state machine associated with each node in the collection.
+ def machine=(new_machine)
+ @machine = new_machine
+ each { |node| node.machine = new_machine }
+ end
+
+ # Gets the number of nodes in this collection
+ def length
+ @nodes.length
+ end
+
+ # Gets the set of unique keys for the given index
+ def keys(index_name = @default_index)
+ index(index_name).keys
+ end
+
+ # Tracks a context that should be evaluated for any nodes that get added
+ # which match the given set of nodes. Matchers can be used so that the
+ # context can get added once and evaluated after multiple adds.
+ def context(nodes, &block)
+ nodes = nodes.first.is_a?(Matcher) ? nodes.first : WhitelistMatcher.new(nodes)
+ @contexts << context = { nodes: nodes, block: block }
+
+ # Evaluate the new context for existing nodes
+ each { |node| eval_context(context, node) }
+
+ context
+ end
+
+ # Adds a new node to the collection. By doing so, this will also add it to
+ # the configured indices. This will also evaluate any existings contexts
+ # that match the new node.
+ def <<(node)
+ @nodes << node
+ @index_names.each { |name| add_to_index(name, value(node, name), node) }
+ @contexts.each { |context| eval_context(context, node) }
+ self
+ end
+
+ # Appends a group of nodes to the collection
+ def concat(nodes)
+ nodes.each { |node| self << node }
+ end
+
+ # Updates the indexed keys for the given node. If the node's attribute
+ # has changed since it was added to the collection, the old indexed keys
+ # will be replaced with the updated ones.
+ def update(node)
+ @index_names.each { |name| update_index(name, node) }
+ end
+
+ # Calls the block once for each element in self, passing that element as a
+ # parameter.
+ #
+ # states = StateMachines::NodeCollection.new
+ # states << StateMachines::State.new(machine, :parked)
+ # states << StateMachines::State.new(machine, :idling)
+ # states.each {|state| puts state.name, ' -- '}
+ #
+ # ...produces:
+ #
+ # parked -- idling --
+ def each
+ @nodes.each { |node| yield node }
+ self
+ end
+
+ # Gets the node at the given index.
+ #
+ # states = StateMachines::NodeCollection.new
+ # states << StateMachines::State.new(machine, :parked)
+ # states << StateMachines::State.new(machine, :idling)
+ #
+ # states.at(0).name # => :parked
+ # states.at(1).name # => :idling
+ def at(index)
+ @nodes[index]
+ end
+
+ # Gets the node indexed by the given key. By default, this will look up the
+ # key in the first index configured for the collection. A custom index can
+ # be specified like so:
+ #
+ # collection['parked', :value]
+ #
+ # The above will look up the "parked" key in a hash indexed by each node's
+ # +value+ attribute.
+ #
+ # If the key cannot be found, then nil will be returned.
+ def [](key, index_name = @default_index)
+ index(index_name)[key] ||
+ index(:"#{index_name}_to_s")[key.to_s] ||
+ to_sym?(key) && index(:"#{index_name}_to_sym")[:"#{key}"] ||
+ nil
+ end
+
+ # Gets the node indexed by the given key. By default, this will look up the
+ # key in the first index configured for the collection. A custom index can
+ # be specified like so:
+ #
+ # collection['parked', :value]
+ #
+ # The above will look up the "parked" key in a hash indexed by each node's
+ # +value+ attribute.
+ #
+ # If the key cannot be found, then an IndexError exception will be raised:
+ #
+ # collection['invalid', :value] # => IndexError: "invalid" is an invalid value
+ def fetch(key, index_name = @default_index)
+ self[key, index_name] || fail(IndexError, "#{key.inspect} is an invalid #{index_name}")
+ end
+
+ protected
+ # Gets the given index. If the index does not exist, then an ArgumentError
+ # is raised.
+ def index(name)
+ fail ArgumentError, 'No indices configured' unless @indices.any?
+ @indices[name] || fail(ArgumentError, "Invalid index: #{name.inspect}")
+ end
+
+ # Gets the value for the given attribute on the node
+ def value(node, attribute)
+ node.send(attribute)
+ end
+
+ # Adds the given key / node combination to an index, including the string
+ # and symbol versions of the index
+ def add_to_index(name, key, node)
+ index(name)[key] = node
+ index(:"#{name}_to_s")[key.to_s] = node
+ index(:"#{name}_to_sym")[:"#{key}"] = node if to_sym?(key)
+ end
+
+ # Removes the given key from an index, including the string and symbol
+ # versions of the index
+ def remove_from_index(name, key)
+ index(name).delete(key)
+ index(:"#{name}_to_s").delete(key.to_s)
+ index(:"#{name}_to_sym").delete(:"#{key}") if to_sym?(key)
+ end
+
+ # Updates the node for the given index, including the string and symbol
+ # versions of the index
+ def update_index(name, node)
+ index = self.index(name)
+ old_key = index.key(node)
+ new_key = value(node, name)
+
+ # Only replace the key if it's changed
+ if old_key != new_key
+ remove_from_index(name, old_key)
+ add_to_index(name, new_key, node)
+ end
+ end
+
+ # Determines whether the given value can be converted to a symbol
+ def to_sym?(value)
+ "#{value}" != ''
+ end
+
+ # Evaluates the given context for a particular node. This will only
+ # evaluate the context if the node matches.
+ def eval_context(context, node)
+ node.context(&context[:block]) if context[:nodes].matches?(node.name)
+ end
+ end
+end
diff --git a/lib/state_machines/path.rb b/lib/state_machines/path.rb
new file mode 100644
index 0000000..3d20a8a
--- /dev/null
+++ b/lib/state_machines/path.rb
@@ -0,0 +1,120 @@
+module StateMachines
+ # A path represents a sequence of transitions that can be run for a particular
+ # object. Paths can walk to new transitions, revealing all of the possible
+ # branches that can be encountered in the object's state machine.
+ class Path < Array
+
+
+ # The object whose state machine is being walked
+ attr_reader :object
+
+ # The state machine this path is walking
+ attr_reader :machine
+
+ # Creates a new transition path for the given object. Initially this is an
+ # empty path. In order to start walking the path, it must be populated with
+ # an initial transition.
+ #
+ # Configuration options:
+ # * <tt>:target</tt> - The target state to end the path on
+ # * <tt>:guard</tt> - Whether to guard transitions with the if/unless
+ # conditionals defined for each one
+ def initialize(object, machine, options = {})
+ options.assert_valid_keys(:target, :guard)
+
+ @object = object
+ @machine = machine
+ @target = options[:target]
+ @guard = options[:guard]
+ end
+
+ def initialize_copy(orig) #:nodoc:
+ super
+ @transitions = nil
+ end
+
+ # The initial state name for this path
+ def from_name
+ first && first.from_name
+ end
+
+ # Lists all of the from states that can be reached through this path.
+ #
+ # For example,
+ #
+ # path.to_states # => [:parked, :idling, :first_gear, ...]
+ def from_states
+ map {|transition| transition.from_name}.uniq
+ end
+
+ # The end state name for this path. If a target state was specified for
+ # the path, then that will be returned if the path is complete.
+ def to_name
+ last && last.to_name
+ end
+
+ # Lists all of the to states that can be reached through this path.
+ #
+ # For example,
+ #
+ # path.to_states # => [:parked, :idling, :first_gear, ...]
+ def to_states
+ map {|transition| transition.to_name}.uniq
+ end
+
+ # Lists all of the events that can be fired through this path.
+ #
+ # For example,
+ #
+ # path.events # => [:park, :ignite, :shift_up, ...]
+ def events
+ map {|transition| transition.event}.uniq
+ end
+
+ # Walks down the next transitions at the end of this path. This will only
+ # walk down paths that are considered valid.
+ def walk
+ transitions.each {|transition| yield dup.push(transition)}
+ end
+
+ # Determines whether or not this path has completed. A path is considered
+ # complete when one of the following conditions is met:
+ # * The last transition in the path ends on the target state
+ # * There are no more transitions remaining to walk and there is no target
+ # state
+ def complete?
+ !empty? && (@target ? to_name == @target : transitions.empty?)
+ end
+
+ private
+ # Calculates the number of times the given state has been walked to
+ def times_walked_to(state)
+ select {|transition| transition.to_name == state}.length
+ end
+
+ # Determines whether the given transition has been recently walked down in
+ # this path. If a target is configured for this path, then this will only
+ # look at transitions walked down since the target was last reached.
+ def recently_walked?(transition)
+ transitions = self
+ if @target && @target != to_name && target_transition = detect {|t| t.to_name == @target}
+ transitions = transitions[index(target_transition) + 1..-1]
+ end
+ transitions.include?(transition)
+ end
+
+ # Determines whether it's possible to walk to the given transition from
+ # the current path. A transition can be walked to if:
+ # * It has not been recently walked and
+ # * If a target is specified, it has not been walked to twice yet
+ def can_walk_to?(transition)
+ !recently_walked?(transition) && (!@target || times_walked_to(@target) < 2)
+ end
+
+ # Get the next set of transitions that can be walked to starting from the
+ # end of this path
+ def transitions
+ @transitions ||= empty? ? [] : machine.events.transitions_for(object, :from => to_name, :guard => @guard).select {|transition| can_walk_to?(transition)}
+ end
+ end
+end
diff --git a/lib/state_machines/path_collection.rb b/lib/state_machines/path_collection.rb
new file mode 100644
index 0000000..13095b7
--- /dev/null
+++ b/lib/state_machines/path_collection.rb
@@ -0,0 +1,88 @@
+module StateMachines
+ # Represents a collection of paths that are generated based on a set of
+ # requirements regarding what states to start and end on
+ class PathCollection < Array
+
+
+ # The object whose state machine is being walked
+ attr_reader :object
+
+ # The state machine these path are walking
+ attr_reader :machine
+
+ # The initial state to start each path from
+ attr_reader :from_name
+
+ # The target state for each path
+ attr_reader :to_name
+
+ # Creates a new collection of paths with the given requirements.
+ #
+ # Configuration options:
+ # * <tt>:from</tt> - The initial state to start from
+ # * <tt>:to</tt> - The target end state
+ # * <tt>:deep</tt> - Whether to enable deep searches for the target state.
+ # * <tt>:guard</tt> - Whether to guard transitions with the if/unless
+ # conditionals defined for each one
+ def initialize(object, machine, options = {})
+ options = {:deep => false, :from => machine.states.match!(object).name}.merge(options)
+ options.assert_valid_keys( :from, :to, :deep, :guard)
+
+ @object = object
+ @machine = machine
+ @from_name = machine.states.fetch(options[:from]).name
+ @to_name = options[:to] && machine.states.fetch(options[:to]).name
+ @guard = options[:guard]
+ @deep = options[:deep]
+
+ initial_paths.each {|path| walk(path)}
+ end
+
+ # Lists all of the states that can be transitioned from through the paths in
+ # this collection.
+ #
+ # For example,
+ #
+ # paths.from_states # => [:parked, :idling, :first_gear, ...]
+ def from_states
+ map {|path| path.from_states}.flatten.uniq
+ end
+
+ # Lists all of the states that can be transitioned to through the paths in
+ # this collection.
+ #
+ # For example,
+ #
+ # paths.to_states # => [:idling, :first_gear, :second_gear, ...]
+ def to_states
+ map {|path| path.to_states}.flatten.uniq
+ end
+
+ # Lists all of the events that can be fired through the paths in this
+ # collection.
+ #
+ # For example,
+ #
+ # paths.events # => [:park, :ignite, :shift_up, ...]
+ def events
+ map {|path| path.events}.flatten.uniq
+ end
+
+ private
+ # Gets the initial set of paths to walk
+ def initial_paths
+ machine.events.transitions_for(object, :from => from_name, :guard => @guard).map do |transition|
+ path = Path.new(object, machine, :target => to_name, :guard => @guard)
+ path << transition
+ path
+ end
+ end
+
+ # Walks down the given path. Each new path that matches the configured
+ # requirements will be added to this collection.
+ def walk(path)
+ self << path if path.complete?
+ path.walk {|next_path| walk(next_path)} unless to_name && path.complete? && !@deep
+ end
+ end
+end
diff --git a/lib/state_machines/state.rb b/lib/state_machines/state.rb
new file mode 100644
index 0000000..5effce0
--- /dev/null
+++ b/lib/state_machines/state.rb
@@ -0,0 +1,272 @@
+module StateMachines
+ # A state defines a value that an attribute can be in after being transitioned
+ # 0 or more times. States can represent a value of any type in Ruby, though
+ # the most common (and default) type is String.
+ #
+ # In addition to defining the machine's value, a state can also define a
+ # behavioral context for an object when that object is in the state. See
+ # StateMachines::Machine#state for more information about how state-driven
+ # behavior can be utilized.
+ class State
+
+ # The state machine for which this state is defined
+ attr_accessor :machine
+
+ # The unique identifier for the state used in event and callback definitions
+ attr_reader :name
+
+ # The fully-qualified identifier for the state, scoped by the machine's
+ # namespace
+ attr_reader :qualified_name
+
+ # The human-readable name for the state
+ attr_writer :human_name
+
+ # The value that is written to a machine's attribute when an object
+ # transitions into this state
+ attr_writer :value
+
+ # Whether this state's value should be cached after being evaluated
+ attr_accessor :cache
+
+ # Whether or not this state is the initial state to use for new objects
+ attr_accessor :initial
+ alias_method :initial?, :initial
+
+ # A custom lambda block for determining whether a given value matches this
+ # state
+ attr_accessor :matcher
+
+ # Creates a new state within the context of the given machine.
+ #
+ # Configuration options:
+ # * <tt>:initial</tt> - Whether this state is the beginning state for the
+ # machine. Default is false.
+ # * <tt>:value</tt> - The value to store when an object transitions to this
+ # state. Default is the name (stringified).
+ # * <tt>:cache</tt> - If a dynamic value (via a lambda block) is being used,
+ # then setting this to true will cache the evaluated result
+ # * <tt>:if</tt> - Determines whether a value matches this state
+ # (e.g. :value => lambda {Time.now}, :if => lambda {|state| !state.nil?}).
+ # By default, the configured value is matched.
+ # * <tt>:human_name</tt> - The human-readable version of this state's name
+ def initialize(machine, name, options = {}) #:nodoc:
+ options.assert_valid_keys(:initial, :value, :cache, :if, :human_name)
+
+ @machine = machine
+ @name = name
+ @qualified_name = name && machine.namespace ? :"#{machine.namespace}_#{name}" : name
+ @human_name = options[:human_name] || (@name ? @name.to_s.tr('_', ' ') : 'nil')
+ @value = options.include?(:value) ? options[:value] : name && name.to_s
+ @cache = options[:cache]
+ @matcher = options[:if]
+ @initial = options[:initial] == true
+ @context = StateContext.new(self)
+
+ if name
+ conflicting_machines = machine.owner_class.state_machines.select { |other_name, other_machine| other_machine != machine && other_machine.states[qualified_name, :qualified_name] }
+
+ # Output a warning if another machine has a conflicting qualified name
+ # for a different attribute
+ if conflict = conflicting_machines.detect { |other_name, other_machine| other_machine.attribute != machine.attribute }
+ name, other_machine = conflict
+ warn "State #{qualified_name.inspect} for #{machine.name.inspect} is already defined in #{other_machine.name.inspect}"
+ elsif conflicting_machines.empty?
+ # Only bother adding predicates when another machine for the same
+ # attribute hasn't already done so
+ add_predicate
+ end
+ end
+ end
+
+ # Creates a copy of this state, excluding the context to prevent conflicts
+ # across different machines.
+ def initialize_copy(orig) #:nodoc:
+ super
+ @context = StateContext.new(self)
+ end
+
+ # Determines whether there are any states that can be transitioned to from
+ # this state. If there are none, then this state is considered *final*.
+ # Any objects in a final state will remain so forever given the current
+ # machine's definition.
+ def final?
+ !machine.events.any? do |event|
+ event.branches.any? do |branch|
+ branch.state_requirements.any? do |requirement|
+ requirement[:from].matches?(name) && !requirement[:to].matches?(name, :from => name)
+ end
+ end
+ end
+ end
+
+ # Transforms the state name into a more human-readable format, such as
+ # "first gear" instead of "first_gear"
+ def human_name(klass = @machine.owner_class)
+ @human_name.is_a?(Proc) ? @human_name.call(self, klass) : @human_name
+ end
+
+ # Generates a human-readable description of this state's name / value:
+ #
+ # For example,
+ #
+ # State.new(machine, :parked).description # => "parked"
+ # State.new(machine, :parked, :value => :parked).description # => "parked"
+ # State.new(machine, :parked, :value => nil).description # => "parked (nil)"
+ # State.new(machine, :parked, :value => 1).description # => "parked (1)"
+ # State.new(machine, :parked, :value => lambda {Time.now}).description # => "parked (*)
+ #
+ # Configuration options:
+ # * <tt>:human_name</tt> - Whether to use this state's human name in the
+ # description or just the internal name
+ def description(options = {})
+ label = options[:human_name] ? human_name : name
+ description = label ? label.to_s : label.inspect
+ description << " (#{@value.is_a?(Proc) ? '*' : @value.inspect})" unless name.to_s == @value.to_s
+ description
+ end
+
+ # The value that represents this state. This will optionally evaluate the
+ # original block if it's a lambda block. Otherwise, the static value is
+ # returned.
+ #
+ # For example,
+ #
+ # State.new(machine, :parked, :value => 1).value # => 1
+ # State.new(machine, :parked, :value => lambda {Time.now}).value # => Tue Jan 01 00:00:00 UTC 2008
+ # State.new(machine, :parked, :value => lambda {Time.now}).value(false) # => <Proc:0xb6ea7ca0 at ...>
+ def value(eval = true)
+ if @value.is_a?(Proc) && eval
+ if cache_value?
+ @value = @value.call
+ machine.states.update(self)
+ @value
+ else
+ @value.call
+ end
+ else
+ @value
+ end
+ end
+
+ # Determines whether this state matches the given value. If no matcher is
+ # configured, then this will check whether the values are equivalent.
+ # Otherwise, the matcher will determine the result.
+ #
+ # For example,
+ #
+ # # Without a matcher
+ # state = State.new(machine, :parked, :value => 1)
+ # state.matches?(1) # => true
+ # state.matches?(2) # => false
+ #
+ # # With a matcher
+ # state = State.new(machine, :parked, :value => lambda {Time.now}, :if => lambda {|value| !value.nil?})
+ # state.matches?(nil) # => false
+ # state.matches?(Time.now) # => true
+ def matches?(other_value)
+ matcher ? matcher.call(other_value) : other_value == value
+ end
+
+ # Defines a context for the state which will be enabled on instances of
+ # the owner class when the machine is in this state.
+ #
+ # This can be called multiple times. Each time a new context is created,
+ # a new module will be included in the owner class.
+ def context(&block)
+ # Include the context
+ context = @context
+ machine.owner_class.class_eval { include context }
+
+ # Evaluate the method definitions and track which ones were added
+ old_methods = context_methods
+ context.class_eval(&block)
+ new_methods = context_methods.to_a.select { |(name, method)| old_methods[name] != method }
+
+ # Alias new methods so that the only execute when the object is in this state
+ new_methods.each do |(method_name, method)|
+ context_name = context_name_for(method_name)
+ context.class_eval <<-end_eval, __FILE__, __LINE__ + 1
+ alias_method :"#{context_name}", :#{method_name}
+ def #{method_name}(*args, &block)
+ state = self.class.state_machine(#{machine.name.inspect}).states.fetch(#{name.inspect})
+ options = {:method_missing => lambda {super(*args, &block)}, :method_name => #{method_name.inspect}}
+ state.call(self, :"#{context_name}", *(args + [options]), &block)
+ end
+ end_eval
+ end
+
+ true
+ end
+
+ # The list of methods that have been defined in this state's context
+ def context_methods
+ @context.instance_methods.inject({}) do |methods, name|
+ methods.merge(name.to_sym => @context.instance_method(name))
+ end
+ end
+
+ # Calls a method defined in this state's context on the given object. All
+ # arguments and any block will be passed into the method defined.
+ #
+ # If the method has never been defined for this state, then a NoMethodError
+ # will be raised.
+ def call(object, method, *args, &block)
+ options = args.last.is_a?(Hash) ? args.pop : {}
+ options = {:method_name => method}.merge(options)
+ state = machine.states.match!(object)
+
+ if state == self && object.respond_to?(method)
+ object.send(method, *args, &block)
+ elsif method_missing = options[:method_missing]
+ # Dispatch to the superclass since the object either isn't in this state
+ # or this state doesn't handle the method
+ begin
+ method_missing.call
+ rescue NoMethodError => ex
+ if ex.name.to_s == options[:method_name].to_s && ex.args == args
+ # No valid context for this method
+ raise InvalidContext.new(object, "State #{state.name.inspect} for #{machine.name.inspect} is not a valid context for calling ##{options[:method_name]}")
+ else
+ raise
+ end
+ end
+ end
+ end
+
+ def draw(graph, options = {})
+ fail NotImplementedError
+ end
+
+ # Generates a nicely formatted description of this state's contents.
+ #
+ # For example,
+ #
+ # state = StateMachines::State.new(machine, :parked, :value => 1, :initial => true)
+ # state # => #<StateMachines::State name=:parked value=1 initial=true context=[]>
+ def inspect
+ attributes = [[:name, name], [:value, @value], [:initial, initial?]]
+ "#<#{self.class} #{attributes.map { |attr, value| "#{attr}=#{value.inspect}" } * ' '}>"
+ end
+
+ private
+ # Should the value be cached after it's evaluated for the first time?
+ def cache_value?
+ @cache
+ end
+
+ # Adds a predicate method to the owner class so long as a name has
+ # actually been configured for the state
+ def add_predicate
+ # Checks whether the current value matches this state
+ machine.define_helper(:instance, "#{qualified_name}?") do |machine, object|
+ machine.states.matches?(object, name)
+ end
+ end
+
+ # Generates the name of the method containing the actual implementation
+ def context_name_for(method)
+ :"__#{machine.name}_#{name}_#{method}_#{@context.object_id}__"
+ end
+ end
+end
diff --git a/lib/state_machines/state_collection.rb b/lib/state_machines/state_collection.rb
new file mode 100644
index 0000000..db8b9dc
--- /dev/null
+++ b/lib/state_machines/state_collection.rb
@@ -0,0 +1,110 @@
+module StateMachines
+ # Represents a collection of states in a state machine
+ class StateCollection < NodeCollection
+ def initialize(machine) #:nodoc:
+ super(machine, :index => [:name, :qualified_name, :value])
+ end
+
+ # Determines whether the given object is in a specific state. If the
+ # object's current value doesn't match the state, then this will return
+ # false, otherwise true. If the given state is unknown, then an IndexError
+ # will be raised.
+ #
+ # == Examples
+ #
+ # class Vehicle
+ # state_machine :initial => :parked do
+ # other_states :idling
+ # end
+ # end
+ #
+ # states = Vehicle.state_machine.states
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
+ #
+ # states.matches?(vehicle, :parked) # => true
+ # states.matches?(vehicle, :idling) # => false
+ # states.matches?(vehicle, :invalid) # => IndexError: :invalid is an invalid key for :name index
+ def matches?(object, name)
+ fetch(name).matches?(machine.read(object, :state))
+ end
+
+ # Determines the current state of the given object as configured by this
+ # state machine. This will attempt to find a known state that matches
+ # the value of the attribute on the object.
+ #
+ # == Examples
+ #
+ # class Vehicle
+ # state_machine :initial => :parked do
+ # other_states :idling
+ # end
+ # end
+ #
+ # states = Vehicle.state_machine.states
+ #
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
+ # states.match(vehicle) # => #<StateMachines::State name=:parked value="parked" initial=true>
+ #
+ # vehicle.state = 'idling'
+ # states.match(vehicle) # => #<StateMachines::State name=:idling value="idling" initial=true>
+ #
+ # vehicle.state = 'invalid'
+ # states.match(vehicle) # => nil
+ def match(object)
+ value = machine.read(object, :state)
+ self[value, :value] || detect { |state| state.matches?(value) }
+ end
+
+ # Determines the current state of the given object as configured by this
+ # state machine. If no state is found, then an ArgumentError will be
+ # raised.
+ #
+ # == Examples
+ #
+ # class Vehicle
+ # state_machine :initial => :parked do
+ # other_states :idling
+ # end
+ # end
+ #
+ # states = Vehicle.state_machine.states
+ #
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
+ # states.match!(vehicle) # => #<StateMachines::State name=:parked value="parked" initial=true>
+ #
+ # vehicle.state = 'invalid'
+ # states.match!(vehicle) # => ArgumentError: "invalid" is not a known state value
+ def match!(object)
+ match(object) || raise(ArgumentError, "#{machine.read(object, :state).inspect} is not a known #{machine.name} value")
+ end
+
+ # Gets the order in which states should be displayed based on where they
+ # were first referenced. This will order states in the following priority:
+ #
+ # 1. Initial state
+ # 2. Event transitions (:from, :except_from, :to, :except_to options)
+ # 3. States with behaviors
+ # 4. States referenced via +state+ or +other_states+
+ # 5. States referenced in callbacks
+ #
+ # This order will determine how the GraphViz visualizations are rendered.
+ def by_priority
+ order = select { |state| state.initial }.map { |state| state.name }
+
+ machine.events.each { |event| order += event.known_states }
+ order += select { |state| state.context_methods.any? }.map { |state| state.name }
+ order += keys(:name) - machine.callbacks.values.flatten.map { |callback| callback.known_states }.flatten
+ order += keys(:name)
+
+ order.uniq!
+ order.map! { |name| self[name] }
+ order
+ end
+
+ private
+ # Gets the value for the given attribute on the node
+ def value(node, attribute)
+ attribute == :value ? node.value(false) : super
+ end
+ end
+end
diff --git a/lib/state_machines/state_context.rb b/lib/state_machines/state_context.rb
new file mode 100644
index 0000000..11ba031
--- /dev/null
+++ b/lib/state_machines/state_context.rb
@@ -0,0 +1,133 @@
+module StateMachines
+
+
+ # Represents a module which will get evaluated within the context of a state.
+ #
+ # Class-level methods are proxied to the owner class, injecting a custom
+ # <tt>:if</tt> condition along with method. This assumes that the method has
+ # support for a set of configuration options, including <tt>:if</tt>. This
+ # condition will check that the object's state matches this context's state.
+ #
+ # Instance-level methods are used to define state-driven behavior on the
+ # state's owner class.
+ #
+ # == Examples
+ #
+ # class Vehicle
+ # class << self
+ # attr_accessor :validations
+ #
+ # def validate(options, &block)
+ # validations << options
+ # end
+ # end
+ #
+ # self.validations = []
+ # attr_accessor :state, :simulate
+ #
+ # def moving?
+ # self.class.validations.all? {|validation| validation[:if].call(self)}
+ # end
+ # end
+ #
+ # In the above class, a simple set of validation behaviors have been defined.
+ # Each validation consists of a configuration like so:
+ #
+ # Vehicle.validate :unless => :simulate
+ # Vehicle.validate :if => lambda {|vehicle| ...}
+ #
+ # In order to scope validations to a particular state context, the class-level
+ # +validate+ method can be invoked like so:
+ #
+ # machine = StateMachines::Machine.new(Vehicle)
+ # context = StateMachines::StateContext.new(machine.state(:first_gear))
+ # context.validate(:unless => :simulate)
+ #
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7ce491c @simulate=nil, @state=nil>
+ # vehicle.moving? # => false
+ #
+ # vehicle.state = 'first_gear'
+ # vehicle.moving? # => true
+ #
+ # vehicle.simulate = true
+ # vehicle.moving? # => false
+ class StateContext < Module
+
+ include EvalHelpers
+
+ # The state machine for which this context's state is defined
+ attr_reader :machine
+
+ # The state that must be present in an object for this context to be active
+ attr_reader :state
+
+ # Creates a new context for the given state
+ def initialize(state)
+ @state = state
+ @machine = state.machine
+
+ state_name = state.name
+ machine_name = machine.name
+ @condition = lambda {|object| object.class.state_machine(machine_name).states.matches?(object, state_name)}
+ end
+
+ # Creates a new transition that determines what to change the current state
+ # to when an event fires from this state.
+ #
+ # Since this transition is being defined within a state context, you do
+ # *not* need to specify the <tt>:from</tt> option for the transition. For
+ # example:
+ #
+ # state_machine do
+ # state :parked do
+ # transition :to => :idling, :on => [:ignite, :shift_up] # Transitions to :idling
+ # transition :from => [:idling, :parked], :on => :park, :unless => :seatbelt_on? # Transitions to :parked if seatbelt is off
+ # end
+ # end
+ #
+ # See StateMachines::Machine#transition for a description of the possible
+ # configurations for defining transitions.
+ def transition(options)
+ options.assert_valid_keys(:from, :to, :on, :if, :unless)
+ raise ArgumentError, 'Must specify :on event' unless options[:on]
+ raise ArgumentError, 'Must specify either :to or :from state' unless !options[:to] ^ !options[:from]
+
+ machine.transition(options.merge(options[:to] ? {:from => state.name} : {:to => state.name}))
+ end
+
+ # Hooks in condition-merging to methods that don't exist in this module
+ def method_missing(*args, &block)
+ # Get the configuration
+ if args.last.is_a?(Hash)
+ options = args.last
+ else
+ args << options = {}
+ end
+
+ # Get any existing condition that may need to be merged
+ if_condition = options.delete(:if)
+ unless_condition = options.delete(:unless)
+
+ # Provide scope access to configuration in case the block is evaluated
+ # within the object instance
+ proxy = self
+ proxy_condition = @condition
+
+ # Replace the configuration condition with the one configured for this
+ # proxy, merging together any existing conditions
+ options[:if] = lambda do |*condition_args|
+ # Block may be executed within the context of the actual object, so
+ # it'll either be the first argument or the executing context
+ object = condition_args.first || self
+
+ proxy.evaluate_method(object, proxy_condition) &&
+ Array(if_condition).all? {|condition| proxy.evaluate_method(object, condition)} &&
+ !Array(unless_condition).any? {|condition| proxy.evaluate_method(object, condition)}
+ end
+
+ # Evaluate the method on the owner class with the condition proxied
+ # through
+ machine.owner_class.send(*args, &block)
+ end
+ end
+end
diff --git a/lib/state_machines/transition.rb b/lib/state_machines/transition.rb
new file mode 100644
index 0000000..940195b
--- /dev/null
+++ b/lib/state_machines/transition.rb
@@ -0,0 +1,414 @@
+module StateMachines
+ # A transition represents a state change for a specific attribute.
+ #
+ # Transitions consist of:
+ # * An event
+ # * A starting state
+ # * An ending state
+ class Transition
+ # The object being transitioned
+ attr_reader :object
+
+ # The state machine for which this transition is defined
+ attr_reader :machine
+
+ # The original state value *before* the transition
+ attr_reader :from
+
+ # The new state value *after* the transition
+ attr_reader :to
+
+ # The arguments passed in to the event that triggered the transition
+ # (does not include the +run_action+ boolean argument if specified)
+ attr_accessor :args
+
+ # The result of invoking the action associated with the machine
+ attr_reader :result
+
+ # Whether the transition is only existing temporarily for the object
+ attr_writer :transient
+
+ # Determines whether the current ruby implementation supports pausing and
+ # resuming transitions
+ def self.pause_supported?
+ %w(ruby maglev).include?(RUBY_ENGINE)
+ end
+
+ # Creates a new, specific transition
+ def initialize(object, machine, event, from_name, to_name, read_state = true) #:nodoc:
+ @object = object
+ @machine = machine
+ @args = []
+ @transient = false
+ @resume_block = nil
+
+ @event = machine.events.fetch(event)
+ @from_state = machine.states.fetch(from_name)
+ @from = read_state ? machine.read(object, :state) : @from_state.value
+ @to_state = machine.states.fetch(to_name)
+ @to = @to_state.value
+
+ reset
+ end
+
+ # The attribute which this transition's machine is defined for
+ def attribute
+ machine.attribute
+ end
+
+ # The action that will be run when this transition is performed
+ def action
+ machine.action
+ end
+
+ # The event that triggered the transition
+ def event
+ @event.name
+ end
+
+ # The fully-qualified name of the event that triggered the transition
+ def qualified_event
+ @event.qualified_name
+ end
+
+ # The human-readable name of the event that triggered the transition
+ def human_event
+ @event.human_name(@object.class)
+ end
+
+ # The state name *before* the transition
+ def from_name
+ @from_state.name
+ end
+
+ # The fully-qualified state name *before* the transition
+ def qualified_from_name
+ @from_state.qualified_name
+ end
+
+ # The human-readable state name *before* the transition
+ def human_from_name
+ @from_state.human_name(@object.class)
+ end
+
+ # The new state name *after* the transition
+ def to_name
+ @to_state.name
+ end
+
+ # The new fully-qualified state name *after* the transition
+ def qualified_to_name
+ @to_state.qualified_name
+ end
+
+ # The new human-readable state name *after* the transition
+ def human_to_name
+ @to_state.human_name(@object.class)
+ end
+
+ # Does this transition represent a loopback (i.e. the from and to state
+ # are the same)
+ #
+ # == Example
+ #
+ # machine = StateMachine.new(Vehicle)
+ # StateMachines::Transition.new(Vehicle.new, machine, :park, :parked, :parked).loopback? # => true
+ # StateMachines::Transition.new(Vehicle.new, machine, :park, :idling, :parked).loopback? # => false
+ def loopback?
+ from_name == to_name
+ end
+
+ # Is this transition existing for a short period only? If this is set, it
+ # indicates that the transition (or the event backing it) should not be
+ # written to the object if it fails.
+ def transient?
+ @transient
+ end
+
+ # A hash of all the core attributes defined for this transition with their
+ # names as keys and values of the attributes as values.
+ #
+ # == Example
+ #
+ # machine = StateMachine.new(Vehicle)
+ # transition = StateMachines::Transition.new(Vehicle.new, machine, :ignite, :parked, :idling)
+ # transition.attributes # => {:object => #<Vehicle:0xb7d60ea4>, :attribute => :state, :event => :ignite, :from => 'parked', :to => 'idling'}
+ def attributes
+ @attributes ||= {:object => object, :attribute => attribute, :event => event, :from => from, :to => to}
+ end
+
+ # Runs the actual transition and any before/after callbacks associated
+ # with the transition. The action associated with the transition/machine
+ # can be skipped by passing in +false+.
+ #
+ # == Examples
+ #
+ # class Vehicle
+ # state_machine :action => :save do
+ # ...
+ # end
+ # end
+ #
+ # vehicle = Vehicle.new
+ # transition = StateMachines::Transition.new(vehicle, machine, :ignite, :parked, :idling)
+ # transition.perform # => Runs the +save+ action after setting the state attribute
+ # transition.perform(false) # => Only sets the state attribute
+ # transition.perform(Time.now) # => Passes in additional arguments and runs the +save+ action
+ # transition.perform(Time.now, false) # => Passes in additional arguments and only sets the state attribute
+ def perform(*args)
+ run_action = [true, false].include?(args.last) ? args.pop : true
+ self.args = args
+
+ # Run the transition
+ !!TransitionCollection.new([self], {use_transactions: machine.use_transactions, actions: run_action}).perform
+ end
+
+ # Runs a block within a transaction for the object being transitioned.
+ # By default, transactions are a no-op unless otherwise defined by the
+ # machine's integration.
+ def within_transaction
+ machine.within_transaction(object) do
+ yield
+ end
+ end
+
+ # Runs the before / after callbacks for this transition. If a block is
+ # provided, then it will be executed between the before and after callbacks.
+ #
+ # Configuration options:
+ # * +before+ - Whether to run before callbacks.
+ # * +after+ - Whether to run after callbacks. If false, then any around
+ # callbacks will be paused until called again with +after+ enabled.
+ # Default is true.
+ #
+ # This will return true if all before callbacks gets executed. After
+ # callbacks will not have an effect on the result.
+ def run_callbacks(options = {}, &block)
+ options = {:before => true, :after => true}.merge(options)
+ @success = false
+
+ halted = pausable { before(options[:after], &block) } if options[:before]
+
+ # After callbacks are only run if:
+ # * An around callback didn't halt after yielding
+ # * They're enabled or the run didn't succeed
+ after if !(@before_run && halted) && (options[:after] || !@success)
+
+ @before_run
+ end
+
+ # Transitions the current value of the state to that specified by the
+ # transition. Once the state is persisted, it cannot be persisted again
+ # until this transition is reset.
+ #
+ # == Example
+ #
+ # class Vehicle
+ # state_machine do
+ # event :ignite do
+ # transition :parked => :idling
+ # end
+ # end
+ # end
+ #
+ # vehicle = Vehicle.new
+ # transition = StateMachines::Transition.new(vehicle, Vehicle.state_machine, :ignite, :parked, :idling)
+ # transition.persist
+ #
+ # vehicle.state # => 'idling'
+ def persist
+ unless @persisted
+ machine.write(object, :state, to)
+ @persisted = true
+ end
+ end
+
+ # Rolls back changes made to the object's state via this transition. This
+ # will revert the state back to the +from+ value.
+ #
+ # == Example
+ #
+ # class Vehicle
+ # state_machine :initial => :parked do
+ # event :ignite do
+ # transition :parked => :idling
+ # end
+ # end
+ # end
+ #
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7b7f568 @state="parked">
+ # transition = StateMachines::Transition.new(vehicle, Vehicle.state_machine, :ignite, :parked, :idling)
+ #
+ # # Persist the new state
+ # vehicle.state # => "parked"
+ # transition.persist
+ # vehicle.state # => "idling"
+ #
+ # # Roll back to the original state
+ # transition.rollback
+ # vehicle.state # => "parked"
+ def rollback
+ reset
+ machine.write(object, :state, from)
+ end
+
+ # Resets any tracking of which callbacks have already been run and whether
+ # the state has already been persisted
+ def reset
+ @before_run = @persisted = @after_run = false
+ @paused_block = nil
+ end
+
+ # Determines equality of transitions by testing whether the object, states,
+ # and event involved in the transition are equal
+ def ==(other)
+ other.instance_of?(self.class) &&
+ other.object == object &&
+ other.machine == machine &&
+ other.from_name == from_name &&
+ other.to_name == to_name &&
+ other.event == event
+ end
+
+ # Generates a nicely formatted description of this transitions's contents.
+ #
+ # For example,
+ #
+ # transition = StateMachines::Transition.new(object, machine, :ignite, :parked, :idling)
+ # transition # => #<StateMachines::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>
+ def inspect
+ "#<#{self.class} #{%w(attribute event from from_name to to_name).map {|attr| "#{attr}=#{send(attr).inspect}"} * ' '}>"
+ end
+
+ private
+ # Runs a block that may get paused. If the block doesn't pause, then
+ # execution will continue as normal. If the block gets paused, then it
+ # will take care of switching the execution context when it's resumed.
+ #
+ # This will return true if the given block halts for a reason other than
+ # getting paused.
+ def pausable
+ begin
+ halted = !catch(:halt) { yield; true }
+ rescue Exception => error
+ raise unless @resume_block
+ end
+
+ if @resume_block
+ @resume_block.call(halted, error)
+ else
+ halted
+ end
+ end
+
+ # Pauses the current callback execution. This should only occur within
+ # around callbacks when the remainder of the callback will be executed at
+ # a later point in time.
+ def pause
+ raise ArgumentError, 'around_transition callbacks cannot be called in multiple execution contexts in java implementations of Ruby. Use before/after_transitions instead.' unless self.class.pause_supported?
+
+ unless @resume_block
+ require 'continuation' unless defined?(callcc)
+ callcc do |block|
+ @paused_block = block
+ throw :halt, true
+ end
+ end
+ end
+
+ # Resumes the execution of a previously paused callback execution. Once
+ # the paused callbacks complete, the current execution will continue.
+ def resume
+ if @paused_block
+ halted, error = callcc do |block|
+ @resume_block = block
+ @paused_block.call
+ end
+
+ @resume_block = @paused_block = nil
+
+ raise error if error
+ !halted
+ else
+ true
+ end
+ end
+
+ # Runs the machine's +before+ callbacks for this transition. Only
+ # callbacks that are configured to match the event, from state, and to
+ # state will be invoked.
+ #
+ # Once the callbacks are run, they cannot be run again until this transition
+ # is reset.
+ def before(complete = true, index = 0, &block)
+ unless @before_run
+ while callback = machine.callbacks[:before][index]
+ index += 1
+
+ if callback.type == :around
+ # Around callback: need to handle recursively. Execution only gets
+ # paused if:
+ # * The block fails and the callback doesn't run on failures OR
+ # * The block succeeds, but after callbacks are disabled (in which
+ # case a continuation is stored for later execution)
+ return if catch(:cancel) do
+ callback.call(object, context, self) do
+ before(complete, index, &block)
+
+ pause if @success && !complete
+ throw :cancel, true unless @success
+ end
+ end
+ else
+ # Normal before callback
+ callback.call(object, context, self)
+ end
+ end
+
+ @before_run = true
+ end
+
+ action = {:success => true}.merge(block_given? ? yield : {})
+ @result, @success = action[:result], action[:success]
+ end
+
+ # Runs the machine's +after+ callbacks for this transition. Only
+ # callbacks that are configured to match the event, from state, and to
+ # state will be invoked.
+ #
+ # Once the callbacks are run, they cannot be run again until this transition
+ # is reset.
+ #
+ # == Halting
+ #
+ # If any callback throws a <tt>:halt</tt> exception, it will be caught
+ # and the callback chain will be automatically stopped. However, this
+ # exception will not bubble up to the caller since +after+ callbacks
+ # should never halt the execution of a +perform+.
+ def after
+ unless @after_run
+ # First resume previously paused callbacks
+ if resume
+ catch(:halt) do
+ type = @success ? :after : :failure
+ machine.callbacks[type].each {|callback| callback.call(object, context, self)}
+ end
+ end
+
+ @after_run = true
+ end
+ end
+
+ # Gets a hash of the context defining this unique transition (including
+ # event, from state, and to state).
+ #
+ # == Example
+ #
+ # machine = StateMachine.new(Vehicle)
+ # transition = StateMachines::Transition.new(Vehicle.new, machine, :ignite, :parked, :idling)
+ # transition.context # => {:on => :ignite, :from => :parked, :to => :idling}
+ def context
+ @context ||= {:on => event, :from => from_name, :to => to_name}
+ end
+ end
+end
diff --git a/lib/state_machines/transition_collection.rb b/lib/state_machines/transition_collection.rb
new file mode 100644
index 0000000..ff5cbd4
--- /dev/null
+++ b/lib/state_machines/transition_collection.rb
@@ -0,0 +1,246 @@
+module StateMachines
+ # Represents a collection of transitions in a state machine
+ class TransitionCollection < Array
+
+
+ # Whether to skip running the action for each transition's machine
+ attr_reader :skip_actions
+
+ # Whether to skip running the after callbacks
+ attr_reader :skip_after
+
+ # Whether transitions should wrapped around a transaction block
+ attr_reader :use_transactions
+
+ # Creates a new collection of transitions that can be run in parallel. Each
+ # transition *must* be for a different attribute.
+ #
+ # Configuration options:
+ # * <tt>:actions</tt> - Whether to run the action configured for each transition
+ # * <tt>:after</tt> - Whether to run after callbacks
+ # * <tt>:transaction</tt> - Whether to wrap transitions within a transaction
+ def initialize(transitions = [], options = {})
+ super(transitions)
+
+ # Determine the validity of the transitions as a whole
+ @valid = all?
+ reject! {|transition| !transition}
+
+ attributes = map {|transition| transition.attribute}.uniq
+ fail ArgumentError, 'Cannot perform multiple transitions in parallel for the same state machine attribute' if attributes.length != length
+
+ options.assert_valid_keys(:actions, :after, :use_transactions)
+ options = {actions: true, after: true, use_transactions: true}.merge(options)
+ @skip_actions = !options[:actions]
+ @skip_after = !options[:after]
+ @use_transactions = options[:use_transactions]
+ end
+
+ # Runs each of the collection's transitions in parallel.
+ #
+ # All transitions will run through the following steps:
+ # 1. Before callbacks
+ # 2. Persist state
+ # 3. Invoke action
+ # 4. After callbacks (if configured)
+ # 5. Rollback (if action is unsuccessful)
+ #
+ # If a block is passed to this method, that block will be called instead
+ # of invoking each transition's action.
+ def perform(&block)
+ reset
+
+ if valid?
+ if use_event_attributes? && !block_given?
+ each do |transition|
+ transition.transient = true
+ transition.machine.write(object, :event_transition, transition)
+ end
+
+ run_actions
+ else
+ within_transaction do
+ catch(:halt) { run_callbacks(&block) }
+ rollback unless success?
+ end
+ end
+ end
+
+ if actions.length == 1 && results.include?(actions.first)
+ results[actions.first]
+ else
+ success?
+ end
+ end
+
+ protected
+ attr_reader :results #:nodoc:
+
+ private
+ # Is this a valid set of transitions? If the collection was creating with
+ # any +false+ values for transitions, then the the collection will be
+ # marked as invalid.
+ def valid?
+ @valid
+ end
+
+ # Did each transition perform successfully? This will only be true if the
+ # following requirements are met:
+ # * No +before+ callbacks halt
+ # * All actions run successfully (always true if skipping actions)
+ def success?
+ @success
+ end
+
+ # Gets the object being transitioned
+ def object
+ first.object
+ end
+
+ # Gets the list of actions to run. If configured to skip actions, then
+ # this will return an empty collection.
+ def actions
+ empty? ? [nil] : map {|transition| transition.action}.uniq
+ end
+
+ # Determines whether an event attribute be used to trigger the transitions
+ # in this collection or whether the transitions be run directly *outside*
+ # of the action.
+ def use_event_attributes?
+ !skip_actions && !skip_after && actions.all? && actions.length == 1 && first.machine.action_hook?
+ end
+
+ # Resets any information tracked from previous attempts to perform the
+ # collection
+ def reset
+ @results = {}
+ @success = false
+ end
+
+ # Runs each transition's callbacks recursively. Once all before callbacks
+ # have been executed, the transitions will then be persisted and the
+ # configured actions will be run.
+ #
+ # If any transition fails to run its callbacks, :halt will be thrown.
+ def run_callbacks(index = 0, &block)
+ if transition = self[index]
+ throw :halt unless transition.run_callbacks(:after => !skip_after) do
+ run_callbacks(index + 1, &block)
+ {:result => results[transition.action], :success => success?}
+ end
+ else
+ persist
+ run_actions(&block)
+ end
+ end
+
+ # Transitions the current value of the object's states to those specified by
+ # each transition
+ def persist
+ each {|transition| transition.persist}
+ end
+
+ # Runs the actions for each transition. If a block is given method, then it
+ # will be called instead of invoking each transition's action.
+ #
+ # The results of the actions will be used to determine #success?.
+ def run_actions
+ catch_exceptions do
+ @success = if block_given?
+ result = yield
+ actions.each {|action| results[action] = result}
+ !!result
+ else
+ actions.compact.each {|action| !skip_actions && results[action] = object.send(action)}
+ results.values.all?
+ end
+ end
+ end
+
+ # Rolls back changes made to the object's states via each transition
+ def rollback
+ each {|transition| transition.rollback}
+ end
+
+ # Wraps the given block with a rescue handler so that any exceptions that
+ # occur will automatically result in the transition rolling back any changes
+ # that were made to the object involved.
+ def catch_exceptions
+ begin
+ yield
+ rescue Exception
+ rollback
+ raise
+ end
+ end
+
+ # Runs a block within a transaction for the object being transitioned. If
+ # transactions are disabled, then this is a no-op.
+ def within_transaction
+ if use_transactions && !empty?
+ first.within_transaction do
+ yield
+ success?
+ end
+ else
+ yield
+ end
+ end
+ end
+
+ # Represents a collection of transitions that were generated from attribute-
+ # based events
+ class AttributeTransitionCollection < TransitionCollection
+ def initialize(transitions = [], options = {}) #:nodoc:
+ super(transitions, {use_transactions: false, :actions => false}.merge(options))
+ end
+
+ private
+ # Hooks into running transition callbacks so that event / event transition
+ # attributes can be properly updated
+ def run_callbacks(index = 0)
+ if index == 0
+ # Clears any traces of the event attribute to prevent it from being
+ # evaluated multiple times if actions are nested
+ each do |transition|
+ transition.machine.write(object, :event, nil)
+ transition.machine.write(object, :event_transition, nil)
+ end
+
+ # Rollback only if exceptions occur during before callbacks
+ begin
+ super
+ rescue Exception
+ rollback unless @before_run
+ @success = nil # mimics ActiveRecord.save behavior on rollback
+ raise
+ end
+
+ # Persists transitions on the object if partial transition was successful.
+ # This allows us to reference them later to complete the transition with
+ # after callbacks.
+ each {|transition| transition.machine.write(object, :event_transition, transition)} if skip_after && success?
+ else
+ super
+ end
+ end
+
+ # Tracks that before callbacks have now completed
+ def persist
+ @before_run = true
+ super
+ end
+
+ # Resets callback tracking
+ def reset
+ super
+ @before_run = false
+ end
+
+ # Resets the event attribute so it can be re-evaluated if attempted again
+ def rollback
+ super
+ each {|transition| transition.machine.write(object, :event, transition.event) unless transition.transient?}
+ end
+ end
+end
diff --git a/lib/state_machines/version.rb b/lib/state_machines/version.rb
new file mode 100644
index 0000000..f8f0a68
--- /dev/null
+++ b/lib/state_machines/version.rb
@@ -0,0 +1,3 @@
+module StateMachines
+ VERSION = '0.4.0'
+end
diff --git a/metadata.yml b/metadata.yml
new file mode 100644
index 0000000..5bd7d43
--- /dev/null
+++ b/metadata.yml
@@ -0,0 +1,965 @@
+--- !ruby/object:Gem::Specification
+name: state_machines
+version: !ruby/object:Gem::Version
+ version: 0.4.0
+platform: ruby
+authors:
+- Abdelkader Boudih
+- Aaron Pfeifer
+autorequire:
+bindir: bin
+cert_chain: []
+date: 2015-06-18 00:00:00.000000000 Z
+dependencies:
+- !ruby/object:Gem::Dependency
+ name: bundler
+ requirement: !ruby/object:Gem::Requirement
+ requirements:
+ - - ">="
+ - !ruby/object:Gem::Version
+ version: 1.7.6
+ type: :development
+ prerelease: false
+ version_requirements: !ruby/object:Gem::Requirement
+ requirements:
+ - - ">="
+ - !ruby/object:Gem::Version
+ version: 1.7.6
+- !ruby/object:Gem::Dependency
+ name: rake
+ requirement: !ruby/object:Gem::Requirement
+ requirements:
+ - - ">="
+ - !ruby/object:Gem::Version
+ version: '0'
+ type: :development
+ prerelease: false
+ version_requirements: !ruby/object:Gem::Requirement
+ requirements:
+ - - ">="
+ - !ruby/object:Gem::Version
+ version: '0'
+- !ruby/object:Gem::Dependency
+ name: minitest
+ requirement: !ruby/object:Gem::Requirement
+ requirements:
+ - - ">="
+ - !ruby/object:Gem::Version
+ version: '5.4'
+ type: :development
+ prerelease: false
+ version_requirements: !ruby/object:Gem::Requirement
+ requirements:
+ - - ">="
+ - !ruby/object:Gem::Version
+ version: '5.4'
+description: Adds support for creating state machines for attributes on any Ruby class
+email:
+- terminale at gmail.com
+- aaron at pluginaweek.org
+executables: []
+extensions: []
+extra_rdoc_files: []
+files:
+- ".gitignore"
+- ".rspec"
+- ".travis.yml"
+- Changelog.md
+- Contributors.md
+- Gemfile
+- LICENSE.txt
+- README.md
+- Rakefile
+- Testing.md
+- lib/state_machines.rb
+- lib/state_machines/assertions.rb
+- lib/state_machines/branch.rb
+- lib/state_machines/callback.rb
+- lib/state_machines/core.rb
+- lib/state_machines/core_ext.rb
+- lib/state_machines/core_ext/class/state_machine.rb
+- lib/state_machines/error.rb
+- lib/state_machines/eval_helpers.rb
+- lib/state_machines/event.rb
+- lib/state_machines/event_collection.rb
+- lib/state_machines/extensions.rb
+- lib/state_machines/helper_module.rb
+- lib/state_machines/integrations.rb
+- lib/state_machines/integrations/base.rb
+- lib/state_machines/machine.rb
+- lib/state_machines/machine_collection.rb
+- lib/state_machines/macro_methods.rb
+- lib/state_machines/matcher.rb
+- lib/state_machines/matcher_helpers.rb
+- lib/state_machines/node_collection.rb
+- lib/state_machines/path.rb
+- lib/state_machines/path_collection.rb
+- lib/state_machines/state.rb
+- lib/state_machines/state_collection.rb
+- lib/state_machines/state_context.rb
+- lib/state_machines/transition.rb
+- lib/state_machines/transition_collection.rb
+- lib/state_machines/version.rb
+- state_machines.gemspec
+- test/files/integrations/event_on_failure_integration.rb
+- test/files/integrations/vehicle.rb
+- test/files/models/auto_shop.rb
+- test/files/models/car.rb
+- test/files/models/model_base.rb
+- test/files/models/motorcycle.rb
+- test/files/models/traffic_light.rb
+- test/files/models/vehicle.rb
+- test/files/node.rb
+- test/files/switch.rb
+- test/functional/auto_shop_available_test.rb
+- test/functional/auto_shop_busy_test.rb
+- test/functional/car_backing_up_test.rb
+- test/functional/car_test.rb
+- test/functional/motorcycle_test.rb
+- test/functional/traffic_light_caution_test.rb
+- test/functional/traffic_light_proceed_test.rb
+- test/functional/traffic_light_stop_test.rb
+- test/functional/vehicle_first_gear_test.rb
+- test/functional/vehicle_idling_test.rb
+- test/functional/vehicle_locked_test.rb
+- test/functional/vehicle_parked_test.rb
+- test/functional/vehicle_repaired_test.rb
+- test/functional/vehicle_second_gear_test.rb
+- test/functional/vehicle_stalled_test.rb
+- test/functional/vehicle_test.rb
+- test/functional/vehicle_third_gear_test.rb
+- test/functional/vehicle_unsaved_test.rb
+- test/functional/vehicle_with_event_attributes_test.rb
+- test/functional/vehicle_with_parallel_events_test.rb
+- test/test_helper.rb
+- test/unit/assertions/assert_exclusive_keys_test.rb
+- test/unit/assertions/assert_valid_key_test.rb
+- test/unit/branch/branch_test.rb
+- test/unit/branch/branch_with_conflicting_conditionals_test.rb
+- test/unit/branch/branch_with_conflicting_from_requirements_test.rb
+- test/unit/branch/branch_with_conflicting_on_requirements_test.rb
+- test/unit/branch/branch_with_conflicting_to_requirements_test.rb
+- test/unit/branch/branch_with_different_requirements_test.rb
+- test/unit/branch/branch_with_except_from_matcher_requirement_test.rb
+- test/unit/branch/branch_with_except_from_requirement_test.rb
+- test/unit/branch/branch_with_except_on_matcher_requirement_test.rb
+- test/unit/branch/branch_with_except_on_requirement_test.rb
+- test/unit/branch/branch_with_except_to_matcher_requirement_test.rb
+- test/unit/branch/branch_with_except_to_requirement_test.rb
+- test/unit/branch/branch_with_from_matcher_requirement_test.rb
+- test/unit/branch/branch_with_from_requirement_test.rb
+- test/unit/branch/branch_with_if_conditional_test.rb
+- test/unit/branch/branch_with_implicit_and_explicit_requirements_test.rb
+- test/unit/branch/branch_with_implicit_from_requirement_matcher_test.rb
+- test/unit/branch/branch_with_implicit_requirement_test.rb
+- test/unit/branch/branch_with_implicit_to_requirement_matcher_test.rb
+- test/unit/branch/branch_with_multiple_except_from_requirements_test.rb
+- test/unit/branch/branch_with_multiple_except_on_requirements_test.rb
+- test/unit/branch/branch_with_multiple_except_to_requirements_test.rb
+- test/unit/branch/branch_with_multiple_from_requirements_test.rb
+- test/unit/branch/branch_with_multiple_if_conditionals_test.rb
+- test/unit/branch/branch_with_multiple_implicit_requirements_test.rb
+- test/unit/branch/branch_with_multiple_on_requirements_test.rb
+- test/unit/branch/branch_with_multiple_to_requirements_test.rb
+- test/unit/branch/branch_with_multiple_unless_conditionals_test.rb
+- test/unit/branch/branch_with_nil_requirements_test.rb
+- test/unit/branch/branch_with_no_requirements_test.rb
+- test/unit/branch/branch_with_on_matcher_requirement_test.rb
+- test/unit/branch/branch_with_on_requirement_test.rb
+- test/unit/branch/branch_with_to_matcher_requirement_test.rb
+- test/unit/branch/branch_with_to_requirement_test.rb
+- test/unit/branch/branch_with_unless_conditional_test.rb
+- test/unit/branch/branch_without_guards_test.rb
+- test/unit/callback/callback_by_default_test.rb
+- test/unit/callback/callback_test.rb
+- test/unit/callback/callback_with_application_bound_object_test.rb
+- test/unit/callback/callback_with_application_terminator_test.rb
+- test/unit/callback/callback_with_arguments_test.rb
+- test/unit/callback/callback_with_around_type_and_arguments_test.rb
+- test/unit/callback/callback_with_around_type_and_block_test.rb
+- test/unit/callback/callback_with_around_type_and_bound_method_test.rb
+- test/unit/callback/callback_with_around_type_and_multiple_methods_test.rb
+- test/unit/callback/callback_with_around_type_and_terminator_test.rb
+- test/unit/callback/callback_with_block_test.rb
+- test/unit/callback/callback_with_bound_method_and_arguments_test.rb
+- test/unit/callback/callback_with_bound_method_test.rb
+- test/unit/callback/callback_with_do_method_test.rb
+- test/unit/callback/callback_with_explicit_requirements_test.rb
+- test/unit/callback/callback_with_if_condition_test.rb
+- test/unit/callback/callback_with_implicit_requirements_test.rb
+- test/unit/callback/callback_with_method_argument_test.rb
+- test/unit/callback/callback_with_mixed_methods_test.rb
+- test/unit/callback/callback_with_multiple_bound_methods_test.rb
+- test/unit/callback/callback_with_multiple_do_methods_test.rb
+- test/unit/callback/callback_with_multiple_method_arguments_test.rb
+- test/unit/callback/callback_with_terminator_test.rb
+- test/unit/callback/callback_with_unbound_method_test.rb
+- test/unit/callback/callback_with_unless_condition_test.rb
+- test/unit/callback/callback_without_arguments_test.rb
+- test/unit/callback/callback_without_terminator_test.rb
+- test/unit/error/error_by_default_test.rb
+- test/unit/error/error_with_message_test.rb
+- test/unit/eval_helper/eval_helpers_base_test.rb
+- test/unit/eval_helper/eval_helpers_proc_block_and_explicit_arguments_test.rb
+- test/unit/eval_helper/eval_helpers_proc_block_and_implicit_arguments_test.rb
+- test/unit/eval_helper/eval_helpers_proc_test.rb
+- test/unit/eval_helper/eval_helpers_proc_with_arguments_test.rb
+- test/unit/eval_helper/eval_helpers_proc_with_block_test.rb
+- test/unit/eval_helper/eval_helpers_proc_with_block_without_arguments_test.rb
+- test/unit/eval_helper/eval_helpers_proc_with_block_without_object_test.rb
+- test/unit/eval_helper/eval_helpers_proc_without_arguments_test.rb
+- test/unit/eval_helper/eval_helpers_string_test.rb
+- test/unit/eval_helper/eval_helpers_string_with_block_test.rb
+- test/unit/eval_helper/eval_helpers_symbol_method_missing_test.rb
+- test/unit/eval_helper/eval_helpers_symbol_private_test.rb
+- test/unit/eval_helper/eval_helpers_symbol_protected_test.rb
+- test/unit/eval_helper/eval_helpers_symbol_tainted_method_test.rb
+- test/unit/eval_helper/eval_helpers_symbol_test.rb
+- test/unit/eval_helper/eval_helpers_symbol_with_arguments_and_block_test.rb
+- test/unit/eval_helper/eval_helpers_symbol_with_arguments_test.rb
+- test/unit/eval_helper/eval_helpers_symbol_with_block_test.rb
+- test/unit/eval_helper/eval_helpers_test.rb
+- test/unit/event/event_after_being_copied_test.rb
+- test/unit/event/event_by_default_test.rb
+- test/unit/event/event_context_test.rb
+- test/unit/event/event_on_failure_test.rb
+- test/unit/event/event_test.rb
+- test/unit/event/event_transitions_test.rb
+- test/unit/event/event_with_conflicting_helpers_after_definition_test.rb
+- test/unit/event/event_with_conflicting_helpers_before_definition_test.rb
+- test/unit/event/event_with_conflicting_machine_test.rb
+- test/unit/event/event_with_dynamic_human_name_test.rb
+- test/unit/event/event_with_human_name_test.rb
+- test/unit/event/event_with_invalid_current_state_test.rb
+- test/unit/event/event_with_machine_action_test.rb
+- test/unit/event/event_with_marshalling_test.rb
+- test/unit/event/event_with_matching_disabled_transitions_test.rb
+- test/unit/event/event_with_matching_enabled_transitions_test.rb
+- test/unit/event/event_with_multiple_transitions_test.rb
+- test/unit/event/event_with_namespace_test.rb
+- test/unit/event/event_with_transition_with_blacklisted_to_state_test.rb
+- test/unit/event/event_with_transition_with_loopback_state_test.rb
+- test/unit/event/event_with_transition_with_nil_to_state_test.rb
+- test/unit/event/event_with_transition_with_whitelisted_to_state_test.rb
+- test/unit/event/event_with_transition_without_to_state_test.rb
+- test/unit/event/event_with_transitions_test.rb
+- test/unit/event/event_without_matching_transitions_test.rb
+- test/unit/event/event_without_transitions_test.rb
+- test/unit/event/invalid_event_test.rb
+- test/unit/event_collection/event_collection_attribute_with_machine_action_test.rb
+- test/unit/event_collection/event_collection_attribute_with_namespaced_machine_test.rb
+- test/unit/event_collection/event_collection_by_default_test.rb
+- test/unit/event_collection/event_collection_test.rb
+- test/unit/event_collection/event_collection_with_custom_machine_attribute_test.rb
+- test/unit/event_collection/event_collection_with_events_with_transitions_test.rb
+- test/unit/event_collection/event_collection_with_multiple_events_test.rb
+- test/unit/event_collection/event_collection_with_validations_test.rb
+- test/unit/event_collection/event_collection_without_machine_action_test.rb
+- test/unit/event_collection/event_string_collection_test.rb
+- test/unit/helper_module_test.rb
+- test/unit/integrations/integration_finder_test.rb
+- test/unit/integrations/integration_matcher_test.rb
+- test/unit/invalid_transition/invalid_parallel_transition_test.rb
+- test/unit/invalid_transition/invalid_transition_test.rb
+- test/unit/invalid_transition/invalid_transition_with_integration_test.rb
+- test/unit/invalid_transition/invalid_transition_with_namespace_test.rb
+- test/unit/machine/machine_after_being_copied_test.rb
+- test/unit/machine/machine_after_changing_initial_state.rb
+- test/unit/machine/machine_after_changing_owner_class_test.rb
+- test/unit/machine/machine_by_default_test.rb
+- test/unit/machine/machine_finder_custom_options_test.rb
+- test/unit/machine/machine_finder_with_existing_machine_on_superclass_test.rb
+- test/unit/machine/machine_finder_with_existing_on_same_class_test.rb
+- test/unit/machine/machine_finder_without_existing_machine_test.rb
+- test/unit/machine/machine_persistence_test.rb
+- test/unit/machine/machine_state_initialization_test.rb
+- test/unit/machine/machine_test.rb
+- test/unit/machine/machine_with_action_already_overridden_test.rb
+- test/unit/machine/machine_with_action_defined_in_class_test.rb
+- test/unit/machine/machine_with_action_defined_in_included_module_test.rb
+- test/unit/machine/machine_with_action_defined_in_superclass_test.rb
+- test/unit/machine/machine_with_action_undefined_test.rb
+- test/unit/machine/machine_with_cached_state_test.rb
+- test/unit/machine/machine_with_class_helpers_test.rb
+- test/unit/machine/machine_with_conflicting_helpers_after_definition_test.rb
+- test/unit/machine/machine_with_conflicting_helpers_before_definition_test.rb
+- test/unit/machine/machine_with_custom_action_test.rb
+- test/unit/machine/machine_with_custom_attribute_test.rb
+- test/unit/machine/machine_with_custom_initialize_test.rb
+- test/unit/machine/machine_with_custom_integration_test.rb
+- test/unit/machine/machine_with_custom_invalidation_test.rb
+- test/unit/machine/machine_with_custom_name_test.rb
+- test/unit/machine/machine_with_custom_plural_test.rb
+- test/unit/machine/machine_with_dynamic_initial_state_test.rb
+- test/unit/machine/machine_with_event_matchers_test.rb
+- test/unit/machine/machine_with_events_test.rb
+- test/unit/machine/machine_with_events_with_custom_human_names_test.rb
+- test/unit/machine/machine_with_events_with_transitions_test.rb
+- test/unit/machine/machine_with_existing_event_test.rb
+- test/unit/machine/machine_with_existing_machines_on_owner_class_test.rb
+- test/unit/machine/machine_with_existing_machines_with_same_attributes_on_owner_class_test.rb
+- test/unit/machine/machine_with_existing_machines_with_same_attributes_on_owner_subclass_test.rb
+- test/unit/machine/machine_with_existing_state_test.rb
+- test/unit/machine/machine_with_failure_callbacks_test.rb
+- test/unit/machine/machine_with_helpers_test.rb
+- test/unit/machine/machine_with_initial_state_with_value_and_owner_default.rb
+- test/unit/machine/machine_with_initialize_and_super_test.rb
+- test/unit/machine/machine_with_initialize_arguments_and_block_test.rb
+- test/unit/machine/machine_with_initialize_without_super_test.rb
+- test/unit/machine/machine_with_instance_helpers_test.rb
+- test/unit/machine/machine_with_integration_test.rb
+- test/unit/machine/machine_with_multiple_events_test.rb
+- test/unit/machine/machine_with_namespace_test.rb
+- test/unit/machine/machine_with_nil_action_test.rb
+- test/unit/machine/machine_with_other_states.rb
+- test/unit/machine/machine_with_owner_subclass_test.rb
+- test/unit/machine/machine_with_paths_test.rb
+- test/unit/machine/machine_with_private_action_test.rb
+- test/unit/machine/machine_with_state_matchers_test.rb
+- test/unit/machine/machine_with_state_with_matchers_test.rb
+- test/unit/machine/machine_with_states_test.rb
+- test/unit/machine/machine_with_states_with_behaviors_test.rb
+- test/unit/machine/machine_with_states_with_custom_human_names_test.rb
+- test/unit/machine/machine_with_states_with_custom_values_test.rb
+- test/unit/machine/machine_with_states_with_runtime_dependencies_test.rb
+- test/unit/machine/machine_with_static_initial_state_test.rb
+- test/unit/machine/machine_with_superclass_conflicting_helpers_after_definition_test.rb
+- test/unit/machine/machine_with_transition_callbacks_test.rb
+- test/unit/machine/machine_with_transitions_test.rb
+- test/unit/machine/machine_without_initialization_test.rb
+- test/unit/machine/machine_without_initialize_test.rb
+- test/unit/machine/machine_without_integration_test.rb
+- test/unit/machine_collection/machine_collection_by_default_test.rb
+- test/unit/machine_collection/machine_collection_fire_attributes_with_validations_test.rb
+- test/unit/machine_collection/machine_collection_fire_test.rb
+- test/unit/machine_collection/machine_collection_fire_with_transactions_test.rb
+- test/unit/machine_collection/machine_collection_fire_with_validations_test.rb
+- test/unit/machine_collection/machine_collection_state_initialization_test.rb
+- test/unit/machine_collection/machine_collection_transitions_with_blank_events_test.rb
+- test/unit/machine_collection/machine_collection_transitions_with_custom_options_test.rb
+- test/unit/machine_collection/machine_collection_transitions_with_different_actions_test.rb
+- test/unit/machine_collection/machine_collection_transitions_with_exisiting_transitions_test.rb
+- test/unit/machine_collection/machine_collection_transitions_with_invalid_events_test.rb
+- test/unit/machine_collection/machine_collection_transitions_with_same_actions_test.rb
+- test/unit/machine_collection/machine_collection_transitions_with_transition_test.rb
+- test/unit/machine_collection/machine_collection_transitions_without_events_test.rb
+- test/unit/machine_collection/machine_collection_transitions_without_transition_test.rb
+- test/unit/matcher/all_matcher_test.rb
+- test/unit/matcher/blacklist_matcher_test.rb
+- test/unit/matcher/loopback_matcher_test.rb
+- test/unit/matcher/matcher_by_default_test.rb
+- test/unit/matcher/matcher_with_multiple_values_test.rb
+- test/unit/matcher/matcher_with_value_test.rb
+- test/unit/matcher/whitelist_matcher_test.rb
+- test/unit/matcher_helpers/matcher_helpers_all_test.rb
+- test/unit/matcher_helpers/matcher_helpers_any_test.rb
+- test/unit/matcher_helpers/matcher_helpers_same_test.rb
+- test/unit/node_collection/node_collection_after_being_copied_test.rb
+- test/unit/node_collection/node_collection_after_update_test.rb
+- test/unit/node_collection/node_collection_by_default_test.rb
+- test/unit/node_collection/node_collection_test.rb
+- test/unit/node_collection/node_collection_with_indices_test.rb
+- test/unit/node_collection/node_collection_with_matcher_contexts_test.rb
+- test/unit/node_collection/node_collection_with_nodes_test.rb
+- test/unit/node_collection/node_collection_with_numeric_index_test.rb
+- test/unit/node_collection/node_collection_with_postdefined_contexts_test.rb
+- test/unit/node_collection/node_collection_with_predefined_contexts_test.rb
+- test/unit/node_collection/node_collection_with_string_index_test.rb
+- test/unit/node_collection/node_collection_with_symbol_index_test.rb
+- test/unit/node_collection/node_collection_without_indices_test.rb
+- test/unit/path/path_by_default_test.rb
+- test/unit/path/path_test.rb
+- test/unit/path/path_with_available_transitions_after_reaching_target_test.rb
+- test/unit/path/path_with_available_transitions_test.rb
+- test/unit/path/path_with_deep_target_reached_test.rb
+- test/unit/path/path_with_deep_target_test.rb
+- test/unit/path/path_with_duplicates_test.rb
+- test/unit/path/path_with_encountered_transitions_test.rb
+- test/unit/path/path_with_guarded_transitions_test.rb
+- test/unit/path/path_with_reached_target_test.rb
+- test/unit/path/path_with_transitions_test.rb
+- test/unit/path/path_with_unreached_target_test.rb
+- test/unit/path/path_without_transitions_test.rb
+- test/unit/path_collection/path_collection_by_default_test.rb
+- test/unit/path_collection/path_collection_test.rb
+- test/unit/path_collection/path_collection_with_deep_paths_test.rb
+- test/unit/path_collection/path_collection_with_duplicate_nodes_test.rb
+- test/unit/path_collection/path_collection_with_from_state_test.rb
+- test/unit/path_collection/path_collection_with_paths_test.rb
+- test/unit/path_collection/path_collection_with_to_state_test.rb
+- test/unit/path_collection/path_with_guarded_paths_test.rb
+- test/unit/state/state_after_being_copied_test.rb
+- test/unit/state/state_by_default_test.rb
+- test/unit/state/state_final_test.rb
+- test/unit/state/state_initial_test.rb
+- test/unit/state/state_not_final_test.rb
+- test/unit/state/state_not_initial_test.rb
+- test/unit/state/state_test.rb
+- test/unit/state/state_with_cached_lambda_value_test.rb
+- test/unit/state/state_with_conflicting_helpers_after_definition_test.rb
+- test/unit/state/state_with_conflicting_helpers_before_definition_test.rb
+- test/unit/state/state_with_conflicting_machine_name_test.rb
+- test/unit/state/state_with_conflicting_machine_test.rb
+- test/unit/state/state_with_context_test.rb
+- test/unit/state/state_with_dynamic_human_name_test.rb
+- test/unit/state/state_with_existing_context_method_test.rb
+- test/unit/state/state_with_human_name_test.rb
+- test/unit/state/state_with_integer_value_test.rb
+- test/unit/state/state_with_invalid_method_call_test.rb
+- test/unit/state/state_with_lambda_value_test.rb
+- test/unit/state/state_with_matcher_test.rb
+- test/unit/state/state_with_multiple_contexts_test.rb
+- test/unit/state/state_with_name_test.rb
+- test/unit/state/state_with_namespace_test.rb
+- test/unit/state/state_with_nil_value_test.rb
+- test/unit/state/state_with_redefined_context_method_test.rb
+- test/unit/state/state_with_symbolic_value_test.rb
+- test/unit/state/state_with_valid_inherited_method_call_for_current_state_test.rb
+- test/unit/state/state_with_valid_method_call_for_current_state_test.rb
+- test/unit/state/state_with_valid_method_call_for_different_state_test.rb
+- test/unit/state/state_without_cached_lambda_value_test.rb
+- test/unit/state/state_without_name_test.rb
+- test/unit/state_collection/state_collection_by_default_test.rb
+- test/unit/state_collection/state_collection_string_test.rb
+- test/unit/state_collection/state_collection_test.rb
+- test/unit/state_collection/state_collection_with_custom_state_values_test.rb
+- test/unit/state_collection/state_collection_with_event_transitions_test.rb
+- test/unit/state_collection/state_collection_with_initial_state_test.rb
+- test/unit/state_collection/state_collection_with_namespace_test.rb
+- test/unit/state_collection/state_collection_with_state_behaviors_test.rb
+- test/unit/state_collection/state_collection_with_state_matchers_test.rb
+- test/unit/state_collection/state_collection_with_transition_callbacks_test.rb
+- test/unit/state_context/state_context_proxy_test.rb
+- test/unit/state_context/state_context_proxy_with_if_and_unless_conditions_test.rb
+- test/unit/state_context/state_context_proxy_with_if_condition_test.rb
+- test/unit/state_context/state_context_proxy_with_multiple_if_conditions_test.rb
+- test/unit/state_context/state_context_proxy_with_multiple_unless_conditions_test.rb
+- test/unit/state_context/state_context_proxy_with_unless_condition_test.rb
+- test/unit/state_context/state_context_proxy_without_conditions_test.rb
+- test/unit/state_context/state_context_test.rb
+- test/unit/state_context/state_context_transition_test.rb
+- test/unit/state_context/state_context_with_matching_transition_test.rb
+- test/unit/state_machine/state_machine_by_default_test.rb
+- test/unit/state_machine/state_machine_test.rb
+- test/unit/transition/transition_after_being_performed_test.rb
+- test/unit/transition/transition_after_being_persisted_test.rb
+- test/unit/transition/transition_after_being_rolled_back_test.rb
+- test/unit/transition/transition_equality_test.rb
+- test/unit/transition/transition_loopback_test.rb
+- test/unit/transition/transition_test.rb
+- test/unit/transition/transition_transient_test.rb
+- test/unit/transition/transition_with_action_test.rb
+- test/unit/transition/transition_with_after_callbacks_skipped_test.rb
+- test/unit/transition/transition_with_after_callbacks_test.rb
+- test/unit/transition/transition_with_around_callbacks_test.rb
+- test/unit/transition/transition_with_before_callbacks_skipped_test.rb
+- test/unit/transition/transition_with_before_callbacks_test.rb
+- test/unit/transition/transition_with_custom_machine_attribute_test.rb
+- test/unit/transition/transition_with_different_states_test.rb
+- test/unit/transition/transition_with_dynamic_to_value_test.rb
+- test/unit/transition/transition_with_failure_callbacks_test.rb
+- test/unit/transition/transition_with_invalid_nodes_test.rb
+- test/unit/transition/transition_with_mixed_callbacks_test.rb
+- test/unit/transition/transition_with_multiple_after_callbacks_test.rb
+- test/unit/transition/transition_with_multiple_around_callbacks_test.rb
+- test/unit/transition/transition_with_multiple_before_callbacks_test.rb
+- test/unit/transition/transition_with_multiple_failure_callbacks_test.rb
+- test/unit/transition/transition_with_namespace_test.rb
+- test/unit/transition/transition_with_perform_arguments_test.rb
+- test/unit/transition/transition_with_transactions_test.rb
+- test/unit/transition/transition_without_callbacks_test.rb
+- test/unit/transition/transition_without_reading_state_test.rb
+- test/unit/transition/transition_without_running_action_test.rb
+- test/unit/transition_collection/attribute_transition_collection_by_default_test.rb
+- test/unit/transition_collection/attribute_transition_collection_marshalling_test.rb
+- test/unit/transition_collection/attribute_transition_collection_with_action_error_test.rb
+- test/unit/transition_collection/attribute_transition_collection_with_action_failed_test.rb
+- test/unit/transition_collection/attribute_transition_collection_with_after_callback_error_test.rb
+- test/unit/transition_collection/attribute_transition_collection_with_after_callback_halt_test.rb
+- test/unit/transition_collection/attribute_transition_collection_with_around_after_yield_callback_error_test.rb
+- test/unit/transition_collection/attribute_transition_collection_with_around_callback_after_yield_error_test.rb
+- test/unit/transition_collection/attribute_transition_collection_with_around_callback_after_yield_halt_test.rb
+- test/unit/transition_collection/attribute_transition_collection_with_around_callback_before_yield_halt_test.rb
+- test/unit/transition_collection/attribute_transition_collection_with_before_callback_error_test.rb
+- test/unit/transition_collection/attribute_transition_collection_with_before_callback_halt_test.rb
+- test/unit/transition_collection/attribute_transition_collection_with_callbacks_test.rb
+- test/unit/transition_collection/attribute_transition_collection_with_event_transitions_test.rb
+- test/unit/transition_collection/attribute_transition_collection_with_events_test.rb
+- test/unit/transition_collection/attribute_transition_collection_with_skipped_after_callbacks_test.rb
+- test/unit/transition_collection/transition_collection_by_default_test.rb
+- test/unit/transition_collection/transition_collection_empty_with_block_test.rb
+- test/unit/transition_collection/transition_collection_empty_without_block_test.rb
+- test/unit/transition_collection/transition_collection_invalid_test.rb
+- test/unit/transition_collection/transition_collection_partial_invalid_test.rb
+- test/unit/transition_collection/transition_collection_test.rb
+- test/unit/transition_collection/transition_collection_valid_test.rb
+- test/unit/transition_collection/transition_collection_with_action_error_test.rb
+- test/unit/transition_collection/transition_collection_with_action_failed_test.rb
+- test/unit/transition_collection/transition_collection_with_action_hook_and_block_test.rb
+- test/unit/transition_collection/transition_collection_with_action_hook_and_skipped_action_test.rb
+- test/unit/transition_collection/transition_collection_with_action_hook_and_skipped_after_callbacks_test.rb
+- test/unit/transition_collection/transition_collection_with_action_hook_base_test.rb
+- test/unit/transition_collection/transition_collection_with_action_hook_error_test.rb
+- test/unit/transition_collection/transition_collection_with_action_hook_invalid_test.rb
+- test/unit/transition_collection/transition_collection_with_action_hook_multiple_test.rb
+- test/unit/transition_collection/transition_collection_with_action_hook_test.rb
+- test/unit/transition_collection/transition_collection_with_action_hook_with_different_actions_test.rb
+- test/unit/transition_collection/transition_collection_with_action_hook_with_nil_action_test.rb
+- test/unit/transition_collection/transition_collection_with_after_callback_halt_test.rb
+- test/unit/transition_collection/transition_collection_with_before_callback_halt_test.rb
+- test/unit/transition_collection/transition_collection_with_block_test.rb
+- test/unit/transition_collection/transition_collection_with_callbacks_test.rb
+- test/unit/transition_collection/transition_collection_with_different_actions_test.rb
+- test/unit/transition_collection/transition_collection_with_duplicate_actions_test.rb
+- test/unit/transition_collection/transition_collection_with_empty_actions_test.rb
+- test/unit/transition_collection/transition_collection_with_mixed_actions_test.rb
+- test/unit/transition_collection/transition_collection_with_skipped_actions_and_block_test.rb
+- test/unit/transition_collection/transition_collection_with_skipped_actions_test.rb
+- test/unit/transition_collection/transition_collection_with_skipped_after_callbacks_and_around_callbacks_test.rb
+- test/unit/transition_collection/transition_collection_with_skipped_after_callbacks_test.rb
+- test/unit/transition_collection/transition_collection_with_transactions_test.rb
+- test/unit/transition_collection/transition_collection_without_transactions_test.rb
+homepage: https://github.com/state-machines/state_machines
+licenses:
+- MIT
+metadata: {}
+post_install_message:
+rdoc_options: []
+require_paths:
+- lib
+required_ruby_version: !ruby/object:Gem::Requirement
+ requirements:
+ - - ">="
+ - !ruby/object:Gem::Version
+ version: 1.9.3
+required_rubygems_version: !ruby/object:Gem::Requirement
+ requirements:
+ - - ">="
+ - !ruby/object:Gem::Version
+ version: '0'
+requirements: []
+rubyforge_project:
+rubygems_version: 2.4.5
+signing_key:
+specification_version: 4
+summary: State machines for attributes
+test_files:
+- test/files/integrations/event_on_failure_integration.rb
+- test/files/integrations/vehicle.rb
+- test/files/models/auto_shop.rb
+- test/files/models/car.rb
+- test/files/models/model_base.rb
+- test/files/models/motorcycle.rb
+- test/files/models/traffic_light.rb
+- test/files/models/vehicle.rb
+- test/files/node.rb
+- test/files/switch.rb
+- test/functional/auto_shop_available_test.rb
+- test/functional/auto_shop_busy_test.rb
+- test/functional/car_backing_up_test.rb
+- test/functional/car_test.rb
+- test/functional/motorcycle_test.rb
+- test/functional/traffic_light_caution_test.rb
+- test/functional/traffic_light_proceed_test.rb
+- test/functional/traffic_light_stop_test.rb
+- test/functional/vehicle_first_gear_test.rb
+- test/functional/vehicle_idling_test.rb
+- test/functional/vehicle_locked_test.rb
+- test/functional/vehicle_parked_test.rb
+- test/functional/vehicle_repaired_test.rb
+- test/functional/vehicle_second_gear_test.rb
+- test/functional/vehicle_stalled_test.rb
+- test/functional/vehicle_test.rb
+- test/functional/vehicle_third_gear_test.rb
+- test/functional/vehicle_unsaved_test.rb
+- test/functional/vehicle_with_event_attributes_test.rb
+- test/functional/vehicle_with_parallel_events_test.rb
+- test/test_helper.rb
+- test/unit/assertions/assert_exclusive_keys_test.rb
+- test/unit/assertions/assert_valid_key_test.rb
+- test/unit/branch/branch_test.rb
+- test/unit/branch/branch_with_conflicting_conditionals_test.rb
+- test/unit/branch/branch_with_conflicting_from_requirements_test.rb
+- test/unit/branch/branch_with_conflicting_on_requirements_test.rb
+- test/unit/branch/branch_with_conflicting_to_requirements_test.rb
+- test/unit/branch/branch_with_different_requirements_test.rb
+- test/unit/branch/branch_with_except_from_matcher_requirement_test.rb
+- test/unit/branch/branch_with_except_from_requirement_test.rb
+- test/unit/branch/branch_with_except_on_matcher_requirement_test.rb
+- test/unit/branch/branch_with_except_on_requirement_test.rb
+- test/unit/branch/branch_with_except_to_matcher_requirement_test.rb
+- test/unit/branch/branch_with_except_to_requirement_test.rb
+- test/unit/branch/branch_with_from_matcher_requirement_test.rb
+- test/unit/branch/branch_with_from_requirement_test.rb
+- test/unit/branch/branch_with_if_conditional_test.rb
+- test/unit/branch/branch_with_implicit_and_explicit_requirements_test.rb
+- test/unit/branch/branch_with_implicit_from_requirement_matcher_test.rb
+- test/unit/branch/branch_with_implicit_requirement_test.rb
+- test/unit/branch/branch_with_implicit_to_requirement_matcher_test.rb
+- test/unit/branch/branch_with_multiple_except_from_requirements_test.rb
+- test/unit/branch/branch_with_multiple_except_on_requirements_test.rb
+- test/unit/branch/branch_with_multiple_except_to_requirements_test.rb
+- test/unit/branch/branch_with_multiple_from_requirements_test.rb
+- test/unit/branch/branch_with_multiple_if_conditionals_test.rb
+- test/unit/branch/branch_with_multiple_implicit_requirements_test.rb
+- test/unit/branch/branch_with_multiple_on_requirements_test.rb
+- test/unit/branch/branch_with_multiple_to_requirements_test.rb
+- test/unit/branch/branch_with_multiple_unless_conditionals_test.rb
+- test/unit/branch/branch_with_nil_requirements_test.rb
+- test/unit/branch/branch_with_no_requirements_test.rb
+- test/unit/branch/branch_with_on_matcher_requirement_test.rb
+- test/unit/branch/branch_with_on_requirement_test.rb
+- test/unit/branch/branch_with_to_matcher_requirement_test.rb
+- test/unit/branch/branch_with_to_requirement_test.rb
+- test/unit/branch/branch_with_unless_conditional_test.rb
+- test/unit/branch/branch_without_guards_test.rb
+- test/unit/callback/callback_by_default_test.rb
+- test/unit/callback/callback_test.rb
+- test/unit/callback/callback_with_application_bound_object_test.rb
+- test/unit/callback/callback_with_application_terminator_test.rb
+- test/unit/callback/callback_with_arguments_test.rb
+- test/unit/callback/callback_with_around_type_and_arguments_test.rb
+- test/unit/callback/callback_with_around_type_and_block_test.rb
+- test/unit/callback/callback_with_around_type_and_bound_method_test.rb
+- test/unit/callback/callback_with_around_type_and_multiple_methods_test.rb
+- test/unit/callback/callback_with_around_type_and_terminator_test.rb
+- test/unit/callback/callback_with_block_test.rb
+- test/unit/callback/callback_with_bound_method_and_arguments_test.rb
+- test/unit/callback/callback_with_bound_method_test.rb
+- test/unit/callback/callback_with_do_method_test.rb
+- test/unit/callback/callback_with_explicit_requirements_test.rb
+- test/unit/callback/callback_with_if_condition_test.rb
+- test/unit/callback/callback_with_implicit_requirements_test.rb
+- test/unit/callback/callback_with_method_argument_test.rb
+- test/unit/callback/callback_with_mixed_methods_test.rb
+- test/unit/callback/callback_with_multiple_bound_methods_test.rb
+- test/unit/callback/callback_with_multiple_do_methods_test.rb
+- test/unit/callback/callback_with_multiple_method_arguments_test.rb
+- test/unit/callback/callback_with_terminator_test.rb
+- test/unit/callback/callback_with_unbound_method_test.rb
+- test/unit/callback/callback_with_unless_condition_test.rb
+- test/unit/callback/callback_without_arguments_test.rb
+- test/unit/callback/callback_without_terminator_test.rb
+- test/unit/error/error_by_default_test.rb
+- test/unit/error/error_with_message_test.rb
+- test/unit/eval_helper/eval_helpers_base_test.rb
+- test/unit/eval_helper/eval_helpers_proc_block_and_explicit_arguments_test.rb
+- test/unit/eval_helper/eval_helpers_proc_block_and_implicit_arguments_test.rb
+- test/unit/eval_helper/eval_helpers_proc_test.rb
+- test/unit/eval_helper/eval_helpers_proc_with_arguments_test.rb
+- test/unit/eval_helper/eval_helpers_proc_with_block_test.rb
+- test/unit/eval_helper/eval_helpers_proc_with_block_without_arguments_test.rb
+- test/unit/eval_helper/eval_helpers_proc_with_block_without_object_test.rb
+- test/unit/eval_helper/eval_helpers_proc_without_arguments_test.rb
+- test/unit/eval_helper/eval_helpers_string_test.rb
+- test/unit/eval_helper/eval_helpers_string_with_block_test.rb
+- test/unit/eval_helper/eval_helpers_symbol_method_missing_test.rb
+- test/unit/eval_helper/eval_helpers_symbol_private_test.rb
+- test/unit/eval_helper/eval_helpers_symbol_protected_test.rb
+- test/unit/eval_helper/eval_helpers_symbol_tainted_method_test.rb
+- test/unit/eval_helper/eval_helpers_symbol_test.rb
+- test/unit/eval_helper/eval_helpers_symbol_with_arguments_and_block_test.rb
+- test/unit/eval_helper/eval_helpers_symbol_with_arguments_test.rb
+- test/unit/eval_helper/eval_helpers_symbol_with_block_test.rb
+- test/unit/eval_helper/eval_helpers_test.rb
+- test/unit/event/event_after_being_copied_test.rb
+- test/unit/event/event_by_default_test.rb
+- test/unit/event/event_context_test.rb
+- test/unit/event/event_on_failure_test.rb
+- test/unit/event/event_test.rb
+- test/unit/event/event_transitions_test.rb
+- test/unit/event/event_with_conflicting_helpers_after_definition_test.rb
+- test/unit/event/event_with_conflicting_helpers_before_definition_test.rb
+- test/unit/event/event_with_conflicting_machine_test.rb
+- test/unit/event/event_with_dynamic_human_name_test.rb
+- test/unit/event/event_with_human_name_test.rb
+- test/unit/event/event_with_invalid_current_state_test.rb
+- test/unit/event/event_with_machine_action_test.rb
+- test/unit/event/event_with_marshalling_test.rb
+- test/unit/event/event_with_matching_disabled_transitions_test.rb
+- test/unit/event/event_with_matching_enabled_transitions_test.rb
+- test/unit/event/event_with_multiple_transitions_test.rb
+- test/unit/event/event_with_namespace_test.rb
+- test/unit/event/event_with_transition_with_blacklisted_to_state_test.rb
+- test/unit/event/event_with_transition_with_loopback_state_test.rb
+- test/unit/event/event_with_transition_with_nil_to_state_test.rb
+- test/unit/event/event_with_transition_with_whitelisted_to_state_test.rb
+- test/unit/event/event_with_transition_without_to_state_test.rb
+- test/unit/event/event_with_transitions_test.rb
+- test/unit/event/event_without_matching_transitions_test.rb
+- test/unit/event/event_without_transitions_test.rb
+- test/unit/event/invalid_event_test.rb
+- test/unit/event_collection/event_collection_attribute_with_machine_action_test.rb
+- test/unit/event_collection/event_collection_attribute_with_namespaced_machine_test.rb
+- test/unit/event_collection/event_collection_by_default_test.rb
+- test/unit/event_collection/event_collection_test.rb
+- test/unit/event_collection/event_collection_with_custom_machine_attribute_test.rb
+- test/unit/event_collection/event_collection_with_events_with_transitions_test.rb
+- test/unit/event_collection/event_collection_with_multiple_events_test.rb
+- test/unit/event_collection/event_collection_with_validations_test.rb
+- test/unit/event_collection/event_collection_without_machine_action_test.rb
+- test/unit/event_collection/event_string_collection_test.rb
+- test/unit/helper_module_test.rb
+- test/unit/integrations/integration_finder_test.rb
+- test/unit/integrations/integration_matcher_test.rb
+- test/unit/invalid_transition/invalid_parallel_transition_test.rb
+- test/unit/invalid_transition/invalid_transition_test.rb
+- test/unit/invalid_transition/invalid_transition_with_integration_test.rb
+- test/unit/invalid_transition/invalid_transition_with_namespace_test.rb
+- test/unit/machine/machine_after_being_copied_test.rb
+- test/unit/machine/machine_after_changing_initial_state.rb
+- test/unit/machine/machine_after_changing_owner_class_test.rb
+- test/unit/machine/machine_by_default_test.rb
+- test/unit/machine/machine_finder_custom_options_test.rb
+- test/unit/machine/machine_finder_with_existing_machine_on_superclass_test.rb
+- test/unit/machine/machine_finder_with_existing_on_same_class_test.rb
+- test/unit/machine/machine_finder_without_existing_machine_test.rb
+- test/unit/machine/machine_persistence_test.rb
+- test/unit/machine/machine_state_initialization_test.rb
+- test/unit/machine/machine_test.rb
+- test/unit/machine/machine_with_action_already_overridden_test.rb
+- test/unit/machine/machine_with_action_defined_in_class_test.rb
+- test/unit/machine/machine_with_action_defined_in_included_module_test.rb
+- test/unit/machine/machine_with_action_defined_in_superclass_test.rb
+- test/unit/machine/machine_with_action_undefined_test.rb
+- test/unit/machine/machine_with_cached_state_test.rb
+- test/unit/machine/machine_with_class_helpers_test.rb
+- test/unit/machine/machine_with_conflicting_helpers_after_definition_test.rb
+- test/unit/machine/machine_with_conflicting_helpers_before_definition_test.rb
+- test/unit/machine/machine_with_custom_action_test.rb
+- test/unit/machine/machine_with_custom_attribute_test.rb
+- test/unit/machine/machine_with_custom_initialize_test.rb
+- test/unit/machine/machine_with_custom_integration_test.rb
+- test/unit/machine/machine_with_custom_invalidation_test.rb
+- test/unit/machine/machine_with_custom_name_test.rb
+- test/unit/machine/machine_with_custom_plural_test.rb
+- test/unit/machine/machine_with_dynamic_initial_state_test.rb
+- test/unit/machine/machine_with_event_matchers_test.rb
+- test/unit/machine/machine_with_events_test.rb
+- test/unit/machine/machine_with_events_with_custom_human_names_test.rb
+- test/unit/machine/machine_with_events_with_transitions_test.rb
+- test/unit/machine/machine_with_existing_event_test.rb
+- test/unit/machine/machine_with_existing_machines_on_owner_class_test.rb
+- test/unit/machine/machine_with_existing_machines_with_same_attributes_on_owner_class_test.rb
+- test/unit/machine/machine_with_existing_machines_with_same_attributes_on_owner_subclass_test.rb
+- test/unit/machine/machine_with_existing_state_test.rb
+- test/unit/machine/machine_with_failure_callbacks_test.rb
+- test/unit/machine/machine_with_helpers_test.rb
+- test/unit/machine/machine_with_initial_state_with_value_and_owner_default.rb
+- test/unit/machine/machine_with_initialize_and_super_test.rb
+- test/unit/machine/machine_with_initialize_arguments_and_block_test.rb
+- test/unit/machine/machine_with_initialize_without_super_test.rb
+- test/unit/machine/machine_with_instance_helpers_test.rb
+- test/unit/machine/machine_with_integration_test.rb
+- test/unit/machine/machine_with_multiple_events_test.rb
+- test/unit/machine/machine_with_namespace_test.rb
+- test/unit/machine/machine_with_nil_action_test.rb
+- test/unit/machine/machine_with_other_states.rb
+- test/unit/machine/machine_with_owner_subclass_test.rb
+- test/unit/machine/machine_with_paths_test.rb
+- test/unit/machine/machine_with_private_action_test.rb
+- test/unit/machine/machine_with_state_matchers_test.rb
+- test/unit/machine/machine_with_state_with_matchers_test.rb
+- test/unit/machine/machine_with_states_test.rb
+- test/unit/machine/machine_with_states_with_behaviors_test.rb
+- test/unit/machine/machine_with_states_with_custom_human_names_test.rb
+- test/unit/machine/machine_with_states_with_custom_values_test.rb
+- test/unit/machine/machine_with_states_with_runtime_dependencies_test.rb
+- test/unit/machine/machine_with_static_initial_state_test.rb
+- test/unit/machine/machine_with_superclass_conflicting_helpers_after_definition_test.rb
+- test/unit/machine/machine_with_transition_callbacks_test.rb
+- test/unit/machine/machine_with_transitions_test.rb
+- test/unit/machine/machine_without_initialization_test.rb
+- test/unit/machine/machine_without_initialize_test.rb
+- test/unit/machine/machine_without_integration_test.rb
+- test/unit/machine_collection/machine_collection_by_default_test.rb
+- test/unit/machine_collection/machine_collection_fire_attributes_with_validations_test.rb
+- test/unit/machine_collection/machine_collection_fire_test.rb
+- test/unit/machine_collection/machine_collection_fire_with_transactions_test.rb
+- test/unit/machine_collection/machine_collection_fire_with_validations_test.rb
+- test/unit/machine_collection/machine_collection_state_initialization_test.rb
+- test/unit/machine_collection/machine_collection_transitions_with_blank_events_test.rb
+- test/unit/machine_collection/machine_collection_transitions_with_custom_options_test.rb
+- test/unit/machine_collection/machine_collection_transitions_with_different_actions_test.rb
+- test/unit/machine_collection/machine_collection_transitions_with_exisiting_transitions_test.rb
+- test/unit/machine_collection/machine_collection_transitions_with_invalid_events_test.rb
+- test/unit/machine_collection/machine_collection_transitions_with_same_actions_test.rb
+- test/unit/machine_collection/machine_collection_transitions_with_transition_test.rb
+- test/unit/machine_collection/machine_collection_transitions_without_events_test.rb
+- test/unit/machine_collection/machine_collection_transitions_without_transition_test.rb
+- test/unit/matcher/all_matcher_test.rb
+- test/unit/matcher/blacklist_matcher_test.rb
+- test/unit/matcher/loopback_matcher_test.rb
+- test/unit/matcher/matcher_by_default_test.rb
+- test/unit/matcher/matcher_with_multiple_values_test.rb
+- test/unit/matcher/matcher_with_value_test.rb
+- test/unit/matcher/whitelist_matcher_test.rb
+- test/unit/matcher_helpers/matcher_helpers_all_test.rb
+- test/unit/matcher_helpers/matcher_helpers_any_test.rb
+- test/unit/matcher_helpers/matcher_helpers_same_test.rb
+- test/unit/node_collection/node_collection_after_being_copied_test.rb
+- test/unit/node_collection/node_collection_after_update_test.rb
+- test/unit/node_collection/node_collection_by_default_test.rb
+- test/unit/node_collection/node_collection_test.rb
+- test/unit/node_collection/node_collection_with_indices_test.rb
+- test/unit/node_collection/node_collection_with_matcher_contexts_test.rb
+- test/unit/node_collection/node_collection_with_nodes_test.rb
+- test/unit/node_collection/node_collection_with_numeric_index_test.rb
+- test/unit/node_collection/node_collection_with_postdefined_contexts_test.rb
+- test/unit/node_collection/node_collection_with_predefined_contexts_test.rb
+- test/unit/node_collection/node_collection_with_string_index_test.rb
+- test/unit/node_collection/node_collection_with_symbol_index_test.rb
+- test/unit/node_collection/node_collection_without_indices_test.rb
+- test/unit/path/path_by_default_test.rb
+- test/unit/path/path_test.rb
+- test/unit/path/path_with_available_transitions_after_reaching_target_test.rb
+- test/unit/path/path_with_available_transitions_test.rb
+- test/unit/path/path_with_deep_target_reached_test.rb
+- test/unit/path/path_with_deep_target_test.rb
+- test/unit/path/path_with_duplicates_test.rb
+- test/unit/path/path_with_encountered_transitions_test.rb
+- test/unit/path/path_with_guarded_transitions_test.rb
+- test/unit/path/path_with_reached_target_test.rb
+- test/unit/path/path_with_transitions_test.rb
+- test/unit/path/path_with_unreached_target_test.rb
+- test/unit/path/path_without_transitions_test.rb
+- test/unit/path_collection/path_collection_by_default_test.rb
+- test/unit/path_collection/path_collection_test.rb
+- test/unit/path_collection/path_collection_with_deep_paths_test.rb
+- test/unit/path_collection/path_collection_with_duplicate_nodes_test.rb
+- test/unit/path_collection/path_collection_with_from_state_test.rb
+- test/unit/path_collection/path_collection_with_paths_test.rb
+- test/unit/path_collection/path_collection_with_to_state_test.rb
+- test/unit/path_collection/path_with_guarded_paths_test.rb
+- test/unit/state/state_after_being_copied_test.rb
+- test/unit/state/state_by_default_test.rb
+- test/unit/state/state_final_test.rb
+- test/unit/state/state_initial_test.rb
+- test/unit/state/state_not_final_test.rb
+- test/unit/state/state_not_initial_test.rb
+- test/unit/state/state_test.rb
+- test/unit/state/state_with_cached_lambda_value_test.rb
+- test/unit/state/state_with_conflicting_helpers_after_definition_test.rb
+- test/unit/state/state_with_conflicting_helpers_before_definition_test.rb
+- test/unit/state/state_with_conflicting_machine_name_test.rb
+- test/unit/state/state_with_conflicting_machine_test.rb
+- test/unit/state/state_with_context_test.rb
+- test/unit/state/state_with_dynamic_human_name_test.rb
+- test/unit/state/state_with_existing_context_method_test.rb
+- test/unit/state/state_with_human_name_test.rb
+- test/unit/state/state_with_integer_value_test.rb
+- test/unit/state/state_with_invalid_method_call_test.rb
+- test/unit/state/state_with_lambda_value_test.rb
+- test/unit/state/state_with_matcher_test.rb
+- test/unit/state/state_with_multiple_contexts_test.rb
+- test/unit/state/state_with_name_test.rb
+- test/unit/state/state_with_namespace_test.rb
+- test/unit/state/state_with_nil_value_test.rb
+- test/unit/state/state_with_redefined_context_method_test.rb
+- test/unit/state/state_with_symbolic_value_test.rb
+- test/unit/state/state_with_valid_inherited_method_call_for_current_state_test.rb
+- test/unit/state/state_with_valid_method_call_for_current_state_test.rb
+- test/unit/state/state_with_valid_method_call_for_different_state_test.rb
+- test/unit/state/state_without_cached_lambda_value_test.rb
+- test/unit/state/state_without_name_test.rb
+- test/unit/state_collection/state_collection_by_default_test.rb
+- test/unit/state_collection/state_collection_string_test.rb
+- test/unit/state_collection/state_collection_test.rb
+- test/unit/state_collection/state_collection_with_custom_state_values_test.rb
+- test/unit/state_collection/state_collection_with_event_transitions_test.rb
+- test/unit/state_collection/state_collection_with_initial_state_test.rb
+- test/unit/state_collection/state_collection_with_namespace_test.rb
+- test/unit/state_collection/state_collection_with_state_behaviors_test.rb
+- test/unit/state_collection/state_collection_with_state_matchers_test.rb
+- test/unit/state_collection/state_collection_with_transition_callbacks_test.rb
+- test/unit/state_context/state_context_proxy_test.rb
+- test/unit/state_context/state_context_proxy_with_if_and_unless_conditions_test.rb
+- test/unit/state_context/state_context_proxy_with_if_condition_test.rb
+- test/unit/state_context/state_context_proxy_with_multiple_if_conditions_test.rb
+- test/unit/state_context/state_context_proxy_with_multiple_unless_conditions_test.rb
+- test/unit/state_context/state_context_proxy_with_unless_condition_test.rb
+- test/unit/state_context/state_context_proxy_without_conditions_test.rb
+- test/unit/state_context/state_context_test.rb
+- test/unit/state_context/state_context_transition_test.rb
+- test/unit/state_context/state_context_with_matching_transition_test.rb
+- test/unit/state_machine/state_machine_by_default_test.rb
+- test/unit/state_machine/state_machine_test.rb
+- test/unit/transition/transition_after_being_performed_test.rb
+- test/unit/transition/transition_after_being_persisted_test.rb
+- test/unit/transition/transition_after_being_rolled_back_test.rb
+- test/unit/transition/transition_equality_test.rb
+- test/unit/transition/transition_loopback_test.rb
+- test/unit/transition/transition_test.rb
+- test/unit/transition/transition_transient_test.rb
+- test/unit/transition/transition_with_action_test.rb
+- test/unit/transition/transition_with_after_callbacks_skipped_test.rb
+- test/unit/transition/transition_with_after_callbacks_test.rb
+- test/unit/transition/transition_with_around_callbacks_test.rb
+- test/unit/transition/transition_with_before_callbacks_skipped_test.rb
+- test/unit/transition/transition_with_before_callbacks_test.rb
+- test/unit/transition/transition_with_custom_machine_attribute_test.rb
+- test/unit/transition/transition_with_different_states_test.rb
+- test/unit/transition/transition_with_dynamic_to_value_test.rb
+- test/unit/transition/transition_with_failure_callbacks_test.rb
+- test/unit/transition/transition_with_invalid_nodes_test.rb
+- test/unit/transition/transition_with_mixed_callbacks_test.rb
+- test/unit/transition/transition_with_multiple_after_callbacks_test.rb
+- test/unit/transition/transition_with_multiple_around_callbacks_test.rb
+- test/unit/transition/transition_with_multiple_before_callbacks_test.rb
+- test/unit/transition/transition_with_multiple_failure_callbacks_test.rb
+- test/unit/transition/transition_with_namespace_test.rb
+- test/unit/transition/transition_with_perform_arguments_test.rb
+- test/unit/transition/transition_with_transactions_test.rb
+- test/unit/transition/transition_without_callbacks_test.rb
+- test/unit/transition/transition_without_reading_state_test.rb
+- test/unit/transition/transition_without_running_action_test.rb
+- test/unit/transition_collection/attribute_transition_collection_by_default_test.rb
+- test/unit/transition_collection/attribute_transition_collection_marshalling_test.rb
+- test/unit/transition_collection/attribute_transition_collection_with_action_error_test.rb
+- test/unit/transition_collection/attribute_transition_collection_with_action_failed_test.rb
+- test/unit/transition_collection/attribute_transition_collection_with_after_callback_error_test.rb
+- test/unit/transition_collection/attribute_transition_collection_with_after_callback_halt_test.rb
+- test/unit/transition_collection/attribute_transition_collection_with_around_after_yield_callback_error_test.rb
+- test/unit/transition_collection/attribute_transition_collection_with_around_callback_after_yield_error_test.rb
+- test/unit/transition_collection/attribute_transition_collection_with_around_callback_after_yield_halt_test.rb
+- test/unit/transition_collection/attribute_transition_collection_with_around_callback_before_yield_halt_test.rb
+- test/unit/transition_collection/attribute_transition_collection_with_before_callback_error_test.rb
+- test/unit/transition_collection/attribute_transition_collection_with_before_callback_halt_test.rb
+- test/unit/transition_collection/attribute_transition_collection_with_callbacks_test.rb
+- test/unit/transition_collection/attribute_transition_collection_with_event_transitions_test.rb
+- test/unit/transition_collection/attribute_transition_collection_with_events_test.rb
+- test/unit/transition_collection/attribute_transition_collection_with_skipped_after_callbacks_test.rb
+- test/unit/transition_collection/transition_collection_by_default_test.rb
+- test/unit/transition_collection/transition_collection_empty_with_block_test.rb
+- test/unit/transition_collection/transition_collection_empty_without_block_test.rb
+- test/unit/transition_collection/transition_collection_invalid_test.rb
+- test/unit/transition_collection/transition_collection_partial_invalid_test.rb
+- test/unit/transition_collection/transition_collection_test.rb
+- test/unit/transition_collection/transition_collection_valid_test.rb
+- test/unit/transition_collection/transition_collection_with_action_error_test.rb
+- test/unit/transition_collection/transition_collection_with_action_failed_test.rb
+- test/unit/transition_collection/transition_collection_with_action_hook_and_block_test.rb
+- test/unit/transition_collection/transition_collection_with_action_hook_and_skipped_action_test.rb
+- test/unit/transition_collection/transition_collection_with_action_hook_and_skipped_after_callbacks_test.rb
+- test/unit/transition_collection/transition_collection_with_action_hook_base_test.rb
+- test/unit/transition_collection/transition_collection_with_action_hook_error_test.rb
+- test/unit/transition_collection/transition_collection_with_action_hook_invalid_test.rb
+- test/unit/transition_collection/transition_collection_with_action_hook_multiple_test.rb
+- test/unit/transition_collection/transition_collection_with_action_hook_test.rb
+- test/unit/transition_collection/transition_collection_with_action_hook_with_different_actions_test.rb
+- test/unit/transition_collection/transition_collection_with_action_hook_with_nil_action_test.rb
+- test/unit/transition_collection/transition_collection_with_after_callback_halt_test.rb
+- test/unit/transition_collection/transition_collection_with_before_callback_halt_test.rb
+- test/unit/transition_collection/transition_collection_with_block_test.rb
+- test/unit/transition_collection/transition_collection_with_callbacks_test.rb
+- test/unit/transition_collection/transition_collection_with_different_actions_test.rb
+- test/unit/transition_collection/transition_collection_with_duplicate_actions_test.rb
+- test/unit/transition_collection/transition_collection_with_empty_actions_test.rb
+- test/unit/transition_collection/transition_collection_with_mixed_actions_test.rb
+- test/unit/transition_collection/transition_collection_with_skipped_actions_and_block_test.rb
+- test/unit/transition_collection/transition_collection_with_skipped_actions_test.rb
+- test/unit/transition_collection/transition_collection_with_skipped_after_callbacks_and_around_callbacks_test.rb
+- test/unit/transition_collection/transition_collection_with_skipped_after_callbacks_test.rb
+- test/unit/transition_collection/transition_collection_with_transactions_test.rb
+- test/unit/transition_collection/transition_collection_without_transactions_test.rb
+has_rdoc:
diff --git a/state_machines.gemspec b/state_machines.gemspec
new file mode 100644
index 0000000..66bf767
--- /dev/null
+++ b/state_machines.gemspec
@@ -0,0 +1,23 @@
+lib = File.expand_path('../lib', __FILE__)
+$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
+require 'state_machines/version'
+
+Gem::Specification.new do |spec|
+ spec.name = 'state_machines'
+ spec.version = StateMachines::VERSION
+ spec.authors = ['Abdelkader Boudih', 'Aaron Pfeifer']
+ spec.email = %w(terminale at gmail.com aaron at pluginaweek.org)
+ spec.summary = %q(State machines for attributes)
+ spec.description = %q(Adds support for creating state machines for attributes on any Ruby class)
+ spec.homepage = 'https://github.com/state-machines/state_machines'
+ spec.license = 'MIT'
+
+ spec.required_ruby_version = '>= 1.9.3'
+ spec.files = `git ls-files -z`.split("\x0")
+ spec.test_files = spec.files.grep(/^test\//)
+ spec.require_paths = ['lib']
+
+ spec.add_development_dependency 'bundler', '>= 1.7.6'
+ spec.add_development_dependency 'rake'
+ spec.add_development_dependency 'minitest', '>= 5.4'
+end
diff --git a/test/files/integrations/event_on_failure_integration.rb b/test/files/integrations/event_on_failure_integration.rb
new file mode 100644
index 0000000..606de58
--- /dev/null
+++ b/test/files/integrations/event_on_failure_integration.rb
@@ -0,0 +1,10 @@
+module EventOnFailureIntegration
+ include StateMachines::Integrations::Base
+ def invalidate(object, _attribute, message, values = [])
+ (object.errors ||= []) << generate_message(message, values)
+ end
+
+ def reset(object)
+ object.errors = []
+ end
+end
\ No newline at end of file
diff --git a/test/files/integrations/vehicle.rb b/test/files/integrations/vehicle.rb
new file mode 100644
index 0000000..c0ce7ed
--- /dev/null
+++ b/test/files/integrations/vehicle.rb
@@ -0,0 +1,7 @@
+module VehicleIntegration
+ include StateMachines::Integrations::Base
+
+ def self.matching_ancestors
+ ['Vehicle']
+ end
+end
\ No newline at end of file
diff --git a/test/files/models/auto_shop.rb b/test/files/models/auto_shop.rb
new file mode 100644
index 0000000..2125a22
--- /dev/null
+++ b/test/files/models/auto_shop.rb
@@ -0,0 +1,31 @@
+class AutoShop
+ attr_accessor :num_customers
+
+ def initialize
+ @num_customers = 0
+ super
+ end
+
+ state_machine initial: :available do
+ after_transition available: any, do: :increment_customers
+ after_transition busy: any, do: :decrement_customers
+
+ event :tow_vehicle do
+ transition available: :busy
+ end
+
+ event :fix_vehicle do
+ transition busy: :available
+ end
+ end
+
+ # Increments the number of customers in service
+ def increment_customers
+ self.num_customers += 1
+ end
+
+ # Decrements the number of customers in service
+ def decrement_customers
+ self.num_customers -= 1
+ end
+end
diff --git a/test/files/models/car.rb b/test/files/models/car.rb
new file mode 100644
index 0000000..44e3c9f
--- /dev/null
+++ b/test/files/models/car.rb
@@ -0,0 +1,21 @@
+require_relative '../../files/models/vehicle'
+
+class Car < Vehicle
+ state_machine do
+ event :reverse do
+ transition [:parked, :idling, :first_gear] => :backing_up
+ end
+
+ event :park do
+ transition backing_up: :parked
+ end
+
+ event :idle do
+ transition backing_up: :idling
+ end
+
+ event :shift_up do
+ transition backing_up: :first_gear
+ end
+ end
+end
diff --git a/test/files/models/model_base.rb b/test/files/models/model_base.rb
new file mode 100644
index 0000000..5f5bf35
--- /dev/null
+++ b/test/files/models/model_base.rb
@@ -0,0 +1,6 @@
+class ModelBase
+ def save
+ @saved = true
+ self
+ end
+end
diff --git a/test/files/models/motorcycle.rb b/test/files/models/motorcycle.rb
new file mode 100644
index 0000000..07439e6
--- /dev/null
+++ b/test/files/models/motorcycle.rb
@@ -0,0 +1,11 @@
+require_relative '../../files/models/vehicle'
+
+class Motorcycle < Vehicle
+ state_machine initial: :idling do
+ state :first_gear do
+ def decibels
+ 1.0
+ end
+ end
+ end
+end
diff --git a/test/files/models/traffic_light.rb b/test/files/models/traffic_light.rb
new file mode 100644
index 0000000..0dc1dc2
--- /dev/null
+++ b/test/files/models/traffic_light.rb
@@ -0,0 +1,47 @@
+class TrafficLight
+ state_machine initial: :stop do
+ event :cycle do
+ transition stop: :proceed, proceed: :caution, caution: :stop
+ end
+
+ state :stop do
+ def color(transform)
+ value = 'red'
+
+ if block_given?
+ yield value
+ else
+ value.send(transform)
+ end
+
+ value
+ end
+ end
+
+ state all - :proceed do
+ def capture_violations?
+ true
+ end
+ end
+
+ state :proceed do
+ def color(_transform)
+ 'green'
+ end
+
+ def capture_violations?
+ false
+ end
+ end
+
+ state :caution do
+ def color(_transform)
+ 'yellow'
+ end
+ end
+ end
+
+ def color(transform = :to_s)
+ super
+ end
+end
diff --git a/test/files/models/vehicle.rb b/test/files/models/vehicle.rb
new file mode 100644
index 0000000..b7263d3
--- /dev/null
+++ b/test/files/models/vehicle.rb
@@ -0,0 +1,127 @@
+require_relative 'model_base'
+require_relative 'auto_shop'
+
+class Vehicle < ModelBase
+ attr_accessor :auto_shop, :seatbelt_on, :insurance_premium, :force_idle, :callbacks, :saved, :time_elapsed, :last_transition_args
+
+ def initialize(attributes = {})
+ attributes = {
+ auto_shop: AutoShop.new,
+ seatbelt_on: false,
+ insurance_premium: 50,
+ force_idle: false,
+ callbacks: [],
+ saved: false
+ }.merge(attributes)
+
+ attributes.each { |attr, value| send("#{attr}=", value) }
+ super()
+ end
+
+ # Defines the state machine for the state of the vehicled
+ state_machine initial: ->(vehicle) { vehicle.force_idle ? :idling : :parked }, action: :save do
+ before_transition { |vehicle, transition| vehicle.last_transition_args = transition.args }
+ before_transition parked: any, do: :put_on_seatbelt
+ before_transition any => :stalled, :do => :increase_insurance_premium
+ after_transition any => :parked, :do => lambda { |vehicle| vehicle.seatbelt_on = false }
+ after_transition on: :crash, do: :tow
+ after_transition on: :repair, do: :fix
+
+ # Callback tracking for initial state callbacks
+ after_transition any => :parked, :do => ->(vehicle) { vehicle.callbacks << 'before_enter_parked' }
+ before_transition any => :idling, :do => ->(vehicle) { vehicle.callbacks << 'before_enter_idling' }
+
+ around_transition do |vehicle, _transition, block|
+ time = Time.now
+ block.call
+ vehicle.time_elapsed = Time.now - time
+ end
+
+ event all do
+ transition locked: :parked
+ end
+
+ event :park do
+ transition [:idling, :first_gear] => :parked
+ end
+
+ event :ignite do
+ transition stalled: :stalled
+ transition parked: :idling
+ end
+
+ event :idle do
+ transition first_gear: :idling
+ end
+
+ event :shift_up do
+ transition idling: :first_gear, first_gear: :second_gear, second_gear: :third_gear
+ end
+
+ event :shift_down do
+ transition third_gear: :second_gear
+ transition second_gear: :first_gear
+ end
+
+ event :crash do
+ transition [:first_gear, :second_gear, :third_gear] => :stalled, :if => ->(vehicle) { vehicle.auto_shop.available? }
+ end
+
+ event :repair do
+ transition stalled: :parked, if: :auto_shop_busy?
+ end
+ end
+
+ state_machine :insurance_state, initial: :inactive, namespace: 'insurance' do
+ event :buy do
+ transition inactive: :active
+ end
+
+ event :cancel do
+ transition active: :inactive
+ end
+ end
+
+ def save
+ super
+ end
+
+ def new_record?
+ @saved == false
+ end
+
+ def park
+ super
+ end
+
+ # Tows the vehicle to the auto shop
+ def tow
+ auto_shop.tow_vehicle
+ end
+
+ # Fixes the vehicle; it will no longer be in the auto shop
+ def fix
+ auto_shop.fix_vehicle
+ end
+
+ def decibels
+ 0.0
+ end
+
+ private
+
+ # Safety first! Puts on our seatbelt
+ def put_on_seatbelt
+ self.seatbelt_on = true
+ end
+
+ # We crashed! Increase the insurance premium on the vehicle
+ def increase_insurance_premium
+ self.insurance_premium += 100
+ end
+
+ # Is the auto shop currently servicing another customer?
+ def auto_shop_busy?
+ auto_shop.busy?
+ end
+end
diff --git a/test/files/node.rb b/test/files/node.rb
new file mode 100644
index 0000000..5014877
--- /dev/null
+++ b/test/files/node.rb
@@ -0,0 +1,5 @@
+class Node < Struct.new(:name, :value, :machine)
+ def context
+ yield
+ end
+end
diff --git a/test/files/switch.rb b/test/files/switch.rb
new file mode 100644
index 0000000..c715434
--- /dev/null
+++ b/test/files/switch.rb
@@ -0,0 +1,15 @@
+class Switch
+ def self.name
+ @name ||= "Switch_#{rand(1_000_000)}"
+ end
+
+ state_machine do
+ event :turn_on do
+ transition all => :on
+ end
+
+ event :turn_off do
+ transition all => :off
+ end
+ end
+end
diff --git a/test/functional/auto_shop_available_test.rb b/test/functional/auto_shop_available_test.rb
new file mode 100644
index 0000000..f327043
--- /dev/null
+++ b/test/functional/auto_shop_available_test.rb
@@ -0,0 +1,20 @@
+require_relative '../test_helper'
+require_relative '../files/models/auto_shop'
+
+class AutoShopAvailableTest < MiniTest::Test
+ def setup
+ @auto_shop = AutoShop.new
+ end
+
+ def test_should_be_in_available_state
+ assert_equal 'available', @auto_shop.state
+ end
+
+ def test_should_allow_tow_vehicle
+ assert @auto_shop.tow_vehicle
+ end
+
+ def test_should_not_allow_fix_vehicle
+ refute @auto_shop.fix_vehicle
+ end
+end
diff --git a/test/functional/auto_shop_busy_test.rb b/test/functional/auto_shop_busy_test.rb
new file mode 100644
index 0000000..f2d7b0b
--- /dev/null
+++ b/test/functional/auto_shop_busy_test.rb
@@ -0,0 +1,25 @@
+require_relative '../test_helper'
+require_relative '../files/models/auto_shop'
+
+class AutoShopBusyTest < MiniTest::Test
+ def setup
+ @auto_shop = AutoShop.new
+ @auto_shop.tow_vehicle
+ end
+
+ def test_should_be_in_busy_state
+ assert_equal 'busy', @auto_shop.state
+ end
+
+ def test_should_have_incremented_number_of_customers
+ assert_equal 1, @auto_shop.num_customers
+ end
+
+ def test_should_not_allow_tow_vehicle
+ refute @auto_shop.tow_vehicle
+ end
+
+ def test_should_allow_fix_vehicle
+ assert @auto_shop.fix_vehicle
+ end
+end
diff --git a/test/functional/car_backing_up_test.rb b/test/functional/car_backing_up_test.rb
new file mode 100644
index 0000000..3b0cc69
--- /dev/null
+++ b/test/functional/car_backing_up_test.rb
@@ -0,0 +1,45 @@
+require_relative '../test_helper'
+require_relative '../files/models/car'
+
+class CarBackingUpTest < MiniTest::Test
+ def setup
+ @car = Car.new
+ @car.reverse
+ end
+
+ def test_should_be_in_backing_up_state
+ assert_equal 'backing_up', @car.state
+ end
+
+ def test_should_allow_park
+ assert @car.park
+ end
+
+ def test_should_not_allow_ignite
+ refute @car.ignite
+ end
+
+ def test_should_allow_idle
+ assert @car.idle
+ end
+
+ def test_should_allow_shift_up
+ assert @car.shift_up
+ end
+
+ def test_should_not_allow_shift_down
+ refute @car.shift_down
+ end
+
+ def test_should_not_allow_crash
+ refute @car.crash
+ end
+
+ def test_should_not_allow_repair
+ refute @car.repair
+ end
+
+ def test_should_not_allow_reverse
+ refute @car.reverse
+ end
+end
diff --git a/test/functional/car_test.rb b/test/functional/car_test.rb
new file mode 100644
index 0000000..b4fb4ec
--- /dev/null
+++ b/test/functional/car_test.rb
@@ -0,0 +1,49 @@
+require_relative '../test_helper'
+require_relative '../files/models/car'
+
+class CarTest < MiniTest::Test
+ def setup
+ @car = Car.new
+ end
+
+ def test_should_be_in_parked_state
+ assert_equal 'parked', @car.state
+ end
+
+ def test_should_not_have_the_seatbelt_on
+ refute @car.seatbelt_on
+ end
+
+ def test_should_not_allow_park
+ refute @car.park
+ end
+
+ def test_should_allow_ignite
+ assert @car.ignite
+ assert_equal 'idling', @car.state
+ end
+
+ def test_should_not_allow_idle
+ refute @car.idle
+ end
+
+ def test_should_not_allow_shift_up
+ refute @car.shift_up
+ end
+
+ def test_should_not_allow_shift_down
+ refute @car.shift_down
+ end
+
+ def test_should_not_allow_crash
+ refute @car.crash
+ end
+
+ def test_should_not_allow_repair
+ refute @car.repair
+ end
+
+ def test_should_allow_reverse
+ assert @car.reverse
+ end
+end
diff --git a/test/functional/motorcycle_test.rb b/test/functional/motorcycle_test.rb
new file mode 100644
index 0000000..c97e610
--- /dev/null
+++ b/test/functional/motorcycle_test.rb
@@ -0,0 +1,46 @@
+require_relative '../test_helper'
+require_relative '../files/models/motorcycle'
+
+class MotorcycleTest < MiniTest::Test
+ def setup
+ @motorcycle = Motorcycle.new
+ end
+
+ def test_should_be_in_idling_state
+ assert_equal 'idling', @motorcycle.state
+ end
+
+ def test_should_allow_park
+ assert @motorcycle.park
+ end
+
+ def test_should_not_allow_ignite
+ refute @motorcycle.ignite
+ end
+
+ def test_should_allow_shift_up
+ assert @motorcycle.shift_up
+ end
+
+ def test_should_not_allow_shift_down
+ refute @motorcycle.shift_down
+ end
+
+ def test_should_not_allow_crash
+ refute @motorcycle.crash
+ end
+
+ def test_should_not_allow_repair
+ refute @motorcycle.repair
+ end
+
+ def test_should_inherit_decibels_from_superclass
+ @motorcycle.park
+ assert_equal 0.0, @motorcycle.decibels
+ end
+
+ def test_should_use_decibels_defined_in_state
+ @motorcycle.shift_up
+ assert_equal 1.0, @motorcycle.decibels
+ end
+end
diff --git a/test/functional/traffic_light_caution_test.rb b/test/functional/traffic_light_caution_test.rb
new file mode 100644
index 0000000..6ad3093
--- /dev/null
+++ b/test/functional/traffic_light_caution_test.rb
@@ -0,0 +1,17 @@
+require_relative '../test_helper'
+require_relative '../files/models/traffic_light'
+
+class TrafficLightCautionTest < MiniTest::Test
+ def setup
+ @light = TrafficLight.new
+ @light.state = 'caution'
+ end
+
+ def test_should_use_caution_color
+ assert_equal 'yellow', @light.color
+ end
+
+ def test_should_use_caution_capture_violations
+ assert_equal true, @light.capture_violations?
+ end
+end
diff --git a/test/functional/traffic_light_proceed_test.rb b/test/functional/traffic_light_proceed_test.rb
new file mode 100644
index 0000000..bc5cdec
--- /dev/null
+++ b/test/functional/traffic_light_proceed_test.rb
@@ -0,0 +1,17 @@
+require_relative '../test_helper'
+require_relative '../files/models/traffic_light'
+
+class TrafficLightProceedTest < MiniTest::Test
+ def setup
+ @light = TrafficLight.new
+ @light.state = 'proceed'
+ end
+
+ def test_should_use_proceed_color
+ assert_equal 'green', @light.color
+ end
+
+ def test_should_use_proceed_capture_violations
+ assert_equal false, @light.capture_violations?
+ end
+end
diff --git a/test/functional/traffic_light_stop_test.rb b/test/functional/traffic_light_stop_test.rb
new file mode 100644
index 0000000..f4d72dc
--- /dev/null
+++ b/test/functional/traffic_light_stop_test.rb
@@ -0,0 +1,26 @@
+require_relative '../test_helper'
+require_relative '../files/models/traffic_light'
+
+class TrafficLightStopTest < MiniTest::Test
+ def setup
+ @light = TrafficLight.new
+ @light.state = 'stop'
+ end
+
+ def test_should_use_stop_color
+ assert_equal 'red', @light.color
+ end
+
+ def test_should_pass_arguments_through
+ assert_equal 'RED', @light.color(:upcase!)
+ end
+
+ def test_should_pass_block_through
+ color = @light.color { |value| value.upcase! }
+ assert_equal 'RED', color
+ end
+
+ def test_should_use_stop_capture_violations
+ assert_equal true, @light.capture_violations?
+ end
+end
diff --git a/test/functional/vehicle_first_gear_test.rb b/test/functional/vehicle_first_gear_test.rb
new file mode 100644
index 0000000..31e2574
--- /dev/null
+++ b/test/functional/vehicle_first_gear_test.rb
@@ -0,0 +1,42 @@
+require_relative '../test_helper'
+require_relative '../files/models/vehicle'
+
+class VehicleFirstGearTest < MiniTest::Test
+ def setup
+ @vehicle = Vehicle.new
+ @vehicle.ignite
+ @vehicle.shift_up
+ end
+
+ def test_should_be_in_first_gear_state
+ assert_equal 'first_gear', @vehicle.state
+ end
+
+ def test_should_be_first_gear
+ assert @vehicle.first_gear?
+ end
+
+ def test_should_allow_park
+ assert @vehicle.park
+ end
+
+ def test_should_allow_idle
+ assert @vehicle.idle
+ end
+
+ def test_should_allow_shift_up
+ assert @vehicle.shift_up
+ end
+
+ def test_should_not_allow_shift_down
+ refute @vehicle.shift_down
+ end
+
+ def test_should_allow_crash
+ assert @vehicle.crash
+ end
+
+ def test_should_not_allow_repair
+ refute @vehicle.repair
+ end
+end
diff --git a/test/functional/vehicle_idling_test.rb b/test/functional/vehicle_idling_test.rb
new file mode 100644
index 0000000..8ea378f
--- /dev/null
+++ b/test/functional/vehicle_idling_test.rb
@@ -0,0 +1,59 @@
+require_relative '../test_helper'
+require_relative '../files/models/vehicle'
+
+class VehicleIdlingTest < MiniTest::Test
+ def setup
+ @vehicle = Vehicle.new
+ @vehicle.ignite
+ end
+
+ def test_should_be_in_idling_state
+ assert_equal 'idling', @vehicle.state
+ end
+
+ def test_should_be_idling
+ assert @vehicle.idling?
+ end
+
+ def test_should_have_seatbelt_on
+ assert @vehicle.seatbelt_on
+ end
+
+ def test_should_track_time_elapsed
+ refute_nil @vehicle.time_elapsed
+ end
+
+ def test_should_allow_park
+ assert @vehicle.park
+ end
+
+ def test_should_call_park_with_bang_action
+ class << @vehicle
+ def park
+ super && 1
+ end
+ end
+
+ assert_equal 1, @vehicle.park!
+ end
+
+ def test_should_not_allow_idle
+ refute @vehicle.idle
+ end
+
+ def test_should_allow_shift_up
+ assert @vehicle.shift_up
+ end
+
+ def test_should_not_allow_shift_down
+ refute @vehicle.shift_down
+ end
+
+ def test_should_not_allow_crash
+ refute @vehicle.crash
+ end
+
+ def test_should_not_allow_repair
+ refute @vehicle.repair
+ end
+end
diff --git a/test/functional/vehicle_locked_test.rb b/test/functional/vehicle_locked_test.rb
new file mode 100644
index 0000000..8b6a5ee
--- /dev/null
+++ b/test/functional/vehicle_locked_test.rb
@@ -0,0 +1,29 @@
+require_relative '../test_helper'
+require_relative '../files/models/vehicle'
+
+class VehicleLockedTest < MiniTest::Test
+ def setup
+ @vehicle = Vehicle.new
+ @vehicle.state = 'locked'
+ end
+
+ def test_should_be_parked_after_park
+ @vehicle.park
+ assert @vehicle.parked?
+ end
+
+ def test_should_be_parked_after_ignite
+ @vehicle.ignite
+ assert @vehicle.parked?
+ end
+
+ def test_should_be_parked_after_shift_up
+ @vehicle.shift_up
+ assert @vehicle.parked?
+ end
+
+ def test_should_be_parked_after_shift_down
+ @vehicle.shift_down
+ assert @vehicle.parked?
+ end
+end
diff --git a/test/functional/vehicle_parked_test.rb b/test/functional/vehicle_parked_test.rb
new file mode 100644
index 0000000..1e73924
--- /dev/null
+++ b/test/functional/vehicle_parked_test.rb
@@ -0,0 +1,53 @@
+require_relative '../test_helper'
+require_relative '../files/models/vehicle'
+
+class VehicleParkedTest < MiniTest::Test
+ def setup
+ @vehicle = Vehicle.new
+ end
+
+ def test_should_be_in_parked_state
+ assert_equal 'parked', @vehicle.state
+ end
+
+ def test_should_not_have_the_seatbelt_on
+ refute @vehicle.seatbelt_on
+ end
+
+ def test_should_not_allow_park
+ refute @vehicle.park
+ end
+
+ def test_should_allow_ignite
+ assert @vehicle.ignite
+ assert_equal 'idling', @vehicle.state
+ end
+
+ def test_should_not_allow_idle
+ refute @vehicle.idle
+ end
+
+ def test_should_not_allow_shift_up
+ refute @vehicle.shift_up
+ end
+
+ def test_should_not_allow_shift_down
+ refute @vehicle.shift_down
+ end
+
+ def test_should_not_allow_crash
+ refute @vehicle.crash
+ end
+
+ def test_should_not_allow_repair
+ refute @vehicle.repair
+ end
+
+ def test_should_raise_exception_if_repair_not_allowed!
+ exception = assert_raises(StateMachines::InvalidTransition) { @vehicle.repair! }
+ assert_equal @vehicle, exception.object
+ assert_equal Vehicle.state_machine(:state), exception.machine
+ assert_equal :repair, exception.event
+ assert_equal 'parked', exception.from
+ end
+end
diff --git a/test/functional/vehicle_repaired_test.rb b/test/functional/vehicle_repaired_test.rb
new file mode 100644
index 0000000..182d45d
--- /dev/null
+++ b/test/functional/vehicle_repaired_test.rb
@@ -0,0 +1,20 @@
+require_relative '../test_helper'
+require_relative '../files/models/vehicle'
+
+class VehicleRepairedTest < MiniTest::Test
+ def setup
+ @vehicle = Vehicle.new
+ @vehicle.ignite
+ @vehicle.shift_up
+ @vehicle.crash
+ @vehicle.repair
+ end
+
+ def test_should_be_in_parked_state
+ assert_equal 'parked', @vehicle.state
+ end
+
+ def test_should_not_have_a_busy_auto_shop
+ assert @vehicle.auto_shop.available?
+ end
+end
diff --git a/test/functional/vehicle_second_gear_test.rb b/test/functional/vehicle_second_gear_test.rb
new file mode 100644
index 0000000..7dc5157
--- /dev/null
+++ b/test/functional/vehicle_second_gear_test.rb
@@ -0,0 +1,42 @@
+require_relative '../test_helper'
+require_relative '../files/models/vehicle'
+
+class VehicleSecondGearTest < MiniTest::Test
+ def setup
+ @vehicle = Vehicle.new
+ @vehicle.ignite
+ 2.times { @vehicle.shift_up }
+ end
+
+ def test_should_be_in_second_gear_state
+ assert_equal 'second_gear', @vehicle.state
+ end
+
+ def test_should_be_second_gear
+ assert @vehicle.second_gear?
+ end
+
+ def test_should_not_allow_park
+ refute @vehicle.park
+ end
+
+ def test_should_not_allow_idle
+ refute @vehicle.idle
+ end
+
+ def test_should_allow_shift_up
+ assert @vehicle.shift_up
+ end
+
+ def test_should_allow_shift_down
+ assert @vehicle.shift_down
+ end
+
+ def test_should_allow_crash
+ assert @vehicle.crash
+ end
+
+ def test_should_not_allow_repair
+ refute @vehicle.repair
+ end
+end
diff --git a/test/functional/vehicle_stalled_test.rb b/test/functional/vehicle_stalled_test.rb
new file mode 100644
index 0000000..3db51dc
--- /dev/null
+++ b/test/functional/vehicle_stalled_test.rb
@@ -0,0 +1,65 @@
+require_relative '../test_helper'
+require_relative '../files/models/vehicle'
+
+class VehicleStalledTest < MiniTest::Test
+ def setup
+ @vehicle = Vehicle.new
+ @vehicle.ignite
+ @vehicle.shift_up
+ @vehicle.crash
+ end
+
+ def test_should_be_in_stalled_state
+ assert_equal 'stalled', @vehicle.state
+ end
+
+ def test_should_be_stalled
+ assert @vehicle.stalled?
+ end
+
+ def test_should_be_towed
+ assert @vehicle.auto_shop.busy?
+ assert_equal 1, @vehicle.auto_shop.num_customers
+ end
+
+ def test_should_have_an_increased_insurance_premium
+ assert_equal 150, @vehicle.insurance_premium
+ end
+
+ def test_should_not_allow_park
+ refute @vehicle.park
+ end
+
+ def test_should_allow_ignite
+ assert @vehicle.ignite
+ end
+
+ def test_should_not_change_state_when_ignited
+ assert_equal 'stalled', @vehicle.state
+ end
+
+ def test_should_not_allow_idle
+ refute @vehicle.idle
+ end
+
+ def test_should_now_allow_shift_up
+ refute @vehicle.shift_up
+ end
+
+ def test_should_not_allow_shift_down
+ refute @vehicle.shift_down
+ end
+
+ def test_should_not_allow_crash
+ refute @vehicle.crash
+ end
+
+ def test_should_allow_repair_if_auto_shop_is_busy
+ assert @vehicle.repair
+ end
+
+ def test_should_not_allow_repair_if_auto_shop_is_available
+ @vehicle.auto_shop.fix_vehicle
+ refute @vehicle.repair
+ end
+end
diff --git a/test/functional/vehicle_test.rb b/test/functional/vehicle_test.rb
new file mode 100644
index 0000000..79866e1
--- /dev/null
+++ b/test/functional/vehicle_test.rb
@@ -0,0 +1,20 @@
+require_relative '../test_helper'
+require_relative '../files/models/vehicle'
+
+class VehicleTest < MiniTest::Test
+ def setup
+ @vehicle = Vehicle.new
+ end
+
+ def test_should_not_allow_access_to_subclass_events
+ refute @vehicle.respond_to?(:reverse)
+ end
+
+ def test_should_have_human_state_names
+ assert_equal 'parked', Vehicle.human_state_name(:parked)
+ end
+
+ def test_should_have_human_state_event_names
+ assert_equal 'park', Vehicle.human_state_event_name(:park)
+ end
+end
diff --git a/test/functional/vehicle_third_gear_test.rb b/test/functional/vehicle_third_gear_test.rb
new file mode 100644
index 0000000..0840e00
--- /dev/null
+++ b/test/functional/vehicle_third_gear_test.rb
@@ -0,0 +1,42 @@
+require_relative '../test_helper'
+require_relative '../files/models/vehicle'
+
+class VehicleThirdGearTest < MiniTest::Test
+ def setup
+ @vehicle = Vehicle.new
+ @vehicle.ignite
+ 3.times { @vehicle.shift_up }
+ end
+
+ def test_should_be_in_third_gear_state
+ assert_equal 'third_gear', @vehicle.state
+ end
+
+ def test_should_be_third_gear
+ assert @vehicle.third_gear?
+ end
+
+ def test_should_not_allow_park
+ refute @vehicle.park
+ end
+
+ def test_should_not_allow_idle
+ refute @vehicle.idle
+ end
+
+ def test_should_not_allow_shift_up
+ refute @vehicle.shift_up
+ end
+
+ def test_should_allow_shift_down
+ assert @vehicle.shift_down
+ end
+
+ def test_should_allow_crash
+ assert @vehicle.crash
+ end
+
+ def test_should_not_allow_repair
+ refute @vehicle.repair
+ end
+end
diff --git a/test/functional/vehicle_unsaved_test.rb b/test/functional/vehicle_unsaved_test.rb
new file mode 100644
index 0000000..f990164
--- /dev/null
+++ b/test/functional/vehicle_unsaved_test.rb
@@ -0,0 +1,181 @@
+require_relative '../test_helper'
+require_relative '../files/models/vehicle'
+
+class VehicleUnsavedTest < MiniTest::Test
+ def setup
+ @vehicle = Vehicle.new
+ end
+
+ def test_should_be_in_parked_state
+ assert_equal 'parked', @vehicle.state
+ end
+
+ def test_should_raise_exception_if_checking_invalid_state
+ assert_raises(IndexError) { @vehicle.state?(:invalid) }
+ end
+
+ def test_should_raise_exception_if_getting_name_of_invalid_state
+ @vehicle.state = 'invalid'
+ assert_raises(ArgumentError) { @vehicle.state_name }
+ end
+
+ def test_should_be_parked
+ assert @vehicle.parked?
+ assert @vehicle.state?(:parked)
+ assert_equal :parked, @vehicle.state_name
+ assert_equal 'parked', @vehicle.human_state_name
+ end
+
+ def test_should_not_be_idling
+ refute @vehicle.idling?
+ end
+
+ def test_should_not_be_first_gear
+ refute @vehicle.first_gear?
+ end
+
+ def test_should_not_be_second_gear
+ refute @vehicle.second_gear?
+ end
+
+ def test_should_not_be_stalled
+ refute @vehicle.stalled?
+ end
+
+ def test_should_not_be_able_to_park
+ refute @vehicle.can_park?
+ end
+
+ def test_should_not_have_a_transition_for_park
+ assert_nil @vehicle.park_transition
+ end
+
+ def test_should_not_allow_park
+ refute @vehicle.park
+ end
+
+ def test_should_be_able_to_ignite
+ assert @vehicle.can_ignite?
+ end
+
+ def test_should_have_a_transition_for_ignite
+ transition = @vehicle.ignite_transition
+ refute_nil transition
+ assert_equal 'parked', transition.from
+ assert_equal 'idling', transition.to
+ assert_equal :ignite, transition.event
+ assert_equal :state, transition.attribute
+ assert_equal @vehicle, transition.object
+ end
+
+ def test_should_have_a_list_of_possible_events
+ assert_equal [:ignite], @vehicle.state_events
+ end
+
+ def test_should_have_a_list_of_possible_transitions
+ assert_equal [{ object: @vehicle, attribute: :state, event: :ignite, from: 'parked', to: 'idling' }], @vehicle.state_transitions.map { |transition| transition.attributes }
+ end
+
+ def test_should_have_a_list_of_possible_paths
+ assert_equal [[
+ StateMachines::Transition.new(@vehicle, Vehicle.state_machine, :ignite, :parked, :idling),
+ StateMachines::Transition.new(@vehicle, Vehicle.state_machine, :shift_up, :idling, :first_gear)
+ ]], @vehicle.state_paths(to: :first_gear)
+ end
+
+ def test_should_allow_generic_event_to_fire
+ assert @vehicle.fire_state_event(:ignite)
+ assert_equal 'idling', @vehicle.state
+ end
+
+ def test_should_pass_arguments_through_to_generic_event_runner
+ @vehicle.fire_state_event(:ignite, 1, 2, 3)
+ assert_equal [1, 2, 3], @vehicle.last_transition_args
+ end
+
+ def test_should_allow_skipping_action_through_generic_event_runner
+ @vehicle.fire_state_event(:ignite, false)
+ assert_equal false, @vehicle.saved
+ end
+
+ def test_should_raise_error_with_invalid_event_through_generic_event_runer
+ assert_raises(IndexError) { @vehicle.fire_state_event(:invalid) }
+ end
+
+ def test_should_allow_ignite
+ assert @vehicle.ignite
+ assert_equal 'idling', @vehicle.state
+ end
+
+ def test_should_allow_ignite_with_skipped_action
+ assert @vehicle.ignite(false)
+ assert @vehicle.new_record?
+ end
+
+ def test_should_allow_ignite_bang
+ assert @vehicle.ignite!
+ end
+
+ def test_should_allow_ignite_bang_with_skipped_action
+ assert @vehicle.ignite!(false)
+ assert @vehicle.new_record?
+ end
+
+ def test_should_be_saved_after_successful_event
+ @vehicle.ignite
+ refute @vehicle.new_record?
+ end
+
+ def test_should_not_allow_idle
+ refute @vehicle.idle
+ end
+
+ def test_should_not_allow_shift_up
+ refute @vehicle.shift_up
+ end
+
+ def test_should_not_allow_shift_down
+ refute @vehicle.shift_down
+ end
+
+ def test_should_not_allow_crash
+ refute @vehicle.crash
+ end
+
+ def test_should_not_allow_repair
+ refute @vehicle.repair
+ end
+
+ def test_should_be_insurance_inactive
+ assert @vehicle.insurance_inactive?
+ end
+
+ def test_should_be_able_to_buy
+ assert @vehicle.can_buy_insurance?
+ end
+
+ def test_should_allow_buying_insurance
+ assert @vehicle.buy_insurance
+ end
+
+ def test_should_allow_buying_insurance_bang
+ assert @vehicle.buy_insurance!
+ end
+
+ def test_should_allow_ignite_buying_insurance_with_skipped_action
+ assert @vehicle.buy_insurance!(false)
+ assert @vehicle.new_record?
+ end
+
+ def test_should_not_be_insurance_active
+ refute @vehicle.insurance_active?
+ end
+
+ def test_should_not_be_able_to_cancel
+ refute @vehicle.can_cancel_insurance?
+ end
+
+ def test_should_not_allow_cancelling_insurance
+ refute @vehicle.cancel_insurance
+ end
+end
diff --git a/test/functional/vehicle_with_event_attributes_test.rb b/test/functional/vehicle_with_event_attributes_test.rb
new file mode 100644
index 0000000..e7fe4b2
--- /dev/null
+++ b/test/functional/vehicle_with_event_attributes_test.rb
@@ -0,0 +1,30 @@
+require_relative '../test_helper'
+require_relative '../files/models/vehicle'
+
+class VehicleWithEventAttributesTest < MiniTest::Test
+ def setup
+ @vehicle = Vehicle.new
+ @vehicle.state_event = 'ignite'
+ end
+
+ def test_should_fail_if_event_is_invalid
+ @vehicle.state_event = 'invalid'
+ refute @vehicle.save
+ assert_equal 'parked', @vehicle.state
+ end
+
+ def test_should_fail_if_event_has_no_transition
+ @vehicle.state_event = 'park'
+ refute @vehicle.save
+ assert_equal 'parked', @vehicle.state
+ end
+
+ def test_should_return_original_action_value_on_success
+ assert_equal @vehicle, @vehicle.save
+ end
+
+ def test_should_transition_state_on_success
+ @vehicle.save
+ assert_equal 'idling', @vehicle.state
+ end
+end
diff --git a/test/functional/vehicle_with_parallel_events_test.rb b/test/functional/vehicle_with_parallel_events_test.rb
new file mode 100644
index 0000000..46fa39f
--- /dev/null
+++ b/test/functional/vehicle_with_parallel_events_test.rb
@@ -0,0 +1,36 @@
+require_relative '../test_helper'
+require_relative '../files/models/vehicle'
+
+class VehicleWithParallelEventsTest < MiniTest::Test
+ def setup
+ @vehicle = Vehicle.new
+ end
+
+ def test_should_fail_if_any_event_cannot_transition
+ refute @vehicle.fire_events(:ignite, :cancel_insurance)
+ end
+
+ def test_should_be_successful_if_all_events_transition
+ assert @vehicle.fire_events(:ignite, :buy_insurance)
+ end
+
+ def test_should_not_save_if_skipping_action
+ assert @vehicle.fire_events(:ignite, :buy_insurance, false)
+ refute @vehicle.saved
+ end
+
+ def test_should_raise_exception_if_any_event_cannot_transition_on_bang
+ exception = assert_raises(StateMachines::InvalidParallelTransition) { @vehicle.fire_events!(:ignite, :cancel_insurance) }
+ assert_equal @vehicle, exception.object
+ assert_equal [:ignite, :cancel_insurance], exception.events
+ end
+
+ def test_should_not_raise_exception_if_all_events_transition_on_bang
+ assert @vehicle.fire_events!(:ignite, :buy_insurance)
+ end
+
+ def test_should_not_save_if_skipping_action_on_bang
+ assert @vehicle.fire_events!(:ignite, :buy_insurance, false)
+ refute @vehicle.saved
+ end
+end
diff --git a/test/test_helper.rb b/test/test_helper.rb
new file mode 100644
index 0000000..336a80b
--- /dev/null
+++ b/test/test_helper.rb
@@ -0,0 +1,15 @@
+require 'state_machines'
+require 'minitest/autorun'
+begin
+ require 'pry-byebug'
+rescue LoadError
+end
+require 'minitest/reporters'
+Minitest::Reporters.use! [Minitest::Reporters::ProgressReporter.new]
+
+class StateMachinesTest < MiniTest::Test
+ def before_setup
+ super
+ StateMachines::Integrations.reset
+ end
+end
diff --git a/test/unit/assertions/assert_exclusive_keys_test.rb b/test/unit/assertions/assert_exclusive_keys_test.rb
new file mode 100644
index 0000000..2ab728b
--- /dev/null
+++ b/test/unit/assertions/assert_exclusive_keys_test.rb
@@ -0,0 +1,22 @@
+require_relative '../../test_helper'
+
+class AssertExclusiveKeysTest < StateMachinesTest
+ def test_should_not_raise_exception_if_no_keys_found
+ { on: :park }.assert_exclusive_keys(:only, :except)
+ end
+
+ def test_should_not_raise_exception_if_one_key_found
+ { only: :parked }.assert_exclusive_keys(:only, :except)
+ { except: :parked }.assert_exclusive_keys(:only, :except)
+ end
+
+ def test_should_raise_exception_if_two_keys_found
+ exception = assert_raises(ArgumentError) { { only: :parked, except: :parked }.assert_exclusive_keys(:only, :except) }
+ assert_equal 'Conflicting keys: only, except', exception.message
+ end
+
+ def test_should_raise_exception_if_multiple_keys_found
+ exception = assert_raises(ArgumentError) { { only: :parked, except: :parked, on: :park }.assert_exclusive_keys(:only, :except, :with) }
+ assert_equal 'Conflicting keys: only, except', exception.message
+ end
+end
diff --git a/test/unit/assertions/assert_valid_key_test.rb b/test/unit/assertions/assert_valid_key_test.rb
new file mode 100644
index 0000000..9ee68ed
--- /dev/null
+++ b/test/unit/assertions/assert_valid_key_test.rb
@@ -0,0 +1,12 @@
+require_relative '../../test_helper'
+
+class AssertValidKeysTest < StateMachinesTest
+ def test_should_not_raise_exception_if_key_is_valid
+ { name: 'foo', value: 'bar' }.assert_valid_keys(:name, :value, :force)
+ end
+
+ def test_should_raise_exception_if_key_is_invalid
+ exception = assert_raises(ArgumentError) { { name: 'foo', value: 'bar', invalid: true }.assert_valid_keys(:name, :value, :force) }
+ assert_equal 'Unknown key: :invalid. Valid keys are: :name, :value, :force', exception.message
+ end
+end
diff --git a/test/unit/branch/branch_test.rb b/test/unit/branch/branch_test.rb
new file mode 100644
index 0000000..cb39be5
--- /dev/null
+++ b/test/unit/branch/branch_test.rb
@@ -0,0 +1,28 @@
+require_relative '../../test_helper'
+
+class BranchTest < StateMachinesTest
+ def setup
+ @branch = StateMachines::Branch.new(from: :parked, to: :idling)
+ end
+
+ def test_should_not_raise_exception_if_implicit_option_specified
+ StateMachines::Branch.new(invalid: :valid)
+ end
+
+ def test_should_not_have_an_if_condition
+ assert_nil @branch.if_condition
+ end
+
+ def test_should_not_have_an_unless_condition
+ assert_nil @branch.unless_condition
+ end
+
+ def test_should_have_a_state_requirement
+ assert_equal 1, @branch.state_requirements.length
+ end
+
+ def test_should_raise_an_exception_if_invalid_match_option_specified
+ exception = assert_raises(ArgumentError) { @branch.match(Object.new, invalid: true) }
+ assert_equal 'Unknown key: :invalid. Valid keys are: :from, :to, :on, :guard', exception.message
+ end
+end
diff --git a/test/unit/branch/branch_with_conflicting_conditionals_test.rb b/test/unit/branch/branch_with_conflicting_conditionals_test.rb
new file mode 100644
index 0000000..8064155
--- /dev/null
+++ b/test/unit/branch/branch_with_conflicting_conditionals_test.rb
@@ -0,0 +1,27 @@
+require_relative '../../test_helper'
+
+class BranchWithConflictingConditionalsTest < StateMachinesTest
+ def setup
+ @object = Object.new
+ end
+
+ def test_should_match_if_if_is_true_and_unless_is_false
+ branch = StateMachines::Branch.new(if: lambda { true }, unless: lambda { false })
+ assert branch.match(@object)
+ end
+
+ def test_should_not_match_if_if_is_false_and_unless_is_true
+ branch = StateMachines::Branch.new(if: lambda { false }, unless: lambda { true })
+ refute branch.match(@object)
+ end
+
+ def test_should_not_match_if_if_is_false_and_unless_is_false
+ branch = StateMachines::Branch.new(if: lambda { false }, unless: lambda { false })
+ refute branch.match(@object)
+ end
+
+ def test_should_not_match_if_if_is_true_and_unless_is_true
+ branch = StateMachines::Branch.new(if: lambda { true }, unless: lambda { true })
+ refute branch.match(@object)
+ end
+end
diff --git a/test/unit/branch/branch_with_conflicting_from_requirements_test.rb b/test/unit/branch/branch_with_conflicting_from_requirements_test.rb
new file mode 100644
index 0000000..759e10a
--- /dev/null
+++ b/test/unit/branch/branch_with_conflicting_from_requirements_test.rb
@@ -0,0 +1,8 @@
+require_relative '../../test_helper'
+
+class BranchWithConflictingFromRequirementsTest < StateMachinesTest
+ def test_should_raise_an_exception
+ exception = assert_raises(ArgumentError) { StateMachines::Branch.new(from: :parked, except_from: :parked) }
+ assert_equal 'Conflicting keys: from, except_from', exception.message
+ end
+end
diff --git a/test/unit/branch/branch_with_conflicting_on_requirements_test.rb b/test/unit/branch/branch_with_conflicting_on_requirements_test.rb
new file mode 100644
index 0000000..a300909
--- /dev/null
+++ b/test/unit/branch/branch_with_conflicting_on_requirements_test.rb
@@ -0,0 +1,8 @@
+require_relative '../../test_helper'
+
+class BranchWithConflictingOnRequirementsTest < StateMachinesTest
+ def test_should_raise_an_exception
+ exception = assert_raises(ArgumentError) { StateMachines::Branch.new(on: :ignite, except_on: :ignite) }
+ assert_equal 'Conflicting keys: on, except_on', exception.message
+ end
+end
diff --git a/test/unit/branch/branch_with_conflicting_to_requirements_test.rb b/test/unit/branch/branch_with_conflicting_to_requirements_test.rb
new file mode 100644
index 0000000..ff4ca25
--- /dev/null
+++ b/test/unit/branch/branch_with_conflicting_to_requirements_test.rb
@@ -0,0 +1,8 @@
+require_relative '../../test_helper'
+
+class BranchWithConflictingToRequirementsTest < StateMachinesTest
+ def test_should_raise_an_exception
+ exception = assert_raises(ArgumentError) { StateMachines::Branch.new(to: :idling, except_to: :idling) }
+ assert_equal 'Conflicting keys: to, except_to', exception.message
+ end
+end
diff --git a/test/unit/branch/branch_with_different_requirements_test.rb b/test/unit/branch/branch_with_different_requirements_test.rb
new file mode 100644
index 0000000..5bde0d1
--- /dev/null
+++ b/test/unit/branch/branch_with_different_requirements_test.rb
@@ -0,0 +1,41 @@
+require_relative '../../test_helper'
+
+class BranchWithDifferentRequirementsTest < StateMachinesTest
+ def setup
+ @object = Object.new
+ @branch = StateMachines::Branch.new(from: :parked, to: :idling, on: :ignite)
+ end
+
+ def test_should_match_empty_query
+ assert @branch.matches?(@object)
+ end
+
+ def test_should_match_if_all_requirements_match
+ assert @branch.matches?(@object, from: :parked, to: :idling, on: :ignite)
+ end
+
+ def test_should_not_match_if_from_not_included
+ refute @branch.matches?(@object, from: :idling)
+ end
+
+ def test_should_not_match_if_to_not_included
+ refute @branch.matches?(@object, to: :parked)
+ end
+
+ def test_should_not_match_if_on_not_included
+ refute @branch.matches?(@object, on: :park)
+ end
+
+ def test_should_be_nil_if_unmatched
+ assert_nil @branch.match(@object, from: :parked, to: :idling, on: :park)
+ end
+
+ def test_should_include_all_known_states
+ assert_equal [:parked, :idling], @branch.known_states
+ end
+
+ def test_should_not_duplicate_known_statse
+ branch = StateMachines::Branch.new(except_from: :idling, to: :idling, on: :ignite)
+ assert_equal [:idling], branch.known_states
+ end
+end
diff --git a/test/unit/branch/branch_with_except_from_matcher_requirement_test.rb b/test/unit/branch/branch_with_except_from_matcher_requirement_test.rb
new file mode 100644
index 0000000..ba07354
--- /dev/null
+++ b/test/unit/branch/branch_with_except_from_matcher_requirement_test.rb
@@ -0,0 +1,8 @@
+require_relative '../../test_helper'
+
+class BranchWithExceptFromMatcherRequirementTest < StateMachinesTest
+ def test_should_raise_an_exception
+ exception = assert_raises(ArgumentError) { StateMachines::Branch.new(except_from: StateMachines::AllMatcher.instance) }
+ assert_equal ':except_from option cannot use matchers; use :from instead', exception.message
+ end
+end
diff --git a/test/unit/branch/branch_with_except_from_requirement_test.rb b/test/unit/branch/branch_with_except_from_requirement_test.rb
new file mode 100644
index 0000000..8332c18
--- /dev/null
+++ b/test/unit/branch/branch_with_except_from_requirement_test.rb
@@ -0,0 +1,36 @@
+require_relative '../../test_helper'
+
+class BranchWithExceptFromRequirementTest < StateMachinesTest
+ def setup
+ @object = Object.new
+ @branch = StateMachines::Branch.new(except_from: :parked)
+ end
+
+ def test_should_use_a_blacklist_matcher
+ assert_instance_of StateMachines::BlacklistMatcher, @branch.state_requirements.first[:from]
+ end
+
+ def test_should_match_if_not_included
+ assert @branch.matches?(@object, from: :idling)
+ end
+
+ def test_should_not_match_if_included
+ refute @branch.matches?(@object, from: :parked)
+ end
+
+ def test_should_match_if_nil
+ assert @branch.matches?(@object, from: nil)
+ end
+
+ def test_should_ignore_to
+ assert @branch.matches?(@object, from: :idling, to: :parked)
+ end
+
+ def test_should_ignore_on
+ assert @branch.matches?(@object, from: :idling, on: :ignite)
+ end
+
+ def test_should_be_included_in_known_states
+ assert_equal [:parked], @branch.known_states
+ end
+end
diff --git a/test/unit/branch/branch_with_except_on_matcher_requirement_test.rb b/test/unit/branch/branch_with_except_on_matcher_requirement_test.rb
new file mode 100644
index 0000000..0289d3e
--- /dev/null
+++ b/test/unit/branch/branch_with_except_on_matcher_requirement_test.rb
@@ -0,0 +1,8 @@
+require_relative '../../test_helper'
+
+class BranchWithExceptOnMatcherRequirementTest < StateMachinesTest
+ def test_should_raise_an_exception
+ exception = assert_raises(ArgumentError) { StateMachines::Branch.new(except_on: StateMachines::AllMatcher.instance) }
+ assert_equal ':except_on option cannot use matchers; use :on instead', exception.message
+ end
+end
diff --git a/test/unit/branch/branch_with_except_on_requirement_test.rb b/test/unit/branch/branch_with_except_on_requirement_test.rb
new file mode 100644
index 0000000..f0c8b27
--- /dev/null
+++ b/test/unit/branch/branch_with_except_on_requirement_test.rb
@@ -0,0 +1,36 @@
+require_relative '../../test_helper'
+
+class BranchWithExceptOnRequirementTest < StateMachinesTest
+ def setup
+ @object = Object.new
+ @branch = StateMachines::Branch.new(except_on: :ignite)
+ end
+
+ def test_should_use_a_blacklist_matcher
+ assert_instance_of StateMachines::BlacklistMatcher, @branch.event_requirement
+ end
+
+ def test_should_match_if_not_included
+ assert @branch.matches?(@object, on: :park)
+ end
+
+ def test_should_not_match_if_included
+ refute @branch.matches?(@object, on: :ignite)
+ end
+
+ def test_should_match_if_nil
+ assert @branch.matches?(@object, on: nil)
+ end
+
+ def test_should_ignore_to
+ assert @branch.matches?(@object, on: :park, to: :idling)
+ end
+
+ def test_should_ignore_from
+ assert @branch.matches?(@object, on: :park, from: :parked)
+ end
+
+ def test_should_not_be_included_in_known_states
+ assert_equal [], @branch.known_states
+ end
+end
diff --git a/test/unit/branch/branch_with_except_to_matcher_requirement_test.rb b/test/unit/branch/branch_with_except_to_matcher_requirement_test.rb
new file mode 100644
index 0000000..0dded7f
--- /dev/null
+++ b/test/unit/branch/branch_with_except_to_matcher_requirement_test.rb
@@ -0,0 +1,8 @@
+require_relative '../../test_helper'
+
+class BranchWithExceptToMatcherRequirementTest < StateMachinesTest
+ def test_should_raise_an_exception
+ exception = assert_raises(ArgumentError) { StateMachines::Branch.new(except_to: StateMachines::AllMatcher.instance) }
+ assert_equal ':except_to option cannot use matchers; use :to instead', exception.message
+ end
+end
diff --git a/test/unit/branch/branch_with_except_to_requirement_test.rb b/test/unit/branch/branch_with_except_to_requirement_test.rb
new file mode 100644
index 0000000..c47695e
--- /dev/null
+++ b/test/unit/branch/branch_with_except_to_requirement_test.rb
@@ -0,0 +1,36 @@
+require_relative '../../test_helper'
+
+class BranchWithExceptToRequirementTest < StateMachinesTest
+ def setup
+ @object = Object.new
+ @branch = StateMachines::Branch.new(except_to: :idling)
+ end
+
+ def test_should_use_a_blacklist_matcher
+ assert_instance_of StateMachines::BlacklistMatcher, @branch.state_requirements.first[:to]
+ end
+
+ def test_should_match_if_not_included
+ assert @branch.matches?(@object, to: :parked)
+ end
+
+ def test_should_not_match_if_included
+ refute @branch.matches?(@object, to: :idling)
+ end
+
+ def test_should_match_if_nil
+ assert @branch.matches?(@object, to: nil)
+ end
+
+ def test_should_ignore_from
+ assert @branch.matches?(@object, to: :parked, from: :idling)
+ end
+
+ def test_should_ignore_on
+ assert @branch.matches?(@object, to: :parked, on: :ignite)
+ end
+
+ def test_should_be_included_in_known_states
+ assert_equal [:idling], @branch.known_states
+ end
+end
diff --git a/test/unit/branch/branch_with_from_matcher_requirement_test.rb b/test/unit/branch/branch_with_from_matcher_requirement_test.rb
new file mode 100644
index 0000000..bd9c784
--- /dev/null
+++ b/test/unit/branch/branch_with_from_matcher_requirement_test.rb
@@ -0,0 +1,20 @@
+require_relative '../../test_helper'
+
+class BranchWithFromMatcherRequirementTest < StateMachinesTest
+ def setup
+ @object = Object.new
+ @branch = StateMachines::Branch.new(from: StateMachines::BlacklistMatcher.new([:idling, :parked]))
+ end
+
+ def test_should_match_if_included
+ assert @branch.matches?(@object, from: :first_gear)
+ end
+
+ def test_should_not_match_if_not_included
+ refute @branch.matches?(@object, from: :idling)
+ end
+
+ def test_include_values_in_known_states
+ assert_equal [:idling, :parked], @branch.known_states
+ end
+end
diff --git a/test/unit/branch/branch_with_from_requirement_test.rb b/test/unit/branch/branch_with_from_requirement_test.rb
new file mode 100644
index 0000000..7ce97c3
--- /dev/null
+++ b/test/unit/branch/branch_with_from_requirement_test.rb
@@ -0,0 +1,45 @@
+require_relative '../../test_helper'
+
+class BranchWithFromRequirementTest < StateMachinesTest
+ def setup
+ @object = Object.new
+ @branch = StateMachines::Branch.new(from: :parked)
+ end
+
+ def test_should_use_a_whitelist_matcher
+ assert_instance_of StateMachines::WhitelistMatcher, @branch.state_requirements.first[:from]
+ end
+
+ def test_should_match_if_not_specified
+ assert @branch.matches?(@object, to: :idling)
+ end
+
+ def test_should_match_if_included
+ assert @branch.matches?(@object, from: :parked)
+ end
+
+ def test_should_not_match_if_not_included
+ refute @branch.matches?(@object, from: :idling)
+ end
+
+ def test_should_not_match_if_nil
+ refute @branch.matches?(@object, from: nil)
+ end
+
+ def test_should_ignore_to
+ assert @branch.matches?(@object, from: :parked, to: :idling)
+ end
+
+ def test_should_ignore_on
+ assert @branch.matches?(@object, from: :parked, on: :ignite)
+ end
+
+ def test_should_be_included_in_known_states
+ assert_equal [:parked], @branch.known_states
+ end
+
+ def test_should_include_requirement_in_match
+ match = @branch.match(@object, from: :parked)
+ assert_equal @branch.state_requirements.first[:from], match[:from]
+ end
+end
diff --git a/test/unit/branch/branch_with_if_conditional_test.rb b/test/unit/branch/branch_with_if_conditional_test.rb
new file mode 100644
index 0000000..2a1db37
--- /dev/null
+++ b/test/unit/branch/branch_with_if_conditional_test.rb
@@ -0,0 +1,27 @@
+require_relative '../../test_helper'
+
+class BranchWithIfConditionalTest < StateMachinesTest
+ def setup
+ @object = Object.new
+ end
+
+ def test_should_have_an_if_condition
+ branch = StateMachines::Branch.new(if: lambda { true })
+ refute_nil branch.if_condition
+ end
+
+ def test_should_match_if_true
+ branch = StateMachines::Branch.new(if: lambda { true })
+ assert branch.matches?(@object)
+ end
+
+ def test_should_not_match_if_false
+ branch = StateMachines::Branch.new(if: lambda { false })
+ refute branch.matches?(@object)
+ end
+
+ def test_should_be_nil_if_unmatched
+ branch = StateMachines::Branch.new(if: lambda { false })
+ assert_nil branch.match(@object)
+ end
+end
diff --git a/test/unit/branch/branch_with_implicit_and_explicit_requirements_test.rb b/test/unit/branch/branch_with_implicit_and_explicit_requirements_test.rb
new file mode 100644
index 0000000..ff182af
--- /dev/null
+++ b/test/unit/branch/branch_with_implicit_and_explicit_requirements_test.rb
@@ -0,0 +1,23 @@
+require_relative '../../test_helper'
+
+class BranchWithImplicitAndExplicitRequirementsTest < StateMachinesTest
+ def setup
+ @branch = StateMachines::Branch.new(parked: :idling, from: :parked)
+ end
+
+ def test_should_create_multiple_requirements
+ assert_equal 2, @branch.state_requirements.length
+ end
+
+ def test_should_create_implicit_requirements_for_implicit_options
+ assert(@branch.state_requirements.any? do |state_requirement|
+ state_requirement[:from].values == [:parked] && state_requirement[:to].values == [:idling]
+ end)
+ end
+
+ def test_should_create_implicit_requirements_for_explicit_options
+ assert(@branch.state_requirements.any? do |state_requirement|
+ state_requirement[:from].values == [:from] && state_requirement[:to].values == [:parked]
+ end)
+ end
+end
diff --git a/test/unit/branch/branch_with_implicit_from_requirement_matcher_test.rb b/test/unit/branch/branch_with_implicit_from_requirement_matcher_test.rb
new file mode 100644
index 0000000..1abf77d
--- /dev/null
+++ b/test/unit/branch/branch_with_implicit_from_requirement_matcher_test.rb
@@ -0,0 +1,20 @@
+require_relative '../../test_helper'
+
+class BranchWithMultipleFromRequirementsTest < StateMachinesTest
+ def setup
+ @object = Object.new
+ @branch = StateMachines::Branch.new(from: [:idling, :parked])
+ end
+
+ def test_should_match_if_included
+ assert @branch.matches?(@object, from: :idling)
+ end
+
+ def test_should_not_match_if_not_included
+ refute @branch.matches?(@object, from: :first_gear)
+ end
+
+ def test_should_be_included_in_known_states
+ assert_equal [:idling, :parked], @branch.known_states
+ end
+end
diff --git a/test/unit/branch/branch_with_implicit_requirement_test.rb b/test/unit/branch/branch_with_implicit_requirement_test.rb
new file mode 100644
index 0000000..daa1f57
--- /dev/null
+++ b/test/unit/branch/branch_with_implicit_requirement_test.rb
@@ -0,0 +1,20 @@
+require_relative '../../test_helper'
+
+class BranchWithImplicitRequirementTest < StateMachinesTest
+ def setup
+ @branch = StateMachines::Branch.new(parked: :idling, on: :ignite)
+ end
+
+ def test_should_create_an_event_requirement
+ assert_instance_of StateMachines::WhitelistMatcher, @branch.event_requirement
+ assert_equal [:ignite], @branch.event_requirement.values
+ end
+
+ def test_should_use_a_whitelist_from_matcher
+ assert_instance_of StateMachines::WhitelistMatcher, @branch.state_requirements.first[:from]
+ end
+
+ def test_should_use_a_whitelist_to_matcher
+ assert_instance_of StateMachines::WhitelistMatcher, @branch.state_requirements.first[:to]
+ end
+end
diff --git a/test/unit/branch/branch_with_implicit_to_requirement_matcher_test.rb b/test/unit/branch/branch_with_implicit_to_requirement_matcher_test.rb
new file mode 100644
index 0000000..da2c2a8
--- /dev/null
+++ b/test/unit/branch/branch_with_implicit_to_requirement_matcher_test.rb
@@ -0,0 +1,16 @@
+require_relative '../../test_helper'
+
+class BranchWithImplicitToRequirementMatcherTest < StateMachinesTest
+ def setup
+ @matcher = StateMachines::BlacklistMatcher.new(:idling)
+ @branch = StateMachines::Branch.new(parked: @matcher)
+ end
+
+ def test_should_convert_from_to_whitelist_matcher
+ assert_instance_of StateMachines::WhitelistMatcher, @branch.state_requirements.first[:from]
+ end
+
+ def test_should_not_convert_to_to_whitelist_matcher
+ assert_equal @matcher, @branch.state_requirements.first[:to]
+ end
+end
diff --git a/test/unit/branch/branch_with_multiple_except_from_requirements_test.rb b/test/unit/branch/branch_with_multiple_except_from_requirements_test.rb
new file mode 100644
index 0000000..72c8ef9
--- /dev/null
+++ b/test/unit/branch/branch_with_multiple_except_from_requirements_test.rb
@@ -0,0 +1,20 @@
+require_relative '../../test_helper'
+
+class BranchWithMultipleExceptFromRequirementsTest < StateMachinesTest
+ def setup
+ @object = Object.new
+ @branch = StateMachines::Branch.new(except_from: [:idling, :parked])
+ end
+
+ def test_should_match_if_not_included
+ assert @branch.matches?(@object, from: :first_gear)
+ end
+
+ def test_should_not_match_if_included
+ refute @branch.matches?(@object, from: :idling)
+ end
+
+ def test_should_be_included_in_known_states
+ assert_equal [:idling, :parked], @branch.known_states
+ end
+end
diff --git a/test/unit/branch/branch_with_multiple_except_on_requirements_test.rb b/test/unit/branch/branch_with_multiple_except_on_requirements_test.rb
new file mode 100644
index 0000000..1d70262
--- /dev/null
+++ b/test/unit/branch/branch_with_multiple_except_on_requirements_test.rb
@@ -0,0 +1,16 @@
+require_relative '../../test_helper'
+
+class BranchWithMultipleExceptOnRequirementsTest < StateMachinesTest
+ def setup
+ @object = Object.new
+ @branch = StateMachines::Branch.new(except_on: [:ignite, :park])
+ end
+
+ def test_should_match_if_not_included
+ assert @branch.matches?(@object, on: :shift_up)
+ end
+
+ def test_should_not_match_if_included
+ refute @branch.matches?(@object, on: :ignite)
+ end
+end
diff --git a/test/unit/branch/branch_with_multiple_except_to_requirements_test.rb b/test/unit/branch/branch_with_multiple_except_to_requirements_test.rb
new file mode 100644
index 0000000..ce84a48
--- /dev/null
+++ b/test/unit/branch/branch_with_multiple_except_to_requirements_test.rb
@@ -0,0 +1,20 @@
+require_relative '../../test_helper'
+
+class BranchWithMultipleExceptToRequirementsTest < StateMachinesTest
+ def setup
+ @object = Object.new
+ @branch = StateMachines::Branch.new(except_to: [:idling, :parked])
+ end
+
+ def test_should_match_if_not_included
+ assert @branch.matches?(@object, to: :first_gear)
+ end
+
+ def test_should_not_match_if_included
+ refute @branch.matches?(@object, to: :idling)
+ end
+
+ def test_should_be_included_in_known_states
+ assert_equal [:idling, :parked], @branch.known_states
+ end
+end
diff --git a/test/unit/branch/branch_with_multiple_from_requirements_test.rb b/test/unit/branch/branch_with_multiple_from_requirements_test.rb
new file mode 100644
index 0000000..e80f4ec
--- /dev/null
+++ b/test/unit/branch/branch_with_multiple_from_requirements_test.rb
@@ -0,0 +1,16 @@
+require_relative '../../test_helper'
+
+class BranchWithImplicitFromRequirementMatcherTest < StateMachinesTest
+ def setup
+ @matcher = StateMachines::BlacklistMatcher.new(:parked)
+ @branch = StateMachines::Branch.new(@matcher => :idling)
+ end
+
+ def test_should_not_convert_from_to_whitelist_matcher
+ assert_equal @matcher, @branch.state_requirements.first[:from]
+ end
+
+ def test_should_convert_to_to_whitelist_matcher
+ assert_instance_of StateMachines::WhitelistMatcher, @branch.state_requirements.first[:to]
+ end
+end
diff --git a/test/unit/branch/branch_with_multiple_if_conditionals_test.rb b/test/unit/branch/branch_with_multiple_if_conditionals_test.rb
new file mode 100644
index 0000000..7a22001
--- /dev/null
+++ b/test/unit/branch/branch_with_multiple_if_conditionals_test.rb
@@ -0,0 +1,20 @@
+require_relative '../../test_helper'
+
+class BranchWithMultipleIfConditionalsTest < StateMachinesTest
+ def setup
+ @object = Object.new
+ end
+
+ def test_should_match_if_all_are_true
+ branch = StateMachines::Branch.new(if: [lambda { true }, lambda { true }])
+ assert branch.match(@object)
+ end
+
+ def test_should_not_match_if_any_are_false
+ branch = StateMachines::Branch.new(if: [lambda { true }, lambda { false }])
+ refute branch.match(@object)
+
+ branch = StateMachines::Branch.new(if: [lambda { false }, lambda { true }])
+ refute branch.match(@object)
+ end
+end
diff --git a/test/unit/branch/branch_with_multiple_implicit_requirements_test.rb b/test/unit/branch/branch_with_multiple_implicit_requirements_test.rb
new file mode 100644
index 0000000..7d27342
--- /dev/null
+++ b/test/unit/branch/branch_with_multiple_implicit_requirements_test.rb
@@ -0,0 +1,53 @@
+require_relative '../../test_helper'
+
+class BranchWithMultipleImplicitRequirementsTest < StateMachinesTest
+ def setup
+ @object = Object.new
+ @branch = StateMachines::Branch.new(parked: :idling, idling: :first_gear, on: :ignite)
+ end
+
+ def test_should_create_multiple_state_requirements
+ assert_equal 2, @branch.state_requirements.length
+ end
+
+ def test_should_not_match_event_as_state_requirement
+ refute @branch.matches?(@object, from: :on, to: :ignite)
+ end
+
+ def test_should_match_if_from_included_in_any
+ assert @branch.matches?(@object, from: :parked)
+ assert @branch.matches?(@object, from: :idling)
+ end
+
+ def test_should_not_match_if_from_not_included_in_any
+ refute @branch.matches?(@object, from: :first_gear)
+ end
+
+ def test_should_match_if_to_included_in_any
+ assert @branch.matches?(@object, to: :idling)
+ assert @branch.matches?(@object, to: :first_gear)
+ end
+
+ def test_should_not_match_if_to_not_included_in_any
+ refute @branch.matches?(@object, to: :parked)
+ end
+
+ def test_should_match_if_all_options_match
+ assert @branch.matches?(@object, from: :parked, to: :idling, on: :ignite)
+ assert @branch.matches?(@object, from: :idling, to: :first_gear, on: :ignite)
+ end
+
+ def test_should_not_match_if_any_options_do_not_match
+ refute @branch.matches?(@object, from: :parked, to: :idling, on: :park)
+ refute @branch.matches?(@object, from: :parked, to: :first_gear, on: :park)
+ end
+
+ def test_should_include_all_known_states
+ assert_equal [:first_gear, :idling, :parked], @branch.known_states.sort_by { |state| state.to_s }
+ end
+
+ def test_should_not_duplicate_known_statse
+ branch = StateMachines::Branch.new(parked: :idling, first_gear: :idling)
+ assert_equal [:first_gear, :idling, :parked], branch.known_states.sort_by { |state| state.to_s }
+ end
+end
diff --git a/test/unit/branch/branch_with_multiple_on_requirements_test.rb b/test/unit/branch/branch_with_multiple_on_requirements_test.rb
new file mode 100644
index 0000000..ce84a48
--- /dev/null
+++ b/test/unit/branch/branch_with_multiple_on_requirements_test.rb
@@ -0,0 +1,20 @@
+require_relative '../../test_helper'
+
+class BranchWithMultipleExceptToRequirementsTest < StateMachinesTest
+ def setup
+ @object = Object.new
+ @branch = StateMachines::Branch.new(except_to: [:idling, :parked])
+ end
+
+ def test_should_match_if_not_included
+ assert @branch.matches?(@object, to: :first_gear)
+ end
+
+ def test_should_not_match_if_included
+ refute @branch.matches?(@object, to: :idling)
+ end
+
+ def test_should_be_included_in_known_states
+ assert_equal [:idling, :parked], @branch.known_states
+ end
+end
diff --git a/test/unit/branch/branch_with_multiple_to_requirements_test.rb b/test/unit/branch/branch_with_multiple_to_requirements_test.rb
new file mode 100644
index 0000000..7c33f5a
--- /dev/null
+++ b/test/unit/branch/branch_with_multiple_to_requirements_test.rb
@@ -0,0 +1,20 @@
+require_relative '../../test_helper'
+
+class BranchWithMultipleToRequirementsTest < StateMachinesTest
+ def setup
+ @object = Object.new
+ @branch = StateMachines::Branch.new(to: [:idling, :parked])
+ end
+
+ def test_should_match_if_included
+ assert @branch.matches?(@object, to: :idling)
+ end
+
+ def test_should_not_match_if_not_included
+ refute @branch.matches?(@object, to: :first_gear)
+ end
+
+ def test_should_be_included_in_known_states
+ assert_equal [:idling, :parked], @branch.known_states
+ end
+end
diff --git a/test/unit/branch/branch_with_multiple_unless_conditionals_test.rb b/test/unit/branch/branch_with_multiple_unless_conditionals_test.rb
new file mode 100644
index 0000000..7b4c640
--- /dev/null
+++ b/test/unit/branch/branch_with_multiple_unless_conditionals_test.rb
@@ -0,0 +1,20 @@
+require_relative '../../test_helper'
+
+class BranchWithMultipleUnlessConditionalsTest < StateMachinesTest
+ def setup
+ @object = Object.new
+ end
+
+ def test_should_match_if_all_are_false
+ branch = StateMachines::Branch.new(unless: [lambda { false }, lambda { false }])
+ assert branch.match(@object)
+ end
+
+ def test_should_not_match_if_any_are_true
+ branch = StateMachines::Branch.new(unless: [lambda { true }, lambda { false }])
+ refute branch.match(@object)
+
+ branch = StateMachines::Branch.new(unless: [lambda { false }, lambda { true }])
+ refute branch.match(@object)
+ end
+end
diff --git a/test/unit/branch/branch_with_nil_requirements_test.rb b/test/unit/branch/branch_with_nil_requirements_test.rb
new file mode 100644
index 0000000..b627d9c
--- /dev/null
+++ b/test/unit/branch/branch_with_nil_requirements_test.rb
@@ -0,0 +1,28 @@
+require_relative '../../test_helper'
+
+class BranchWithNilRequirementsTest < StateMachinesTest
+ def setup
+ @object = Object.new
+ @branch = StateMachines::Branch.new(from: nil, to: nil)
+ end
+
+ def test_should_match_empty_query
+ assert @branch.matches?(@object)
+ end
+
+ def test_should_match_if_all_requirements_match
+ assert @branch.matches?(@object, from: nil, to: nil)
+ end
+
+ def test_should_not_match_if_from_not_included
+ refute @branch.matches?(@object, from: :parked)
+ end
+
+ def test_should_not_match_if_to_not_included
+ refute @branch.matches?(@object, to: :idling)
+ end
+
+ def test_should_include_all_known_states
+ assert_equal [nil], @branch.known_states
+ end
+end
diff --git a/test/unit/branch/branch_with_no_requirements_test.rb b/test/unit/branch/branch_with_no_requirements_test.rb
new file mode 100644
index 0000000..e1f831b
--- /dev/null
+++ b/test/unit/branch/branch_with_no_requirements_test.rb
@@ -0,0 +1,36 @@
+require_relative '../../test_helper'
+
+class BranchWithNoRequirementsTest < StateMachinesTest
+ def setup
+ @object = Object.new
+ @branch = StateMachines::Branch.new
+ end
+
+ def test_should_use_all_matcher_for_event_requirement
+ assert_equal StateMachines::AllMatcher.instance, @branch.event_requirement
+ end
+
+ def test_should_use_all_matcher_for_from_state_requirement
+ assert_equal StateMachines::AllMatcher.instance, @branch.state_requirements.first[:from]
+ end
+
+ def test_should_use_all_matcher_for_to_state_requirement
+ assert_equal StateMachines::AllMatcher.instance, @branch.state_requirements.first[:to]
+ end
+
+ def test_should_match_empty_query
+ assert @branch.matches?(@object, {})
+ end
+
+ def test_should_match_non_empty_query
+ assert @branch.matches?(@object, to: :idling, from: :parked, on: :ignite)
+ end
+
+ def test_should_include_all_requirements_in_match
+ match = @branch.match(@object, {})
+
+ assert_equal @branch.state_requirements.first[:from], match[:from]
+ assert_equal @branch.state_requirements.first[:to], match[:to]
+ assert_equal @branch.event_requirement, match[:on]
+ end
+end
diff --git a/test/unit/branch/branch_with_on_matcher_requirement_test.rb b/test/unit/branch/branch_with_on_matcher_requirement_test.rb
new file mode 100644
index 0000000..b9f3de6
--- /dev/null
+++ b/test/unit/branch/branch_with_on_matcher_requirement_test.rb
@@ -0,0 +1,16 @@
+require_relative '../../test_helper'
+
+class BranchWithOnMatcherRequirementTest < StateMachinesTest
+ def setup
+ @object = Object.new
+ @branch = StateMachines::Branch.new(on: StateMachines::BlacklistMatcher.new([:ignite, :park]))
+ end
+
+ def test_should_match_if_included
+ assert @branch.matches?(@object, on: :shift_up)
+ end
+
+ def test_should_not_match_if_not_included
+ refute @branch.matches?(@object, on: :ignite)
+ end
+end
diff --git a/test/unit/branch/branch_with_on_requirement_test.rb b/test/unit/branch/branch_with_on_requirement_test.rb
new file mode 100644
index 0000000..3f96e14
--- /dev/null
+++ b/test/unit/branch/branch_with_on_requirement_test.rb
@@ -0,0 +1,45 @@
+require_relative '../../test_helper'
+
+class BranchWithOnRequirementTest < StateMachinesTest
+ def setup
+ @object = Object.new
+ @branch = StateMachines::Branch.new(on: :ignite)
+ end
+
+ def test_should_use_a_whitelist_matcher
+ assert_instance_of StateMachines::WhitelistMatcher, @branch.event_requirement
+ end
+
+ def test_should_match_if_not_specified
+ assert @branch.matches?(@object, from: :parked)
+ end
+
+ def test_should_match_if_included
+ assert @branch.matches?(@object, on: :ignite)
+ end
+
+ def test_should_not_match_if_not_included
+ refute @branch.matches?(@object, on: :park)
+ end
+
+ def test_should_not_match_if_nil
+ refute @branch.matches?(@object, on: nil)
+ end
+
+ def test_should_ignore_to
+ assert @branch.matches?(@object, on: :ignite, to: :parked)
+ end
+
+ def test_should_ignore_from
+ assert @branch.matches?(@object, on: :ignite, from: :parked)
+ end
+
+ def test_should_not_be_included_in_known_states
+ assert_equal [], @branch.known_states
+ end
+
+ def test_should_include_requirement_in_match
+ match = @branch.match(@object, on: :ignite)
+ assert_equal @branch.event_requirement, match[:on]
+ end
+end
diff --git a/test/unit/branch/branch_with_to_matcher_requirement_test.rb b/test/unit/branch/branch_with_to_matcher_requirement_test.rb
new file mode 100644
index 0000000..5a4f42e
--- /dev/null
+++ b/test/unit/branch/branch_with_to_matcher_requirement_test.rb
@@ -0,0 +1,20 @@
+require_relative '../../test_helper'
+
+class BranchWithToMatcherRequirementTest < StateMachinesTest
+ def setup
+ @object = Object.new
+ @branch = StateMachines::Branch.new(to: StateMachines::BlacklistMatcher.new([:idling, :parked]))
+ end
+
+ def test_should_match_if_included
+ assert @branch.matches?(@object, to: :first_gear)
+ end
+
+ def test_should_not_match_if_not_included
+ refute @branch.matches?(@object, to: :idling)
+ end
+
+ def test_include_values_in_known_states
+ assert_equal [:idling, :parked], @branch.known_states
+ end
+end
diff --git a/test/unit/branch/branch_with_to_requirement_test.rb b/test/unit/branch/branch_with_to_requirement_test.rb
new file mode 100644
index 0000000..68080e8
--- /dev/null
+++ b/test/unit/branch/branch_with_to_requirement_test.rb
@@ -0,0 +1,45 @@
+require_relative '../../test_helper'
+
+class BranchWithToRequirementTest < StateMachinesTest
+ def setup
+ @object = Object.new
+ @branch = StateMachines::Branch.new(to: :idling)
+ end
+
+ def test_should_use_a_whitelist_matcher
+ assert_instance_of StateMachines::WhitelistMatcher, @branch.state_requirements.first[:to]
+ end
+
+ def test_should_match_if_not_specified
+ assert @branch.matches?(@object, from: :parked)
+ end
+
+ def test_should_match_if_included
+ assert @branch.matches?(@object, to: :idling)
+ end
+
+ def test_should_not_match_if_not_included
+ refute @branch.matches?(@object, to: :parked)
+ end
+
+ def test_should_not_match_if_nil
+ refute @branch.matches?(@object, to: nil)
+ end
+
+ def test_should_ignore_from
+ assert @branch.matches?(@object, to: :idling, from: :parked)
+ end
+
+ def test_should_ignore_on
+ assert @branch.matches?(@object, to: :idling, on: :ignite)
+ end
+
+ def test_should_be_included_in_known_states
+ assert_equal [:idling], @branch.known_states
+ end
+
+ def test_should_include_requirement_in_match
+ match = @branch.match(@object, to: :idling)
+ assert_equal @branch.state_requirements.first[:to], match[:to]
+ end
+end
diff --git a/test/unit/branch/branch_with_unless_conditional_test.rb b/test/unit/branch/branch_with_unless_conditional_test.rb
new file mode 100644
index 0000000..e7342bd
--- /dev/null
+++ b/test/unit/branch/branch_with_unless_conditional_test.rb
@@ -0,0 +1,27 @@
+require_relative '../../test_helper'
+
+class BranchWithUnlessConditionalTest < StateMachinesTest
+ def setup
+ @object = Object.new
+ end
+
+ def test_should_have_an_unless_condition
+ branch = StateMachines::Branch.new(unless: lambda { true })
+ refute_nil branch.unless_condition
+ end
+
+ def test_should_match_if_false
+ branch = StateMachines::Branch.new(unless: lambda { false })
+ assert branch.matches?(@object)
+ end
+
+ def test_should_not_match_if_true
+ branch = StateMachines::Branch.new(unless: lambda { true })
+ refute branch.matches?(@object)
+ end
+
+ def test_should_be_nil_if_unmatched
+ branch = StateMachines::Branch.new(unless: lambda { true })
+ assert_nil branch.match(@object)
+ end
+end
diff --git a/test/unit/branch/branch_without_guards_test.rb b/test/unit/branch/branch_without_guards_test.rb
new file mode 100644
index 0000000..7bf64d7
--- /dev/null
+++ b/test/unit/branch/branch_without_guards_test.rb
@@ -0,0 +1,27 @@
+require_relative '../../test_helper'
+
+class BranchWithoutGuardsTest < StateMachinesTest
+ def setup
+ @object = Object.new
+ end
+
+ def test_should_match_if_if_is_false
+ branch = StateMachines::Branch.new(if: lambda { false })
+ assert branch.matches?(@object, guard: false)
+ end
+
+ def test_should_match_if_if_is_true
+ branch = StateMachines::Branch.new(if: lambda { true })
+ assert branch.matches?(@object, guard: false)
+ end
+
+ def test_should_match_if_unless_is_false
+ branch = StateMachines::Branch.new(unless: lambda { false })
+ assert branch.matches?(@object, guard: false)
+ end
+
+ def test_should_match_if_unless_is_true
+ branch = StateMachines::Branch.new(unless: lambda { true })
+ assert branch.matches?(@object, guard: false)
+ end
+end
diff --git a/test/unit/callback/callback_by_default_test.rb b/test/unit/callback/callback_by_default_test.rb
new file mode 100644
index 0000000..3547725
--- /dev/null
+++ b/test/unit/callback/callback_by_default_test.rb
@@ -0,0 +1,25 @@
+require_relative '../../test_helper'
+
+class CallbackByDefaultTest < StateMachinesTest
+ def setup
+ @callback = StateMachines::Callback.new(:before) {}
+ end
+
+ def test_should_have_type
+ assert_equal :before, @callback.type
+ end
+
+ def test_should_not_have_a_terminator
+ assert_nil @callback.terminator
+ end
+
+ def test_should_have_a_branch_with_all_matcher_requirements
+ assert_equal StateMachines::AllMatcher.instance, @callback.branch.event_requirement
+ assert_equal StateMachines::AllMatcher.instance, @callback.branch.state_requirements.first[:from]
+ assert_equal StateMachines::AllMatcher.instance, @callback.branch.state_requirements.first[:to]
+ end
+
+ def test_should_not_have_any_known_states
+ assert_equal [], @callback.known_states
+ end
+end
diff --git a/test/unit/callback/callback_test.rb b/test/unit/callback/callback_test.rb
new file mode 100644
index 0000000..c990a4d
--- /dev/null
+++ b/test/unit/callback/callback_test.rb
@@ -0,0 +1,53 @@
+require_relative '../../test_helper'
+
+class CallbackTest < StateMachinesTest
+ def test_should_raise_exception_if_invalid_type_specified
+ exception = assert_raises(ArgumentError) { StateMachines::Callback.new(:invalid) {} }
+ assert_equal 'Type must be :before, :after, :around, or :failure', exception.message
+ end
+
+ def test_should_not_raise_exception_if_using_before_type
+ StateMachines::Callback.new(:before) {}
+ end
+
+ def test_should_not_raise_exception_if_using_after_type
+ StateMachines::Callback.new(:after) {}
+ end
+
+ def test_should_not_raise_exception_if_using_around_type
+ StateMachines::Callback.new(:around) {}
+ end
+
+ def test_should_not_raise_exception_if_using_failure_type
+ StateMachines::Callback.new(:failure) {}
+ end
+
+ def test_should_raise_exception_if_no_methods_specified
+ exception = assert_raises(ArgumentError) { StateMachines::Callback.new(:before) }
+ assert_equal 'Method(s) for callback must be specified', exception.message
+ end
+
+ def test_should_not_raise_exception_if_method_specified_in_do_option
+ StateMachines::Callback.new(:before, do: :run)
+ end
+
+ def test_should_not_raise_exception_if_method_specified_as_argument
+ StateMachines::Callback.new(:before, :run)
+ end
+
+ def test_should_not_raise_exception_if_method_specified_as_block
+ StateMachines::Callback.new(:before, :run) {}
+ end
+
+ def test_should_not_raise_exception_if_implicit_option_specified
+ StateMachines::Callback.new(:before, do: :run, invalid: :valid)
+ end
+
+ def test_should_not_bind_to_objects
+ refute StateMachines::Callback.bind_to_object
+ end
+
+ def test_should_not_have_a_terminator
+ assert_nil StateMachines::Callback.terminator
+ end
+end
diff --git a/test/unit/callback/callback_with_application_bound_object_test.rb b/test/unit/callback/callback_with_application_bound_object_test.rb
new file mode 100644
index 0000000..f9e547a
--- /dev/null
+++ b/test/unit/callback/callback_with_application_bound_object_test.rb
@@ -0,0 +1,23 @@
+require_relative '../../test_helper'
+
+class CallbackWithApplicationBoundObjectTest < StateMachinesTest
+ def setup
+ @original_bind_to_object = StateMachines::Callback.bind_to_object
+ StateMachines::Callback.bind_to_object = true
+
+ context = nil
+ @callback = StateMachines::Callback.new(:before, do: lambda { |*_args| context = self })
+
+ @object = Object.new
+ @callback.call(@object)
+ @context = context
+ end
+
+ def test_should_call_method_within_the_context_of_the_object
+ assert_equal @object, @context
+ end
+
+ def teardown
+ StateMachines::Callback.bind_to_object = @original_bind_to_object
+ end
+end
diff --git a/test/unit/callback/callback_with_application_terminator_test.rb b/test/unit/callback/callback_with_application_terminator_test.rb
new file mode 100644
index 0000000..47fa1ef
--- /dev/null
+++ b/test/unit/callback/callback_with_application_terminator_test.rb
@@ -0,0 +1,24 @@
+require_relative '../../test_helper'
+
+class CallbackWithApplicationTerminatorTest < StateMachinesTest
+ def setup
+ @original_terminator = StateMachines::Callback.terminator
+ StateMachines::Callback.terminator = lambda { |result| result == false }
+
+ @object = Object.new
+ end
+
+ def test_should_not_halt_if_terminator_does_not_match
+ callback = StateMachines::Callback.new(:before, do: lambda { true })
+ callback.call(@object)
+ end
+
+ def test_should_halt_if_terminator_matches
+ callback = StateMachines::Callback.new(:before, do: lambda { false })
+ assert_throws(:halt) { callback.call(@object) }
+ end
+
+ def teardown
+ StateMachines::Callback.terminator = @original_terminator
+ end
+end
diff --git a/test/unit/callback/callback_with_arguments_test.rb b/test/unit/callback/callback_with_arguments_test.rb
new file mode 100644
index 0000000..8393844
--- /dev/null
+++ b/test/unit/callback/callback_with_arguments_test.rb
@@ -0,0 +1,14 @@
+require_relative '../../test_helper'
+
+class CallbackWithArgumentsTest < StateMachinesTest
+ def setup
+ @callback = StateMachines::Callback.new(:before, do: lambda { |*args| @args = args })
+
+ @object = Object.new
+ @callback.call(@object, {}, 1, 2, 3)
+ end
+
+ def test_should_call_method_with_all_arguments
+ assert_equal [@object, 1, 2, 3], @args
+ end
+end
diff --git a/test/unit/callback/callback_with_around_type_and_arguments_test.rb b/test/unit/callback/callback_with_around_type_and_arguments_test.rb
new file mode 100644
index 0000000..395876d
--- /dev/null
+++ b/test/unit/callback/callback_with_around_type_and_arguments_test.rb
@@ -0,0 +1,25 @@
+require_relative '../../test_helper'
+
+class CallbackWithAroundTypeAndArgumentsTest < StateMachinesTest
+ def setup
+ @object = Object.new
+ end
+
+ def test_should_include_object_if_specified
+ callback = StateMachines::Callback.new(:around, lambda { |object, block| @args = [object]; block.call })
+ callback.call(@object)
+ assert_equal [@object], @args
+ end
+
+ def test_should_include_arguments_if_specified
+ callback = StateMachines::Callback.new(:around, lambda { |object, arg1, arg2, arg3, block| @args = [object, arg1, arg2, arg3]; block.call })
+ callback.call(@object, {}, 1, 2, 3)
+ assert_equal [@object, 1, 2, 3], @args
+ end
+
+ def test_should_include_arguments_if_splat_used
+ callback = StateMachines::Callback.new(:around, lambda { |*args| block = args.pop; @args = args; block.call })
+ callback.call(@object, {}, 1, 2, 3)
+ assert_equal [@object, 1, 2, 3], @args
+ end
+end
diff --git a/test/unit/callback/callback_with_around_type_and_block_test.rb b/test/unit/callback/callback_with_around_type_and_block_test.rb
new file mode 100644
index 0000000..820cc28
--- /dev/null
+++ b/test/unit/callback/callback_with_around_type_and_block_test.rb
@@ -0,0 +1,44 @@
+require_relative '../../test_helper'
+
+class CallbackWithAroundTypeAndBlockTest < StateMachinesTest
+ def setup
+ @object = Object.new
+ @callbacks = []
+ end
+
+ def test_should_evaluate_before_without_after
+ callback = StateMachines::Callback.new(:around, lambda { |*args| block = args.pop; @args = args; block.call })
+ assert callback.call(@object)
+ assert_equal [@object], @args
+ end
+
+ def test_should_evaluate_after_without_before
+ callback = StateMachines::Callback.new(:around, lambda { |*args| block = args.pop; block.call; @args = args })
+ assert callback.call(@object)
+ assert_equal [@object], @args
+ end
+
+ def test_should_halt_if_not_yielded
+ callback = StateMachines::Callback.new(:around, lambda { |_block| })
+ assert_throws(:halt) { callback.call(@object) }
+ end
+
+ def test_should_call_block_after_before
+ callback = StateMachines::Callback.new(:around, lambda { |block| @callbacks << :before; block.call })
+ assert callback.call(@object) { @callbacks << :block }
+ assert_equal [:before, :block], @callbacks
+ end
+
+ def test_should_call_block_before_after
+ @callbacks = []
+ callback = StateMachines::Callback.new(:around, lambda { |block| block.call; @callbacks << :after })
+ assert callback.call(@object) { @callbacks << :block }
+ assert_equal [:block, :after], @callbacks
+ end
+
+ def test_should_halt_if_block_halts
+ callback = StateMachines::Callback.new(:around, lambda { |block| block.call; @callbacks << :after })
+ assert_throws(:halt) { callback.call(@object) { throw :halt } }
+ assert_equal [], @callbacks
+ end
+end
diff --git a/test/unit/callback/callback_with_around_type_and_bound_method_test.rb b/test/unit/callback/callback_with_around_type_and_bound_method_test.rb
new file mode 100644
index 0000000..3633aad
--- /dev/null
+++ b/test/unit/callback/callback_with_around_type_and_bound_method_test.rb
@@ -0,0 +1,23 @@
+require_relative '../../test_helper'
+
+class CallbackWithAroundTypeAndBoundMethodTest < StateMachinesTest
+ def setup
+ @object = Object.new
+ end
+
+ def test_should_call_method_within_the_context_of_the_object
+ context = nil
+ callback = StateMachines::Callback.new(:around, do: lambda { |block| context = self; block.call }, bind_to_object: true)
+ callback.call(@object, {}, 1, 2, 3)
+
+ assert_equal @object, context
+ end
+
+ def test_should_include_arguments_if_specified
+ context = nil
+ callback = StateMachines::Callback.new(:around, do: lambda { |*args| block = args.pop; context = args; block.call }, bind_to_object: true)
+ callback.call(@object, {}, 1, 2, 3)
+
+ assert_equal [1, 2, 3], context
+ end
+end
diff --git a/test/unit/callback/callback_with_around_type_and_multiple_methods_test.rb b/test/unit/callback/callback_with_around_type_and_multiple_methods_test.rb
new file mode 100644
index 0000000..2904f27
--- /dev/null
+++ b/test/unit/callback/callback_with_around_type_and_multiple_methods_test.rb
@@ -0,0 +1,93 @@
+require_relative '../../test_helper'
+
+class CallbackWithAroundTypeAndMultipleMethodsTest < StateMachinesTest
+ def setup
+ @callback = StateMachines::Callback.new(:around, :run_1, :run_2)
+
+ class << @object = Object.new
+ attr_accessor :before_callbacks
+ attr_accessor :after_callbacks
+
+ def run_1
+ (@before_callbacks ||= []) << :run_1
+ yield
+ (@after_callbacks ||= []) << :run_1
+ end
+
+ def run_2
+ (@before_callbacks ||= []) << :run_2
+ yield
+ (@after_callbacks ||= []) << :run_2
+ end
+ end
+ end
+
+ def test_should_succeed
+ assert @callback.call(@object)
+ end
+
+ def test_should_evaluate_before_callbacks_in_order
+ @callback.call(@object)
+ assert_equal [:run_1, :run_2], @object.before_callbacks
+ end
+
+ def test_should_evaluate_after_callbacks_in_reverse_order
+ @callback.call(@object)
+ assert_equal [:run_2, :run_1], @object.after_callbacks
+ end
+
+ def test_should_call_block_after_before_callbacks
+ @callback.call(@object) { (@object.before_callbacks ||= []) << :block }
+ assert_equal [:run_1, :run_2, :block], @object.before_callbacks
+ end
+
+ def test_should_call_block_before_after_callbacks
+ @callback.call(@object) { (@object.after_callbacks ||= []) << :block }
+ assert_equal [:block, :run_2, :run_1], @object.after_callbacks
+ end
+
+ def test_should_halt_if_first_doesnt_yield
+ class << @object
+ remove_method :run_1
+ def run_1
+ (@before_callbacks ||= []) << :run_1
+ end
+ end
+
+ catch(:halt) do
+ @callback.call(@object) { (@object.before_callbacks ||= []) << :block }
+ end
+
+ assert_equal [:run_1], @object.before_callbacks
+ assert_nil @object.after_callbacks
+ end
+
+ def test_should_halt_if_last_doesnt_yield
+ class << @object
+ remove_method :run_2
+ def run_2
+ (@before_callbacks ||= []) << :run_2
+ end
+ end
+
+ catch(:halt) { @callback.call(@object) }
+ assert_equal [:run_1, :run_2], @object.before_callbacks
+ assert_nil @object.after_callbacks
+ end
+
+ def test_should_not_evaluate_further_methods_if_after_halts
+ class << @object
+ remove_method :run_2
+ def run_2
+ (@before_callbacks ||= []) << :run_2
+ yield
+ (@after_callbacks ||= []) << :run_2
+ throw :halt
+ end
+ end
+
+ catch(:halt) { @callback.call(@object) }
+ assert_equal [:run_1, :run_2], @object.before_callbacks
+ assert_equal [:run_2], @object.after_callbacks
+ end
+end
diff --git a/test/unit/callback/callback_with_around_type_and_terminator_test.rb b/test/unit/callback/callback_with_around_type_and_terminator_test.rb
new file mode 100644
index 0000000..c2c1c80
--- /dev/null
+++ b/test/unit/callback/callback_with_around_type_and_terminator_test.rb
@@ -0,0 +1,17 @@
+require_relative '../../test_helper'
+
+class CallbackWithAroundTypeAndTerminatorTest < StateMachinesTest
+ def setup
+ @object = Object.new
+ end
+
+ def test_should_not_halt_if_terminator_does_not_match
+ callback = StateMachines::Callback.new(:around, do: lambda { |block| block.call(false); false }, terminator: lambda { |result| result == true })
+ callback.call(@object)
+ end
+
+ def test_should_not_halt_if_terminator_matches
+ callback = StateMachines::Callback.new(:around, do: lambda { |block| block.call(false); false }, terminator: lambda { |result| result == false })
+ callback.call(@object)
+ end
+end
diff --git a/test/unit/callback/callback_with_block_test.rb b/test/unit/callback/callback_with_block_test.rb
new file mode 100644
index 0000000..ad94cd2
--- /dev/null
+++ b/test/unit/callback/callback_with_block_test.rb
@@ -0,0 +1,20 @@
+require_relative '../../test_helper'
+
+class CallbackWithBlockTest < StateMachinesTest
+ def setup
+ @callback = StateMachines::Callback.new(:before) do |*args|
+ @args = args
+ end
+
+ @object = Object.new
+ @result = @callback.call(@object)
+ end
+
+ def test_should_be_successful
+ assert @result
+ end
+
+ def test_should_call_with_empty_context
+ assert_equal [@object], @args
+ end
+end
diff --git a/test/unit/callback/callback_with_bound_method_and_arguments_test.rb b/test/unit/callback/callback_with_bound_method_and_arguments_test.rb
new file mode 100644
index 0000000..3a3e4d9
--- /dev/null
+++ b/test/unit/callback/callback_with_bound_method_and_arguments_test.rb
@@ -0,0 +1,28 @@
+require_relative '../../test_helper'
+
+class CallbackWithBoundMethodAndArgumentsTest < StateMachinesTest
+ def setup
+ @object = Object.new
+ end
+
+ def test_should_include_single_argument_if_specified
+ context = nil
+ callback = StateMachines::Callback.new(:before, do: lambda { |arg1| context = [arg1] }, bind_to_object: true)
+ callback.call(@object, {}, 1)
+ assert_equal [1], context
+ end
+
+ def test_should_include_multiple_arguments_if_specified
+ context = nil
+ callback = StateMachines::Callback.new(:before, do: lambda { |arg1, arg2, arg3| context = [arg1, arg2, arg3] }, bind_to_object: true)
+ callback.call(@object, {}, 1, 2, 3)
+ assert_equal [1, 2, 3], context
+ end
+
+ def test_should_include_arguments_if_splat_used
+ context = nil
+ callback = StateMachines::Callback.new(:before, do: lambda { |*args| context = args }, bind_to_object: true)
+ callback.call(@object, {}, 1, 2, 3)
+ assert_equal [1, 2, 3], context
+ end
+end
diff --git a/test/unit/callback/callback_with_bound_method_test.rb b/test/unit/callback/callback_with_bound_method_test.rb
new file mode 100644
index 0000000..622b437
--- /dev/null
+++ b/test/unit/callback/callback_with_bound_method_test.rb
@@ -0,0 +1,35 @@
+require_relative '../../test_helper'
+
+class CallbackWithBoundMethodTest < StateMachinesTest
+ def setup
+ @object = Object.new
+ end
+
+ def test_should_call_method_within_the_context_of_the_object_for_block_methods
+ context = nil
+ callback = StateMachines::Callback.new(:before, do: lambda { |*args| context = [self] + args }, bind_to_object: true)
+ callback.call(@object, {}, 1, 2, 3)
+
+ assert_equal [@object, 1, 2, 3], context
+ end
+
+ def test_should_ignore_option_for_symbolic_methods
+ class << @object
+ attr_reader :context
+
+ def after_ignite(*args)
+ @context = args
+ end
+ end
+
+ callback = StateMachines::Callback.new(:before, do: :after_ignite, bind_to_object: true)
+ callback.call(@object)
+
+ assert_equal [], @object.context
+ end
+
+ def test_should_ignore_option_for_string_methods
+ callback = StateMachines::Callback.new(:before, do: '[1, 2, 3]', bind_to_object: true)
+ assert callback.call(@object)
+ end
+end
diff --git a/test/unit/callback/callback_with_do_method_test.rb b/test/unit/callback/callback_with_do_method_test.rb
new file mode 100644
index 0000000..0666151
--- /dev/null
+++ b/test/unit/callback/callback_with_do_method_test.rb
@@ -0,0 +1,18 @@
+require_relative '../../test_helper'
+
+class CallbackWithDoMethodTest < StateMachinesTest
+ def setup
+ @callback = StateMachines::Callback.new(:before, do: lambda { |*args| @args = args })
+
+ @object = Object.new
+ @result = @callback.call(@object)
+ end
+
+ def test_should_be_successful
+ assert @result
+ end
+
+ def test_should_call_with_empty_context
+ assert_equal [@object], @args
+ end
+end
diff --git a/test/unit/callback/callback_with_explicit_requirements_test.rb b/test/unit/callback/callback_with_explicit_requirements_test.rb
new file mode 100644
index 0000000..84cb032
--- /dev/null
+++ b/test/unit/callback/callback_with_explicit_requirements_test.rb
@@ -0,0 +1,32 @@
+require_relative '../../test_helper'
+
+class CallbackWithExplicitRequirementsTest < StateMachinesTest
+ def setup
+ @object = Object.new
+ @callback = StateMachines::Callback.new(:before, from: :parked, to: :idling, on: :ignite, do: lambda {})
+ end
+
+ def test_should_call_with_empty_context
+ assert @callback.call(@object, {})
+ end
+
+ def test_should_not_call_if_from_not_included
+ refute @callback.call(@object, from: :idling)
+ end
+
+ def test_should_not_call_if_to_not_included
+ refute @callback.call(@object, to: :parked)
+ end
+
+ def test_should_not_call_if_on_not_included
+ refute @callback.call(@object, on: :park)
+ end
+
+ def test_should_call_if_all_requirements_met
+ assert @callback.call(@object, from: :parked, to: :idling, on: :ignite)
+ end
+
+ def test_should_include_in_known_states
+ assert_equal [:parked, :idling], @callback.known_states
+ end
+end
diff --git a/test/unit/callback/callback_with_if_condition_test.rb b/test/unit/callback/callback_with_if_condition_test.rb
new file mode 100644
index 0000000..36b396d
--- /dev/null
+++ b/test/unit/callback/callback_with_if_condition_test.rb
@@ -0,0 +1,17 @@
+require_relative '../../test_helper'
+
+class CallbackWithIfConditionTest < StateMachinesTest
+ def setup
+ @object = Object.new
+ end
+
+ def test_should_call_if_true
+ callback = StateMachines::Callback.new(:before, if: lambda { true }, do: lambda {})
+ assert callback.call(@object)
+ end
+
+ def test_should_not_call_if_false
+ callback = StateMachines::Callback.new(:before, if: lambda { false }, do: lambda {})
+ refute callback.call(@object)
+ end
+end
diff --git a/test/unit/callback/callback_with_implicit_requirements_test.rb b/test/unit/callback/callback_with_implicit_requirements_test.rb
new file mode 100644
index 0000000..e8d2718
--- /dev/null
+++ b/test/unit/callback/callback_with_implicit_requirements_test.rb
@@ -0,0 +1,32 @@
+require_relative '../../test_helper'
+
+class CallbackWithImplicitRequirementsTest < StateMachinesTest
+ def setup
+ @object = Object.new
+ @callback = StateMachines::Callback.new(:before, parked: :idling, on: :ignite, do: lambda {})
+ end
+
+ def test_should_call_with_empty_context
+ assert @callback.call(@object, {})
+ end
+
+ def test_should_not_call_if_from_not_included
+ refute @callback.call(@object, from: :idling)
+ end
+
+ def test_should_not_call_if_to_not_included
+ refute @callback.call(@object, to: :parked)
+ end
+
+ def test_should_not_call_if_on_not_included
+ refute @callback.call(@object, on: :park)
+ end
+
+ def test_should_call_if_all_requirements_met
+ assert @callback.call(@object, from: :parked, to: :idling, on: :ignite)
+ end
+
+ def test_should_include_in_known_states
+ assert_equal [:parked, :idling], @callback.known_states
+ end
+end
diff --git a/test/unit/callback/callback_with_method_argument_test.rb b/test/unit/callback/callback_with_method_argument_test.rb
new file mode 100644
index 0000000..14f889b
--- /dev/null
+++ b/test/unit/callback/callback_with_method_argument_test.rb
@@ -0,0 +1,18 @@
+require_relative '../../test_helper'
+
+class CallbackWithMethodArgumentTest < StateMachinesTest
+ def setup
+ @callback = StateMachines::Callback.new(:before, lambda { |*args| @args = args })
+
+ @object = Object.new
+ @result = @callback.call(@object)
+ end
+
+ def test_should_be_successful
+ assert @result
+ end
+
+ def test_should_call_with_empty_context
+ assert_equal [@object], @args
+ end
+end
diff --git a/test/unit/callback/callback_with_mixed_methods_test.rb b/test/unit/callback/callback_with_mixed_methods_test.rb
new file mode 100644
index 0000000..f51f68a
--- /dev/null
+++ b/test/unit/callback/callback_with_mixed_methods_test.rb
@@ -0,0 +1,31 @@
+require_relative '../../test_helper'
+
+class CallbackWithMixedMethodsTest < StateMachinesTest
+ def setup
+ @callback = StateMachines::Callback.new(:before, :run_argument, do: :run_do) do |object|
+ object.callbacks << :block
+ end
+
+ class << @object = Object.new
+ attr_accessor :callbacks
+
+ def run_argument
+ (@callbacks ||= []) << :argument
+ end
+
+ def run_do
+ (@callbacks ||= []) << :do
+ end
+ end
+
+ @result = @callback.call(@object)
+ end
+
+ def test_should_be_successful
+ assert @result
+ end
+
+ def test_should_call_each_callback_in_order
+ assert_equal [:argument, :do, :block], @object.callbacks
+ end
+end
diff --git a/test/unit/callback/callback_with_multiple_bound_methods_test.rb b/test/unit/callback/callback_with_multiple_bound_methods_test.rb
new file mode 100644
index 0000000..41bc9da
--- /dev/null
+++ b/test/unit/callback/callback_with_multiple_bound_methods_test.rb
@@ -0,0 +1,21 @@
+require_relative '../../test_helper'
+
+class CallbackWithMultipleBoundMethodsTest < StateMachinesTest
+ def setup
+ @object = Object.new
+
+ first_context = nil
+ second_context = nil
+
+ @callback = StateMachines::Callback.new(:before, do: [lambda { first_context = self }, lambda { second_context = self }], bind_to_object: true)
+ @callback.call(@object)
+
+ @first_context = first_context
+ @second_context = second_context
+ end
+
+ def test_should_call_each_method_within_the_context_of_the_object
+ assert_equal @object, @first_context
+ assert_equal @object, @second_context
+ end
+end
diff --git a/test/unit/callback/callback_with_multiple_do_methods_test.rb b/test/unit/callback/callback_with_multiple_do_methods_test.rb
new file mode 100644
index 0000000..2b088f1
--- /dev/null
+++ b/test/unit/callback/callback_with_multiple_do_methods_test.rb
@@ -0,0 +1,29 @@
+require_relative '../../test_helper'
+
+class CallbackWithMultipleDoMethodsTest < StateMachinesTest
+ def setup
+ @callback = StateMachines::Callback.new(:before, do: [:run_1, :run_2])
+
+ class << @object = Object.new
+ attr_accessor :callbacks
+
+ def run_1
+ (@callbacks ||= []) << :run_1
+ end
+
+ def run_2
+ (@callbacks ||= []) << :run_2
+ end
+ end
+
+ @result = @callback.call(@object)
+ end
+
+ def test_should_be_successful
+ assert @result
+ end
+
+ def test_should_call_each_callback_in_order
+ assert_equal [:run_1, :run_2], @object.callbacks
+ end
+end
diff --git a/test/unit/callback/callback_with_multiple_method_arguments_test.rb b/test/unit/callback/callback_with_multiple_method_arguments_test.rb
new file mode 100644
index 0000000..5724012
--- /dev/null
+++ b/test/unit/callback/callback_with_multiple_method_arguments_test.rb
@@ -0,0 +1,29 @@
+require_relative '../../test_helper'
+
+class CallbackWithMultipleMethodArgumentsTest < StateMachinesTest
+ def setup
+ @callback = StateMachines::Callback.new(:before, :run_1, :run_2)
+
+ class << @object = Object.new
+ attr_accessor :callbacks
+
+ def run_1
+ (@callbacks ||= []) << :run_1
+ end
+
+ def run_2
+ (@callbacks ||= []) << :run_2
+ end
+ end
+
+ @result = @callback.call(@object)
+ end
+
+ def test_should_be_successful
+ assert @result
+ end
+
+ def test_should_call_each_callback_in_order
+ assert_equal [:run_1, :run_2], @object.callbacks
+ end
+end
diff --git a/test/unit/callback/callback_with_terminator_test.rb b/test/unit/callback/callback_with_terminator_test.rb
new file mode 100644
index 0000000..c513a52
--- /dev/null
+++ b/test/unit/callback/callback_with_terminator_test.rb
@@ -0,0 +1,22 @@
+require_relative '../../test_helper'
+
+class CallbackWithTerminatorTest < StateMachinesTest
+ def setup
+ @object = Object.new
+ end
+
+ def test_should_not_halt_if_terminator_does_not_match
+ callback = StateMachines::Callback.new(:before, do: lambda { false }, terminator: lambda { |result| result == true })
+ callback.call(@object)
+ end
+
+ def test_should_halt_if_terminator_matches
+ callback = StateMachines::Callback.new(:before, do: lambda { false }, terminator: lambda { |result| result == false })
+ assert_throws(:halt) { callback.call(@object) }
+ end
+
+ def test_should_halt_if_terminator_matches_any_method
+ callback = StateMachines::Callback.new(:before, do: [lambda { true }, lambda { false }], terminator: lambda { |result| result == false })
+ assert_throws(:halt) { callback.call(@object) }
+ end
+end
diff --git a/test/unit/callback/callback_with_unbound_method_test.rb b/test/unit/callback/callback_with_unbound_method_test.rb
new file mode 100644
index 0000000..3127399
--- /dev/null
+++ b/test/unit/callback/callback_with_unbound_method_test.rb
@@ -0,0 +1,14 @@
+require_relative '../../test_helper'
+
+class CallbackWithUnboundMethodTest < StateMachinesTest
+ def setup
+ @callback = StateMachines::Callback.new(:before, do: lambda { |*args| @context = args.unshift(self) })
+
+ @object = Object.new
+ @callback.call(@object, {}, 1, 2, 3)
+ end
+
+ def test_should_call_method_outside_the_context_of_the_object
+ assert_equal [self, @object, 1, 2, 3], @context
+ end
+end
diff --git a/test/unit/callback/callback_with_unless_condition_test.rb b/test/unit/callback/callback_with_unless_condition_test.rb
new file mode 100644
index 0000000..b13fba1
--- /dev/null
+++ b/test/unit/callback/callback_with_unless_condition_test.rb
@@ -0,0 +1,17 @@
+require_relative '../../test_helper'
+
+class CallbackWithUnlessConditionTest < StateMachinesTest
+ def setup
+ @object = Object.new
+ end
+
+ def test_should_call_if_false
+ callback = StateMachines::Callback.new(:before, unless: lambda { false }, do: lambda {})
+ assert callback.call(@object)
+ end
+
+ def test_should_not_call_if_true
+ callback = StateMachines::Callback.new(:before, unless: lambda { true }, do: lambda {})
+ refute callback.call(@object)
+ end
+end
diff --git a/test/unit/callback/callback_without_arguments_test.rb b/test/unit/callback/callback_without_arguments_test.rb
new file mode 100644
index 0000000..992a211
--- /dev/null
+++ b/test/unit/callback/callback_without_arguments_test.rb
@@ -0,0 +1,14 @@
+require_relative '../../test_helper'
+
+class CallbackWithoutArgumentsTest < StateMachinesTest
+ def setup
+ @callback = StateMachines::Callback.new(:before, do: lambda { |object| @arg = object })
+
+ @object = Object.new
+ @callback.call(@object, {}, 1, 2, 3)
+ end
+
+ def test_should_call_method_with_object_as_argument
+ assert_equal @object, @arg
+ end
+end
diff --git a/test/unit/callback/callback_without_terminator_test.rb b/test/unit/callback/callback_without_terminator_test.rb
new file mode 100644
index 0000000..5878700
--- /dev/null
+++ b/test/unit/callback/callback_without_terminator_test.rb
@@ -0,0 +1,12 @@
+require_relative '../../test_helper'
+
+class CallbackWithoutTerminatorTest < StateMachinesTest
+ def setup
+ @object = Object.new
+ end
+
+ def test_should_not_halt_if_result_is_false
+ callback = StateMachines::Callback.new(:before, do: lambda { false }, terminator: nil)
+ callback.call(@object)
+ end
+end
diff --git a/test/unit/error/error_by_default_test.rb b/test/unit/error/error_by_default_test.rb
new file mode 100644
index 0000000..e725bc8
--- /dev/null
+++ b/test/unit/error/error_by_default_test.rb
@@ -0,0 +1,21 @@
+require_relative '../../test_helper'
+
+class ErrorByDefaultTest < StateMachinesTest
+ def setup
+ @machine = StateMachines::Machine.new(Class.new)
+ @collection = StateMachines::NodeCollection.new(@machine)
+ end
+
+ def test_should_not_have_any_nodes
+ assert_equal 0, @collection.length
+ end
+
+ def test_should_have_a_machine
+ assert_equal @machine, @collection.machine
+ end
+
+ def test_should_index_by_name
+ @collection << object = Struct.new(:name).new(:parked)
+ assert_equal object, @collection[:parked]
+ end
+end
diff --git a/test/unit/error/error_with_message_test.rb b/test/unit/error/error_with_message_test.rb
new file mode 100644
index 0000000..a5c4ea0
--- /dev/null
+++ b/test/unit/error/error_with_message_test.rb
@@ -0,0 +1,23 @@
+require_relative '../../test_helper'
+
+class ErrorWithMessageTest < StateMachinesTest
+ def setup
+ @machine = StateMachines::Machine.new(Class.new)
+ @collection = StateMachines::NodeCollection.new(@machine)
+ end
+
+ def test_should_raise_exception_if_invalid_option_specified
+ exception = assert_raises(ArgumentError) { StateMachines::NodeCollection.new(@machine, invalid: true) }
+ assert_equal 'Unknown key: :invalid. Valid keys are: :index', exception.message
+ end
+
+ def test_should_raise_exception_on_lookup_if_invalid_index_specified
+ exception = assert_raises(ArgumentError) { @collection[:something, :invalid] }
+ assert_equal 'Invalid index: :invalid', exception.message
+ end
+
+ def test_should_raise_exception_on_fetch_if_invalid_index_specified
+ exception = assert_raises(ArgumentError) { @collection.fetch(:something, :invalid) }
+ assert_equal 'Invalid index: :invalid', exception.message
+ end
+end
diff --git a/test/unit/eval_helper/eval_helpers_base_test.rb b/test/unit/eval_helper/eval_helpers_base_test.rb
new file mode 100644
index 0000000..d7cbbbf
--- /dev/null
+++ b/test/unit/eval_helper/eval_helpers_base_test.rb
@@ -0,0 +1,8 @@
+require_relative '../../test_helper'
+
+class EvalHelpersBaseTest < StateMachinesTest
+ include StateMachines::EvalHelpers
+
+ def default_test
+ end
+end
diff --git a/test/unit/eval_helper/eval_helpers_proc_block_and_explicit_arguments_test.rb b/test/unit/eval_helper/eval_helpers_proc_block_and_explicit_arguments_test.rb
new file mode 100644
index 0000000..bfd8c1b
--- /dev/null
+++ b/test/unit/eval_helper/eval_helpers_proc_block_and_explicit_arguments_test.rb
@@ -0,0 +1,14 @@
+require_relative '../../test_helper'
+require_relative '../../unit/eval_helper/eval_helpers_base_test'
+
+class EvalHelpersProcBlockAndExplicitArgumentsTest < EvalHelpersBaseTest
+ def setup
+ @object = Object.new
+ @proc = lambda { |object, arg1, arg2, arg3, block| [object, arg1, arg2, arg3, block] }
+ end
+
+ def test_should_call_method_on_object_with_all_arguments_and_block
+ block = lambda { true }
+ assert_equal [@object, 1, 2, 3, block], evaluate_method(@object, @proc, 1, 2, 3, &block)
+ end
+end
diff --git a/test/unit/eval_helper/eval_helpers_proc_block_and_implicit_arguments_test.rb b/test/unit/eval_helper/eval_helpers_proc_block_and_implicit_arguments_test.rb
new file mode 100644
index 0000000..3e520cc
--- /dev/null
+++ b/test/unit/eval_helper/eval_helpers_proc_block_and_implicit_arguments_test.rb
@@ -0,0 +1,14 @@
+require_relative '../../test_helper'
+require_relative '../../unit/eval_helper/eval_helpers_base_test'
+
+class EvalHelpersProcBlockAndImplicitArgumentsTest < EvalHelpersBaseTest
+ def setup
+ @object = Object.new
+ @proc = lambda { |*args| args }
+ end
+
+ def test_should_call_method_on_object_with_all_arguments_and_block
+ block = lambda { true }
+ assert_equal [@object, 1, 2, 3, block], evaluate_method(@object, @proc, 1, 2, 3, &block)
+ end
+end
diff --git a/test/unit/eval_helper/eval_helpers_proc_test.rb b/test/unit/eval_helper/eval_helpers_proc_test.rb
new file mode 100644
index 0000000..f6ded78
--- /dev/null
+++ b/test/unit/eval_helper/eval_helpers_proc_test.rb
@@ -0,0 +1,13 @@
+require_relative '../../test_helper'
+require_relative '../../unit/eval_helper/eval_helpers_base_test'
+
+class EvalHelpersProcTest < EvalHelpersBaseTest
+ def setup
+ @object = Object.new
+ @proc = ->(obj) { obj }
+ end
+
+ def test_should_call_proc_with_object_as_argument
+ assert_equal @object, evaluate_method(@object, @proc, 1, 2, 3)
+ end
+end
diff --git a/test/unit/eval_helper/eval_helpers_proc_with_arguments_test.rb b/test/unit/eval_helper/eval_helpers_proc_with_arguments_test.rb
new file mode 100644
index 0000000..30bdb87
--- /dev/null
+++ b/test/unit/eval_helper/eval_helpers_proc_with_arguments_test.rb
@@ -0,0 +1,13 @@
+require_relative '../../test_helper'
+require_relative '../../unit/eval_helper/eval_helpers_base_test'
+
+class EvalHelpersProcWithArgumentsTest < EvalHelpersBaseTest
+ def setup
+ @object = Object.new
+ @proc = lambda { |*args| args }
+ end
+
+ def test_should_call_method_with_all_arguments
+ assert_equal [@object, 1, 2, 3], evaluate_method(@object, @proc, 1, 2, 3)
+ end
+end
\ No newline at end of file
diff --git a/test/unit/eval_helper/eval_helpers_proc_with_block_test.rb b/test/unit/eval_helper/eval_helpers_proc_with_block_test.rb
new file mode 100644
index 0000000..a05eeb1
--- /dev/null
+++ b/test/unit/eval_helper/eval_helpers_proc_with_block_test.rb
@@ -0,0 +1,13 @@
+require_relative '../../test_helper'
+require_relative '../../unit/eval_helper/eval_helpers_base_test'
+
+class EvalHelpersProcWithBlockTest < EvalHelpersBaseTest
+ def setup
+ @object = Object.new
+ @proc = lambda { |_obj, block| block.call }
+ end
+
+ def test_should_call_method_on_object_with_block
+ assert_equal true, evaluate_method(@object, @proc, 1, 2, 3) { true }
+ end
+end
diff --git a/test/unit/eval_helper/eval_helpers_proc_with_block_without_arguments_test.rb b/test/unit/eval_helper/eval_helpers_proc_with_block_without_arguments_test.rb
new file mode 100644
index 0000000..8184182
--- /dev/null
+++ b/test/unit/eval_helper/eval_helpers_proc_with_block_without_arguments_test.rb
@@ -0,0 +1,18 @@
+require_relative '../../test_helper'
+require_relative '../../unit/eval_helper/eval_helpers_base_test'
+
+class EvalHelpersProcWithoutArgumentsTest < EvalHelpersBaseTest
+ def setup
+ @object = Object.new
+ @proc = lambda { |*args| args }
+ class << @proc
+ def arity
+ 0
+ end
+ end
+ end
+
+ def test_should_call_proc_with_no_arguments
+ assert_equal [], evaluate_method(@object, @proc, 1, 2, 3)
+ end
+end
diff --git a/test/unit/eval_helper/eval_helpers_proc_with_block_without_object_test.rb b/test/unit/eval_helper/eval_helpers_proc_with_block_without_object_test.rb
new file mode 100644
index 0000000..78d633e
--- /dev/null
+++ b/test/unit/eval_helper/eval_helpers_proc_with_block_without_object_test.rb
@@ -0,0 +1,14 @@
+require_relative '../../test_helper'
+require_relative '../../unit/eval_helper/eval_helpers_base_test'
+
+class EvalHelpersProcWithBlockWithoutObjectTest < EvalHelpersBaseTest
+ def setup
+ @object = Object.new
+ @proc = lambda { |block| [block] }
+ end
+
+ def test_should_call_proc_with_block_only
+ block = lambda { true }
+ assert_equal [block], evaluate_method(@object, @proc, 1, 2, 3, &block)
+ end
+end
diff --git a/test/unit/eval_helper/eval_helpers_proc_without_arguments_test.rb b/test/unit/eval_helper/eval_helpers_proc_without_arguments_test.rb
new file mode 100644
index 0000000..0405dc9
--- /dev/null
+++ b/test/unit/eval_helper/eval_helpers_proc_without_arguments_test.rb
@@ -0,0 +1,19 @@
+require_relative '../../test_helper'
+require_relative '../../unit/eval_helper/eval_helpers_base_test'
+
+class EvalHelpersProcWithBlockWithoutArgumentsTest < EvalHelpersBaseTest
+ def setup
+ @object = Object.new
+ @proc = lambda { |*args| args }
+ class << @proc
+ def arity
+ 0
+ end
+ end
+ end
+
+ def test_should_call_proc_without_arguments
+ block = lambda { true }
+ assert_equal [], evaluate_method(@object, @proc, 1, 2, 3, &block)
+ end
+end
diff --git a/test/unit/eval_helper/eval_helpers_string_test.rb b/test/unit/eval_helper/eval_helpers_string_test.rb
new file mode 100644
index 0000000..0977c5f
--- /dev/null
+++ b/test/unit/eval_helper/eval_helpers_string_test.rb
@@ -0,0 +1,25 @@
+require_relative '../../test_helper'
+require_relative '../../unit/eval_helper/eval_helpers_base_test.rb'
+
+class EvalHelpersStringTest < EvalHelpersBaseTest
+ def setup
+ @object = Object.new
+ end
+
+ def test_should_evaluate_string
+ assert_equal 1, evaluate_method(@object, '1')
+ end
+
+ def test_should_evaluate_string_within_object_context
+ @object.instance_variable_set('@value', 1)
+ assert_equal 1, evaluate_method(@object, '@value')
+ end
+
+ def test_should_ignore_additional_arguments
+ assert_equal 1, evaluate_method(@object, '1', 2, 3, 4)
+ end
+end
+
+
+
+
diff --git a/test/unit/eval_helper/eval_helpers_string_with_block_test.rb b/test/unit/eval_helper/eval_helpers_string_with_block_test.rb
new file mode 100644
index 0000000..1094bfd
--- /dev/null
+++ b/test/unit/eval_helper/eval_helpers_string_with_block_test.rb
@@ -0,0 +1,12 @@
+require_relative '../../test_helper'
+require_relative '../../unit/eval_helper/eval_helpers_base_test'
+
+class EvalHelpersStringWithBlockTest < EvalHelpersBaseTest
+ def setup
+ @object = Object.new
+ end
+
+ def test_should_call_method_on_object_with_block
+ assert_equal 1, evaluate_method(@object, 'yield') { 1 }
+ end
+end
diff --git a/test/unit/eval_helper/eval_helpers_symbol_method_missing_test.rb b/test/unit/eval_helper/eval_helpers_symbol_method_missing_test.rb
new file mode 100644
index 0000000..698010c
--- /dev/null
+++ b/test/unit/eval_helper/eval_helpers_symbol_method_missing_test.rb
@@ -0,0 +1,20 @@
+require_relative '../../test_helper'
+require_relative '../../unit/eval_helper/eval_helpers_base_test'
+
+class EvalHelpersSymbolMethodMissingTest < EvalHelpersBaseTest
+ def setup
+ class << (@object = Object.new)
+ def method_missing(symbol, *args)
+ send("method_missing_#{symbol}", *args)
+ end
+
+ def method_missing_callback(*args)
+ args
+ end
+ end
+ end
+
+ def test_should_call_dynamic_method_with_all_arguments
+ assert_equal [1, 2, 3], evaluate_method(@object, :callback, 1, 2, 3)
+ end
+end
\ No newline at end of file
diff --git a/test/unit/eval_helper/eval_helpers_symbol_private_test.rb b/test/unit/eval_helper/eval_helpers_symbol_private_test.rb
new file mode 100644
index 0000000..d24d5a2
--- /dev/null
+++ b/test/unit/eval_helper/eval_helpers_symbol_private_test.rb
@@ -0,0 +1,17 @@
+require_relative '../../test_helper'
+require_relative '../../unit/eval_helper/eval_helpers_base_test'
+
+class EvalHelpersSymbolPrivateTest < EvalHelpersBaseTest
+ def setup
+ class << (@object = Object.new)
+ private
+ def callback
+ true
+ end
+ end
+ end
+
+ def test_should_call_method_on_object_with_no_arguments
+ assert_equal true, evaluate_method(@object, :callback, 1, 2, 3)
+ end
+end
diff --git a/test/unit/eval_helper/eval_helpers_symbol_protected_test.rb b/test/unit/eval_helper/eval_helpers_symbol_protected_test.rb
new file mode 100644
index 0000000..de2e96d
--- /dev/null
+++ b/test/unit/eval_helper/eval_helpers_symbol_protected_test.rb
@@ -0,0 +1,17 @@
+require_relative '../../test_helper'
+require_relative '../../unit/eval_helper/eval_helpers_base_test'
+
+class EvalHelpersSymbolProtectedTest < EvalHelpersBaseTest
+ def setup
+ class << (@object = Object.new)
+ protected
+ def callback
+ true
+ end
+ end
+ end
+
+ def test_should_call_method_on_object_with_no_arguments
+ assert_equal true, evaluate_method(@object, :callback, 1, 2, 3)
+ end
+end
diff --git a/test/unit/eval_helper/eval_helpers_symbol_tainted_method_test.rb b/test/unit/eval_helper/eval_helpers_symbol_tainted_method_test.rb
new file mode 100644
index 0000000..122e2e7
--- /dev/null
+++ b/test/unit/eval_helper/eval_helpers_symbol_tainted_method_test.rb
@@ -0,0 +1,18 @@
+require_relative '../../test_helper'
+require_relative '../../unit/eval_helper/eval_helpers_base_test'
+
+class EvalHelpersSymbolTaintedMethodTest < EvalHelpersBaseTest
+ def setup
+ class << (@object = Object.new)
+ def callback
+ true
+ end
+
+ taint
+ end
+ end
+
+ def test_should_not_raise_security_error
+ evaluate_method(@object, :callback, 1, 2, 3)
+ end
+end
diff --git a/test/unit/eval_helper/eval_helpers_symbol_test.rb b/test/unit/eval_helper/eval_helpers_symbol_test.rb
new file mode 100644
index 0000000..426952a
--- /dev/null
+++ b/test/unit/eval_helper/eval_helpers_symbol_test.rb
@@ -0,0 +1,16 @@
+require_relative '../../test_helper'
+require_relative '../../unit/eval_helper/eval_helpers_base_test'
+
+class EvalHelpersSymbolTest < EvalHelpersBaseTest
+ def setup
+ class << (@object = Object.new)
+ def callback
+ true
+ end
+ end
+ end
+
+ def test_should_call_method_on_object_with_no_arguments
+ assert_equal true, evaluate_method(@object, :callback, 1, 2, 3)
+ end
+end
diff --git a/test/unit/eval_helper/eval_helpers_symbol_with_arguments_and_block_test.rb b/test/unit/eval_helper/eval_helpers_symbol_with_arguments_and_block_test.rb
new file mode 100644
index 0000000..26f199c
--- /dev/null
+++ b/test/unit/eval_helper/eval_helpers_symbol_with_arguments_and_block_test.rb
@@ -0,0 +1,16 @@
+require_relative '../../test_helper'
+require_relative '../../unit/eval_helper/eval_helpers_base_test'
+
+class EvalHelpersSymbolWithArgumentsAndBlockTest < EvalHelpersBaseTest
+ def setup
+ class << (@object = Object.new)
+ def callback(*args)
+ args << yield
+ end
+ end
+ end
+
+ def test_should_call_method_on_object_with_all_arguments_and_block
+ assert_equal [1, 2, 3, true], evaluate_method(@object, :callback, 1, 2, 3) { true }
+ end
+end
diff --git a/test/unit/eval_helper/eval_helpers_symbol_with_arguments_test.rb b/test/unit/eval_helper/eval_helpers_symbol_with_arguments_test.rb
new file mode 100644
index 0000000..bccdb8f
--- /dev/null
+++ b/test/unit/eval_helper/eval_helpers_symbol_with_arguments_test.rb
@@ -0,0 +1,16 @@
+require_relative '../../test_helper'
+require_relative '../../unit/eval_helper/eval_helpers_base_test'
+
+class EvalHelpersSymbolWithArgumentsTest < EvalHelpersBaseTest
+ def setup
+ class << (@object = Object.new)
+ def callback(*args)
+ args
+ end
+ end
+ end
+
+ def test_should_call_method_with_all_arguments
+ assert_equal [1, 2, 3], evaluate_method(@object, :callback, 1, 2, 3)
+ end
+end
diff --git a/test/unit/eval_helper/eval_helpers_symbol_with_block_test.rb b/test/unit/eval_helper/eval_helpers_symbol_with_block_test.rb
new file mode 100644
index 0000000..ffd2f36
--- /dev/null
+++ b/test/unit/eval_helper/eval_helpers_symbol_with_block_test.rb
@@ -0,0 +1,16 @@
+require_relative '../../test_helper'
+require_relative '../../unit/eval_helper/eval_helpers_base_test'
+
+class EvalHelpersSymbolWithBlockTest < EvalHelpersBaseTest
+ def setup
+ class << (@object = Object.new)
+ def callback
+ yield
+ end
+ end
+ end
+
+ def test_should_call_method_on_object_with_block
+ assert_equal true, evaluate_method(@object, :callback) { true }
+ end
+end
diff --git a/test/unit/eval_helper/eval_helpers_test.rb b/test/unit/eval_helper/eval_helpers_test.rb
new file mode 100644
index 0000000..1b395b5
--- /dev/null
+++ b/test/unit/eval_helper/eval_helpers_test.rb
@@ -0,0 +1,13 @@
+require_relative '../../test_helper'
+require_relative '../../unit/eval_helper/eval_helpers_base_test'
+
+class EvalHelpersTest < EvalHelpersBaseTest
+ def setup
+ @object = Object.new
+ end
+
+ def test_should_raise_exception_if_method_is_not_symbol_string_or_proc
+ exception = assert_raises(ArgumentError) { evaluate_method(@object, 1) }
+ assert_match(/Methods must/, exception.message)
+ end
+end
diff --git a/test/unit/event/event_after_being_copied_test.rb b/test/unit/event/event_after_being_copied_test.rb
new file mode 100644
index 0000000..8ff8d90
--- /dev/null
+++ b/test/unit/event/event_after_being_copied_test.rb
@@ -0,0 +1,17 @@
+require_relative '../../test_helper'
+
+class EventAfterBeingCopiedTest < StateMachinesTest
+ def setup
+ @machine = StateMachines::Machine.new(Class.new)
+ @machine.events << @event = StateMachines::Event.new(@machine, :ignite)
+ @copied_event = @event.dup
+ end
+
+ def test_should_not_have_the_same_collection_of_branches
+ refute_same @event.branches, @copied_event.branches
+ end
+
+ def test_should_not_have_the_same_collection_of_known_states
+ refute_same @event.known_states, @copied_event.known_states
+ end
+end
diff --git a/test/unit/event/event_by_default_test.rb b/test/unit/event/event_by_default_test.rb
new file mode 100644
index 0000000..e35c072
--- /dev/null
+++ b/test/unit/event/event_by_default_test.rb
@@ -0,0 +1,60 @@
+require_relative '../../test_helper'
+
+
+class EventByDefaultTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.events << @event = StateMachines::Event.new(@machine, :ignite)
+
+ @object = @klass.new
+ end
+
+ def test_should_have_a_machine
+ assert_equal @machine, @event.machine
+ end
+
+ def test_should_have_a_name
+ assert_equal :ignite, @event.name
+ end
+
+ def test_should_have_a_qualified_name
+ assert_equal :ignite, @event.qualified_name
+ end
+
+ def test_should_have_a_human_name
+ assert_equal 'ignite', @event.human_name
+ end
+
+ def test_should_not_have_any_branches
+ assert @event.branches.empty?
+ end
+
+ def test_should_have_no_known_states
+ assert @event.known_states.empty?
+ end
+
+ def test_should_not_be_able_to_fire
+ refute @event.can_fire?(@object)
+ end
+
+ def test_should_not_have_a_transition
+ assert_nil @event.transition_for(@object)
+ end
+
+ def test_should_define_a_predicate
+ assert @object.respond_to?(:can_ignite?)
+ end
+
+ def test_should_define_a_transition_accessor
+ assert @object.respond_to?(:ignite_transition)
+ end
+
+ def test_should_define_an_action
+ assert @object.respond_to?(:ignite)
+ end
+
+ def test_should_define_a_bang_action
+ assert @object.respond_to?(:ignite!)
+ end
+end
diff --git a/test/unit/event/event_context_test.rb b/test/unit/event/event_context_test.rb
new file mode 100644
index 0000000..a57182c
--- /dev/null
+++ b/test/unit/event/event_context_test.rb
@@ -0,0 +1,16 @@
+require_relative '../../test_helper'
+
+class EventContextTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.events << @event = StateMachines::Event.new(@machine, :ignite, human_name: 'start')
+ end
+
+ def test_should_evaluate_within_the_event
+ scope = nil
+ @event.context { scope = self }
+ assert_equal @event, scope
+ end
+end
+
diff --git a/test/unit/event/event_on_failure_test.rb b/test/unit/event/event_on_failure_test.rb
new file mode 100644
index 0000000..5199419
--- /dev/null
+++ b/test/unit/event/event_on_failure_test.rb
@@ -0,0 +1,44 @@
+require_relative '../../test_helper'
+require_relative '../../files/integrations/event_on_failure_integration'
+
+class EventOnFailureTest < StateMachinesTest
+ def setup
+ StateMachines::Integrations.reset
+ StateMachines::Integrations.register(EventOnFailureIntegration)
+ @klass = Class.new do
+ attr_accessor :errors
+ end
+
+ @machine = StateMachines::Machine.new(@klass, integration: :event_on_failure_integration)
+ @machine.state :parked
+ @machine.events << @event = StateMachines::Event.new(@machine, :ignite)
+
+ @object = @klass.new
+ @object.state = 'parked'
+ end
+
+ def test_should_invalidate_the_state
+ @event.fire(@object)
+ assert_equal ['cannot transition via "ignite"'], @object.errors
+ end
+
+ def test_should_run_failure_callbacks
+ callback_args = nil
+ @machine.after_failure { |*args| callback_args = args }
+
+ @event.fire(@object)
+
+ object, transition = callback_args
+ assert_equal @object, object
+ refute_nil transition
+ assert_equal @object, transition.object
+ assert_equal @machine, transition.machine
+ assert_equal :ignite, transition.event
+ assert_equal :parked, transition.from_name
+ assert_equal :parked, transition.to_name
+ end
+
+ def teardown
+ StateMachines::Integrations.reset
+ end
+end
diff --git a/test/unit/event/event_test.rb b/test/unit/event/event_test.rb
new file mode 100644
index 0000000..1d42418
--- /dev/null
+++ b/test/unit/event/event_test.rb
@@ -0,0 +1,34 @@
+require_relative '../../test_helper'
+
+class EventTest < StateMachinesTest
+ def setup
+ @machine = StateMachines::Machine.new(Class.new)
+ @machine.events << @event = StateMachines::Event.new(@machine, :ignite)
+ @event.transition parked: :idling
+ end
+
+ def test_should_allow_changing_machine
+ new_machine = StateMachines::Machine.new(Class.new)
+ @event.machine = new_machine
+ assert_equal new_machine, @event.machine
+ end
+
+ def test_should_allow_changing_human_name
+ @event.human_name = 'Stop'
+ assert_equal 'Stop', @event.human_name
+ end
+
+ def test_should_provide_matcher_helpers_during_initialization
+ matchers = []
+
+ @event.instance_eval do
+ matchers = [all, any, same]
+ end
+
+ assert_equal [StateMachines::AllMatcher.instance, StateMachines::AllMatcher.instance, StateMachines::LoopbackMatcher.instance], matchers
+ end
+
+ def test_should_use_pretty_inspect
+ assert_match '#<StateMachines::Event name=:ignite transitions=[:parked => :idling]>', @event.inspect
+ end
+end
diff --git a/test/unit/event/event_transitions_test.rb b/test/unit/event/event_transitions_test.rb
new file mode 100644
index 0000000..e4d9aa3
--- /dev/null
+++ b/test/unit/event/event_transitions_test.rb
@@ -0,0 +1,62 @@
+require_relative '../../test_helper'
+
+class EventTransitionsTest < StateMachinesTest
+ def setup
+ @machine = StateMachines::Machine.new(Class.new)
+ @machine.events << @event = StateMachines::Event.new(@machine, :ignite)
+ end
+
+ def test_should_not_raise_exception_if_implicit_option_specified
+ @event.transition(invalid: :valid)
+ end
+
+ def test_should_not_allow_on_option
+ exception = assert_raises(ArgumentError) { @event.transition(on: :ignite) }
+ assert_equal 'Unknown key: :on. Valid keys are: :from, :to, :except_from, :except_to, :if, :unless', exception.message
+ end
+
+ def test_should_automatically_set_on_option
+ branch = @event.transition(to: :idling)
+ assert_instance_of StateMachines::WhitelistMatcher, branch.event_requirement
+ assert_equal [:ignite], branch.event_requirement.values
+ end
+
+ def test_should_not_allow_except_on_option
+ exception = assert_raises(ArgumentError) { @event.transition(except_on: :ignite) }
+ assert_equal 'Unknown key: :except_on. Valid keys are: :from, :to, :except_from, :except_to, :if, :unless', exception.message
+ end
+
+ def test_should_allow_transitioning_without_a_to_state
+ @event.transition(from: :parked)
+ end
+
+ def test_should_allow_transitioning_without_a_from_state
+ @event.transition(to: :idling)
+ end
+
+ def test_should_allow_except_from_option
+ @event.transition(except_from: :idling)
+ end
+
+ def test_should_allow_except_to_option
+ @event.transition(except_to: :idling)
+ end
+
+ def test_should_allow_transitioning_from_a_single_state
+ assert @event.transition(parked: :idling)
+ end
+
+ def test_should_allow_transitioning_from_multiple_states
+ assert @event.transition([:parked, :idling] => :idling)
+ end
+
+ def test_should_allow_transitions_to_multiple_states
+ assert @event.transition(parked: [:parked, :idling])
+ end
+
+ def test_should_have_transitions
+ branch = @event.transition(to: :idling)
+ assert_equal [branch], @event.branches
+ end
+end
+
diff --git a/test/unit/event/event_with_conflicting_helpers_after_definition_test.rb b/test/unit/event/event_with_conflicting_helpers_after_definition_test.rb
new file mode 100644
index 0000000..6e8c134
--- /dev/null
+++ b/test/unit/event/event_with_conflicting_helpers_after_definition_test.rb
@@ -0,0 +1,79 @@
+require_relative '../../test_helper'
+require 'stringio'
+
+class EventWithConflictingHelpersAfterDefinitionTest < StateMachinesTest
+ def setup
+ @original_stderr, $stderr = $stderr, StringIO.new
+
+ @klass = Class.new do
+ def can_ignite?
+ 0
+ end
+
+ def ignite_transition
+ 0
+ end
+
+ def ignite
+ 0
+ end
+
+ def ignite!
+ 0
+ end
+ end
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.events << @event = StateMachines::Event.new(@machine, :ignite)
+ @object = @klass.new
+ end
+
+ def test_should_not_redefine_predicate
+ assert_equal 0, @object.can_ignite?
+ end
+
+ def test_should_not_redefine_transition_accessor
+ assert_equal 0, @object.ignite_transition
+ end
+
+ def test_should_not_redefine_action
+ assert_equal 0, @object.ignite
+ end
+
+ def test_should_not_redefine_bang_action
+ assert_equal 0, @object.ignite!
+ end
+
+ def test_should_allow_super_chaining
+ @klass.class_eval do
+ def can_ignite?
+ super
+ end
+
+ def ignite_transition
+ super
+ end
+
+ def ignite
+ super
+ end
+
+ def ignite!
+ super
+ end
+ end
+
+ assert_equal false, @object.can_ignite?
+ assert_equal nil, @object.ignite_transition
+ assert_equal false, @object.ignite
+ assert_raises(StateMachines::InvalidTransition) { @object.ignite! }
+ end
+
+ def test_should_not_output_warning
+ assert_equal '', $stderr.string
+ end
+
+ def teardown
+ $stderr = @original_stderr
+ end
+end
+
diff --git a/test/unit/event/event_with_conflicting_helpers_before_definition_test.rb b/test/unit/event/event_with_conflicting_helpers_before_definition_test.rb
new file mode 100644
index 0000000..f4a539a
--- /dev/null
+++ b/test/unit/event/event_with_conflicting_helpers_before_definition_test.rb
@@ -0,0 +1,58 @@
+require_relative '../../test_helper'
+require 'stringio'
+
+class EventWithConflictingHelpersBeforeDefinitionTest < StateMachinesTest
+ def setup
+ @original_stderr, $stderr = $stderr, StringIO.new
+
+ @superclass = Class.new do
+ def can_ignite?
+ 0
+ end
+
+ def ignite_transition
+ 0
+ end
+
+ def ignite
+ 0
+ end
+
+ def ignite!
+ 0
+ end
+ end
+ @klass = Class.new(@superclass)
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.events << @event = StateMachines::Event.new(@machine, :ignite)
+ @object = @klass.new
+ end
+
+ def test_should_not_redefine_predicate
+ assert_equal 0, @object.can_ignite?
+ end
+
+ def test_should_not_redefine_transition_accessor
+ assert_equal 0, @object.ignite_transition
+ end
+
+ def test_should_not_redefine_action
+ assert_equal 0, @object.ignite
+ end
+
+ def test_should_not_redefine_bang_action
+ assert_equal 0, @object.ignite!
+ end
+
+ def test_should_output_warning
+ expected = %w(can_ignite? ignite_transition ignite ignite!).map do |method|
+ "Instance method \"#{method}\" is already defined in #{@superclass}, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true.\n"
+ end.join
+
+ assert_equal expected, $stderr.string
+ end
+
+ def teardown
+ $stderr = @original_stderr
+ end
+end
diff --git a/test/unit/event/event_with_conflicting_machine_test.rb b/test/unit/event/event_with_conflicting_machine_test.rb
new file mode 100644
index 0000000..2b85af8
--- /dev/null
+++ b/test/unit/event/event_with_conflicting_machine_test.rb
@@ -0,0 +1,48 @@
+require_relative '../../test_helper'
+require 'stringio'
+
+class EventWithConflictingMachineTest < StateMachinesTest
+ def setup
+ @original_stderr, $stderr = $stderr, StringIO.new
+
+ @klass = Class.new
+ @state_machine = StateMachines::Machine.new(@klass, :state)
+ @state_machine.state :parked, :idling
+ @state_machine.events << @state_event = StateMachines::Event.new(@state_machine, :ignite)
+ end
+
+ def test_should_not_overwrite_first_event
+ @status_machine = StateMachines::Machine.new(@klass, :status)
+ @status_machine.state :first_gear, :second_gear
+ @status_machine.events << @status_event = StateMachines::Event.new(@status_machine, :ignite)
+
+ @object = @klass.new
+ @object.state = 'parked'
+ @object.status = 'first_gear'
+
+ @state_event.transition(parked: :idling)
+ @status_event.transition(parked: :first_gear)
+
+ @object.ignite
+ assert_equal 'idling', @object.state
+ assert_equal 'first_gear', @object.status
+ end
+
+ def test_should_output_warning
+ @status_machine = StateMachines::Machine.new(@klass, :status)
+ @status_machine.events << @status_event = StateMachines::Event.new(@status_machine, :ignite)
+
+ assert_equal "Event :ignite for :status is already defined in :state\n", $stderr.string
+ end
+
+ def test_should_not_output_warning_if_using_different_namespace
+ @status_machine = StateMachines::Machine.new(@klass, :status, namespace: 'alarm')
+ @status_machine.events << @status_event = StateMachines::Event.new(@status_machine, :ignite)
+
+ assert_equal '', $stderr.string
+ end
+
+ def teardown
+ $stderr = @original_stderr
+ end
+end
diff --git a/test/unit/event/event_with_dynamic_human_name_test.rb b/test/unit/event/event_with_dynamic_human_name_test.rb
new file mode 100644
index 0000000..11f05ea
--- /dev/null
+++ b/test/unit/event/event_with_dynamic_human_name_test.rb
@@ -0,0 +1,26 @@
+require_relative '../../test_helper'
+
+class EventWithDynamicHumanNameTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.events << @event = StateMachines::Event.new(@machine, :ignite, human_name: lambda { |_event, object| ['start', object] })
+ end
+
+ def test_should_use_custom_human_name
+ human_name, klass = @event.human_name
+ assert_equal 'start', human_name
+ assert_equal @klass, klass
+ end
+
+ def test_should_allow_custom_class_to_be_passed_through
+ human_name, klass = @event.human_name(1)
+ assert_equal 'start', human_name
+ assert_equal 1, klass
+ end
+
+ def test_should_not_cache_value
+ refute_same @event.human_name, @event.human_name
+ end
+end
+
diff --git a/test/unit/event/event_with_human_name_test.rb b/test/unit/event/event_with_human_name_test.rb
new file mode 100644
index 0000000..9254ac7
--- /dev/null
+++ b/test/unit/event/event_with_human_name_test.rb
@@ -0,0 +1,13 @@
+require_relative '../../test_helper'
+
+class EventWithHumanNameTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.events << @event = StateMachines::Event.new(@machine, :ignite, human_name: 'start')
+ end
+
+ def test_should_use_custom_human_name
+ assert_equal 'start', @event.human_name
+ end
+end
diff --git a/test/unit/event/event_with_invalid_current_state_test.rb b/test/unit/event/event_with_invalid_current_state_test.rb
new file mode 100644
index 0000000..c754595
--- /dev/null
+++ b/test/unit/event/event_with_invalid_current_state_test.rb
@@ -0,0 +1,30 @@
+require_relative '../../test_helper'
+
+class EventWithInvalidCurrentStateTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked, :idling
+
+ @machine.events << @event = StateMachines::Event.new(@machine, :ignite)
+ @event.transition(parked: :idling)
+
+ @object = @klass.new
+ @object.state = 'invalid'
+ end
+
+ def test_should_raise_exception_when_checking_availability
+ exception = assert_raises(ArgumentError) { @event.can_fire?(@object) }
+ assert_equal '"invalid" is not a known state value', exception.message
+ end
+
+ def test_should_raise_exception_when_finding_transition
+ exception = assert_raises(ArgumentError) { @event.transition_for(@object) }
+ assert_equal '"invalid" is not a known state value', exception.message
+ end
+
+ def test_should_raise_exception_when_firing
+ exception = assert_raises(ArgumentError) { @event.fire(@object) }
+ assert_equal '"invalid" is not a known state value', exception.message
+ end
+end
diff --git a/test/unit/event/event_with_machine_action_test.rb b/test/unit/event/event_with_machine_action_test.rb
new file mode 100644
index 0000000..476917c
--- /dev/null
+++ b/test/unit/event/event_with_machine_action_test.rb
@@ -0,0 +1,33 @@
+require_relative '../../test_helper'
+
+class EventWithMachineActionTest < StateMachinesTest
+ def setup
+ @klass = Class.new do
+ attr_reader :saved
+
+ def save
+ @saved = true
+ end
+ end
+
+ @machine = StateMachines::Machine.new(@klass, action: :save)
+ @machine.state :parked, :idling
+
+ @machine.events << @event = StateMachines::Event.new(@machine, :ignite)
+ @event.transition(parked: :idling)
+
+ @object = @klass.new
+ @object.state = 'parked'
+ end
+
+ def test_should_run_action_on_fire
+ @event.fire(@object)
+ assert @object.saved
+ end
+
+ def test_should_not_run_action_if_configured_to_skip
+ @event.fire(@object, false)
+ refute @object.saved
+ end
+end
+
diff --git a/test/unit/event/event_with_marshalling_test.rb b/test/unit/event/event_with_marshalling_test.rb
new file mode 100644
index 0000000..63d0c56
--- /dev/null
+++ b/test/unit/event/event_with_marshalling_test.rb
@@ -0,0 +1,47 @@
+require_relative '../../test_helper'
+
+class EventWithMarshallingTest < StateMachinesTest
+ def setup
+ @klass = Class.new do
+ def save
+ true
+ end
+ end
+ self.class.const_set('Example', @klass)
+
+ @machine = StateMachines::Machine.new(@klass, action: :save)
+ @machine.state :parked, :idling
+
+ @machine.events << @event = StateMachines::Event.new(@machine, :ignite)
+ @event.transition(parked: :idling)
+
+ @object = @klass.new
+ @object.state = 'parked'
+ end
+
+ def test_should_marshal_during_before_callbacks
+ @machine.before_transition { |object, _transition| Marshal.dump(object) }
+ @event.fire(@object)
+ end
+
+ def test_should_marshal_during_action
+ @klass.class_eval do
+ remove_method :save
+
+ def save
+ Marshal.dump(self)
+ end
+ end
+
+ @event.fire(@object)
+ end
+
+ def test_should_marshal_during_after_callbacks
+ @machine.after_transition { |object, _transition| Marshal.dump(object) }
+ @event.fire(@object)
+ end
+
+ def teardown
+ self.class.send(:remove_const, 'Example')
+ end
+end
diff --git a/test/unit/event/event_with_matching_disabled_transitions_test.rb b/test/unit/event/event_with_matching_disabled_transitions_test.rb
new file mode 100644
index 0000000..a1e486d
--- /dev/null
+++ b/test/unit/event/event_with_matching_disabled_transitions_test.rb
@@ -0,0 +1,115 @@
+require_relative '../../test_helper'
+
+class EventWithMatchingDisabledTransitionsTest < StateMachinesTest
+ module Custom
+ include StateMachines::Integrations::Base
+
+ def invalidate(object, _attribute, message, values = [])
+ (object.errors ||= []) << generate_message(message, values)
+ end
+
+ def reset(object)
+ object.errors = []
+ end
+ end
+
+ def setup
+ StateMachines::Integrations.register(EventWithMatchingDisabledTransitionsTest::Custom)
+
+ @klass = Class.new do
+ attr_accessor :errors
+ end
+
+ @machine = StateMachines::Machine.new(@klass, integration: :custom)
+ @machine.state :parked, :idling
+
+ @machine.events << @event = StateMachines::Event.new(@machine, :ignite)
+ @event.transition(parked: :idling, if: lambda { false })
+
+ @object = @klass.new
+ @object.state = 'parked'
+ end
+
+ def test_should_not_be_able_to_fire
+ refute @event.can_fire?(@object)
+ end
+
+ def test_should_be_able_to_fire_with_disabled_guards
+ assert @event.can_fire?(@object, guard: false)
+ end
+
+ def test_should_not_have_a_transition
+ assert_nil @event.transition_for(@object)
+ end
+
+ def test_should_have_a_transition_with_disabled_guards
+ refute_nil @event.transition_for(@object, guard: false)
+ end
+
+ def test_should_not_fire
+ refute @event.fire(@object)
+ end
+
+ def test_should_not_change_the_current_state
+ @event.fire(@object)
+ assert_equal 'parked', @object.state
+ end
+
+ def test_should_invalidate_the_state
+ @event.fire(@object)
+ assert_equal ['cannot transition via "ignite"'], @object.errors
+ end
+
+ def test_should_invalidate_with_human_event_name
+ @event.human_name = 'start'
+ @event.fire(@object)
+ assert_equal ['cannot transition via "start"'], @object.errors
+ end
+
+ def test_should_invalid_with_human_state_name_if_specified
+ klass = Class.new do
+ attr_accessor :errors
+ end
+
+ machine = StateMachines::Machine.new(klass, integration: :custom, messages: { invalid_transition: 'cannot transition via "%s" from "%s"' })
+ parked, idling = machine.state :parked, :idling
+ parked.human_name = 'stopped'
+
+ machine.events << event = StateMachines::Event.new(machine, :ignite)
+ event.transition(parked: :idling, if: lambda { false })
+
+ object = @klass.new
+ object.state = 'parked'
+
+ event.fire(object)
+ assert_equal ['cannot transition via "ignite" from "stopped"'], object.errors
+ end
+
+ def test_should_reset_existing_error
+ @object.errors = ['invalid']
+
+ @event.fire(@object)
+ assert_equal ['cannot transition via "ignite"'], @object.errors
+ end
+
+ def test_should_run_failure_callbacks
+ callback_args = nil
+ @machine.after_failure { |*args| callback_args = args }
+
+ @event.fire(@object)
+
+ object, transition = callback_args
+ assert_equal @object, object
+ refute_nil transition
+ assert_equal @object, transition.object
+ assert_equal @machine, transition.machine
+ assert_equal :ignite, transition.event
+ assert_equal :parked, transition.from_name
+ assert_equal :parked, transition.to_name
+ end
+
+ def teardown
+ StateMachines::Integrations.reset
+ end
+end
+
diff --git a/test/unit/event/event_with_matching_enabled_transitions_test.rb b/test/unit/event/event_with_matching_enabled_transitions_test.rb
new file mode 100644
index 0000000..f8d9e55
--- /dev/null
+++ b/test/unit/event/event_with_matching_enabled_transitions_test.rb
@@ -0,0 +1,75 @@
+require_relative '../../test_helper'
+
+class EventWithMatchingEnabledTransitionsTest < StateMachinesTest
+ module Custom
+ include StateMachines::Integrations::Base
+
+ def invalidate(object, _attribute, message, values = [])
+ (object.errors ||= []) << generate_message(message, values)
+ end
+
+ def reset(object)
+ object.errors = []
+ end
+ end
+
+ def setup
+ StateMachines::Integrations.register(EventWithMatchingEnabledTransitionsTest::Custom)
+
+ @klass = Class.new do
+ attr_accessor :errors
+ end
+
+ @machine = StateMachines::Machine.new(@klass, integration: :custom)
+ @machine.state :parked, :idling
+
+ @machine.events << @event = StateMachines::Event.new(@machine, :ignite)
+ @event.transition(parked: :idling)
+
+ @object = @klass.new
+ @object.state = 'parked'
+ end
+
+ def test_should_be_able_to_fire
+ assert @event.can_fire?(@object)
+ end
+
+ def test_should_have_a_transition
+ transition = @event.transition_for(@object)
+ refute_nil transition
+ assert_equal 'parked', transition.from
+ assert_equal 'idling', transition.to
+ assert_equal :ignite, transition.event
+ end
+
+ def test_should_fire
+ assert @event.fire(@object)
+ end
+
+ def test_should_change_the_current_state
+ @event.fire(@object)
+ assert_equal 'idling', @object.state
+ end
+
+ def test_should_reset_existing_error
+ @object.errors = ['invalid']
+
+ @event.fire(@object)
+ assert_equal [], @object.errors
+ end
+
+ def test_should_not_invalidate_the_state
+ @event.fire(@object)
+ assert_equal [], @object.errors
+ end
+
+ def test_should_not_be_able_to_fire_on_reset
+ @event.reset
+ refute @event.can_fire?(@object)
+ end
+
+ def teardown
+ StateMachines::Integrations.reset
+ end
+end
+
diff --git a/test/unit/event/event_with_multiple_transitions_test.rb b/test/unit/event/event_with_multiple_transitions_test.rb
new file mode 100644
index 0000000..49198ee
--- /dev/null
+++ b/test/unit/event/event_with_multiple_transitions_test.rb
@@ -0,0 +1,61 @@
+require_relative '../../test_helper'
+
+class EventWithMultipleTransitionsTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked, :idling
+
+ @machine.events << @event = StateMachines::Event.new(@machine, :ignite)
+ @event.transition(idling: :idling)
+ @event.transition(parked: :idling)
+ @event.transition(parked: :parked)
+
+ @object = @klass.new
+ @object.state = 'parked'
+ end
+
+ def test_should_be_able_to_fire
+ assert @event.can_fire?(@object)
+ end
+
+ def test_should_have_a_transition
+ transition = @event.transition_for(@object)
+ refute_nil transition
+ assert_equal 'parked', transition.from
+ assert_equal 'idling', transition.to
+ assert_equal :ignite, transition.event
+ end
+
+ def test_should_allow_specific_transition_selection_using_from
+ transition = @event.transition_for(@object, from: :idling)
+
+ refute_nil transition
+ assert_equal 'idling', transition.from
+ assert_equal 'idling', transition.to
+ assert_equal :ignite, transition.event
+ end
+
+ def test_should_allow_specific_transition_selection_using_to
+ transition = @event.transition_for(@object, from: :parked, to: :parked)
+
+ refute_nil transition
+ assert_equal 'parked', transition.from
+ assert_equal 'parked', transition.to
+ assert_equal :ignite, transition.event
+ end
+
+ def test_should_not_allow_specific_transition_selection_using_on
+ exception = assert_raises(ArgumentError) { @event.transition_for(@object, on: :park) }
+ assert_equal 'Unknown key: :on. Valid keys are: :from, :to, :guard', exception.message
+ end
+
+ def test_should_fire
+ assert @event.fire(@object)
+ end
+
+ def test_should_change_the_current_state
+ @event.fire(@object)
+ assert_equal 'idling', @object.state
+ end
+end
diff --git a/test/unit/event/event_with_namespace_test.rb b/test/unit/event/event_with_namespace_test.rb
new file mode 100644
index 0000000..39ea26f
--- /dev/null
+++ b/test/unit/event/event_with_namespace_test.rb
@@ -0,0 +1,34 @@
+require_relative '../../test_helper'
+
+class EventWithNamespaceTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass, namespace: 'alarm')
+ @machine.events << @event = StateMachines::Event.new(@machine, :enable)
+ @object = @klass.new
+ end
+
+ def test_should_have_a_name
+ assert_equal :enable, @event.name
+ end
+
+ def test_should_have_a_qualified_name
+ assert_equal :enable_alarm, @event.qualified_name
+ end
+
+ def test_should_namespace_predicate
+ assert @object.respond_to?(:can_enable_alarm?)
+ end
+
+ def test_should_namespace_transition_accessor
+ assert @object.respond_to?(:enable_alarm_transition)
+ end
+
+ def test_should_namespace_action
+ assert @object.respond_to?(:enable_alarm)
+ end
+
+ def test_should_namespace_bang_action
+ assert @object.respond_to?(:enable_alarm!)
+ end
+end
diff --git a/test/unit/event/event_with_transition_with_blacklisted_to_state_test.rb b/test/unit/event/event_with_transition_with_blacklisted_to_state_test.rb
new file mode 100644
index 0000000..eae5de6
--- /dev/null
+++ b/test/unit/event/event_with_transition_with_blacklisted_to_state_test.rb
@@ -0,0 +1,60 @@
+require_relative '../../test_helper'
+class EventWithTransitionWithBlacklistedToStateTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass, initial: :parked)
+ @machine.state :parked, :idling, :first_gear, :second_gear
+
+ @machine.events << @event = StateMachines::Event.new(@machine, :ignite)
+ @event.transition(from: :parked, to: StateMachines::BlacklistMatcher.new([:parked, :idling]))
+
+ @object = @klass.new
+ @object.state = 'parked'
+ end
+
+ def test_should_be_able_to_fire
+ assert @event.can_fire?(@object)
+ end
+
+ def test_should_have_a_transition
+ transition = @event.transition_for(@object)
+ refute_nil transition
+ assert_equal 'parked', transition.from
+ assert_equal 'first_gear', transition.to
+ assert_equal :ignite, transition.event
+ end
+
+ def test_should_allow_loopback_first_when_possible
+ @event.transition(from: :second_gear, to: StateMachines::BlacklistMatcher.new([:parked, :idling]))
+ @object.state = 'second_gear'
+
+ transition = @event.transition_for(@object)
+ refute_nil transition
+ assert_equal 'second_gear', transition.from
+ assert_equal 'second_gear', transition.to
+ assert_equal :ignite, transition.event
+ end
+
+ def test_should_allow_specific_transition_selection_using_to
+ transition = @event.transition_for(@object, from: :parked, to: :second_gear)
+
+ refute_nil transition
+ assert_equal 'parked', transition.from
+ assert_equal 'second_gear', transition.to
+ assert_equal :ignite, transition.event
+ end
+
+ def test_should_not_allow_transition_selection_if_not_matching
+ transition = @event.transition_for(@object, from: :parked, to: :parked)
+ assert_nil transition
+ end
+
+ def test_should_fire
+ assert @event.fire(@object)
+ end
+
+ def test_should_change_the_current_state
+ @event.fire(@object)
+ assert_equal 'first_gear', @object.state
+ end
+end
diff --git a/test/unit/event/event_with_transition_with_loopback_state_test.rb b/test/unit/event/event_with_transition_with_loopback_state_test.rb
new file mode 100644
index 0000000..9dfabfa
--- /dev/null
+++ b/test/unit/event/event_with_transition_with_loopback_state_test.rb
@@ -0,0 +1,36 @@
+require_relative '../../test_helper'
+
+class EventWithTransitionWithLoopbackStateTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked
+
+ @machine.events << @event = StateMachines::Event.new(@machine, :park)
+ @event.transition(from: :parked, to: StateMachines::LoopbackMatcher.instance)
+
+ @object = @klass.new
+ @object.state = 'parked'
+ end
+
+ def test_should_be_able_to_fire
+ assert @event.can_fire?(@object)
+ end
+
+ def test_should_have_a_transition
+ transition = @event.transition_for(@object)
+ refute_nil transition
+ assert_equal 'parked', transition.from
+ assert_equal 'parked', transition.to
+ assert_equal :park, transition.event
+ end
+
+ def test_should_fire
+ assert @event.fire(@object)
+ end
+
+ def test_should_not_change_the_current_state
+ @event.fire(@object)
+ assert_equal 'parked', @object.state
+ end
+end
diff --git a/test/unit/event/event_with_transition_with_nil_to_state_test.rb b/test/unit/event/event_with_transition_with_nil_to_state_test.rb
new file mode 100644
index 0000000..b57a0d8
--- /dev/null
+++ b/test/unit/event/event_with_transition_with_nil_to_state_test.rb
@@ -0,0 +1,36 @@
+require_relative '../../test_helper'
+
+class EventWithTransitionWithNilToStateTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state nil, :idling
+
+ @machine.events << @event = StateMachines::Event.new(@machine, :park)
+ @event.transition(idling: nil)
+
+ @object = @klass.new
+ @object.state = 'idling'
+ end
+
+ def test_should_be_able_to_fire
+ assert @event.can_fire?(@object)
+ end
+
+ def test_should_have_a_transition
+ transition = @event.transition_for(@object)
+ refute_nil transition
+ assert_equal 'idling', transition.from
+ assert_equal nil, transition.to
+ assert_equal :park, transition.event
+ end
+
+ def test_should_fire
+ assert @event.fire(@object)
+ end
+
+ def test_should_not_change_the_current_state
+ @event.fire(@object)
+ assert_equal nil, @object.state
+ end
+end
diff --git a/test/unit/event/event_with_transition_with_whitelisted_to_state_test.rb b/test/unit/event/event_with_transition_with_whitelisted_to_state_test.rb
new file mode 100644
index 0000000..3a48481
--- /dev/null
+++ b/test/unit/event/event_with_transition_with_whitelisted_to_state_test.rb
@@ -0,0 +1,51 @@
+require_relative '../../test_helper'
+
+class EventWithTransitionWithWhitelistedToStateTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass, initial: :parked)
+ @machine.state :parked, :idling, :first_gear, :second_gear
+
+ @machine.events << @event = StateMachines::Event.new(@machine, :ignite)
+ @event.transition(from: :parked, to: StateMachines::WhitelistMatcher.new([:first_gear, :second_gear]))
+
+ @object = @klass.new
+ @object.state = 'parked'
+ end
+
+ def test_should_be_able_to_fire
+ assert @event.can_fire?(@object)
+ end
+
+ def test_should_have_a_transition
+ transition = @event.transition_for(@object)
+ refute_nil transition
+ assert_equal 'parked', transition.from
+ assert_equal 'first_gear', transition.to
+ assert_equal :ignite, transition.event
+ end
+
+ def test_should_allow_specific_transition_selection_using_to
+ transition = @event.transition_for(@object, from: :parked, to: :second_gear)
+
+ refute_nil transition
+ assert_equal 'parked', transition.from
+ assert_equal 'second_gear', transition.to
+ assert_equal :ignite, transition.event
+ end
+
+ def test_should_not_allow_transition_selection_if_not_matching
+ transition = @event.transition_for(@object, from: :parked, to: :parked)
+ assert_nil transition
+ end
+
+ def test_should_fire
+ assert @event.fire(@object)
+ end
+
+ def test_should_change_the_current_state
+ @event.fire(@object)
+ assert_equal 'first_gear', @object.state
+ end
+end
+
diff --git a/test/unit/event/event_with_transition_without_to_state_test.rb b/test/unit/event/event_with_transition_without_to_state_test.rb
new file mode 100644
index 0000000..c065dff
--- /dev/null
+++ b/test/unit/event/event_with_transition_without_to_state_test.rb
@@ -0,0 +1,36 @@
+require_relative '../../test_helper'
+
+class EventWithTransitionWithoutToStateTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked
+
+ @machine.events << @event = StateMachines::Event.new(@machine, :park)
+ @event.transition(from: :parked)
+
+ @object = @klass.new
+ @object.state = 'parked'
+ end
+
+ def test_should_be_able_to_fire
+ assert @event.can_fire?(@object)
+ end
+
+ def test_should_have_a_transition
+ transition = @event.transition_for(@object)
+ refute_nil transition
+ assert_equal 'parked', transition.from
+ assert_equal 'parked', transition.to
+ assert_equal :park, transition.event
+ end
+
+ def test_should_fire
+ assert @event.fire(@object)
+ end
+
+ def test_should_not_change_the_current_state
+ @event.fire(@object)
+ assert_equal 'parked', @object.state
+ end
+end
diff --git a/test/unit/event/event_with_transitions_test.rb b/test/unit/event/event_with_transitions_test.rb
new file mode 100644
index 0000000..d73154a
--- /dev/null
+++ b/test/unit/event/event_with_transitions_test.rb
@@ -0,0 +1,32 @@
+require_relative '../../test_helper'
+
+class EventWithTransitionsTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.events << @event = StateMachines::Event.new(@machine, :ignite)
+ @event.transition(parked: :idling)
+ @event.transition(first_gear: :idling)
+ end
+
+ def test_should_include_all_transition_states_in_known_states
+ assert_equal [:parked, :idling, :first_gear], @event.known_states
+ end
+
+ def test_should_include_new_transition_states_after_calling_known_states
+ @event.known_states
+ @event.transition(stalled: :idling)
+
+ assert_equal [:parked, :idling, :first_gear, :stalled], @event.known_states
+ end
+
+ def test_should_clear_known_states_on_reset
+ @event.reset
+ assert_equal [], @event.known_states
+ end
+
+ def test_should_use_pretty_inspect
+ assert_match '#<StateMachines::Event name=:ignite transitions=[:parked => :idling, :first_gear => :idling]>', @event.inspect
+ end
+end
+
diff --git a/test/unit/event/event_without_matching_transitions_test.rb b/test/unit/event/event_without_matching_transitions_test.rb
new file mode 100644
index 0000000..8b055aa
--- /dev/null
+++ b/test/unit/event/event_without_matching_transitions_test.rb
@@ -0,0 +1,41 @@
+require_relative '../../test_helper'
+
+class EventWithoutMatchingTransitionsTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked, :idling
+
+ @machine.events << @event = StateMachines::Event.new(@machine, :ignite)
+ @event.transition(parked: :idling)
+
+ @object = @klass.new
+ @object.state = 'idling'
+ end
+
+ def test_should_not_be_able_to_fire
+ refute @event.can_fire?(@object)
+ end
+
+ def test_should_be_able_to_fire_with_custom_from_state
+ assert @event.can_fire?(@object, from: :parked)
+ end
+
+ def test_should_not_have_a_transition
+ assert_nil @event.transition_for(@object)
+ end
+
+ def test_should_have_a_transition_with_custom_from_state
+ refute_nil @event.transition_for(@object, from: :parked)
+ end
+
+ def test_should_not_fire
+ refute @event.fire(@object)
+ end
+
+ def test_should_not_change_the_current_state
+ @event.fire(@object)
+ assert_equal 'idling', @object.state
+ end
+end
+
diff --git a/test/unit/event/event_without_transitions_test.rb b/test/unit/event/event_without_transitions_test.rb
new file mode 100644
index 0000000..20e9d10
--- /dev/null
+++ b/test/unit/event/event_without_transitions_test.rb
@@ -0,0 +1,28 @@
+require_relative '../../test_helper'
+
+class EventWithoutTransitionsTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.events << @event = StateMachines::Event.new(@machine, :ignite)
+ @object = @klass.new
+ end
+
+ def test_should_not_be_able_to_fire
+ refute @event.can_fire?(@object)
+ end
+
+ def test_should_not_have_a_transition
+ assert_nil @event.transition_for(@object)
+ end
+
+ def test_should_not_fire
+ refute @event.fire(@object)
+ end
+
+ def test_should_not_change_the_current_state
+ @event.fire(@object)
+ assert_nil @object.state
+ end
+end
+
diff --git a/test/unit/event/invalid_event_test.rb b/test/unit/event/invalid_event_test.rb
new file mode 100644
index 0000000..ba26f52
--- /dev/null
+++ b/test/unit/event/invalid_event_test.rb
@@ -0,0 +1,20 @@
+require_relative '../../test_helper'
+
+class InvalidEventTest < StateMachinesTest
+ def setup
+ @object = Object.new
+ @invalid_event = StateMachines::InvalidEvent.new(@object, :invalid)
+ end
+
+ def test_should_have_an_object
+ assert_equal @object, @invalid_event.object
+ end
+
+ def test_should_have_an_event
+ assert_equal :invalid, @invalid_event.event
+ end
+
+ def test_should_generate_a_message
+ assert_equal ':invalid is an unknown state machine event', @invalid_event.message
+ end
+end
diff --git a/test/unit/event_collection/event_collection_attribute_with_machine_action_test.rb b/test/unit/event_collection/event_collection_attribute_with_machine_action_test.rb
new file mode 100644
index 0000000..520afcd
--- /dev/null
+++ b/test/unit/event_collection/event_collection_attribute_with_machine_action_test.rb
@@ -0,0 +1,62 @@
+require_relative '../../test_helper'
+
+class EventCollectionAttributeWithMachineActionTest < StateMachinesTest
+ def setup
+ @klass = Class.new do
+ def save
+ end
+ end
+
+ @machine = StateMachines::Machine.new(@klass, initial: :parked, action: :save)
+ @events = StateMachines::EventCollection.new(@machine)
+
+ @machine.state :parked, :idling
+ @events << @ignite = StateMachines::Event.new(@machine, :ignite)
+ @machine.events.concat(@events)
+
+ @object = @klass.new
+ end
+
+ def test_should_not_have_transition_if_nil
+ @object.state_event = nil
+ assert_nil @events.attribute_transition_for(@object)
+ end
+
+ def test_should_not_have_transition_if_empty
+ @object.state_event = ''
+ assert_nil @events.attribute_transition_for(@object)
+ end
+
+ def test_should_have_invalid_transition_if_invalid_event_specified
+ @object.state_event = 'invalid'
+ assert_equal false, @events.attribute_transition_for(@object)
+ end
+
+ def test_should_have_invalid_transition_if_event_cannot_be_fired
+ @object.state_event = 'ignite'
+ assert_equal false, @events.attribute_transition_for(@object)
+ end
+
+ def test_should_have_valid_transition_if_event_can_be_fired
+ @ignite.transition parked: :idling
+ @object.state_event = 'ignite'
+
+ assert_instance_of StateMachines::Transition, @events.attribute_transition_for(@object)
+ end
+
+ def test_should_have_valid_transition_if_already_defined_in_transition_cache
+ @ignite.transition parked: :idling
+ @object.state_event = nil
+ @object.send(:state_event_transition=, transition = @ignite.transition_for(@object))
+
+ assert_equal transition, @events.attribute_transition_for(@object)
+ end
+
+ def test_should_use_transition_cache_if_both_event_and_transition_are_present
+ @ignite.transition parked: :idling
+ @object.state_event = 'ignite'
+ @object.send(:state_event_transition=, transition = @ignite.transition_for(@object))
+
+ assert_equal transition, @events.attribute_transition_for(@object)
+ end
+end
diff --git a/test/unit/event_collection/event_collection_attribute_with_namespaced_machine_test.rb b/test/unit/event_collection/event_collection_attribute_with_namespaced_machine_test.rb
new file mode 100644
index 0000000..2874693
--- /dev/null
+++ b/test/unit/event_collection/event_collection_attribute_with_namespaced_machine_test.rb
@@ -0,0 +1,36 @@
+require_relative '../../test_helper'
+
+class EventCollectionAttributeWithNamespacedMachineTest < StateMachinesTest
+ def setup
+ @klass = Class.new do
+ def save
+ end
+ end
+
+ @machine = StateMachines::Machine.new(@klass, namespace: 'alarm', initial: :active, action: :save)
+ @events = StateMachines::EventCollection.new(@machine)
+
+ @machine.state :active, :off
+ @events << @disable = StateMachines::Event.new(@machine, :disable)
+ @machine.events.concat(@events)
+
+ @object = @klass.new
+ end
+
+ def test_should_not_have_transition_if_nil
+ @object.state_event = nil
+ assert_nil @events.attribute_transition_for(@object)
+ end
+
+ def test_should_have_invalid_transition_if_event_cannot_be_fired
+ @object.state_event = 'disable'
+ assert_equal false, @events.attribute_transition_for(@object)
+ end
+
+ def test_should_have_valid_transition_if_event_can_be_fired
+ @disable.transition active: :off
+ @object.state_event = 'disable'
+
+ assert_instance_of StateMachines::Transition, @events.attribute_transition_for(@object)
+ end
+end
diff --git a/test/unit/event_collection/event_collection_by_default_test.rb b/test/unit/event_collection/event_collection_by_default_test.rb
new file mode 100644
index 0000000..0fe5709
--- /dev/null
+++ b/test/unit/event_collection/event_collection_by_default_test.rb
@@ -0,0 +1,26 @@
+require_relative '../../test_helper'
+
+class EventCollectionByDefaultTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @events = StateMachines::EventCollection.new(@machine)
+ @object = @klass.new
+ end
+
+ def test_should_not_have_any_nodes
+ assert_equal 0, @events.length
+ end
+
+ def test_should_have_a_machine
+ assert_equal @machine, @events.machine
+ end
+
+ def test_should_not_have_any_valid_events_for_an_object
+ assert @events.valid_for(@object).empty?
+ end
+
+ def test_should_not_have_any_transitions_for_an_object
+ assert @events.transitions_for(@object).empty?
+ end
+end
diff --git a/test/unit/event_collection/event_collection_test.rb b/test/unit/event_collection/event_collection_test.rb
new file mode 100644
index 0000000..65d60c3
--- /dev/null
+++ b/test/unit/event_collection/event_collection_test.rb
@@ -0,0 +1,39 @@
+require_relative '../../test_helper'
+
+class EventCollectionTest < StateMachinesTest
+ def setup
+ machine = StateMachines::Machine.new(Class.new, namespace: 'alarm')
+ @events = StateMachines::EventCollection.new(machine)
+
+ @events << @open = StateMachines::Event.new(machine, :enable)
+ machine.events.concat(@events)
+ end
+
+ def test_should_index_by_name
+ assert_equal @open, @events[:enable, :name]
+ end
+
+ def test_should_index_by_name_by_default
+ assert_equal @open, @events[:enable]
+ end
+
+ def test_should_index_by_string_name
+ assert_equal @open, @events['enable']
+ end
+
+ def test_should_index_by_qualified_name
+ assert_equal @open, @events[:enable_alarm, :qualified_name]
+ end
+
+ def test_should_index_by_string_qualified_name
+ assert_equal @open, @events['enable_alarm', :qualified_name]
+ end
+end
+
+
+
+
+
+
+
+
diff --git a/test/unit/event_collection/event_collection_with_custom_machine_attribute_test.rb b/test/unit/event_collection/event_collection_with_custom_machine_attribute_test.rb
new file mode 100644
index 0000000..b0b25cd
--- /dev/null
+++ b/test/unit/event_collection/event_collection_with_custom_machine_attribute_test.rb
@@ -0,0 +1,31 @@
+require_relative '../../test_helper'
+
+class EventCollectionWithCustomMachineAttributeTest < StateMachinesTest
+ def setup
+ @klass = Class.new do
+ def save
+ end
+ end
+
+ @machine = StateMachines::Machine.new(@klass, :state, attribute: :state_id, initial: :parked, action: :save)
+ @events = StateMachines::EventCollection.new(@machine)
+
+ @machine.state :parked, :idling
+ @events << @ignite = StateMachines::Event.new(@machine, :ignite)
+ @machine.events.concat(@events)
+
+ @object = @klass.new
+ end
+
+ def test_should_not_have_transition_if_nil
+ @object.state_event = nil
+ assert_nil @events.attribute_transition_for(@object)
+ end
+
+ def test_should_have_valid_transition_if_event_can_be_fired
+ @ignite.transition parked: :idling
+ @object.state_event = 'ignite'
+
+ assert_instance_of StateMachines::Transition, @events.attribute_transition_for(@object)
+ end
+end
diff --git a/test/unit/event_collection/event_collection_with_events_with_transitions_test.rb b/test/unit/event_collection/event_collection_with_events_with_transitions_test.rb
new file mode 100644
index 0000000..6bc3b7a
--- /dev/null
+++ b/test/unit/event_collection/event_collection_with_events_with_transitions_test.rb
@@ -0,0 +1,76 @@
+require_relative '../../test_helper'
+
+class EventCollectionWithEventsWithTransitionsTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass, initial: :parked)
+ @events = StateMachines::EventCollection.new(@machine)
+
+ @machine.state :idling, :first_gear
+
+ @events << @ignite = StateMachines::Event.new(@machine, :ignite)
+ @ignite.transition parked: :idling
+
+ @events << @park = StateMachines::Event.new(@machine, :park)
+ @park.transition idling: :parked
+
+ @events << @shift_up = StateMachines::Event.new(@machine, :shift_up)
+ @shift_up.transition parked: :first_gear
+ @shift_up.transition idling: :first_gear, if: lambda { false }
+
+ @machine.events.concat(@events)
+
+ @object = @klass.new
+ end
+
+ def test_should_find_valid_events_based_on_current_state
+ assert_equal [@ignite, @shift_up], @events.valid_for(@object)
+ end
+
+ def test_should_filter_valid_events_by_from_state
+ assert_equal [@park], @events.valid_for(@object, from: :idling)
+ end
+
+ def test_should_filter_valid_events_by_to_state
+ assert_equal [@shift_up], @events.valid_for(@object, to: :first_gear)
+ end
+
+ def test_should_filter_valid_events_by_event
+ assert_equal [@ignite], @events.valid_for(@object, on: :ignite)
+ end
+
+ def test_should_filter_valid_events_by_multiple_requirements
+ assert_equal [], @events.valid_for(@object, from: :idling, to: :first_gear)
+ end
+
+ def test_should_allow_finding_valid_events_without_guards
+ assert_equal [@shift_up], @events.valid_for(@object, from: :idling, to: :first_gear, guard: false)
+ end
+
+ def test_should_find_valid_transitions_based_on_current_state
+ assert_equal [
+ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling),
+ StateMachines::Transition.new(@object, @machine, :shift_up, :parked, :first_gear)
+ ], @events.transitions_for(@object)
+ end
+
+ def test_should_filter_valid_transitions_by_from_state
+ assert_equal [StateMachines::Transition.new(@object, @machine, :park, :idling, :parked)], @events.transitions_for(@object, from: :idling)
+ end
+
+ def test_should_filter_valid_transitions_by_to_state
+ assert_equal [StateMachines::Transition.new(@object, @machine, :shift_up, :parked, :first_gear)], @events.transitions_for(@object, to: :first_gear)
+ end
+
+ def test_should_filter_valid_transitions_by_event
+ assert_equal [StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)], @events.transitions_for(@object, on: :ignite)
+ end
+
+ def test_should_filter_valid_transitions_by_multiple_requirements
+ assert_equal [], @events.transitions_for(@object, from: :idling, to: :first_gear)
+ end
+
+ def test_should_allow_finding_valid_transitions_without_guards
+ assert_equal [StateMachines::Transition.new(@object, @machine, :shift_up, :idling, :first_gear)], @events.transitions_for(@object, from: :idling, to: :first_gear, guard: false)
+ end
+end
diff --git a/test/unit/event_collection/event_collection_with_multiple_events_test.rb b/test/unit/event_collection/event_collection_with_multiple_events_test.rb
new file mode 100644
index 0000000..45508b1
--- /dev/null
+++ b/test/unit/event_collection/event_collection_with_multiple_events_test.rb
@@ -0,0 +1,27 @@
+require_relative '../../test_helper'
+
+class EventCollectionWithMultipleEventsTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass, initial: :parked)
+ @events = StateMachines::EventCollection.new(@machine)
+
+ @machine.state :first_gear
+ @park, @shift_down = @machine.event :park, :shift_down
+
+ @events << @park
+ @park.transition first_gear: :parked
+
+ @events << @shift_down
+ @shift_down.transition first_gear: :parked
+
+ @machine.events.concat(@events)
+ end
+
+ def test_should_only_include_all_valid_events_for_an_object
+ object = @klass.new
+ object.state = 'first_gear'
+ assert_equal [@park, @shift_down], @events.valid_for(object)
+ end
+end
+
diff --git a/test/unit/event_collection/event_collection_with_validations_test.rb b/test/unit/event_collection/event_collection_with_validations_test.rb
new file mode 100644
index 0000000..d42e27a
--- /dev/null
+++ b/test/unit/event_collection/event_collection_with_validations_test.rb
@@ -0,0 +1,74 @@
+require_relative '../../test_helper'
+
+class EventCollectionWithValidationsTest < StateMachinesTest
+ module Custom
+ include StateMachines::Integrations::Base
+
+ def invalidate(object, _attribute, message, values = [])
+ (object.errors ||= []) << generate_message(message, values)
+ end
+
+ def reset(object)
+ object.errors = []
+ end
+ end
+
+ def setup
+ StateMachines::Integrations.register(EventCollectionWithValidationsTest::Custom)
+
+ @klass = Class.new do
+ attr_accessor :errors
+
+ def initialize
+ @errors = []
+ super
+ end
+ end
+
+ @machine = StateMachines::Machine.new(@klass, initial: :parked, action: :save, integration: :custom)
+ @events = StateMachines::EventCollection.new(@machine)
+
+ @parked, @idling = @machine.state :parked, :idling
+ @events << @ignite = StateMachines::Event.new(@machine, :ignite)
+ @machine.events.concat(@events)
+
+ @object = @klass.new
+ end
+
+ def test_should_invalidate_if_invalid_event_specified
+ @object.state_event = 'invalid'
+ @events.attribute_transition_for(@object, true)
+
+ assert_equal ['is invalid'], @object.errors
+ end
+
+ def test_should_invalidate_if_event_cannot_be_fired
+ @object.state = 'idling'
+ @object.state_event = 'ignite'
+ @events.attribute_transition_for(@object, true)
+
+ assert_equal ['cannot transition when idling'], @object.errors
+ end
+
+ def test_should_invalidate_with_human_name_if_invalid_event_specified
+ @idling.human_name = 'waiting'
+ @object.state = 'idling'
+ @object.state_event = 'ignite'
+ @events.attribute_transition_for(@object, true)
+
+ assert_equal ['cannot transition when waiting'], @object.errors
+ end
+
+ def test_should_not_invalidate_event_can_be_fired
+ @ignite.transition parked: :idling
+ @object.state_event = 'ignite'
+ @events.attribute_transition_for(@object, true)
+
+ assert_equal [], @object.errors
+ end
+
+ def teardown
+ StateMachines::Integrations.reset
+ end
+end
+
diff --git a/test/unit/event_collection/event_collection_without_machine_action_test.rb b/test/unit/event_collection/event_collection_without_machine_action_test.rb
new file mode 100644
index 0000000..6d005ff
--- /dev/null
+++ b/test/unit/event_collection/event_collection_without_machine_action_test.rb
@@ -0,0 +1,18 @@
+require_relative '../../test_helper'
+
+class EventCollectionWithoutMachineActionTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass, initial: :parked)
+ @events = StateMachines::EventCollection.new(@machine)
+ @events << StateMachines::Event.new(@machine, :ignite)
+ @machine.events.concat(@events)
+
+ @object = @klass.new
+ end
+
+ def test_should_not_have_an_attribute_transition
+ assert_nil @events.attribute_transition_for(@object)
+ end
+end
+
diff --git a/test/unit/event_collection/event_string_collection_test.rb b/test/unit/event_collection/event_string_collection_test.rb
new file mode 100644
index 0000000..e1abe4f
--- /dev/null
+++ b/test/unit/event_collection/event_string_collection_test.rb
@@ -0,0 +1,31 @@
+require_relative '../../test_helper'
+
+class EventStringCollectionTest < StateMachinesTest
+ def setup
+ machine = StateMachines::Machine.new(Class.new, namespace: 'alarm')
+ @events = StateMachines::EventCollection.new(machine)
+
+ @events << @open = StateMachines::Event.new(machine, 'enable')
+ machine.events.concat(@events)
+ end
+
+ def test_should_index_by_name
+ assert_equal @open, @events['enable', :name]
+ end
+
+ def test_should_index_by_name_by_default
+ assert_equal @open, @events['enable']
+ end
+
+ def test_should_index_by_symbol_name
+ assert_equal @open, @events[:enable]
+ end
+
+ def test_should_index_by_qualified_name
+ assert_equal @open, @events['enable_alarm', :qualified_name]
+ end
+
+ def test_should_index_by_symbol_qualified_name
+ assert_equal @open, @events[:enable_alarm, :qualified_name]
+ end
+end
diff --git a/test/unit/helper_module_test.rb b/test/unit/helper_module_test.rb
new file mode 100644
index 0000000..18b5656
--- /dev/null
+++ b/test/unit/helper_module_test.rb
@@ -0,0 +1,17 @@
+require_relative '../test_helper'
+
+class HelperModuleTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @helper_module = StateMachines::HelperModule.new(@machine, :instance)
+ end
+
+ def test_should_not_have_a_name
+ assert_equal '', @helper_module.name.to_s
+ end
+
+ def test_should_provide_human_readable_to_s
+ assert_equal "#{@klass} :state instance helpers", @helper_module.to_s
+ end
+end
diff --git a/test/unit/integrations/integration_finder_test.rb b/test/unit/integrations/integration_finder_test.rb
new file mode 100644
index 0000000..dd89fab
--- /dev/null
+++ b/test/unit/integrations/integration_finder_test.rb
@@ -0,0 +1,16 @@
+require_relative '../../test_helper'
+
+class IntegrationFinderTest < StateMachinesTest
+ def setup
+ StateMachines::Integrations.reset
+ end
+
+ def test_should_raise_an_exception_if_invalid
+ exception = assert_raises(StateMachines::IntegrationNotFound) { StateMachines::Integrations.find_by_name(:invalid) }
+ assert_equal ':invalid is an invalid integration. No integrations registered', exception.message
+ end
+
+ def test_should_have_no_integrations
+ assert_equal([], StateMachines::Integrations.list)
+ end
+end
diff --git a/test/unit/integrations/integration_matcher_test.rb b/test/unit/integrations/integration_matcher_test.rb
new file mode 100644
index 0000000..993fdae
--- /dev/null
+++ b/test/unit/integrations/integration_matcher_test.rb
@@ -0,0 +1,27 @@
+require_relative '../../test_helper'
+require_relative '../../files/models/vehicle'
+require_relative '../../files/integrations/vehicle'
+
+class IntegrationMatcherTest < StateMachinesTest
+ def setup
+ StateMachines::Integrations.reset
+ end
+
+ def test_should_return_nil_if_no_match_found
+ assert_nil StateMachines::Integrations.match(Vehicle)
+ end
+
+ def test_should_return_integration_class_if_match_found
+ StateMachines::Integrations.register(VehicleIntegration)
+ assert_equal VehicleIntegration, StateMachines::Integrations.match(Vehicle)
+ end
+
+ def test_should_return_nil_if_no_match_found_with_ancestors
+ assert_nil StateMachines::Integrations.match_ancestors(['Fake'])
+ end
+
+ def test_should_return_integration_class_if_match_found_with_ancestors
+ StateMachines::Integrations.register(VehicleIntegration)
+ assert_equal VehicleIntegration, StateMachines::Integrations.match_ancestors(['Fake', 'Vehicle'])
+ end
+end
diff --git a/test/unit/invalid_transition/invalid_parallel_transition_test.rb b/test/unit/invalid_transition/invalid_parallel_transition_test.rb
new file mode 100644
index 0000000..19d07a4
--- /dev/null
+++ b/test/unit/invalid_transition/invalid_parallel_transition_test.rb
@@ -0,0 +1,18 @@
+require_relative '../../test_helper'
+
+class InvalidParallelTransitionTest < StateMachinesTest
+ def setup
+ @object = Object.new
+ @events = [:ignite, :disable_alarm]
+
+ @invalid_transition = StateMachines::InvalidParallelTransition.new(@object, @events)
+ end
+
+ def test_should_have_an_object
+ assert_equal @object, @invalid_transition.object
+ end
+
+ def test_should_have_events
+ assert_equal @events, @invalid_transition.events
+ end
+end
diff --git a/test/unit/invalid_transition/invalid_transition_test.rb b/test/unit/invalid_transition/invalid_transition_test.rb
new file mode 100644
index 0000000..2682cd7
--- /dev/null
+++ b/test/unit/invalid_transition/invalid_transition_test.rb
@@ -0,0 +1,47 @@
+require_relative '../../test_helper'
+
+class InvalidTransitionTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @state = @machine.state :parked
+ @machine.event :ignite
+
+ @object = @klass.new
+ @object.state = 'parked'
+
+ @invalid_transition = StateMachines::InvalidTransition.new(@object, @machine, :ignite)
+ end
+
+ def test_should_have_an_object
+ assert_equal @object, @invalid_transition.object
+ end
+
+ def test_should_have_a_machine
+ assert_equal @machine, @invalid_transition.machine
+ end
+
+ def test_should_have_an_event
+ assert_equal :ignite, @invalid_transition.event
+ end
+
+ def test_should_have_a_qualified_event
+ assert_equal :ignite, @invalid_transition.qualified_event
+ end
+
+ def test_should_have_a_from_value
+ assert_equal 'parked', @invalid_transition.from
+ end
+
+ def test_should_have_a_from_name
+ assert_equal :parked, @invalid_transition.from_name
+ end
+
+ def test_should_have_a_qualified_from_name
+ assert_equal :parked, @invalid_transition.qualified_from_name
+ end
+
+ def test_should_generate_a_message
+ assert_equal 'Cannot transition state via :ignite from :parked', @invalid_transition.message
+ end
+end
diff --git a/test/unit/invalid_transition/invalid_transition_with_integration_test.rb b/test/unit/invalid_transition/invalid_transition_with_integration_test.rb
new file mode 100644
index 0000000..1ab5489
--- /dev/null
+++ b/test/unit/invalid_transition/invalid_transition_with_integration_test.rb
@@ -0,0 +1,45 @@
+require_relative '../../test_helper'
+
+class InvalidTransitionWithIntegrationTest < StateMachinesTest
+ module Custom
+ include StateMachines::Integrations::Base
+
+ def errors_for(object)
+ object.errors
+ end
+ end
+
+ def setup
+ StateMachines::Integrations.register(InvalidTransitionWithIntegrationTest::Custom)
+
+ @klass = Class.new do
+ attr_accessor :errors
+ end
+ @machine = StateMachines::Machine.new(@klass, integration: :custom)
+ @machine.state :parked
+ @machine.event :ignite
+
+ @object = @klass.new
+ @object.state = 'parked'
+ end
+
+ def fix_test
+ skip
+ end
+
+ def test_should_generate_a_message_without_reasons_if_empty
+ @object.errors = ''
+ invalid_transition = StateMachines::InvalidTransition.new(@object, @machine, :ignite)
+ assert_equal 'Cannot transition state via :ignite from :parked', invalid_transition.message
+ end
+
+ def test_should_generate_a_message_with_error_reasons_if_errors_found
+ @object.errors = 'Id is invalid, Name is invalid'
+ invalid_transition = StateMachines::InvalidTransition.new(@object, @machine, :ignite)
+ assert_equal 'Cannot transition state via :ignite from :parked (Reason(s): Id is invalid, Name is invalid)', invalid_transition.message
+ end
+
+ def teardown
+ StateMachines::Integrations.reset
+ end
+end
diff --git a/test/unit/invalid_transition/invalid_transition_with_namespace_test.rb b/test/unit/invalid_transition/invalid_transition_with_namespace_test.rb
new file mode 100644
index 0000000..88e0c20
--- /dev/null
+++ b/test/unit/invalid_transition/invalid_transition_with_namespace_test.rb
@@ -0,0 +1,32 @@
+require_relative '../../test_helper'
+
+class InvalidTransitionWithNamespaceTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass, namespace: 'alarm')
+ @state = @machine.state :active
+ @machine.event :disable
+
+ @object = @klass.new
+ @object.state = 'active'
+
+ @invalid_transition = StateMachines::InvalidTransition.new(@object, @machine, :disable)
+ end
+
+ def test_should_have_an_event
+ assert_equal :disable, @invalid_transition.event
+ end
+
+ def test_should_have_a_qualified_event
+ assert_equal :disable_alarm, @invalid_transition.qualified_event
+ end
+
+ def test_should_have_a_from_name
+ assert_equal :active, @invalid_transition.from_name
+ end
+
+ def test_should_have_a_qualified_from_name
+ assert_equal :alarm_active, @invalid_transition.qualified_from_name
+ end
+end
+
diff --git a/test/unit/machine/machine_after_being_copied_test.rb b/test/unit/machine/machine_after_being_copied_test.rb
new file mode 100644
index 0000000..98ff08b
--- /dev/null
+++ b/test/unit/machine/machine_after_being_copied_test.rb
@@ -0,0 +1,62 @@
+require_relative '../../test_helper'
+
+class MachineAfterBeingCopiedTest < StateMachinesTest
+ def setup
+ @machine = StateMachines::Machine.new(Class.new, :state, initial: :parked)
+ @machine.event(:ignite) {}
+ @machine.before_transition(lambda {})
+ @machine.after_transition(lambda {})
+ @machine.around_transition(lambda {})
+ @machine.after_failure(lambda {})
+
+ @copied_machine = @machine.clone
+ end
+
+ def test_should_not_have_the_same_collection_of_states
+ refute_same @copied_machine.states, @machine.states
+ end
+
+ def test_should_copy_each_state
+ refute_same @copied_machine.states[:parked], @machine.states[:parked]
+ end
+
+ def test_should_update_machine_for_each_state
+ assert_equal @copied_machine, @copied_machine.states[:parked].machine
+ end
+
+ def test_should_not_update_machine_for_original_state
+ assert_equal @machine, @machine.states[:parked].machine
+ end
+
+ def test_should_not_have_the_same_collection_of_events
+ refute_same @copied_machine.events, @machine.events
+ end
+
+ def test_should_copy_each_event
+ refute_same @copied_machine.events[:ignite], @machine.events[:ignite]
+ end
+
+ def test_should_update_machine_for_each_event
+ assert_equal @copied_machine, @copied_machine.events[:ignite].machine
+ end
+
+ def test_should_not_update_machine_for_original_event
+ assert_equal @machine, @machine.events[:ignite].machine
+ end
+
+ def test_should_not_have_the_same_callbacks
+ refute_same @copied_machine.callbacks, @machine.callbacks
+ end
+
+ def test_should_not_have_the_same_before_callbacks
+ refute_same @copied_machine.callbacks[:before], @machine.callbacks[:before]
+ end
+
+ def test_should_not_have_the_same_after_callbacks
+ refute_same @copied_machine.callbacks[:after], @machine.callbacks[:after]
+ end
+
+ def test_should_not_have_the_same_failure_callbacks
+ refute_same @copied_machine.callbacks[:failure], @machine.callbacks[:failure]
+ end
+end
diff --git a/test/unit/machine/machine_after_changing_initial_state.rb b/test/unit/machine/machine_after_changing_initial_state.rb
new file mode 100644
index 0000000..1ed79ca
--- /dev/null
+++ b/test/unit/machine/machine_after_changing_initial_state.rb
@@ -0,0 +1,28 @@
+require_relative '../../test_helper'
+
+class MachineAfterChangingInitialState < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass, initial: :parked)
+ @machine.initial_state = :idling
+
+ @object = @klass.new
+ end
+
+ def test_should_change_the_initial_state
+ assert_equal :idling, @machine.initial_state(@object).name
+ end
+
+ def test_should_include_in_known_states
+ assert_equal [:parked, :idling], @machine.states.map { |state| state.name }
+ end
+
+ def test_should_reset_original_initial_state
+ refute @machine.state(:parked).initial
+ end
+
+ def test_should_set_new_state_to_initial
+ assert @machine.state(:idling).initial
+ end
+end
+
diff --git a/test/unit/machine/machine_after_changing_owner_class_test.rb b/test/unit/machine/machine_after_changing_owner_class_test.rb
new file mode 100644
index 0000000..47981fb
--- /dev/null
+++ b/test/unit/machine/machine_after_changing_owner_class_test.rb
@@ -0,0 +1,31 @@
+require_relative '../../test_helper'
+
+class MachineAfterChangingOwnerClassTest < StateMachinesTest
+ def setup
+ @original_class = Class.new
+ @machine = StateMachines::Machine.new(@original_class)
+
+ @new_class = Class.new(@original_class)
+ @new_machine = @machine.clone
+ @new_machine.owner_class = @new_class
+
+ @object = @new_class.new
+ end
+
+ def test_should_update_owner_class
+ assert_equal @new_class, @new_machine.owner_class
+ end
+
+ def test_should_not_change_original_owner_class
+ assert_equal @original_class, @machine.owner_class
+ end
+
+ def test_should_change_the_associated_machine_in_the_new_class
+ assert_equal @new_machine, @new_class.state_machines[:state]
+ end
+
+ def test_should_not_change_the_associated_machine_in_the_original_class
+ assert_equal @machine, @original_class.state_machines[:state]
+ end
+end
+
diff --git a/test/unit/machine/machine_by_default_test.rb b/test/unit/machine/machine_by_default_test.rb
new file mode 100644
index 0000000..f840001
--- /dev/null
+++ b/test/unit/machine/machine_by_default_test.rb
@@ -0,0 +1,160 @@
+require_relative '../../test_helper'
+
+class MachineByDefaultTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @object = @klass.new
+ end
+
+ def test_should_have_an_owner_class
+ assert_equal @klass, @machine.owner_class
+ end
+
+ def test_should_have_a_name
+ assert_equal :state, @machine.name
+ end
+
+ def test_should_have_an_attribute
+ assert_equal :state, @machine.attribute
+ end
+
+ def test_should_prefix_custom_attributes_with_attribute
+ assert_equal :state_event, @machine.attribute(:event)
+ end
+
+ def test_should_have_an_initial_state
+ refute_nil @machine.initial_state(@object)
+ end
+
+ def test_should_have_a_nil_initial_state
+ assert_nil @machine.initial_state(@object).value
+ end
+
+ def test_should_not_have_any_events
+ refute @machine.events.any?
+ end
+
+ def test_should_not_have_any_before_callbacks
+ assert @machine.callbacks[:before].empty?
+ end
+
+ def test_should_not_have_any_after_callbacks
+ assert @machine.callbacks[:after].empty?
+ end
+
+ def test_should_not_have_any_failure_callbacks
+ assert @machine.callbacks[:failure].empty?
+ end
+
+ def test_should_not_have_an_action
+ assert_nil @machine.action
+ end
+
+ def test_should_use_tranactions
+ assert_equal true, @machine.use_transactions
+ end
+
+ def test_should_not_have_a_namespace
+ assert_nil @machine.namespace
+ end
+
+ def test_should_have_a_nil_state
+ assert_equal [nil], @machine.states.keys
+ end
+
+ def test_should_set_initial_on_nil_state
+ assert @machine.state(nil).initial
+ end
+
+ def test_should_generate_default_messages
+ assert_equal 'is invalid', @machine.generate_message(:invalid)
+ assert_equal 'cannot transition when parked', @machine.generate_message(:invalid_event, [[:state, :parked]])
+ assert_equal 'cannot transition via "park"', @machine.generate_message(:invalid_transition, [[:event, :park]])
+ end
+
+ def test_should_define_a_reader_attribute_for_the_attribute
+ assert @object.respond_to?(:state)
+ end
+
+ def test_should_define_a_writer_attribute_for_the_attribute
+ assert @object.respond_to?(:state=)
+ end
+
+ def test_should_define_a_predicate_for_the_attribute
+ assert @object.respond_to?(:state?)
+ end
+
+ def test_should_define_a_name_reader_for_the_attribute
+ assert @object.respond_to?(:state_name)
+ end
+
+ def test_should_define_an_event_reader_for_the_attribute
+ assert @object.respond_to?(:state_events)
+ end
+
+ def test_should_define_a_transition_reader_for_the_attribute
+ assert @object.respond_to?(:state_transitions)
+ end
+
+ def test_should_define_a_path_reader_for_the_attribute
+ assert @object.respond_to?(:state_paths)
+ end
+
+ def test_should_define_an_event_runner_for_the_attribute
+ assert @object.respond_to?(:fire_state_event)
+ end
+
+ def test_should_not_define_an_event_attribute_reader
+ refute @object.respond_to?(:state_event)
+ end
+
+ def test_should_not_define_an_event_attribute_writer
+ refute @object.respond_to?(:state_event=)
+ end
+
+ def test_should_not_define_an_event_transition_attribute_reader
+ refute @object.respond_to?(:state_event_transition)
+ end
+
+ def test_should_not_define_an_event_transition_attribute_writer
+ refute @object.respond_to?(:state_event_transition=)
+ end
+
+ def test_should_define_a_human_attribute_name_reader_for_the_attribute
+ assert @klass.respond_to?(:human_state_name)
+ end
+
+ def test_should_define_a_human_event_name_reader_for_the_attribute
+ assert @klass.respond_to?(:human_state_event_name)
+ end
+
+ def test_should_not_define_singular_with_scope
+ refute @klass.respond_to?(:with_state)
+ end
+
+ def test_should_not_define_singular_without_scope
+ refute @klass.respond_to?(:without_state)
+ end
+
+ def test_should_not_define_plural_with_scope
+ refute @klass.respond_to?(:with_states)
+ end
+
+ def test_should_not_define_plural_without_scope
+ refute @klass.respond_to?(:without_states)
+ end
+
+ def test_should_extend_owner_class_with_class_methods
+ assert((class << @klass; ancestors; end).include?(StateMachines::ClassMethods))
+ end
+
+ def test_should_include_instance_methods_in_owner_class
+ assert @klass.included_modules.include?(StateMachines::InstanceMethods)
+ end
+
+ def test_should_define_state_machines_reader
+ expected = { state: @machine }
+ assert_equal expected, @klass.state_machines
+ end
+end
diff --git a/test/unit/machine/machine_finder_custom_options_test.rb b/test/unit/machine/machine_finder_custom_options_test.rb
new file mode 100644
index 0000000..181278a
--- /dev/null
+++ b/test/unit/machine/machine_finder_custom_options_test.rb
@@ -0,0 +1,17 @@
+require_relative '../../test_helper'
+
+class MachineFinderCustomOptionsTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.find_or_create(@klass, :status, initial: :parked)
+ @object = @klass.new
+ end
+
+ def test_should_use_custom_attribute
+ assert_equal :status, @machine.attribute
+ end
+
+ def test_should_set_custom_initial_state
+ assert_equal :parked, @machine.initial_state(@object).name
+ end
+end
diff --git a/test/unit/machine/machine_finder_with_existing_machine_on_superclass_test.rb b/test/unit/machine/machine_finder_with_existing_machine_on_superclass_test.rb
new file mode 100644
index 0000000..f005113
--- /dev/null
+++ b/test/unit/machine/machine_finder_with_existing_machine_on_superclass_test.rb
@@ -0,0 +1,85 @@
+require_relative '../../test_helper'
+
+class MachineFinderWithExistingMachineOnSuperclassTest < StateMachinesTest
+ module Custom
+ include StateMachines::Integrations::Base
+
+ def self.matches?(_klass)
+ false
+ end
+ end
+
+ def setup
+ StateMachines::Integrations.register(MachineFinderWithExistingMachineOnSuperclassTest::Custom)
+
+ @base_class = Class.new
+ @base_machine = StateMachines::Machine.new(@base_class, :status, action: :save, integration: :custom)
+ @base_machine.event(:ignite) {}
+ @base_machine.before_transition(-> {})
+ @base_machine.after_transition(-> {})
+ @base_machine.around_transition(-> {})
+
+ @klass = Class.new(@base_class)
+ @machine = StateMachines::Machine.find_or_create(@klass, :status) {}
+ end
+
+ def test_should_accept_a_block
+ called = false
+ StateMachines::Machine.find_or_create(Class.new(@base_class)) do
+ called = respond_to?(:event)
+ end
+
+ assert called
+ end
+
+ def test_should_not_create_a_new_machine_if_no_block_or_options
+ machine = StateMachines::Machine.find_or_create(Class.new(@base_class), :status)
+
+ assert_same machine, @base_machine
+ end
+
+ def test_should_create_a_new_machine_if_given_options
+ machine = StateMachines::Machine.find_or_create(@klass, :status, initial: :parked)
+
+ refute_nil machine
+ refute_same machine, @base_machine
+ end
+
+ def test_should_create_a_new_machine_if_given_block
+ refute_nil @machine
+ refute_same @machine, @base_machine
+ end
+
+ def test_should_copy_the_base_attribute
+ assert_equal :status, @machine.attribute
+ end
+
+ def test_should_copy_the_base_configuration
+ assert_equal :save, @machine.action
+ end
+
+ def test_should_copy_events
+ # Can't assert equal arrays since their machines change
+ assert_equal 1, @machine.events.length
+ end
+
+ def test_should_copy_before_callbacks
+ assert_equal @base_machine.callbacks[:before], @machine.callbacks[:before]
+ end
+
+ def test_should_copy_after_transitions
+ assert_equal @base_machine.callbacks[:after], @machine.callbacks[:after]
+ end
+
+ def test_should_use_the_same_integration
+ class_ancestors = class << @machine
+ ancestors
+ end
+
+ assert(class_ancestors.include?(MachineFinderWithExistingMachineOnSuperclassTest::Custom))
+ end
+
+ def teardown
+ StateMachines::Integrations.reset
+ end
+end
diff --git a/test/unit/machine/machine_finder_with_existing_on_same_class_test.rb b/test/unit/machine/machine_finder_with_existing_on_same_class_test.rb
new file mode 100644
index 0000000..6390e10
--- /dev/null
+++ b/test/unit/machine/machine_finder_with_existing_on_same_class_test.rb
@@ -0,0 +1,23 @@
+require_relative '../../test_helper'
+
+class MachineFinderWithExistingOnSameClassTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @existing_machine = StateMachines::Machine.new(@klass)
+ @machine = StateMachines::Machine.find_or_create(@klass)
+ end
+
+ def test_should_accept_a_block
+ called = false
+ StateMachines::Machine.find_or_create(@klass) do
+ called = respond_to?(:event)
+ end
+
+ assert called
+ end
+
+ def test_should_not_create_a_new_machine
+ assert_same @machine, @existing_machine
+ end
+end
+
diff --git a/test/unit/machine/machine_finder_without_existing_machine_test.rb b/test/unit/machine/machine_finder_without_existing_machine_test.rb
new file mode 100644
index 0000000..24cf23e
--- /dev/null
+++ b/test/unit/machine/machine_finder_without_existing_machine_test.rb
@@ -0,0 +1,25 @@
+require_relative '../../test_helper'
+
+class MachineFinderWithoutExistingMachineTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.find_or_create(@klass)
+ end
+
+ def test_should_accept_a_block
+ called = false
+ StateMachines::Machine.find_or_create(Class.new) do
+ called = respond_to?(:event)
+ end
+
+ assert called
+ end
+
+ def test_should_create_a_new_machine
+ refute_nil @machine
+ end
+
+ def test_should_use_default_state
+ assert_equal :state, @machine.attribute
+ end
+end
diff --git a/test/unit/machine/machine_persistence_test.rb b/test/unit/machine/machine_persistence_test.rb
new file mode 100644
index 0000000..d7c05e8
--- /dev/null
+++ b/test/unit/machine/machine_persistence_test.rb
@@ -0,0 +1,52 @@
+require_relative '../../test_helper'
+
+class MachinePersistenceTest < StateMachinesTest
+ def setup
+ @klass = Class.new do
+ attr_accessor :state_event
+ end
+ @machine = StateMachines::Machine.new(@klass, initial: :parked)
+ @object = @klass.new
+ end
+
+ def test_should_allow_reading_state
+ assert_equal 'parked', @machine.read(@object, :state)
+ end
+
+ def test_should_allow_reading_custom_attributes
+ assert_nil @machine.read(@object, :event)
+
+ @object.state_event = 'ignite'
+ assert_equal 'ignite', @machine.read(@object, :event)
+ end
+
+ def test_should_allow_reading_custom_instance_variables
+ @klass.class_eval do
+ attr_writer :state_value
+ end
+
+ @object.state_value = 1
+ assert_raises(NoMethodError) { @machine.read(@object, :value) }
+ assert_equal 1, @machine.read(@object, :value, true)
+ end
+
+ def test_should_allow_writing_state
+ @machine.write(@object, :state, 'idling')
+ assert_equal 'idling', @object.state
+ end
+
+ def test_should_allow_writing_custom_attributes
+ @machine.write(@object, :event, 'ignite')
+ assert_equal 'ignite', @object.state_event
+ end
+
+ def test_should_allow_writing_custom_instance_variables
+ @klass.class_eval do
+ attr_reader :state_value
+ end
+
+ assert_raises(NoMethodError) { @machine.write(@object, :value, 1) }
+ assert_equal 1, @machine.write(@object, :value, 1, true)
+ assert_equal 1, @object.state_value
+ end
+end
diff --git a/test/unit/machine/machine_state_initialization_test.rb b/test/unit/machine/machine_state_initialization_test.rb
new file mode 100644
index 0000000..1a642a8
--- /dev/null
+++ b/test/unit/machine/machine_state_initialization_test.rb
@@ -0,0 +1,56 @@
+require_relative '../../test_helper'
+
+class MachineStateInitializationTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass, :state, initial: :parked, initialize: false)
+
+ @object = @klass.new
+ @object.state = nil
+ end
+
+ def test_should_set_states_if_nil
+ @machine.initialize_state(@object)
+
+ assert_equal 'parked', @object.state
+ end
+
+ def test_should_set_states_if_empty
+ @object.state = ''
+ @machine.initialize_state(@object)
+
+ assert_equal 'parked', @object.state
+ end
+
+ def test_should_not_set_states_if_not_empty
+ @object.state = 'idling'
+ @machine.initialize_state(@object)
+
+ assert_equal 'idling', @object.state
+ end
+
+ def test_should_set_states_if_not_empty_and_forced
+ @object.state = 'idling'
+ @machine.initialize_state(@object, force: true)
+
+ assert_equal 'parked', @object.state
+ end
+
+ def test_should_not_set_state_if_nil_and_nil_is_valid_state
+ @machine.state :initial, value: nil
+ @machine.initialize_state(@object)
+
+ assert_nil @object.state
+ end
+
+ def test_should_write_to_hash_if_specified
+ @machine.initialize_state(@object, to: hash = {})
+ assert_equal({ 'state' => 'parked' }, hash)
+ end
+
+ def test_should_not_write_to_object_if_writing_to_hash
+ @machine.initialize_state(@object, to: {})
+ assert_nil @object.state
+ end
+end
+
diff --git a/test/unit/machine/machine_test.rb b/test/unit/machine/machine_test.rb
new file mode 100644
index 0000000..44718a1
--- /dev/null
+++ b/test/unit/machine/machine_test.rb
@@ -0,0 +1,30 @@
+require_relative '../../test_helper'
+
+class MachineTest < StateMachinesTest
+ def test_should_raise_exception_if_invalid_option_specified
+ assert_raises(ArgumentError) { StateMachines::Machine.new(Class.new, invalid: true) }
+ end
+
+ def test_should_not_raise_exception_if_custom_messages_specified
+ StateMachines::Machine.new(Class.new, messages: { invalid_transition: 'custom' })
+ end
+
+ def test_should_evaluate_a_block_during_initialization
+ called = true
+ StateMachines::Machine.new(Class.new) do
+ called = respond_to?(:event)
+ end
+
+ assert called
+ end
+
+ def test_should_provide_matcher_helpers_during_initialization
+ matchers = []
+
+ StateMachines::Machine.new(Class.new) do
+ matchers = [all, any, same]
+ end
+
+ assert_equal [StateMachines::AllMatcher.instance, StateMachines::AllMatcher.instance, StateMachines::LoopbackMatcher.instance], matchers
+ end
+end
diff --git a/test/unit/machine/machine_with_action_already_overridden_test.rb b/test/unit/machine/machine_with_action_already_overridden_test.rb
new file mode 100644
index 0000000..0efcf8c
--- /dev/null
+++ b/test/unit/machine/machine_with_action_already_overridden_test.rb
@@ -0,0 +1,23 @@
+require_relative '../../test_helper'
+
+class MachineWithActionAlreadyOverriddenTest < StateMachinesTest
+ def setup
+ @superclass = Class.new do
+ def save
+ end
+ end
+ @klass = Class.new(@superclass)
+
+ StateMachines::Machine.new(@klass, action: :save)
+ @machine = StateMachines::Machine.new(@klass, :status, action: :save)
+ @object = @klass.new
+ end
+
+ def test_should_not_redefine_action
+ assert_equal 1, @klass.ancestors.select { |ancestor| ![@klass, @superclass].include?(ancestor) && ancestor.method_defined?(:save) }.length
+ end
+
+ def test_should_mark_action_hook_as_defined
+ assert @machine.action_hook?
+ end
+end
diff --git a/test/unit/machine/machine_with_action_defined_in_class_test.rb b/test/unit/machine/machine_with_action_defined_in_class_test.rb
new file mode 100644
index 0000000..44f0bee
--- /dev/null
+++ b/test/unit/machine/machine_with_action_defined_in_class_test.rb
@@ -0,0 +1,37 @@
+require_relative '../../test_helper'
+
+class MachineWithActionDefinedInClassTest < StateMachinesTest
+ def setup
+ @klass = Class.new do
+ def save
+ end
+ end
+
+ @machine = StateMachines::Machine.new(@klass, action: :save)
+ @object = @klass.new
+ end
+
+ def test_should_define_an_event_attribute_reader
+ assert @object.respond_to?(:state_event)
+ end
+
+ def test_should_define_an_event_attribute_writer
+ assert @object.respond_to?(:state_event=)
+ end
+
+ def test_should_define_an_event_transition_attribute_reader
+ assert @object.respond_to?(:state_event_transition, true)
+ end
+
+ def test_should_define_an_event_transition_attribute_writer
+ assert @object.respond_to?(:state_event_transition=, true)
+ end
+
+ def test_should_not_define_action
+ refute @klass.ancestors.any? { |ancestor| ancestor != @klass && ancestor.method_defined?(:save) }
+ end
+
+ def test_should_not_mark_action_hook_as_defined
+ refute @machine.action_hook?
+ end
+end
diff --git a/test/unit/machine/machine_with_action_defined_in_included_module_test.rb b/test/unit/machine/machine_with_action_defined_in_included_module_test.rb
new file mode 100644
index 0000000..cd46a5b
--- /dev/null
+++ b/test/unit/machine/machine_with_action_defined_in_included_module_test.rb
@@ -0,0 +1,46 @@
+require_relative '../../test_helper'
+
+class MachineWithActionDefinedInIncludedModuleTest < StateMachinesTest
+ def setup
+ @mod = mod = Module.new do
+ def save
+ end
+ end
+
+ @klass = Class.new do
+ include mod
+ end
+
+ @machine = StateMachines::Machine.new(@klass, action: :save)
+ @object = @klass.new
+ end
+
+ def test_should_define_an_event_attribute_reader
+ assert @object.respond_to?(:state_event)
+ end
+
+ def test_should_define_an_event_attribute_writer
+ assert @object.respond_to?(:state_event=)
+ end
+
+ def test_should_define_an_event_transition_attribute_reader
+ assert @object.respond_to?(:state_event_transition, true)
+ end
+
+ def test_should_define_an_event_transition_attribute_writer
+ assert @object.respond_to?(:state_event_transition=, true)
+ end
+
+ def test_should_define_action
+ assert @klass.ancestors.any? { |ancestor| ![@klass, @mod].include?(ancestor) && ancestor.method_defined?(:save) }
+ end
+
+ def test_should_keep_action_public
+ assert @klass.public_method_defined?(:save)
+ end
+
+ def test_should_mark_action_hook_as_defined
+ assert @machine.action_hook?
+ end
+end
+
diff --git a/test/unit/machine/machine_with_action_defined_in_superclass_test.rb b/test/unit/machine/machine_with_action_defined_in_superclass_test.rb
new file mode 100644
index 0000000..007ae7f
--- /dev/null
+++ b/test/unit/machine/machine_with_action_defined_in_superclass_test.rb
@@ -0,0 +1,43 @@
+require_relative '../../test_helper'
+
+class MachineWithActionDefinedInSuperclassTest < StateMachinesTest
+ def setup
+ @superclass = Class.new do
+ def save
+ end
+ end
+ @klass = Class.new(@superclass)
+
+ @machine = StateMachines::Machine.new(@klass, action: :save)
+ @object = @klass.new
+ end
+
+ def test_should_define_an_event_attribute_reader
+ assert @object.respond_to?(:state_event)
+ end
+
+ def test_should_define_an_event_attribute_writer
+ assert @object.respond_to?(:state_event=)
+ end
+
+ def test_should_define_an_event_transition_attribute_reader
+ assert @object.respond_to?(:state_event_transition, true)
+ end
+
+ def test_should_define_an_event_transition_attribute_writer
+ assert @object.respond_to?(:state_event_transition=, true)
+ end
+
+ def test_should_define_action
+ assert @klass.ancestors.any? { |ancestor| ![@klass, @superclass].include?(ancestor) && ancestor.method_defined?(:save) }
+ end
+
+ def test_should_keep_action_public
+ assert @klass.public_method_defined?(:save)
+ end
+
+ def test_should_mark_action_hook_as_defined
+ assert @machine.action_hook?
+ end
+end
+
diff --git a/test/unit/machine/machine_with_action_undefined_test.rb b/test/unit/machine/machine_with_action_undefined_test.rb
new file mode 100644
index 0000000..4239ed8
--- /dev/null
+++ b/test/unit/machine/machine_with_action_undefined_test.rb
@@ -0,0 +1,33 @@
+require_relative '../../test_helper'
+
+class MachineWithActionUndefinedTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass, action: :save)
+ @object = @klass.new
+ end
+
+ def test_should_define_an_event_attribute_reader
+ assert @object.respond_to?(:state_event)
+ end
+
+ def test_should_define_an_event_attribute_writer
+ assert @object.respond_to?(:state_event=)
+ end
+
+ def test_should_define_an_event_transition_attribute_reader
+ assert @object.respond_to?(:state_event_transition, true)
+ end
+
+ def test_should_define_an_event_transition_attribute_writer
+ assert @object.respond_to?(:state_event_transition=, true)
+ end
+
+ def test_should_not_define_action
+ refute @object.respond_to?(:save)
+ end
+
+ def test_should_not_mark_action_hook_as_defined
+ refute @machine.action_hook?
+ end
+end
diff --git a/test/unit/machine/machine_with_cached_state_test.rb b/test/unit/machine/machine_with_cached_state_test.rb
new file mode 100644
index 0000000..14ad32a
--- /dev/null
+++ b/test/unit/machine/machine_with_cached_state_test.rb
@@ -0,0 +1,20 @@
+require_relative '../../test_helper'
+
+class MachineWithCachedStateTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass, initial: :parked)
+ @state = @machine.state :parked, value: -> { Object.new }, cache: true
+
+ @object = @klass.new
+ end
+
+ def test_should_use_evaluated_value
+ assert_instance_of Object, @object.state
+ end
+
+ def test_use_same_value_across_multiple_objects
+ assert_equal @object.state, @klass.new.state
+ end
+end
+
diff --git a/test/unit/machine/machine_with_class_helpers_test.rb b/test/unit/machine/machine_with_class_helpers_test.rb
new file mode 100644
index 0000000..5d04798
--- /dev/null
+++ b/test/unit/machine/machine_with_class_helpers_test.rb
@@ -0,0 +1,179 @@
+require_relative '../../test_helper'
+require 'stringio'
+
+class MachineWithClassHelpersTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ end
+
+ def test_should_not_redefine_existing_public_methods
+ class << @klass
+ def states
+ []
+ end
+ end
+
+ @machine.define_helper(:class, :states) {}
+ assert_equal [], @klass.states
+ end
+
+ def test_should_not_redefine_existing_protected_methods
+ class << @klass
+ protected
+ def states
+ []
+ end
+ end
+
+ @machine.define_helper(:class, :states) {}
+ assert_equal [], @klass.send(:states)
+ end
+
+ def test_should_not_redefine_existing_private_methods
+ class << @klass
+ private
+ def states
+ []
+ end
+ end
+
+ @machine.define_helper(:class, :states) {}
+ assert_equal [], @klass.send(:states)
+ end
+
+ def test_should_warn_if_defined_in_superclass
+ @original_stderr, $stderr = $stderr, StringIO.new
+
+ superclass = Class.new do
+ def self.park
+ end
+ end
+ klass = Class.new(superclass)
+ machine = StateMachines::Machine.new(klass)
+
+ machine.define_helper(:class, :park) {}
+ assert_equal "Class method \"park\" is already defined in #{superclass}, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true.\n", $stderr.string
+ ensure
+ $stderr = @original_stderr
+ end
+
+ def test_should_warn_if_defined_in_multiple_superclasses
+ @original_stderr, $stderr = $stderr, StringIO.new
+
+ superclass1 = Class.new do
+ def self.park
+ end
+ end
+ superclass2 = Class.new(superclass1) do
+ def self.park
+ end
+ end
+ klass = Class.new(superclass2)
+ machine = StateMachines::Machine.new(klass)
+
+ machine.define_helper(:class, :park) {}
+ assert_equal "Class method \"park\" is already defined in #{superclass1}, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true.\n", $stderr.string
+ ensure
+ $stderr = @original_stderr
+ end
+
+ def test_should_warn_if_defined_in_module_prior_to_helper_module
+ @original_stderr, $stderr = $stderr, StringIO.new
+
+ mod = Module.new do
+ def park
+ end
+ end
+ klass = Class.new do
+ extend mod
+ end
+ machine = StateMachines::Machine.new(klass)
+
+ machine.define_helper(:class, :park) {}
+ assert_equal "Class method \"park\" is already defined in #{mod}, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true.\n", $stderr.string
+ ensure
+ $stderr = @original_stderr
+ end
+
+ def test_should_not_warn_if_defined_in_module_after_helper_module
+ @original_stderr, $stderr = $stderr, StringIO.new
+
+ klass = Class.new
+ machine = StateMachines::Machine.new(klass)
+
+ mod = Module.new do
+ def park
+ end
+ end
+ klass.class_eval do
+ extend mod
+ end
+
+ machine.define_helper(:class, :park) {}
+ assert_equal '', $stderr.string
+ ensure
+ $stderr = @original_stderr
+ end
+
+ def test_should_define_if_ignoring_method_conflicts_and_defined_in_superclass
+ @original_stderr, $stderr = $stderr, StringIO.new
+ StateMachines::Machine.ignore_method_conflicts = true
+
+ superclass = Class.new do
+ def self.park
+ end
+ end
+ klass = Class.new(superclass)
+ machine = StateMachines::Machine.new(klass)
+
+ machine.define_helper(:class, :park) { true }
+ assert_equal '', $stderr.string
+ assert_equal true, klass.park
+ ensure
+ StateMachines::Machine.ignore_method_conflicts = false
+ $stderr = @original_stderr
+ end
+
+ def test_should_define_nonexistent_methods
+ @machine.define_helper(:class, :states) { [] }
+ assert_equal [], @klass.states
+ end
+
+ def test_should_warn_if_defined_multiple_times
+ @original_stderr, $stderr = $stderr, StringIO.new
+
+ @machine.define_helper(:class, :states) {}
+ @machine.define_helper(:class, :states) {}
+
+ assert_equal "Class method \"states\" is already defined in #{@klass} :state class helpers, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true.\n", $stderr.string
+ ensure
+ $stderr = @original_stderr
+ end
+
+ def test_should_pass_context_as_arguments
+ helper_args = nil
+ @machine.define_helper(:class, :states) { |*args| helper_args = args }
+ @klass.states
+ assert_equal 2, helper_args.length
+ assert_equal [@machine, @klass], helper_args
+ end
+
+ def test_should_pass_method_arguments_through
+ helper_args = nil
+ @machine.define_helper(:class, :states) { |*args| helper_args = args }
+ @klass.states(1, 2, 3)
+ assert_equal 5, helper_args.length
+ assert_equal [@machine, @klass, 1, 2, 3], helper_args
+ end
+
+ def test_should_allow_string_evaluation
+ @machine.define_helper :class, <<-end_eval, __FILE__, __LINE__ + 1
+ def states
+ []
+ end
+ end_eval
+ assert_equal [], @klass.states
+ end
+end
+
diff --git a/test/unit/machine/machine_with_conflicting_helpers_after_definition_test.rb b/test/unit/machine/machine_with_conflicting_helpers_after_definition_test.rb
new file mode 100644
index 0000000..fd09267
--- /dev/null
+++ b/test/unit/machine/machine_with_conflicting_helpers_after_definition_test.rb
@@ -0,0 +1,244 @@
+require_relative '../../test_helper'
+
+class MachineWithConflictingHelpersAfterDefinitionTest < StateMachinesTest
+ module Custom
+ include StateMachines::Integrations::Base
+
+ def create_with_scope(_name)
+ ->(_klass, _values) { [] }
+ end
+
+ def create_without_scope(_name)
+ ->(_klass, _values) { [] }
+ end
+ end
+ def setup
+ @original_stderr, $stderr = $stderr, StringIO.new
+ StateMachines::Integrations.register(MachineWithConflictingHelpersAfterDefinitionTest::Custom)
+ @klass = Class.new do
+ def self.with_state
+ :with_state
+ end
+
+ def self.with_states
+ :with_states
+ end
+
+ def self.without_state
+ :without_state
+ end
+
+ def self.without_states
+ :without_states
+ end
+
+ def self.human_state_name
+ :human_state_name
+ end
+
+ def self.human_state_event_name
+ :human_state_event_name
+ end
+
+ attr_accessor :status
+
+ def state
+ 'parked'
+ end
+
+ def state=(value)
+ self.status = value
+ end
+
+ def state?
+ true
+ end
+
+ def state_name
+ :parked
+ end
+
+ def human_state_name
+ 'parked'
+ end
+
+ def state_events
+ [:ignite]
+ end
+
+ def state_transitions
+ [{ parked: :idling }]
+ end
+
+ def state_paths
+ [[{ parked: :idling }]]
+ end
+
+ def fire_state_event
+ true
+ end
+ end
+
+
+
+ @machine = StateMachines::Machine.new(@klass, integration: :custom)
+ @machine.state :parked, :idling
+ @machine.event :ignite
+ @object = @klass.new
+ end
+
+ def test_should_not_redefine_singular_with_scope
+ assert_equal :with_state, @klass.with_state
+ end
+
+ def test_should_not_redefine_plural_with_scope
+ assert_equal :with_states, @klass.with_states
+ end
+
+ def test_should_not_redefine_singular_without_scope
+ assert_equal :without_state, @klass.without_state
+ end
+
+ def test_should_not_redefine_plural_without_scope
+ assert_equal :without_states, @klass.without_states
+ end
+
+ def test_should_not_redefine_human_attribute_name_reader
+ assert_equal :human_state_name, @klass.human_state_name
+ end
+
+ def test_should_not_redefine_human_event_name_reader
+ assert_equal :human_state_event_name, @klass.human_state_event_name
+ end
+
+ def test_should_not_redefine_attribute_reader
+ assert_equal 'parked', @object.state
+ end
+
+ def test_should_not_redefine_attribute_writer
+ @object.state = 'parked'
+ assert_equal 'parked', @object.status
+ end
+
+ def test_should_not_define_attribute_predicate
+ assert @object.state?
+ end
+
+ def test_should_not_redefine_attribute_name_reader
+ assert_equal :parked, @object.state_name
+ end
+
+ def test_should_not_redefine_attribute_human_name_reader
+ assert_equal 'parked', @object.human_state_name
+ end
+
+ def test_should_not_redefine_attribute_events_reader
+ assert_equal [:ignite], @object.state_events
+ end
+
+ def test_should_not_redefine_attribute_transitions_reader
+ assert_equal [{ parked: :idling }], @object.state_transitions
+ end
+
+ def test_should_not_redefine_attribute_paths_reader
+ assert_equal [[{ parked: :idling }]], @object.state_paths
+ end
+
+ def test_should_not_redefine_event_runner
+ assert_equal true, @object.fire_state_event
+ end
+
+ def test_should_allow_super_chaining
+ @klass.class_eval do
+ def self.with_state(*states)
+ super
+ end
+
+ def self.with_states(*states)
+ super
+ end
+
+ def self.without_state(*states)
+ super
+ end
+
+ def self.without_states(*states)
+ super
+ end
+
+ def self.human_state_name(state)
+ super
+ end
+
+ def self.human_state_event_name(event)
+ super
+ end
+
+ attr_accessor :status
+
+ def state
+ super
+ end
+
+ def state=(value)
+ super
+ end
+
+ def state?(state)
+ super
+ end
+
+ def state_name
+ super
+ end
+
+ def human_state_name
+ super
+ end
+
+ def state_events
+ super
+ end
+
+ def state_transitions
+ super
+ end
+
+ def state_paths
+ super
+ end
+
+ def fire_state_event(event)
+ super
+ end
+ end
+
+ assert_equal [], @klass.with_state
+ assert_equal [], @klass.with_states
+ assert_equal [], @klass.without_state
+ assert_equal [], @klass.without_states
+ assert_equal 'parked', @klass.human_state_name(:parked)
+ assert_equal 'ignite', @klass.human_state_event_name(:ignite)
+
+ assert_equal nil, @object.state
+ @object.state = 'idling'
+ assert_equal 'idling', @object.state
+ assert_equal nil, @object.status
+ assert_equal false, @object.state?(:parked)
+ assert_equal :idling, @object.state_name
+ assert_equal 'idling', @object.human_state_name
+ assert_equal [], @object.state_events
+ assert_equal [], @object.state_transitions
+ assert_equal [], @object.state_paths
+ assert_equal false, @object.fire_state_event(:ignite)
+ end
+
+ def test_should_not_output_warning
+ assert_equal '', $stderr.string
+ end
+
+ def teardown
+ $stderr = @original_stderr
+ StateMachines::Integrations.reset
+ end
+end
diff --git a/test/unit/machine/machine_with_conflicting_helpers_before_definition_test.rb b/test/unit/machine/machine_with_conflicting_helpers_before_definition_test.rb
new file mode 100644
index 0000000..e242958
--- /dev/null
+++ b/test/unit/machine/machine_with_conflicting_helpers_before_definition_test.rb
@@ -0,0 +1,175 @@
+require_relative '../../test_helper'
+
+class MachineWithConflictingHelpersBeforeDefinitionTest < StateMachinesTest
+ module Custom
+ include StateMachines::Integrations::Base
+
+ def create_with_scope(_name)
+ lambda { |_klass, _values| [] }
+ end
+
+ def create_without_scope(_name)
+ lambda { |_klass, _values| [] }
+ end
+ end
+
+ def setup
+ @original_stderr, $stderr = $stderr, StringIO.new
+
+ StateMachines::Integrations.register(MachineWithConflictingHelpersBeforeDefinitionTest::Custom)
+
+ @superclass = Class.new do
+ def self.with_state
+ :with_state
+ end
+
+ def self.with_states
+ :with_states
+ end
+
+ def self.without_state
+ :without_state
+ end
+
+ def self.without_states
+ :without_states
+ end
+
+ def self.human_state_name
+ :human_state_name
+ end
+
+ def self.human_state_event_name
+ :human_state_event_name
+ end
+
+ attr_accessor :status
+
+ def state
+ 'parked'
+ end
+
+ def state=(value)
+ self.status = value
+ end
+
+ def state?
+ true
+ end
+
+ def state_name
+ :parked
+ end
+
+ def human_state_name
+ 'parked'
+ end
+
+ def state_events
+ [:ignite]
+ end
+
+ def state_transitions
+ [{ parked: :idling }]
+ end
+
+ def state_paths
+ [[{ parked: :idling }]]
+ end
+
+ def fire_state_event
+ true
+ end
+ end
+ @klass = Class.new(@superclass)
+ @machine = StateMachines::Machine.new(@klass, integration: :custom)
+ @machine.state :parked, :idling
+ @machine.event :ignite
+ @object = @klass.new
+ end
+
+ def test_should_not_redefine_singular_with_scope
+ assert_equal :with_state, @klass.with_state
+ end
+
+ def test_should_not_redefine_plural_with_scope
+ assert_equal :with_states, @klass.with_states
+ end
+
+ def test_should_not_redefine_singular_without_scope
+ assert_equal :without_state, @klass.without_state
+ end
+
+ def test_should_not_redefine_plural_without_scope
+ assert_equal :without_states, @klass.without_states
+ end
+
+ def test_should_not_redefine_human_attribute_name_reader
+ assert_equal :human_state_name, @klass.human_state_name
+ end
+
+ def test_should_not_redefine_human_event_name_reader
+ assert_equal :human_state_event_name, @klass.human_state_event_name
+ end
+
+ def test_should_not_redefine_attribute_reader
+ assert_equal 'parked', @object.state
+ end
+
+ def test_should_not_redefine_attribute_writer
+ @object.state = 'parked'
+ assert_equal 'parked', @object.status
+ end
+
+ def test_should_not_define_attribute_predicate
+ assert @object.state?
+ end
+
+ def test_should_not_redefine_attribute_name_reader
+ assert_equal :parked, @object.state_name
+ end
+
+ def test_should_not_redefine_attribute_human_name_reader
+ assert_equal 'parked', @object.human_state_name
+ end
+
+ def test_should_not_redefine_attribute_events_reader
+ assert_equal [:ignite], @object.state_events
+ end
+
+ def test_should_not_redefine_attribute_transitions_reader
+ assert_equal [{ parked: :idling }], @object.state_transitions
+ end
+
+ def test_should_not_redefine_attribute_paths_reader
+ assert_equal [[{ parked: :idling }]], @object.state_paths
+ end
+
+ def test_should_not_redefine_event_runner
+ assert_equal true, @object.fire_state_event
+ end
+
+ def test_should_output_warning
+ expected = [
+ 'Instance method "state_events"',
+ 'Instance method "state_transitions"',
+ 'Instance method "fire_state_event"',
+ 'Instance method "state_paths"',
+ 'Class method "human_state_name"',
+ 'Class method "human_state_event_name"',
+ 'Instance method "state_name"',
+ 'Instance method "human_state_name"',
+ 'Class method "with_state"',
+ 'Class method "with_states"',
+ 'Class method "without_state"',
+ 'Class method "without_states"'
+ ].map { |method| "#{method} is already defined in #{@superclass}, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true.\n" }.join
+
+ assert_equal expected, $stderr.string
+ end
+
+ def teardown
+ $stderr = @original_stderr
+ end
+end
+
diff --git a/test/unit/machine/machine_with_custom_action_test.rb b/test/unit/machine/machine_with_custom_action_test.rb
new file mode 100644
index 0000000..2252d47
--- /dev/null
+++ b/test/unit/machine/machine_with_custom_action_test.rb
@@ -0,0 +1,11 @@
+require_relative '../../test_helper'
+
+class MachineWithCustomActionTest < StateMachinesTest
+ def setup
+ @machine = StateMachines::Machine.new(Class.new, action: :save)
+ end
+
+ def test_should_use_the_custom_action
+ assert_equal :save, @machine.action
+ end
+end
diff --git a/test/unit/machine/machine_with_custom_attribute_test.rb b/test/unit/machine/machine_with_custom_attribute_test.rb
new file mode 100644
index 0000000..5acef98
--- /dev/null
+++ b/test/unit/machine/machine_with_custom_attribute_test.rb
@@ -0,0 +1,103 @@
+require_relative '../../test_helper'
+
+module MachineWithCustomAttributeIntegration
+ include StateMachines::Integrations::Base
+
+ def self.integration_name
+ :custom_attribute
+ end
+
+ @defaults = { action: :save, use_transactions: false }
+
+ def create_with_scope(_name)
+ -> {}
+ end
+
+ def create_without_scope(_name)
+ -> {}
+ end
+end
+
+class MachineWithCustomAttributeTest < StateMachinesTest
+ def setup
+ StateMachines::Integrations.register(MachineWithCustomAttributeIntegration)
+
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass, :state, attribute: :state_id, initial: :active, integration: :custom_attribute) do
+ event :ignite do
+ transition parked: :idling
+ end
+ end
+ @object = @klass.new
+ end
+
+ def test_should_define_a_reader_attribute_for_the_attribute
+ assert @object.respond_to?(:state_id)
+ end
+
+ def test_should_define_a_writer_attribute_for_the_attribute
+ assert @object.respond_to?(:state_id=)
+ end
+
+ def test_should_define_a_predicate_for_the_attribute
+ assert @object.respond_to?(:state?)
+ end
+
+ def test_should_define_a_name_reader_for_the_attribute
+ assert @object.respond_to?(:state_name)
+ end
+
+ def test_should_define_a_human_name_reader_for_the_attribute
+ assert @object.respond_to?(:state_name)
+ end
+
+ def test_should_define_an_event_reader_for_the_attribute
+ assert @object.respond_to?(:state_events)
+ end
+
+ def test_should_define_a_transition_reader_for_the_attribute
+ assert @object.respond_to?(:state_transitions)
+ end
+
+ def test_should_define_a_path_reader_for_the_attribute
+ assert @object.respond_to?(:state_paths)
+ end
+
+ def test_should_define_an_event_runner_for_the_attribute
+ assert @object.respond_to?(:fire_state_event)
+ end
+
+ def test_should_define_a_human_attribute_name_reader
+ assert @klass.respond_to?(:human_state_name)
+ end
+
+ def test_should_define_a_human_event_name_reader
+ assert @klass.respond_to?(:human_state_event_name)
+ end
+
+ def test_should_define_singular_with_scope
+ assert @klass.respond_to?(:with_state)
+ end
+
+ def test_should_define_singular_without_scope
+ assert @klass.respond_to?(:without_state)
+ end
+
+ def test_should_define_plural_with_scope
+ assert @klass.respond_to?(:with_states)
+ end
+
+ def test_should_define_plural_without_scope
+ assert @klass.respond_to?(:without_states)
+ end
+
+ def test_should_define_state_machines_reader
+ expected = { state: @machine }
+ assert_equal expected, @klass.state_machines
+ end
+
+ def teardown
+ StateMachines::Integrations.reset
+ end
+end
+
diff --git a/test/unit/machine/machine_with_custom_initialize_test.rb b/test/unit/machine/machine_with_custom_initialize_test.rb
new file mode 100644
index 0000000..54dc9dd
--- /dev/null
+++ b/test/unit/machine/machine_with_custom_initialize_test.rb
@@ -0,0 +1,24 @@
+require_relative '../../test_helper'
+
+class MachineWithCustomInitializeTest < StateMachinesTest
+ def setup
+ @klass = Class.new do
+ def initialize(state = nil, options = {})
+ @state = state
+ initialize_state_machines(options)
+ end
+ end
+ @machine = StateMachines::Machine.new(@klass, initial: :parked)
+ @object = @klass.new
+ end
+
+ def test_should_initialize_state
+ assert_equal 'parked', @object.state
+ end
+
+ def test_should_allow_custom_options
+ @machine.state :idling
+ @object = @klass.new('idling', static: :force)
+ assert_equal 'parked', @object.state
+ end
+end
diff --git a/test/unit/machine/machine_with_custom_integration_test.rb b/test/unit/machine/machine_with_custom_integration_test.rb
new file mode 100644
index 0000000..da319dd
--- /dev/null
+++ b/test/unit/machine/machine_with_custom_integration_test.rb
@@ -0,0 +1,72 @@
+require_relative '../../test_helper'
+require_relative '../../../test/files/models/vehicle'
+
+class MachineWithCustomIntegrationTest < StateMachinesTest
+ module Custom
+ include StateMachines::Integrations::Base
+
+ def self.matching_ancestors
+ ['Vehicle']
+ end
+ end
+
+ def setup
+ StateMachines::Integrations.register(MachineWithCustomIntegrationTest::Custom)
+
+ @klass = Vehicle
+ end
+
+ def test_should_be_extended_by_the_integration_if_explicit
+ machine = StateMachines::Machine.new(@klass, integration: :custom)
+ assert((class << machine; ancestors; end).include?(MachineWithCustomIntegrationTest::Custom))
+ end
+
+ def test_should_not_be_extended_by_the_integration_if_implicit_but_not_available
+ MachineWithCustomIntegrationTest::Custom.class_eval do
+ class << self; remove_method :matching_ancestors; end
+ def self.matching_ancestors
+ []
+ end
+ end
+
+ machine = StateMachines::Machine.new(@klass)
+ assert(!(class << machine; ancestors; end).include?(MachineWithCustomIntegrationTest::Custom))
+ end
+
+ def test_should_not_be_extended_by_the_integration_if_implicit_but_not_matched
+ MachineWithCustomIntegrationTest::Custom.class_eval do
+ class << self; remove_method :matching_ancestors; end
+ def self.matching_ancestors
+ []
+ end
+ end
+
+ machine = StateMachines::Machine.new(@klass)
+ assert(!(class << machine; ancestors; end).include?(MachineWithCustomIntegrationTest::Custom))
+ end
+
+ def test_should_be_extended_by_the_integration_if_implicit_and_available_and_matches
+ machine = StateMachines::Machine.new(@klass)
+ assert((class << machine; ancestors; end).include?(MachineWithCustomIntegrationTest::Custom))
+ end
+
+ def test_should_not_be_extended_by_the_integration_if_nil
+ machine = StateMachines::Machine.new(@klass, integration: nil)
+ assert(!(class << machine; ancestors; end).include?(MachineWithCustomIntegrationTest::Custom))
+ end
+
+ def test_should_not_be_extended_by_the_integration_if_false
+ machine = StateMachines::Machine.new(@klass, integration: false)
+ assert(!(class << machine; ancestors; end).include?(MachineWithCustomIntegrationTest::Custom))
+ end
+
+ def teardown
+ StateMachines::Integrations.reset
+ MachineWithCustomIntegrationTest::Custom.class_eval do
+ class << self; remove_method :matching_ancestors; end
+ def self.matching_ancestors
+ ['Vehicle']
+ end
+ end
+ end
+end
diff --git a/test/unit/machine/machine_with_custom_invalidation_test.rb b/test/unit/machine/machine_with_custom_invalidation_test.rb
new file mode 100644
index 0000000..fe7371d
--- /dev/null
+++ b/test/unit/machine/machine_with_custom_invalidation_test.rb
@@ -0,0 +1,39 @@
+require_relative '../../test_helper'
+
+class MachineWithCustomInvalidationTest < StateMachinesTest
+ module Custom
+ include StateMachines::Integrations::Base
+
+ def invalidate(object, _attribute, message, values = [])
+ object.error = generate_message(message, values)
+ end
+ end
+
+ def setup
+ StateMachines::Integrations.register(MachineWithCustomInvalidationTest::Custom)
+
+ @klass = Class.new do
+ attr_accessor :error
+ end
+
+ @machine = StateMachines::Machine.new(@klass, integration: :custom, messages: {invalid_transition: 'cannot %s'})
+ @machine.state :parked
+
+ @object = @klass.new
+ @object.state = 'parked'
+ end
+
+ def test_generate_custom_message
+ assert_equal 'cannot park', @machine.generate_message(:invalid_transition, [[:event, :park]])
+ end
+
+ def test_use_custom_message
+ @machine.invalidate(@object, :state, :invalid_transition, [[:event, 'park']])
+ assert_equal 'cannot park', @object.error
+ end
+
+ def teardown
+ StateMachines::Integrations.reset
+ end
+end
+
diff --git a/test/unit/machine/machine_with_custom_name_test.rb b/test/unit/machine/machine_with_custom_name_test.rb
new file mode 100644
index 0000000..402fdf8
--- /dev/null
+++ b/test/unit/machine/machine_with_custom_name_test.rb
@@ -0,0 +1,57 @@
+require_relative '../../test_helper'
+
+class MachineWithCustomNameTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass, :status)
+ @object = @klass.new
+ end
+
+ def test_should_use_custom_name
+ assert_equal :status, @machine.name
+ end
+
+ def test_should_use_custom_name_for_attribute
+ assert_equal :status, @machine.attribute
+ end
+
+ def test_should_prefix_custom_attributes_with_custom_name
+ assert_equal :status_event, @machine.attribute(:event)
+ end
+
+ def test_should_define_a_reader_attribute_for_the_attribute
+ assert @object.respond_to?(:status)
+ end
+
+ def test_should_define_a_writer_attribute_for_the_attribute
+ assert @object.respond_to?(:status=)
+ end
+
+ def test_should_define_a_predicate_for_the_attribute
+ assert @object.respond_to?(:status?)
+ end
+
+ def test_should_define_a_name_reader_for_the_attribute
+ assert @object.respond_to?(:status_name)
+ end
+
+ def test_should_define_an_event_reader_for_the_attribute
+ assert @object.respond_to?(:status_events)
+ end
+
+ def test_should_define_a_transition_reader_for_the_attribute
+ assert @object.respond_to?(:status_transitions)
+ end
+
+ def test_should_define_an_event_runner_for_the_attribute
+ assert @object.respond_to?(:fire_status_event)
+ end
+
+ def test_should_define_a_human_attribute_name_reader_for_the_attribute
+ assert @klass.respond_to?(:human_status_name)
+ end
+
+ def test_should_define_a_human_event_name_reader_for_the_attribute
+ assert @klass.respond_to?(:human_status_event_name)
+ end
+end
diff --git a/test/unit/machine/machine_with_custom_plural_test.rb b/test/unit/machine/machine_with_custom_plural_test.rb
new file mode 100644
index 0000000..c42388b
--- /dev/null
+++ b/test/unit/machine/machine_with_custom_plural_test.rb
@@ -0,0 +1,52 @@
+require_relative '../../test_helper'
+
+class MachineWithCustomPluralTest < StateMachinesTest
+ def setup
+ @integration = Module.new do
+ include StateMachines::Integrations::Base
+
+ class << self; attr_accessor :with_scopes, :without_scopes; end
+ @with_scopes = []
+ @without_scopes = []
+
+ def create_with_scope(name)
+ MachineWithCustomPluralTest::Custom.with_scopes << name
+ lambda {}
+ end
+
+ def create_without_scope(name)
+ MachineWithCustomPluralTest::Custom.without_scopes << name
+ lambda {}
+ end
+ end
+
+ MachineWithCustomPluralTest.const_set('Custom', @integration)
+ StateMachines::Integrations.register(MachineWithCustomPluralTest::Custom)
+ end
+
+ def test_should_define_a_singular_and_plural_with_scope
+ StateMachines::Machine.new(Class.new, integration: :custom, plural: 'staties')
+ assert_equal %w(with_state with_staties), @integration.with_scopes
+ end
+
+ def test_should_define_a_singular_and_plural_without_scope
+ StateMachines::Machine.new(Class.new, integration: :custom, plural: 'staties')
+ assert_equal %w(without_state without_staties), @integration.without_scopes
+ end
+
+ def test_should_define_single_with_scope_if_singular_same_as_plural
+ StateMachines::Machine.new(Class.new, integration: :custom, plural: 'state')
+ assert_equal %w(with_state), @integration.with_scopes
+ end
+
+ def test_should_define_single_without_scope_if_singular_same_as_plural
+ StateMachines::Machine.new(Class.new, integration: :custom, plural: 'state')
+ assert_equal %w(without_state), @integration.without_scopes
+ end
+
+ def teardown
+ StateMachines::Integrations.reset
+ MachineWithCustomPluralTest.send(:remove_const, 'Custom')
+ end
+end
+
diff --git a/test/unit/machine/machine_with_dynamic_initial_state_test.rb b/test/unit/machine/machine_with_dynamic_initial_state_test.rb
new file mode 100644
index 0000000..13f49cc
--- /dev/null
+++ b/test/unit/machine/machine_with_dynamic_initial_state_test.rb
@@ -0,0 +1,65 @@
+require_relative '../../test_helper'
+
+class MachineWithDynamicInitialStateTest < StateMachinesTest
+ def setup
+ @klass = Class.new do
+ attr_accessor :initial_state
+ end
+ @machine = StateMachines::Machine.new(@klass, initial: lambda { |object| object.initial_state || :default })
+ @machine.state :parked, :idling, :default
+ @object = @klass.new
+ end
+
+ def test_should_have_dynamic_initial_state
+ assert @machine.dynamic_initial_state?
+ end
+
+ def test_should_use_the_record_for_determining_the_initial_state
+ @object.initial_state = :parked
+ assert_equal :parked, @machine.initial_state(@object).name
+
+ @object.initial_state = :idling
+ assert_equal :idling, @machine.initial_state(@object).name
+ end
+
+ def test_should_write_to_attribute_when_initializing_state
+ object = @klass.allocate
+ object.initial_state = :parked
+ @machine.initialize_state(object)
+ assert_equal 'parked', object.state
+ end
+
+ def test_should_set_initial_state_on_created_object
+ assert_equal 'default', @object.state
+ end
+
+ def test_should_not_set_initial_state_even_if_not_empty
+ @klass.class_eval do
+ def initialize(_attributes = {})
+ self.state = 'parked'
+ super()
+ end
+ end
+ object = @klass.new
+ assert_equal 'parked', object.state
+ end
+
+ def test_should_set_initial_state_after_initialization
+ base = Class.new do
+ attr_accessor :state_on_init
+
+ def initialize
+ self.state_on_init = state
+ end
+ end
+ klass = Class.new(base)
+ machine = StateMachines::Machine.new(klass, initial: lambda { |_object| :parked })
+ machine.state :parked
+
+ assert_nil klass.new.state_on_init
+ end
+
+ def test_should_not_be_included_in_known_states
+ assert_equal [:parked, :idling, :default], @machine.states.map { |state| state.name }
+ end
+end
diff --git a/test/unit/machine/machine_with_event_matchers_test.rb b/test/unit/machine/machine_with_event_matchers_test.rb
new file mode 100644
index 0000000..58350ba
--- /dev/null
+++ b/test/unit/machine/machine_with_event_matchers_test.rb
@@ -0,0 +1,41 @@
+require_relative '../../test_helper'
+
+class MachineWithEventMatchersTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ end
+
+ def test_should_empty_array_for_all_matcher
+ assert_equal [], @machine.event(StateMachines::AllMatcher.instance)
+ end
+
+ def test_should_return_referenced_events_for_blacklist_matcher
+ assert_instance_of StateMachines::Event, @machine.event(StateMachines::BlacklistMatcher.new([:park]))
+ end
+
+ def test_should_not_allow_configurations
+ exception = assert_raises(ArgumentError) { @machine.event(StateMachines::BlacklistMatcher.new([:park]), human_name: 'Park') }
+ assert_equal 'Cannot configure events when using matchers (using {:human_name=>"Park"})', exception.message
+ end
+
+ def test_should_track_referenced_events
+ @machine.event(StateMachines::BlacklistMatcher.new([:park]))
+ assert_equal [:park], @machine.events.map { |event| event.name }
+ end
+
+ def test_should_eval_context_for_matching_events
+ contexts_run = []
+ @machine.event(StateMachines::BlacklistMatcher.new([:park])) { contexts_run << name }
+
+ @machine.event :park
+ assert_equal [], contexts_run
+
+ @machine.event :ignite
+ assert_equal [:ignite], contexts_run
+
+ @machine.event :shift_up, :shift_down
+ assert_equal [:ignite, :shift_up, :shift_down], contexts_run
+ end
+end
+
diff --git a/test/unit/machine/machine_with_events_test.rb b/test/unit/machine/machine_with_events_test.rb
new file mode 100644
index 0000000..4cbec00
--- /dev/null
+++ b/test/unit/machine/machine_with_events_test.rb
@@ -0,0 +1,52 @@
+require_relative '../../test_helper'
+
+class MachineWithEventsTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ end
+
+ def test_should_return_the_created_event
+ assert_instance_of StateMachines::Event, @machine.event(:ignite)
+ end
+
+ def test_should_create_event_with_given_name
+ event = @machine.event(:ignite) {}
+ assert_equal :ignite, event.name
+ end
+
+ def test_should_evaluate_block_within_event_context
+ responded = false
+ @machine.event :ignite do
+ responded = respond_to?(:transition)
+ end
+
+ assert responded
+ end
+
+ def test_should_be_aliased_as_on
+ event = @machine.on(:ignite) {}
+ assert_equal :ignite, event.name
+ end
+
+ def test_should_have_events
+ event = @machine.event(:ignite)
+ assert_equal [event], @machine.events.to_a
+ end
+
+ def test_should_allow_human_state_name_lookup
+ @machine.event(:ignite)
+ assert_equal 'ignite', @klass.human_state_event_name(:ignite)
+ end
+
+ def test_should_raise_exception_on_invalid_human_state_event_name_lookup
+ exception = assert_raises(IndexError) { @klass.human_state_event_name(:invalid) }
+ assert_equal ':invalid is an invalid name', exception.message
+ end
+
+ def test_should_raise_exception_if_conflicting_type_used_for_name
+ @machine.event :park
+ exception = assert_raises(ArgumentError) { @machine.event 'ignite' }
+ assert_equal '"ignite" event defined as String, :park defined as Symbol; all events must be consistent', exception.message
+ end
+end
diff --git a/test/unit/machine/machine_with_events_with_custom_human_names_test.rb b/test/unit/machine/machine_with_events_with_custom_human_names_test.rb
new file mode 100644
index 0000000..1aec611
--- /dev/null
+++ b/test/unit/machine/machine_with_events_with_custom_human_names_test.rb
@@ -0,0 +1,18 @@
+require_relative '../../test_helper'
+
+class MachineWithEventsWithCustomHumanNamesTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @event = @machine.event(:ignite, human_name: 'start')
+ end
+
+ def test_should_use_custom_human_name
+ assert_equal 'start', @event.human_name
+ end
+
+ def test_should_allow_human_state_name_lookup
+ assert_equal 'start', @klass.human_state_event_name(:ignite)
+ end
+end
+
diff --git a/test/unit/machine/machine_with_events_with_transitions_test.rb b/test/unit/machine/machine_with_events_with_transitions_test.rb
new file mode 100644
index 0000000..b30a744
--- /dev/null
+++ b/test/unit/machine/machine_with_events_with_transitions_test.rb
@@ -0,0 +1,37 @@
+require_relative '../../test_helper'
+
+class MachineWithEventsWithTransitionsTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass, initial: :parked)
+ @event = @machine.event(:ignite) do
+ transition parked: :idling
+ transition stalled: :idling
+ end
+ end
+
+ def test_should_have_events
+ assert_equal [@event], @machine.events.to_a
+ end
+
+ def test_should_track_states_defined_in_event_transitions
+ assert_equal [:parked, :idling, :stalled], @machine.states.map { |state| state.name }
+ end
+
+ def test_should_not_duplicate_states_defined_in_multiple_event_transitions
+ @machine.event :park do
+ transition idling: :parked
+ end
+
+ assert_equal [:parked, :idling, :stalled], @machine.states.map { |state| state.name }
+ end
+
+ def test_should_track_state_from_new_events
+ @machine.event :shift_up do
+ transition idling: :first_gear
+ end
+
+ assert_equal [:parked, :idling, :stalled, :first_gear], @machine.states.map { |state| state.name }
+ end
+end
+
diff --git a/test/unit/machine/machine_with_existing_event_test.rb b/test/unit/machine/machine_with_existing_event_test.rb
new file mode 100644
index 0000000..40ab000
--- /dev/null
+++ b/test/unit/machine/machine_with_existing_event_test.rb
@@ -0,0 +1,17 @@
+require_relative '../../test_helper'
+
+class MachineWithExistingEventTest < StateMachinesTest
+ def setup
+ @machine = StateMachines::Machine.new(Class.new)
+ @event = @machine.event(:ignite)
+ @same_event = @machine.event(:ignite)
+ end
+
+ def test_should_not_create_new_event
+ assert_same @event, @same_event
+ end
+
+ def test_should_allow_accessing_event_without_block
+ assert_equal @event, @machine.event(:ignite)
+ end
+end
diff --git a/test/unit/machine/machine_with_existing_machines_on_owner_class_test.rb b/test/unit/machine/machine_with_existing_machines_on_owner_class_test.rb
new file mode 100644
index 0000000..8c6129a
--- /dev/null
+++ b/test/unit/machine/machine_with_existing_machines_on_owner_class_test.rb
@@ -0,0 +1,20 @@
+require_relative '../../test_helper'
+
+class MachineWithExistingMachinesOnOwnerClassTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass, initial: :parked)
+ @second_machine = StateMachines::Machine.new(@klass, :status, initial: :idling)
+ @object = @klass.new
+ end
+
+ def test_should_track_each_state_machine
+ expected = { state: @machine, status: @second_machine }
+ assert_equal expected, @klass.state_machines
+ end
+
+ def test_should_initialize_state_for_both_machines
+ assert_equal 'parked', @object.state
+ assert_equal 'idling', @object.status
+ end
+end
diff --git a/test/unit/machine/machine_with_existing_machines_with_same_attributes_on_owner_class_test.rb b/test/unit/machine/machine_with_existing_machines_with_same_attributes_on_owner_class_test.rb
new file mode 100644
index 0000000..99d898e
--- /dev/null
+++ b/test/unit/machine/machine_with_existing_machines_with_same_attributes_on_owner_class_test.rb
@@ -0,0 +1,71 @@
+require_relative '../../test_helper'
+
+class MachineWithExistingMachinesWithSameAttributesOnOwnerClassTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass, initial: :parked)
+ @second_machine = StateMachines::Machine.new(@klass, :public_state, initial: :idling, attribute: :state)
+ @object = @klass.new
+ end
+
+ def test_should_track_each_state_machine
+ expected = { state: @machine, public_state: @second_machine }
+ assert_equal expected, @klass.state_machines
+ end
+
+ def test_should_write_to_state_only_once
+ @klass.class_eval do
+ attr_reader :write_count
+
+ def state=(_value)
+ @write_count ||= 0
+ @write_count += 1
+ end
+ end
+ object = @klass.new
+
+ assert_equal 1, object.write_count
+ end
+
+ def test_should_initialize_based_on_first_machine
+ assert_equal 'parked', @object.state
+ end
+
+ def test_should_not_allow_second_machine_to_initialize_state
+ @object.state = nil
+ @second_machine.initialize_state(@object)
+ assert_nil @object.state
+ end
+
+ def test_should_allow_transitions_on_both_machines
+ @machine.event :ignite do
+ transition parked: :idling
+ end
+
+ @second_machine.event :park do
+ transition idling: :parked
+ end
+
+ @object.ignite
+ assert_equal 'idling', @object.state
+
+ @object.park
+ assert_equal 'parked', @object.state
+ end
+
+ def test_should_copy_new_states_to_sibling_machines
+ @first_gear = @machine.state :first_gear
+ assert_equal @first_gear, @second_machine.state(:first_gear)
+
+ @second_gear = @second_machine.state :second_gear
+ assert_equal @second_gear, @machine.state(:second_gear)
+ end
+
+ def test_should_copy_all_existing_states_to_new_machines
+ third_machine = StateMachines::Machine.new(@klass, :protected_state, attribute: :state)
+
+ assert_equal @machine.state(:parked), third_machine.state(:parked)
+ assert_equal @machine.state(:idling), third_machine.state(:idling)
+ end
+end
+
diff --git a/test/unit/machine/machine_with_existing_machines_with_same_attributes_on_owner_subclass_test.rb b/test/unit/machine/machine_with_existing_machines_with_same_attributes_on_owner_subclass_test.rb
new file mode 100644
index 0000000..f00d369
--- /dev/null
+++ b/test/unit/machine/machine_with_existing_machines_with_same_attributes_on_owner_subclass_test.rb
@@ -0,0 +1,31 @@
+require_relative '../../test_helper'
+
+class MachineWithExistingMachinesWithSameAttributesOnOwnerSubclassTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass, initial: :parked)
+ @second_machine = StateMachines::Machine.new(@klass, :public_state, initial: :idling, attribute: :state)
+
+ @subclass = Class.new(@klass)
+ @object = @subclass.new
+ end
+
+ def test_should_not_copy_sibling_machines_to_subclass_after_initialization
+ @subclass.state_machine(:state) {}
+ assert_equal @klass.state_machine(:public_state), @subclass.state_machine(:public_state)
+ end
+
+ def test_should_copy_sibling_machines_to_subclass_after_new_state
+ subclass_machine = @subclass.state_machine(:state) {}
+ subclass_machine.state :first_gear
+ refute_equal @klass.state_machine(:public_state), @subclass.state_machine(:public_state)
+ end
+
+ def test_should_copy_new_states_to_sibling_machines
+ subclass_machine = @subclass.state_machine(:state) {}
+ @first_gear = subclass_machine.state :first_gear
+
+ second_subclass_machine = @subclass.state_machine(:public_state)
+ assert_equal @first_gear, second_subclass_machine.state(:first_gear)
+ end
+end
diff --git a/test/unit/machine/machine_with_existing_state_test.rb b/test/unit/machine/machine_with_existing_state_test.rb
new file mode 100644
index 0000000..0950ec3
--- /dev/null
+++ b/test/unit/machine/machine_with_existing_state_test.rb
@@ -0,0 +1,27 @@
+require_relative '../../test_helper'
+
+class MachineWithExistingStateTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @state = @machine.state :parked
+ @same_state = @machine.state :parked, value: 1
+ end
+
+ def test_should_not_create_a_new_state
+ assert_same @state, @same_state
+ end
+
+ def test_should_update_attributes
+ assert_equal 1, @state.value
+ end
+
+ def test_should_no_longer_be_able_to_look_up_state_by_original_value
+ assert_nil @machine.states['parked', :value]
+ end
+
+ def test_should_be_able_to_look_up_state_by_new_value
+ assert_equal @state, @machine.states[1, :value]
+ end
+end
+
diff --git a/test/unit/machine/machine_with_failure_callbacks_test.rb b/test/unit/machine/machine_with_failure_callbacks_test.rb
new file mode 100644
index 0000000..929aa37
--- /dev/null
+++ b/test/unit/machine/machine_with_failure_callbacks_test.rb
@@ -0,0 +1,48 @@
+require_relative '../../test_helper'
+
+class MachineWithFailureCallbacksTest < StateMachinesTest
+ def setup
+ @klass = Class.new do
+ attr_accessor :callbacks
+ end
+
+ @machine = StateMachines::Machine.new(@klass, initial: :parked)
+ @event = @machine.event :ignite
+
+ @object = @klass.new
+ @object.callbacks = []
+ end
+
+ def test_should_raise_exception_if_implicit_option_specified
+ exception = assert_raises(ArgumentError) { @machine.after_failure invalid: :valid, do: lambda {} }
+ assert_equal 'Unknown key: :invalid. Valid keys are: :on, :do, :if, :unless', exception.message
+ end
+
+ def test_should_raise_exception_if_method_not_specified
+ exception = assert_raises(ArgumentError) { @machine.after_failure on: :ignite }
+ assert_equal 'Method(s) for callback must be specified', exception.message
+ end
+
+ def test_should_invoke_callbacks_during_failed_transition
+ @machine.after_failure lambda { |object| object.callbacks << 'failure' }
+
+ @event.fire(@object)
+ assert_equal %w(failure), @object.callbacks
+ end
+
+ def test_should_allow_multiple_callbacks
+ @machine.after_failure lambda { |object| object.callbacks << 'failure1' }, lambda { |object| object.callbacks << 'failure2' }
+
+ @event.fire(@object)
+ assert_equal %w(failure1 failure2), @object.callbacks
+ end
+
+ def test_should_allow_multiple_callbacks_with_requirements
+ @machine.after_failure lambda { |object| object.callbacks << 'failure_ignite1' }, lambda { |object| object.callbacks << 'failure_ignite2' }, on: :ignite
+ @machine.after_failure lambda { |object| object.callbacks << 'failure_park1' }, lambda { |object| object.callbacks << 'failure_park2' }, on: :park
+
+ @event.fire(@object)
+ assert_equal %w(failure_ignite1 failure_ignite2), @object.callbacks
+ end
+end
+
diff --git a/test/unit/machine/machine_with_helpers_test.rb b/test/unit/machine/machine_with_helpers_test.rb
new file mode 100644
index 0000000..252938c
--- /dev/null
+++ b/test/unit/machine/machine_with_helpers_test.rb
@@ -0,0 +1,14 @@
+require_relative '../../test_helper'
+
+class MachineWithHelpersTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @object = @klass.new
+ end
+
+ def test_should_throw_exception_with_invalid_scope
+ assert_raises(RUBY_VERSION < '1.9' ? IndexError : KeyError) { @machine.define_helper(:invalid, :park) {} }
+ end
+end
+
diff --git a/test/unit/machine/machine_with_initial_state_with_value_and_owner_default.rb b/test/unit/machine/machine_with_initial_state_with_value_and_owner_default.rb
new file mode 100644
index 0000000..3e605d9
--- /dev/null
+++ b/test/unit/machine/machine_with_initial_state_with_value_and_owner_default.rb
@@ -0,0 +1,25 @@
+require_relative '../../test_helper'
+
+class MachineWithInitialStateWithValueAndOwnerDefault < StateMachinesTest
+ def setup
+ @original_stderr, $stderr = $stderr, StringIO.new
+
+ state_machine_with_defaults = Class.new(StateMachines::Machine) do
+ def owner_class_attribute_default
+ 0
+ end
+ end
+ @klass = Class.new
+ @machine = state_machine_with_defaults.new(@klass, initial: :parked) do
+ state :parked, value: 0
+ end
+ end
+
+ def test_should_not_warn_about_wrong_default
+ assert_equal '', $stderr.string
+ end
+
+ def teardown
+ $stderr = @original_stderr
+ end
+end
diff --git a/test/unit/machine/machine_with_initialize_and_super_test.rb b/test/unit/machine/machine_with_initialize_and_super_test.rb
new file mode 100644
index 0000000..f4b01af
--- /dev/null
+++ b/test/unit/machine/machine_with_initialize_and_super_test.rb
@@ -0,0 +1,17 @@
+require_relative '../../test_helper'
+
+class MachineWithInitializeAndSuperTest < StateMachinesTest
+ def setup
+ @klass = Class.new do
+ def initialize
+ super()
+ end
+ end
+ @machine = StateMachines::Machine.new(@klass, initial: :parked)
+ @object = @klass.new
+ end
+
+ def test_should_initialize_state
+ assert_equal 'parked', @object.state
+ end
+end
diff --git a/test/unit/machine/machine_with_initialize_arguments_and_block_test.rb b/test/unit/machine/machine_with_initialize_arguments_and_block_test.rb
new file mode 100644
index 0000000..0a57079
--- /dev/null
+++ b/test/unit/machine/machine_with_initialize_arguments_and_block_test.rb
@@ -0,0 +1,31 @@
+require_relative '../../test_helper'
+
+class MachineWithInitializeArgumentsAndBlockTest < StateMachinesTest
+ def setup
+ @superclass = Class.new do
+ attr_reader :args
+ attr_reader :block_given
+
+ def initialize(*args)
+ @args = args
+ @block_given = block_given?
+ end
+ end
+ @klass = Class.new(@superclass)
+ @machine = StateMachines::Machine.new(@klass, initial: :parked)
+ @object = @klass.new(1, 2, 3) {}
+ end
+
+ def test_should_initialize_state
+ assert_equal 'parked', @object.state
+ end
+
+ def test_should_preserve_arguments
+ assert_equal [1, 2, 3], @object.args
+ end
+
+ def test_should_preserve_block
+ assert @object.block_given
+ end
+end
+
diff --git a/test/unit/machine/machine_with_initialize_without_super_test.rb b/test/unit/machine/machine_with_initialize_without_super_test.rb
new file mode 100644
index 0000000..49bffee
--- /dev/null
+++ b/test/unit/machine/machine_with_initialize_without_super_test.rb
@@ -0,0 +1,17 @@
+require_relative '../../test_helper'
+
+class MachineWithInitializeWithoutSuperTest < StateMachinesTest
+ def setup
+ @klass = Class.new do
+ def initialize
+ end
+ end
+ @machine = StateMachines::Machine.new(@klass, initial: :parked)
+ @object = @klass.new
+ end
+
+ def test_should_not_initialize_state
+ assert_nil @object.state
+ end
+end
+
diff --git a/test/unit/machine/machine_with_instance_helpers_test.rb b/test/unit/machine/machine_with_instance_helpers_test.rb
new file mode 100644
index 0000000..485b573
--- /dev/null
+++ b/test/unit/machine/machine_with_instance_helpers_test.rb
@@ -0,0 +1,179 @@
+require_relative '../../test_helper'
+
+class MachineWithInstanceHelpersTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @object = @klass.new
+ end
+
+ def test_should_not_redefine_existing_public_methods
+ @klass.class_eval do
+ def park
+ true
+ end
+ end
+
+ @machine.define_helper(:instance, :park) {}
+ assert_equal true, @object.park
+ end
+
+ def test_should_not_redefine_existing_protected_methods
+ @klass.class_eval do
+ protected
+ def park
+ true
+ end
+ end
+
+ @machine.define_helper(:instance, :park) {}
+ assert_equal true, @object.send(:park)
+ end
+
+ def test_should_not_redefine_existing_private_methods
+ @klass.class_eval do
+ private
+ def park
+ true
+ end
+ end
+
+ @machine.define_helper(:instance, :park) {}
+ assert_equal true, @object.send(:park)
+ end
+
+ def test_should_warn_if_defined_in_superclass
+ @original_stderr, $stderr = $stderr, StringIO.new
+
+ superclass = Class.new do
+ def park
+ end
+ end
+ klass = Class.new(superclass)
+ machine = StateMachines::Machine.new(klass)
+
+ machine.define_helper(:instance, :park) {}
+ assert_equal "Instance method \"park\" is already defined in #{superclass}, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true.\n", $stderr.string
+ ensure
+ $stderr = @original_stderr
+ end
+
+ def test_should_warn_if_defined_in_multiple_superclasses
+ @original_stderr, $stderr = $stderr, StringIO.new
+
+ superclass1 = Class.new do
+ def park
+ end
+ end
+ superclass2 = Class.new(superclass1) do
+ def park
+ end
+ end
+ klass = Class.new(superclass2)
+ machine = StateMachines::Machine.new(klass)
+
+ machine.define_helper(:instance, :park) {}
+ assert_equal "Instance method \"park\" is already defined in #{superclass1}, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true.\n", $stderr.string
+ ensure
+ $stderr = @original_stderr
+ end
+
+ def test_should_warn_if_defined_in_module_prior_to_helper_module
+ @original_stderr, $stderr = $stderr, StringIO.new
+
+ mod = Module.new do
+ def park
+ end
+ end
+ klass = Class.new do
+ include mod
+ end
+ machine = StateMachines::Machine.new(klass)
+
+ machine.define_helper(:instance, :park) {}
+ assert_equal "Instance method \"park\" is already defined in #{mod}, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true.\n", $stderr.string
+ ensure
+ $stderr = @original_stderr
+ end
+
+ def test_should_not_warn_if_defined_in_module_after_helper_module
+ @original_stderr, $stderr = $stderr, StringIO.new
+
+ klass = Class.new
+ machine = StateMachines::Machine.new(klass)
+
+ mod = Module.new do
+ def park
+ end
+ end
+ klass.class_eval do
+ include mod
+ end
+
+ machine.define_helper(:instance, :park) {}
+ assert_equal '', $stderr.string
+ ensure
+ $stderr = @original_stderr
+ end
+
+ def test_should_define_if_ignoring_method_conflicts_and_defined_in_superclass
+ @original_stderr, $stderr = $stderr, StringIO.new
+ StateMachines::Machine.ignore_method_conflicts = true
+
+ superclass = Class.new do
+ def park
+ end
+ end
+ klass = Class.new(superclass)
+ machine = StateMachines::Machine.new(klass)
+
+ machine.define_helper(:instance, :park) { true }
+ assert_equal '', $stderr.string
+ assert_equal true, klass.new.park
+ ensure
+ StateMachines::Machine.ignore_method_conflicts = false
+ $stderr = @original_stderr
+ end
+
+ def test_should_define_nonexistent_methods
+ @machine.define_helper(:instance, :park) { false }
+ assert_equal false, @object.park
+ end
+
+ def test_should_warn_if_defined_multiple_times
+ @original_stderr, $stderr = $stderr, StringIO.new
+
+ @machine.define_helper(:instance, :park) {}
+ @machine.define_helper(:instance, :park) {}
+
+ assert_equal "Instance method \"park\" is already defined in #{@klass} :state instance helpers, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true.\n", $stderr.string
+ ensure
+ $stderr = @original_stderr
+ end
+
+ def test_should_pass_context_as_arguments
+ helper_args = nil
+ @machine.define_helper(:instance, :park) { |*args| helper_args = args }
+ @object.park
+ assert_equal 2, helper_args.length
+ assert_equal [@machine, @object], helper_args
+ end
+
+ def test_should_pass_method_arguments_through
+ helper_args = nil
+ @machine.define_helper(:instance, :park) { |*args| helper_args = args }
+ @object.park(1, 2, 3)
+ assert_equal 5, helper_args.length
+ assert_equal [@machine, @object, 1, 2, 3], helper_args
+ end
+
+ def test_should_allow_string_evaluation
+ @machine.define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
+ def park
+ false
+ end
+ end_eval
+ assert_equal false, @object.park
+ end
+end
+
diff --git a/test/unit/machine/machine_with_integration_test.rb b/test/unit/machine/machine_with_integration_test.rb
new file mode 100644
index 0000000..42a9be2
--- /dev/null
+++ b/test/unit/machine/machine_with_integration_test.rb
@@ -0,0 +1,72 @@
+require_relative '../../test_helper'
+
+class MachineWithIntegrationTest < StateMachinesTest
+
+ module Custom
+ include StateMachines::Integrations::Base
+
+ @defaults = {action: :save, use_transactions: false}
+
+ attr_reader :initialized, :with_scopes, :without_scopes, :ran_transaction
+
+ def after_initialize
+ @initialized = true
+ end
+
+ def create_with_scope(name)
+ (@with_scopes ||= []) << name
+ lambda {}
+ end
+
+ def create_without_scope(name)
+ (@without_scopes ||= []) << name
+ lambda {}
+ end
+
+ def transaction(_)
+ @ran_transaction = true
+ yield
+ end
+ end
+
+ def setup
+ StateMachines::Integrations.register(MachineWithIntegrationTest::Custom)
+
+
+ @machine = StateMachines::Machine.new(Class.new, integration: :custom)
+ end
+
+ def test_should_call_after_initialize_hook
+ assert @machine.initialized
+ end
+
+ def test_should_use_the_default_action
+ assert_equal :save, @machine.action
+ end
+
+ def test_should_use_the_custom_action_if_specified
+ machine = StateMachines::Machine.new(Class.new, integration: :custom, action: :save!)
+ assert_equal :save!, machine.action
+ end
+
+ def test_should_use_the_default_use_transactions
+ assert_equal false, @machine.use_transactions
+ end
+
+ def test_should_use_the_custom_use_transactions_if_specified
+ machine = StateMachines::Machine.new(Class.new, integration: :custom, use_transactions: true)
+ assert_equal true, machine.use_transactions
+ end
+
+ def test_should_define_a_singular_and_plural_with_scope
+ assert_equal %w(with_state with_states), @machine.with_scopes
+ end
+
+ def test_should_define_a_singular_and_plural_without_scope
+ assert_equal %w(without_state without_states), @machine.without_scopes
+ end
+
+ def teardown
+ StateMachines::Integrations.reset
+ end
+end
diff --git a/test/unit/machine/machine_with_multiple_events_test.rb b/test/unit/machine/machine_with_multiple_events_test.rb
new file mode 100644
index 0000000..1235d2b
--- /dev/null
+++ b/test/unit/machine/machine_with_multiple_events_test.rb
@@ -0,0 +1,32 @@
+require_relative '../../test_helper'
+
+class MachineWithMultipleEventsTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass, initial: :parked)
+ @park, @shift_down = @machine.event(:park, :shift_down) do
+ transition first_gear: :parked
+ end
+ end
+
+ def test_should_have_events
+ assert_equal [@park, @shift_down], @machine.events.to_a
+ end
+
+ def test_should_define_transitions_for_each_event
+ [@park, @shift_down].each { |event| assert_equal 1, event.branches.size }
+ end
+
+ def test_should_transition_the_same_for_each_event
+ object = @klass.new
+ object.state = 'first_gear'
+ object.park
+ assert_equal 'parked', object.state
+
+ object = @klass.new
+ object.state = 'first_gear'
+ object.shift_down
+ assert_equal 'parked', object.state
+ end
+end
+
diff --git a/test/unit/machine/machine_with_namespace_test.rb b/test/unit/machine/machine_with_namespace_test.rb
new file mode 100644
index 0000000..552c5a7
--- /dev/null
+++ b/test/unit/machine/machine_with_namespace_test.rb
@@ -0,0 +1,48 @@
+require_relative '../../test_helper'
+
+class MachineWithNamespaceTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass, namespace: 'alarm', initial: :active) do
+ event :enable do
+ transition off: :active
+ end
+
+ event :disable do
+ transition active: :off
+ end
+ end
+ @object = @klass.new
+ end
+
+ def test_should_namespace_state_predicates
+ [:alarm_active?, :alarm_off?].each do |name|
+ assert @object.respond_to?(name)
+ end
+ end
+
+ def test_should_namespace_event_checks
+ [:can_enable_alarm?, :can_disable_alarm?].each do |name|
+ assert @object.respond_to?(name)
+ end
+ end
+
+ def test_should_namespace_event_transition_readers
+ [:enable_alarm_transition, :disable_alarm_transition].each do |name|
+ assert @object.respond_to?(name)
+ end
+ end
+
+ def test_should_namespace_events
+ [:enable_alarm, :disable_alarm].each do |name|
+ assert @object.respond_to?(name)
+ end
+ end
+
+ def test_should_namespace_bang_events
+ [:enable_alarm!, :disable_alarm!].each do |name|
+ assert @object.respond_to?(name)
+ end
+ end
+end
+
diff --git a/test/unit/machine/machine_with_nil_action_test.rb b/test/unit/machine/machine_with_nil_action_test.rb
new file mode 100644
index 0000000..18b58d9
--- /dev/null
+++ b/test/unit/machine/machine_with_nil_action_test.rb
@@ -0,0 +1,27 @@
+require_relative '../../test_helper'
+
+class MachineWithNilActionTest < StateMachinesTest
+ module Custom
+ include StateMachines::Integrations::Base
+
+ @defaults = {action: :save}
+ end
+
+ def setup
+ StateMachines::Integrations.register(MachineWithNilActionTest::Custom)
+ end
+
+ def test_should_have_a_nil_action
+ machine = StateMachines::Machine.new(Class.new, action: nil, integration: :custom)
+ assert_nil machine.action
+ end
+
+ def test_should_have_default_action
+ machine = StateMachines::Machine.new(Class.new, integration: :custom)
+ assert_equal :save, machine.action
+ end
+
+ def teardown
+ StateMachines::Integrations.reset
+ end
+end
diff --git a/test/unit/machine/machine_with_other_states.rb b/test/unit/machine/machine_with_other_states.rb
new file mode 100644
index 0000000..4a0f56c
--- /dev/null
+++ b/test/unit/machine/machine_with_other_states.rb
@@ -0,0 +1,22 @@
+require_relative '../../test_helper'
+
+class MachineWithOtherStates < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass, initial: :parked)
+ @parked, @idling = @machine.other_states(:parked, :idling)
+ end
+
+ def test_should_include_other_states_in_known_states
+ assert_equal [@parked, @idling], @machine.states.to_a
+ end
+
+ def test_should_use_default_value
+ assert_equal 'idling', @idling.value
+ end
+
+ def test_should_not_create_matcher
+ assert_nil @idling.matcher
+ end
+end
+
diff --git a/test/unit/machine/machine_with_owner_subclass_test.rb b/test/unit/machine/machine_with_owner_subclass_test.rb
new file mode 100644
index 0000000..785f5b5
--- /dev/null
+++ b/test/unit/machine/machine_with_owner_subclass_test.rb
@@ -0,0 +1,18 @@
+require_relative '../../test_helper'
+
+class MachineWithOwnerSubclassTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @subclass = Class.new(@klass)
+ end
+
+ def test_should_have_a_different_collection_of_state_machines
+ refute_same @klass.state_machines, @subclass.state_machines
+ end
+
+ def test_should_have_the_same_attribute_associated_state_machines
+ assert_equal @klass.state_machines, @subclass.state_machines
+ end
+end
+
diff --git a/test/unit/machine/machine_with_paths_test.rb b/test/unit/machine/machine_with_paths_test.rb
new file mode 100644
index 0000000..4739131
--- /dev/null
+++ b/test/unit/machine/machine_with_paths_test.rb
@@ -0,0 +1,25 @@
+require_relative '../../test_helper'
+
+class MachineWithPathsTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.event :ignite do
+ transition parked: :idling
+ end
+ @machine.event :shift_up do
+ transition first_gear: :second_gear
+ end
+
+ @object = @klass.new
+ @object.state = 'parked'
+ end
+
+ def test_should_have_paths
+ assert_equal [[StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)]], @machine.paths_for(@object)
+ end
+
+ def test_should_allow_requirement_configuration
+ assert_equal [[StateMachines::Transition.new(@object, @machine, :shift_up, :first_gear, :second_gear)]], @machine.paths_for(@object, from: :first_gear)
+ end
+end
diff --git a/test/unit/machine/machine_with_private_action_test.rb b/test/unit/machine/machine_with_private_action_test.rb
new file mode 100644
index 0000000..33750f3
--- /dev/null
+++ b/test/unit/machine/machine_with_private_action_test.rb
@@ -0,0 +1,43 @@
+require_relative '../../test_helper'
+
+class MachineWithPrivateActionTest < StateMachinesTest
+ def setup
+ @superclass = Class.new do
+ private
+ def save
+ end
+ end
+ @klass = Class.new(@superclass)
+
+ @machine = StateMachines::Machine.new(@klass, action: :save)
+ @object = @klass.new
+ end
+
+ def test_should_define_an_event_attribute_reader
+ assert @object.respond_to?(:state_event)
+ end
+
+ def test_should_define_an_event_attribute_writer
+ assert @object.respond_to?(:state_event=)
+ end
+
+ def test_should_define_an_event_transition_attribute_reader
+ assert @object.respond_to?(:state_event_transition, true)
+ end
+
+ def test_should_define_an_event_transition_attribute_writer
+ assert @object.respond_to?(:state_event_transition=, true)
+ end
+
+ def test_should_define_action
+ assert @klass.ancestors.any? { |ancestor| ![@klass, @superclass].include?(ancestor) && ancestor.private_method_defined?(:save) }
+ end
+
+ def test_should_keep_action_private
+ assert @klass.private_method_defined?(:save)
+ end
+
+ def test_should_mark_action_hook_as_defined
+ assert @machine.action_hook?
+ end
+end
diff --git a/test/unit/machine/machine_with_state_matchers_test.rb b/test/unit/machine/machine_with_state_matchers_test.rb
new file mode 100644
index 0000000..91b1d0e
--- /dev/null
+++ b/test/unit/machine/machine_with_state_matchers_test.rb
@@ -0,0 +1,41 @@
+require_relative '../../test_helper'
+
+class MachineWithStateMatchersTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ end
+
+ def test_should_empty_array_for_all_matcher
+ assert_equal [], @machine.state(StateMachines::AllMatcher.instance)
+ end
+
+ def test_should_return_referenced_states_for_blacklist_matcher
+ assert_instance_of StateMachines::State, @machine.state(StateMachines::BlacklistMatcher.new([:parked]))
+ end
+
+ def test_should_not_allow_configurations
+ exception = assert_raises(ArgumentError) { @machine.state(StateMachines::BlacklistMatcher.new([:parked]), human_name: 'Parked') }
+ assert_equal 'Cannot configure states when using matchers (using {:human_name=>"Parked"})', exception.message
+ end
+
+ def test_should_track_referenced_states
+ @machine.state(StateMachines::BlacklistMatcher.new([:parked]))
+ assert_equal [nil, :parked], @machine.states.map { |state| state.name }
+ end
+
+ def test_should_eval_context_for_matching_states
+ contexts_run = []
+ @machine.event(StateMachines::BlacklistMatcher.new([:parked])) { contexts_run << name }
+
+ @machine.event :parked
+ assert_equal [], contexts_run
+
+ @machine.event :idling
+ assert_equal [:idling], contexts_run
+
+ @machine.event :first_gear, :second_gear
+ assert_equal [:idling, :first_gear, :second_gear], contexts_run
+ end
+end
+
diff --git a/test/unit/machine/machine_with_state_with_matchers_test.rb b/test/unit/machine/machine_with_state_with_matchers_test.rb
new file mode 100644
index 0000000..852a422
--- /dev/null
+++ b/test/unit/machine/machine_with_state_with_matchers_test.rb
@@ -0,0 +1,19 @@
+require_relative '../../test_helper'
+
+class MachineWithStateWithMatchersTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @state = @machine.state :parked, if: ->(value) {!value.nil? }
+
+ @object = @klass.new
+ @object.state = 1
+ end
+
+ def test_should_use_custom_matcher
+ refute_nil @state.matcher
+ assert @state.matches?(1)
+ refute @state.matches?(nil)
+ end
+end
+
diff --git a/test/unit/machine/machine_with_states_test.rb b/test/unit/machine/machine_with_states_test.rb
new file mode 100644
index 0000000..9ae18ca
--- /dev/null
+++ b/test/unit/machine/machine_with_states_test.rb
@@ -0,0 +1,55 @@
+require_relative '../../test_helper'
+
+class MachineWithStatesTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @parked, @idling = @machine.state :parked, :idling
+
+ @object = @klass.new
+ end
+
+ def test_should_have_states
+ assert_equal [nil, :parked, :idling], @machine.states.map { |state| state.name }
+ end
+
+ def test_should_allow_state_lookup_by_name
+ assert_equal @parked, @machine.states[:parked]
+ end
+
+ def test_should_allow_state_lookup_by_value
+ assert_equal @parked, @machine.states['parked', :value]
+ end
+
+ def test_should_allow_human_state_name_lookup
+ assert_equal 'parked', @klass.human_state_name(:parked)
+ end
+
+ def test_should_raise_exception_on_invalid_human_state_name_lookup
+ exception = assert_raises(IndexError) { @klass.human_state_name(:invalid) }
+ assert_equal ':invalid is an invalid name', exception.message
+ end
+
+ def test_should_use_stringified_name_for_value
+ assert_equal 'parked', @parked.value
+ end
+
+ def test_should_not_use_custom_matcher
+ assert_nil @parked.matcher
+ end
+
+ def test_should_raise_exception_if_invalid_option_specified
+ exception = assert_raises(ArgumentError) { @machine.state(:first_gear, invalid: true) }
+ assert_equal 'Unknown key: :invalid. Valid keys are: :value, :cache, :if, :human_name', exception.message
+ end
+
+ def test_should_raise_exception_if_conflicting_type_used_for_name
+ exception = assert_raises(ArgumentError) { @machine.state 'first_gear' }
+ assert_equal '"first_gear" state defined as String, :parked defined as Symbol; all states must be consistent', exception.message
+ end
+
+ def test_should_not_raise_exception_if_conflicting_type_is_nil_for_name
+ @machine.state nil
+ end
+end
+
diff --git a/test/unit/machine/machine_with_states_with_behaviors_test.rb b/test/unit/machine/machine_with_states_with_behaviors_test.rb
new file mode 100644
index 0000000..87cf8ad
--- /dev/null
+++ b/test/unit/machine/machine_with_states_with_behaviors_test.rb
@@ -0,0 +1,23 @@
+require_relative '../../test_helper'
+
+class MachineWithStatesWithBehaviorsTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+
+ @parked, @idling = @machine.state :parked, :idling do
+ def speed
+ 0
+ end
+ end
+ end
+
+ def test_should_define_behaviors_for_each_state
+ refute_nil @parked.context_methods[:speed]
+ refute_nil @idling.context_methods[:speed]
+ end
+
+ def test_should_define_different_behaviors_for_each_state
+ refute_equal @parked.context_methods[:speed], @idling.context_methods[:speed]
+ end
+end
diff --git a/test/unit/machine/machine_with_states_with_custom_human_names_test.rb b/test/unit/machine/machine_with_states_with_custom_human_names_test.rb
new file mode 100644
index 0000000..6df4a9e
--- /dev/null
+++ b/test/unit/machine/machine_with_states_with_custom_human_names_test.rb
@@ -0,0 +1,18 @@
+require_relative '../../test_helper'
+
+class MachineWithStatesWithCustomHumanNamesTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @state = @machine.state :parked, human_name: 'stopped'
+ end
+
+ def test_should_use_custom_human_name
+ assert_equal 'stopped', @state.human_name
+ end
+
+ def test_should_allow_human_state_name_lookup
+ assert_equal 'stopped', @klass.human_state_name(:parked)
+ end
+end
+
diff --git a/test/unit/machine/machine_with_states_with_custom_values_test.rb b/test/unit/machine/machine_with_states_with_custom_values_test.rb
new file mode 100644
index 0000000..df2e2a8
--- /dev/null
+++ b/test/unit/machine/machine_with_states_with_custom_values_test.rb
@@ -0,0 +1,21 @@
+require_relative '../../test_helper'
+
+class MachineWithStatesWithCustomValuesTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @state = @machine.state :parked, value: 1
+
+ @object = @klass.new
+ @object.state = 1
+ end
+
+ def test_should_use_custom_value
+ assert_equal 1, @state.value
+ end
+
+ def test_should_allow_lookup_by_custom_value
+ assert_equal @state, @machine.states[1, :value]
+ end
+end
+
diff --git a/test/unit/machine/machine_with_states_with_runtime_dependencies_test.rb b/test/unit/machine/machine_with_states_with_runtime_dependencies_test.rb
new file mode 100644
index 0000000..2f9c3ac
--- /dev/null
+++ b/test/unit/machine/machine_with_states_with_runtime_dependencies_test.rb
@@ -0,0 +1,19 @@
+require_relative '../../test_helper'
+
+class MachineWithStatesWithRuntimeDependenciesTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked
+ end
+
+ def test_should_not_evaluate_value_during_definition
+ @machine.state :parked, value: -> { fail ArgumentError }
+ end
+
+ def test_should_not_evaluate_if_not_initial_state
+ @machine.state :parked, value: -> { fail ArgumentError }
+ @klass.new
+ end
+end
+
diff --git a/test/unit/machine/machine_with_static_initial_state_test.rb b/test/unit/machine/machine_with_static_initial_state_test.rb
new file mode 100644
index 0000000..15abccc
--- /dev/null
+++ b/test/unit/machine/machine_with_static_initial_state_test.rb
@@ -0,0 +1,49 @@
+require_relative '../../test_helper'
+
+class MachineWithStaticInitialStateTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass, initial: :parked)
+ end
+
+ def test_should_not_have_dynamic_initial_state
+ refute @machine.dynamic_initial_state?
+ end
+
+ def test_should_have_an_initial_state
+ object = @klass.new
+ assert_equal 'parked', @machine.initial_state(object).value
+ end
+
+ def test_should_write_to_attribute_when_initializing_state
+ object = @klass.allocate
+ @machine.initialize_state(object)
+ assert_equal 'parked', object.state
+ end
+
+ def test_should_set_initial_on_state_object
+ assert @machine.state(:parked).initial
+ end
+
+ def test_should_set_initial_state_on_created_object
+ assert_equal 'parked', @klass.new.state
+ end
+
+ def test_should_not_initial_state_prior_to_initialization
+ base = Class.new do
+ attr_accessor :state_on_init
+
+ def initialize
+ self.state_on_init = state
+ end
+ end
+ klass = Class.new(base)
+ StateMachines::Machine.new(klass, initial: :parked)
+
+ assert_nil klass.new.state_on_init
+ end
+
+ def test_should_be_included_in_known_states
+ assert_equal [:parked], @machine.states.keys
+ end
+end
diff --git a/test/unit/machine/machine_with_superclass_conflicting_helpers_after_definition_test.rb b/test/unit/machine/machine_with_superclass_conflicting_helpers_after_definition_test.rb
new file mode 100644
index 0000000..426d53f
--- /dev/null
+++ b/test/unit/machine/machine_with_superclass_conflicting_helpers_after_definition_test.rb
@@ -0,0 +1,36 @@
+require_relative '../../test_helper'
+require 'stringio'
+
+class MachineWithSuperclassConflictingHelpersAfterDefinitionTest < StateMachinesTest
+ def setup
+ @original_stderr, $stderr = $stderr, StringIO.new
+
+ @superclass = Class.new
+ @klass = Class.new(@superclass)
+
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked, :idling
+ @machine.event :ignite
+
+ @superclass.class_eval do
+ def state?
+ true
+ end
+ end
+
+ @object = @klass.new
+ end
+
+ def test_should_call_superclass_attribute_predicate_without_arguments
+ assert @object.state?
+ end
+
+ def test_should_define_attribute_predicate_with_arguments
+ refute @object.state?(:parked)
+ end
+
+ def teardown
+ $stderr = @original_stderr
+ end
+end
+
diff --git a/test/unit/machine/machine_with_transition_callbacks_test.rb b/test/unit/machine/machine_with_transition_callbacks_test.rb
new file mode 100644
index 0000000..60eea74
--- /dev/null
+++ b/test/unit/machine/machine_with_transition_callbacks_test.rb
@@ -0,0 +1,144 @@
+require_relative '../../test_helper'
+
+class MachineWithTransitionCallbacksTest < StateMachinesTest
+ def setup
+ @klass = Class.new do
+ attr_accessor :callbacks
+ end
+
+ @machine = StateMachines::Machine.new(@klass, initial: :parked)
+ @event = @machine.event :ignite do
+ transition parked: :idling
+ end
+
+ @object = @klass.new
+ @object.callbacks = []
+ end
+
+ def test_should_not_raise_exception_if_implicit_option_specified
+ @machine.before_transition invalid: :valid, do: -> {}
+ end
+
+ def test_should_raise_exception_if_method_not_specified
+ exception = assert_raises(ArgumentError) { @machine.before_transition to: :idling }
+ assert_equal 'Method(s) for callback must be specified', exception.message
+ end
+
+ def test_should_invoke_callbacks_during_transition
+ @machine.before_transition lambda { |object| object.callbacks << 'before' }
+ @machine.after_transition lambda { |object| object.callbacks << 'after' }
+ @machine.around_transition lambda { |object, _transition, block| object.callbacks << 'before_around'; block.call; object.callbacks << 'after_around' }
+
+ @event.fire(@object)
+ assert_equal %w(before before_around after_around after), @object.callbacks
+ end
+
+ def test_should_allow_multiple_callbacks
+ @machine.before_transition lambda { |object| object.callbacks << 'before1' }, lambda { |object| object.callbacks << 'before2' }
+ @machine.after_transition lambda { |object| object.callbacks << 'after1' }, lambda { |object| object.callbacks << 'after2' }
+ @machine.around_transition(
+ lambda { |object, _transition, block| object.callbacks << 'before_around1'; block.call; object.callbacks << 'after_around1' },
+ lambda { |object, _transition, block| object.callbacks << 'before_around2'; block.call; object.callbacks << 'after_around2' }
+ )
+
+ @event.fire(@object)
+ assert_equal %w(before1 before2 before_around1 before_around2 after_around2 after_around1 after1 after2), @object.callbacks
+ end
+
+ def test_should_allow_multiple_callbacks_with_requirements
+ @machine.before_transition lambda { |object| object.callbacks << 'before_parked1' }, lambda { |object| object.callbacks << 'before_parked2' }, from: :parked
+ @machine.before_transition lambda { |object| object.callbacks << 'before_idling1' }, lambda { |object| object.callbacks << 'before_idling2' }, from: :idling
+ @machine.after_transition lambda { |object| object.callbacks << 'after_parked1' }, lambda { |object| object.callbacks << 'after_parked2' }, from: :parked
+ @machine.after_transition lambda { |object| object.callbacks << 'after_idling1' }, lambda { |object| object.callbacks << 'after_idling2' }, from: :idling
+ @machine.around_transition(
+ lambda { |object, _transition, block| object.callbacks << 'before_around_parked1'; block.call; object.callbacks << 'after_around_parked1' },
+ lambda { |object, _transition, block| object.callbacks << 'before_around_parked2'; block.call; object.callbacks << 'after_around_parked2' },
+ from: :parked
+ )
+ @machine.around_transition(
+ lambda { |object, _transition, block| object.callbacks << 'before_around_idling1'; block.call; object.callbacks << 'after_around_idling1' },
+ lambda { |object, _transition, block| object.callbacks << 'before_around_idling2'; block.call; object.callbacks << 'after_around_idling2' },
+ from: :idling
+ )
+
+ @event.fire(@object)
+ assert_equal %w(before_parked1 before_parked2 before_around_parked1 before_around_parked2 after_around_parked2 after_around_parked1 after_parked1 after_parked2), @object.callbacks
+ end
+
+ def test_should_support_from_requirement
+ @machine.before_transition from: :parked, do: lambda { |object| object.callbacks << :parked }
+ @machine.before_transition from: :idling, do: lambda { |object| object.callbacks << :idling }
+
+ @event.fire(@object)
+ assert_equal [:parked], @object.callbacks
+ end
+
+ def test_should_support_except_from_requirement
+ @machine.before_transition except_from: :parked, do: lambda { |object| object.callbacks << :parked }
+ @machine.before_transition except_from: :idling, do: lambda { |object| object.callbacks << :idling }
+
+ @event.fire(@object)
+ assert_equal [:idling], @object.callbacks
+ end
+
+ def test_should_support_to_requirement
+ @machine.before_transition to: :parked, do: lambda { |object| object.callbacks << :parked }
+ @machine.before_transition to: :idling, do: lambda { |object| object.callbacks << :idling }
+
+ @event.fire(@object)
+ assert_equal [:idling], @object.callbacks
+ end
+
+ def test_should_support_except_to_requirement
+ @machine.before_transition except_to: :parked, do: lambda { |object| object.callbacks << :parked }
+ @machine.before_transition except_to: :idling, do: lambda { |object| object.callbacks << :idling }
+
+ @event.fire(@object)
+ assert_equal [:parked], @object.callbacks
+ end
+
+ def test_should_support_on_requirement
+ @machine.before_transition on: :park, do: lambda { |object| object.callbacks << :park }
+ @machine.before_transition on: :ignite, do: lambda { |object| object.callbacks << :ignite }
+
+ @event.fire(@object)
+ assert_equal [:ignite], @object.callbacks
+ end
+
+ def test_should_support_except_on_requirement
+ @machine.before_transition except_on: :park, do: lambda { |object| object.callbacks << :park }
+ @machine.before_transition except_on: :ignite, do: lambda { |object| object.callbacks << :ignite }
+
+ @event.fire(@object)
+ assert_equal [:park], @object.callbacks
+ end
+
+ def test_should_support_implicit_requirement
+ @machine.before_transition parked: :idling, do: lambda { |object| object.callbacks << :parked }
+ @machine.before_transition idling: :parked, do: lambda { |object| object.callbacks << :idling }
+
+ @event.fire(@object)
+ assert_equal [:parked], @object.callbacks
+ end
+
+ def test_should_track_states_defined_in_transition_callbacks
+ @machine.before_transition parked: :idling, do: lambda {}
+ @machine.after_transition first_gear: :second_gear, do: lambda {}
+ @machine.around_transition third_gear: :fourth_gear, do: lambda {}
+
+ assert_equal [:parked, :idling, :first_gear, :second_gear, :third_gear, :fourth_gear], @machine.states.map { |state| state.name }
+ end
+
+ def test_should_not_duplicate_states_defined_in_multiple_event_transitions
+ @machine.before_transition parked: :idling, do: lambda {}
+ @machine.after_transition first_gear: :second_gear, do: lambda {}
+ @machine.after_transition parked: :idling, do: lambda {}
+ @machine.around_transition parked: :idling, do: lambda {}
+
+ assert_equal [:parked, :idling, :first_gear, :second_gear], @machine.states.map { |state| state.name }
+ end
+
+ def test_should_define_predicates_for_each_state
+ [:parked?, :idling?].each { |predicate| assert @object.respond_to?(predicate) }
+ end
+end
diff --git a/test/unit/machine/machine_with_transitions_test.rb b/test/unit/machine/machine_with_transitions_test.rb
new file mode 100644
index 0000000..1d707ec
--- /dev/null
+++ b/test/unit/machine/machine_with_transitions_test.rb
@@ -0,0 +1,87 @@
+require_relative '../../test_helper'
+
+class MachineWithTransitionsTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass, initial: :parked)
+ end
+
+ def test_should_require_on_event
+ exception = assert_raises(ArgumentError) { @machine.transition(parked: :idling) }
+ assert_equal 'Must specify :on event', exception.message
+ end
+
+ def test_should_not_allow_except_on_option
+ exception = assert_raises(ArgumentError) { @machine.transition(except_on: :ignite, on: :ignite) }
+ assert_equal 'Unknown key: :except_on. Valid keys are: :from, :to, :except_from, :except_to, :if, :unless', exception.message
+ end
+
+ def test_should_allow_transitioning_without_a_to_state
+ @machine.transition(from: :parked, on: :ignite)
+ end
+
+ def test_should_allow_transitioning_without_a_from_state
+ @machine.transition(to: :idling, on: :ignite)
+ end
+
+ def test_should_allow_except_from_option
+ @machine.transition(except_from: :idling, on: :ignite)
+ end
+
+ def test_should_allow_except_to_option
+ @machine.transition(except_to: :parked, on: :ignite)
+ end
+
+ def test_should_allow_implicit_options
+ branch = @machine.transition(first_gear: :second_gear, on: :shift_up)
+ assert_instance_of StateMachines::Branch, branch
+
+ state_requirements = branch.state_requirements
+ assert_equal 1, state_requirements.length
+
+ assert_instance_of StateMachines::WhitelistMatcher, state_requirements[0][:from]
+ assert_equal [:first_gear], state_requirements[0][:from].values
+ assert_instance_of StateMachines::WhitelistMatcher, state_requirements[0][:to]
+ assert_equal [:second_gear], state_requirements[0][:to].values
+ assert_instance_of StateMachines::WhitelistMatcher, branch.event_requirement
+ assert_equal [:shift_up], branch.event_requirement.values
+ end
+
+ def test_should_allow_multiple_implicit_options
+ branch = @machine.transition(first_gear: :second_gear, second_gear: :third_gear, on: :shift_up)
+
+ state_requirements = branch.state_requirements
+ assert_equal 2, state_requirements.length
+ end
+
+ def test_should_allow_verbose_options
+ branch = @machine.transition(from: :parked, to: :idling, on: :ignite)
+ assert_instance_of StateMachines::Branch, branch
+ end
+
+ def test_should_include_all_transition_states_in_machine_states
+ @machine.transition(parked: :idling, on: :ignite)
+
+ assert_equal [:parked, :idling], @machine.states.map { |state| state.name }
+ end
+
+ def test_should_include_all_transition_events_in_machine_events
+ @machine.transition(parked: :idling, on: :ignite)
+
+ assert_equal [:ignite], @machine.events.map { |event| event.name }
+ end
+
+ def test_should_allow_multiple_events
+ branches = @machine.transition(parked: :ignite, on: [:ignite, :shift_up])
+
+ assert_equal 2, branches.length
+ assert_equal [:ignite, :shift_up], @machine.events.map { |event| event.name }
+ end
+
+ def test_should_not_modify_options
+ options = { parked: :idling, on: :ignite }
+ @machine.transition(options)
+
+ assert_equal options, parked: :idling, on: :ignite
+ end
+end
diff --git a/test/unit/machine/machine_without_initialization_test.rb b/test/unit/machine/machine_without_initialization_test.rb
new file mode 100644
index 0000000..6158d6e
--- /dev/null
+++ b/test/unit/machine/machine_without_initialization_test.rb
@@ -0,0 +1,31 @@
+require_relative '../../test_helper'
+
+class MachineWithoutInitializationTest < StateMachinesTest
+ def setup
+ @klass = Class.new do
+ def initialize(attributes = {})
+ attributes.each { |attr, value| send("#{attr}=", value) }
+ super()
+ end
+ end
+
+ @machine = StateMachines::Machine.new(@klass, initial: :parked, initialize: false)
+ end
+
+ def test_should_not_have_an_initial_state
+ object = @klass.new
+ assert_nil object.state
+ end
+
+ def test_should_still_allow_manual_initialization
+ @klass.send(:include, Module.new do
+ def initialize(_attributes = {})
+ super()
+ initialize_state_machines
+ end
+ end)
+
+ object = @klass.new
+ assert_equal 'parked', object.state
+ end
+end
diff --git a/test/unit/machine/machine_without_initialize_test.rb b/test/unit/machine/machine_without_initialize_test.rb
new file mode 100644
index 0000000..1b7faa2
--- /dev/null
+++ b/test/unit/machine/machine_without_initialize_test.rb
@@ -0,0 +1,14 @@
+require_relative '../../test_helper'
+
+class MachineWithoutInitializeTest < StateMachinesTest
+ def setup
+ klass = Class.new
+ StateMachines::Machine.new(klass, initial: :parked)
+ @object = klass.new
+ end
+
+ def test_should_initialize_state
+ assert_equal 'parked', @object.state
+ end
+end
+
diff --git a/test/unit/machine/machine_without_integration_test.rb b/test/unit/machine/machine_without_integration_test.rb
new file mode 100644
index 0000000..cbf3e93
--- /dev/null
+++ b/test/unit/machine/machine_without_integration_test.rb
@@ -0,0 +1,31 @@
+require_relative '../../test_helper'
+
+class MachineWithoutIntegrationTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @object = @klass.new
+ end
+
+ def test_transaction_should_yield
+ @yielded = false
+ @machine.within_transaction(@object) do
+ @yielded = true
+ end
+
+ assert @yielded
+ end
+
+ def test_invalidation_should_do_nothing
+ assert_nil @machine.invalidate(@object, :state, :invalid_transition, [[:event, 'park']])
+ end
+
+ def test_reset_should_do_nothing
+ assert_nil @machine.reset(@object)
+ end
+
+ def test_errors_for_should_be_empty
+ assert_equal '', @machine.errors_for(@object)
+ end
+end
+
diff --git a/test/unit/machine_collection/machine_collection_by_default_test.rb b/test/unit/machine_collection/machine_collection_by_default_test.rb
new file mode 100644
index 0000000..b2ed7a0
--- /dev/null
+++ b/test/unit/machine_collection/machine_collection_by_default_test.rb
@@ -0,0 +1,11 @@
+require_relative '../../test_helper'
+
+class MachineCollectionByDefaultTest < StateMachinesTest
+ def setup
+ @machines = StateMachines::MachineCollection.new
+ end
+
+ def test_should_not_have_any_machines
+ assert @machines.empty?
+ end
+end
diff --git a/test/unit/machine_collection/machine_collection_fire_attributes_with_validations_test.rb b/test/unit/machine_collection/machine_collection_fire_attributes_with_validations_test.rb
new file mode 100644
index 0000000..4399e23
--- /dev/null
+++ b/test/unit/machine_collection/machine_collection_fire_attributes_with_validations_test.rb
@@ -0,0 +1,72 @@
+require_relative '../../test_helper'
+
+class MachineCollectionFireWithValidationsTest < StateMachinesTest
+ module Custom
+ include StateMachines::Integrations::Base
+
+ def invalidate(object, _attribute, message, values = [])
+ (object.errors ||= []) << generate_message(message, values)
+ end
+
+ def reset(object)
+ object.errors = []
+ end
+ end
+
+ def setup
+ StateMachines::Integrations.register(MachineCollectionFireWithValidationsTest::Custom)
+
+ @klass = Class.new do
+ attr_accessor :errors
+
+ def initialize
+ @errors = []
+ super
+ end
+ end
+
+ @machines = StateMachines::MachineCollection.new
+ @machines[:state] = @state = StateMachines::Machine.new(@klass, :state, initial: :parked, integration: :custom)
+ @state.event :ignite do
+ transition parked: :idling
+ end
+
+ @machines[:alarm_state] = @alarm_state = StateMachines::Machine.new(@klass, :alarm_state, initial: :active, namespace: 'alarm', integration: :custom)
+ @alarm_state.event :disable do
+ transition active: :off
+ end
+
+ @object = @klass.new
+ end
+
+ def test_should_not_invalidate_if_transitions_exist
+ assert @machines.fire_events(@object, :ignite, :disable_alarm)
+ assert_equal [], @object.errors
+ end
+
+ def test_should_invalidate_if_no_transitions_exist
+ @object.state = 'idling'
+ @object.alarm_state = 'off'
+
+ refute @machines.fire_events(@object, :ignite, :disable_alarm)
+ assert_equal ['cannot transition via "ignite"', 'cannot transition via "disable"'], @object.errors
+ end
+
+ def test_should_run_failure_callbacks_if_no_transitions_exist
+ @object.state = 'idling'
+ @object.alarm_state = 'off'
+ @state_failure_run = @alarm_state_failure_run = false
+
+ @machines[:state].after_failure { @state_failure_run = true }
+ @machines[:alarm_state].after_failure { @alarm_state_failure_run = true }
+
+ refute @machines.fire_events(@object, :ignite, :disable_alarm)
+ assert @state_failure_run
+ assert @alarm_state_failure_run
+ end
+
+ def teardown
+ StateMachines::Integrations.reset
+ end
+end
+
diff --git a/test/unit/machine_collection/machine_collection_fire_test.rb b/test/unit/machine_collection/machine_collection_fire_test.rb
new file mode 100644
index 0000000..f3801ca
--- /dev/null
+++ b/test/unit/machine_collection/machine_collection_fire_test.rb
@@ -0,0 +1,80 @@
+require_relative '../../test_helper'
+
+class MachineCollectionFireTest < StateMachinesTest
+ def setup
+ @machines = StateMachines::MachineCollection.new
+
+ @klass = Class.new do
+ attr_reader :saved
+
+ def save
+ @saved = true
+ end
+ end
+
+ # First machine
+ @machines[:state] = @state = StateMachines::Machine.new(@klass, :state, initial: :parked, action: :save)
+ @state.event :ignite do
+ transition parked: :idling
+ end
+ @state.event :park do
+ transition idling: :parked
+ end
+
+ # Second machine
+ @machines[:alarm_state] = @alarm_state = StateMachines::Machine.new(@klass, :alarm_state, initial: :active, action: :save, namespace: 'alarm')
+ @alarm_state.event :enable do
+ transition off: :active
+ end
+ @alarm_state.event :disable do
+ transition active: :off
+ end
+
+ @object = @klass.new
+ end
+
+ def test_should_raise_exception_if_invalid_event_specified
+ exception = assert_raises(StateMachines::InvalidEvent) { @machines.fire_events(@object, :invalid) }
+ assert_equal :invalid, exception.event
+
+ exception = assert_raises(StateMachines::InvalidEvent) { @machines.fire_events(@object, :ignite, :invalid) }
+ assert_equal :invalid, exception.event
+ end
+
+ def test_should_fail_if_any_event_cannot_transition
+ refute @machines.fire_events(@object, :park, :disable_alarm)
+ assert_equal 'parked', @object.state
+ assert_equal 'active', @object.alarm_state
+ refute @object.saved
+
+ refute @machines.fire_events(@object, :ignite, :enable_alarm)
+ assert_equal 'parked', @object.state
+ assert_equal 'active', @object.alarm_state
+ refute @object.saved
+ end
+
+ def test_should_run_failure_callbacks_if_any_event_cannot_transition
+ @state_failure_run = @alarm_state_failure_run = false
+
+ @machines[:state].after_failure { @state_failure_run = true }
+ @machines[:alarm_state].after_failure { @alarm_state_failure_run = true }
+
+ refute @machines.fire_events(@object, :park, :disable_alarm)
+ assert @state_failure_run
+ refute @alarm_state_failure_run
+ end
+
+ def test_should_be_successful_if_all_events_transition
+ assert @machines.fire_events(@object, :ignite, :disable_alarm)
+ assert_equal 'idling', @object.state
+ assert_equal 'off', @object.alarm_state
+ assert @object.saved
+ end
+
+ def test_should_not_save_if_skipping_action
+ assert @machines.fire_events(@object, :ignite, :disable_alarm, false)
+ assert_equal 'idling', @object.state
+ assert_equal 'off', @object.alarm_state
+ refute @object.saved
+ end
+end
diff --git a/test/unit/machine_collection/machine_collection_fire_with_transactions_test.rb b/test/unit/machine_collection/machine_collection_fire_with_transactions_test.rb
new file mode 100644
index 0000000..1ef66b2
--- /dev/null
+++ b/test/unit/machine_collection/machine_collection_fire_with_transactions_test.rb
@@ -0,0 +1,54 @@
+require_relative '../../test_helper'
+
+class MachineCollectionFireAttributesWithValidationsTest < StateMachinesTest
+ def setup
+ @klass = Class.new do
+ attr_accessor :errors
+
+ def initialize
+ @errors = []
+ super
+ end
+ end
+
+ @machines = StateMachines::MachineCollection.new
+ @machines[:state] = @machine = StateMachines::Machine.new(@klass, :state, initial: :parked, action: :save)
+ @machine.event :ignite do
+ transition parked: :idling
+ end
+
+ class << @machine
+ def invalidate(object, _attribute, message, values = [])
+ (object.errors ||= []) << generate_message(message, values)
+ end
+
+ def reset(object)
+ object.errors = []
+ end
+ end
+
+ @object = @klass.new
+ end
+
+ def test_should_invalidate_if_event_is_invalid
+ @object.state_event = 'invalid'
+ @machines.transitions(@object, :save)
+
+ refute @object.errors.empty?
+ end
+
+ def test_should_invalidate_if_no_transition_exists
+ @object.state = 'idling'
+ @object.state_event = 'ignite'
+ @machines.transitions(@object, :save)
+
+ refute @object.errors.empty?
+ end
+
+ def test_should_not_invalidate_if_transition_exists
+ @object.state_event = 'ignite'
+ @machines.transitions(@object, :save)
+
+ assert @object.errors.empty?
+ end
+end
diff --git a/test/unit/machine_collection/machine_collection_fire_with_validations_test.rb b/test/unit/machine_collection/machine_collection_fire_with_validations_test.rb
new file mode 100644
index 0000000..53f72cf
--- /dev/null
+++ b/test/unit/machine_collection/machine_collection_fire_with_validations_test.rb
@@ -0,0 +1,76 @@
+require_relative '../../test_helper'
+
+module MachineCollectionFireWithValidationsIntegration
+ include StateMachines::Integrations::Base
+
+ def self.integration_name
+ :custom_validation
+ end
+
+ def invalidate(object, _attribute, message, values = [])
+ (object.errors ||= []) << generate_message(message, values)
+ end
+
+ def reset(object)
+ object.errors = []
+ end
+end
+
+class MachineCollectionFireWithValidationsTest < StateMachinesTest
+ def setup
+ StateMachines::Integrations.reset
+ StateMachines::Integrations.register(MachineCollectionFireWithValidationsIntegration)
+
+ @klass = Class.new do
+ attr_accessor :errors
+
+ def initialize
+ @errors = []
+ super
+ end
+ end
+
+ @machines = StateMachines::MachineCollection.new
+ @machines[:state] = @state = StateMachines::Machine.new(@klass, :state, initial: :parked, integration: :custom_validation)
+ @state.event :ignite do
+ transition parked: :idling
+ end
+
+ @machines[:alarm_state] = @alarm_state = StateMachines::Machine.new(@klass, :alarm_state, initial: :active, namespace: 'alarm', integration: :custom_validation)
+ @alarm_state.event :disable do
+ transition active: :off
+ end
+
+ @object = @klass.new
+ end
+
+ def test_should_not_invalidate_if_transitions_exist
+ assert @machines.fire_events(@object, :ignite, :disable_alarm)
+ assert_equal [], @object.errors
+ end
+
+ def test_should_invalidate_if_no_transitions_exist
+ @object.state = 'idling'
+ @object.alarm_state = 'off'
+
+ refute @machines.fire_events(@object, :ignite, :disable_alarm)
+ assert_equal ['cannot transition via "ignite"', 'cannot transition via "disable"'], @object.errors
+ end
+
+ def test_should_run_failure_callbacks_if_no_transitions_exist
+ @object.state = 'idling'
+ @object.alarm_state = 'off'
+ @state_failure_run = @alarm_state_failure_run = false
+
+ @machines[:state].after_failure { @state_failure_run = true }
+ @machines[:alarm_state].after_failure { @alarm_state_failure_run = true }
+
+ refute @machines.fire_events(@object, :ignite, :disable_alarm)
+ assert @state_failure_run
+ assert @alarm_state_failure_run
+ end
+
+ def teardown
+ StateMachines::Integrations.reset
+ end
+end
diff --git a/test/unit/machine_collection/machine_collection_state_initialization_test.rb b/test/unit/machine_collection/machine_collection_state_initialization_test.rb
new file mode 100644
index 0000000..f4ca2fc
--- /dev/null
+++ b/test/unit/machine_collection/machine_collection_state_initialization_test.rb
@@ -0,0 +1,111 @@
+require_relative '../../test_helper'
+
+class MachineCollectionStateInitializationTest < StateMachinesTest
+ def setup
+ @machines = StateMachines::MachineCollection.new
+
+ @klass = Class.new
+
+ @machines[:state] = StateMachines::Machine.new(@klass, :state, initial: :parked)
+ @machines[:alarm_state] = StateMachines::Machine.new(@klass, :alarm_state, initial: ->(_object) { :active })
+ @machines[:alarm_state].state :active, value: -> { 'active' }
+
+ # Prevent the auto-initialization hook from firing
+ @klass.class_eval do
+ def initialize
+ end
+ end
+
+ @object = @klass.new
+ @object.state = nil
+ @object.alarm_state = nil
+ end
+
+ def test_should_raise_exception_if_invalid_option_specified
+ assert_raises(ArgumentError) { @machines.initialize_states(@object, invalid: true) }
+ end
+
+ def test_should_initialize_static_states_after_block
+ @machines.initialize_states(@object) do
+ @state_in_block = @object.state
+ @alarm_state_in_block = @object.alarm_state
+ end
+
+ assert_nil @state_in_block
+ assert_nil @alarm_state_in_block
+ end
+
+ def test_should_initialize_dynamic_states_after_block
+ @machines.initialize_states(@object) do
+ @alarm_state_in_block = @object.alarm_state
+ end
+
+ assert_nil @alarm_state_in_block
+ assert_equal 'active', @object.alarm_state
+ end
+
+ def test_should_initialize_all_states_without_block
+ @machines.initialize_states(@object)
+
+ assert_equal 'parked', @object.state
+ assert_equal 'active', @object.alarm_state
+ end
+
+ def test_should_skip_static_states_if_disabled
+ @machines.initialize_states(@object, static: false)
+ assert_nil @object.state
+ assert_equal 'active', @object.alarm_state
+ end
+
+ def test_should_initialize_existing_static_states_by_default
+ @object.state = 'idling'
+ @machines.initialize_states(@object)
+ assert_equal 'parked', @object.state
+ end
+
+ def test_should_initialize_existing_static_states_if_forced
+ @object.state = 'idling'
+ @machines.initialize_states(@object, static: :force)
+ assert_equal 'parked', @object.state
+ end
+
+ def test_should_initialize_existing_static_states_if_not_forced
+ @object.state = 'idling'
+ @machines.initialize_states(@object, static: true)
+ assert_equal 'parked', @object.state
+ end
+
+ def test_should_skip_dynamic_states_if_disabled
+ @machines.initialize_states(@object, dynamic: false)
+ assert_equal 'parked', @object.state
+ assert_nil @object.alarm_state
+ end
+
+ def test_should_not_initialize_existing_dynamic_states_by_default
+ @object.alarm_state = 'inactive'
+ @machines.initialize_states(@object)
+ assert_equal 'inactive', @object.alarm_state
+ end
+
+ def test_should_initialize_existing_dynamic_states_if_forced
+ @object.alarm_state = 'inactive'
+ @machines.initialize_states(@object, dynamic: :force)
+ assert_equal 'active', @object.alarm_state
+ end
+
+ def test_should_not_initialize_existing_dynamic_states_if_not_forced
+ @object.alarm_state = 'inactive'
+ @machines.initialize_states(@object, dynamic: true)
+ assert_equal 'inactive', @object.alarm_state
+ end
+
+ def test_shouldnt_force_state_given_either_as_string_or_symbol
+ @object.state = 'notparked'
+
+ @machines.initialize_states(@object, {}, { state: "parked" })
+ assert_equal 'notparked', @object.state
+
+ @machines.initialize_states(@object, {}, { "state" => "parked" })
+ assert_equal 'notparked', @object.state
+ end
+end
diff --git a/test/unit/machine_collection/machine_collection_transitions_with_blank_events_test.rb b/test/unit/machine_collection/machine_collection_transitions_with_blank_events_test.rb
new file mode 100644
index 0000000..3e17c53
--- /dev/null
+++ b/test/unit/machine_collection/machine_collection_transitions_with_blank_events_test.rb
@@ -0,0 +1,25 @@
+require_relative '../../test_helper'
+
+class MachineCollectionTransitionsWithBlankEventsTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+
+ @machines = StateMachines::MachineCollection.new
+ @machines[:state] = @machine = StateMachines::Machine.new(@klass, :state, initial: :parked, action: :save)
+ @machine.event :ignite do
+ transition parked: :idling
+ end
+
+ @object = @klass.new
+ @object.state_event = ''
+ @transitions = @machines.transitions(@object, :save)
+ end
+
+ def test_should_be_empty
+ assert @transitions.empty?
+ end
+
+ def test_should_perform
+ assert_equal true, @transitions.perform
+ end
+end
diff --git a/test/unit/machine_collection/machine_collection_transitions_with_custom_options_test.rb b/test/unit/machine_collection/machine_collection_transitions_with_custom_options_test.rb
new file mode 100644
index 0000000..726f6a1
--- /dev/null
+++ b/test/unit/machine_collection/machine_collection_transitions_with_custom_options_test.rb
@@ -0,0 +1,20 @@
+require_relative '../../test_helper'
+
+class MachineCollectionTransitionsWithCustomOptionsTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+
+ @machines = StateMachines::MachineCollection.new
+ @machines[:state] = @machine = StateMachines::Machine.new(@klass, :state, initial: :parked, action: :save)
+ @machine.event :ignite do
+ transition parked: :idling
+ end
+
+ @object = @klass.new
+ @transitions = @machines.transitions(@object, :save, after: false)
+ end
+
+ def test_should_use_custom_options
+ assert @transitions.skip_after
+ end
+end
diff --git a/test/unit/machine_collection/machine_collection_transitions_with_different_actions_test.rb b/test/unit/machine_collection/machine_collection_transitions_with_different_actions_test.rb
new file mode 100644
index 0000000..8212996
--- /dev/null
+++ b/test/unit/machine_collection/machine_collection_transitions_with_different_actions_test.rb
@@ -0,0 +1,26 @@
+require_relative '../../test_helper'
+
+class MachineCollectionTransitionsWithDifferentActionsTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+
+ @machines = StateMachines::MachineCollection.new
+ @machines[:state] = @state = StateMachines::Machine.new(@klass, :state, initial: :parked, action: :save)
+ @state.event :ignite do
+ transition parked: :idling
+ end
+ @machines[:status] = @status = StateMachines::Machine.new(@klass, :status, initial: :first_gear, action: :persist)
+ @status.event :shift_up do
+ transition first_gear: :second_gear
+ end
+
+ @object = @klass.new
+ @object.state_event = 'ignite'
+ @object.status_event = 'shift_up'
+ @transitions = @machines.transitions(@object, :save)
+ end
+
+ def test_should_only_select_matching_actions
+ assert_equal 1, @transitions.length
+ end
+end
diff --git a/test/unit/machine_collection/machine_collection_transitions_with_exisiting_transitions_test.rb b/test/unit/machine_collection/machine_collection_transitions_with_exisiting_transitions_test.rb
new file mode 100644
index 0000000..49a799c
--- /dev/null
+++ b/test/unit/machine_collection/machine_collection_transitions_with_exisiting_transitions_test.rb
@@ -0,0 +1,25 @@
+require_relative '../../test_helper'
+
+class MachineCollectionTransitionsWithExisitingTransitionsTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+
+ @machines = StateMachines::MachineCollection.new
+ @machines[:state] = @machine = StateMachines::Machine.new(@klass, :state, initial: :parked, action: :save)
+ @machine.event :ignite do
+ transition parked: :idling
+ end
+
+ @object = @klass.new
+ @object.send(:state_event_transition=, StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling))
+ @transitions = @machines.transitions(@object, :save)
+ end
+
+ def test_should_not_be_empty
+ assert_equal 1, @transitions.length
+ end
+
+ def test_should_perform
+ assert_equal true, @transitions.perform
+ end
+end
diff --git a/test/unit/machine_collection/machine_collection_transitions_with_invalid_events_test.rb b/test/unit/machine_collection/machine_collection_transitions_with_invalid_events_test.rb
new file mode 100644
index 0000000..588fe8a
--- /dev/null
+++ b/test/unit/machine_collection/machine_collection_transitions_with_invalid_events_test.rb
@@ -0,0 +1,25 @@
+require_relative '../../test_helper'
+
+class MachineCollectionTransitionsWithInvalidEventsTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+
+ @machines = StateMachines::MachineCollection.new
+ @machines[:state] = @machine = StateMachines::Machine.new(@klass, :state, initial: :parked, action: :save)
+ @machine.event :ignite do
+ transition parked: :idling
+ end
+
+ @object = @klass.new
+ @object.state_event = 'invalid'
+ @transitions = @machines.transitions(@object, :save)
+ end
+
+ def test_should_be_empty
+ assert @transitions.empty?
+ end
+
+ def test_should_not_perform
+ assert_equal false, @transitions.perform
+ end
+end
diff --git a/test/unit/machine_collection/machine_collection_transitions_with_same_actions_test.rb b/test/unit/machine_collection/machine_collection_transitions_with_same_actions_test.rb
new file mode 100644
index 0000000..e67ce4d
--- /dev/null
+++ b/test/unit/machine_collection/machine_collection_transitions_with_same_actions_test.rb
@@ -0,0 +1,31 @@
+require_relative '../../test_helper'
+
+class MachineCollectionTransitionsWithSameActionsTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+
+ @machines = StateMachines::MachineCollection.new
+ @machines[:state] = @machine = StateMachines::Machine.new(@klass, :state, initial: :parked, action: :save)
+ @machine.event :ignite do
+ transition parked: :idling
+ end
+ @machines[:status] = @machine = StateMachines::Machine.new(@klass, :status, initial: :first_gear, action: :save)
+ @machine.event :shift_up do
+ transition first_gear: :second_gear
+ end
+
+ @object = @klass.new
+ @object.state_event = 'ignite'
+ @object.status_event = 'shift_up'
+ @transitions = @machines.transitions(@object, :save)
+ end
+
+ def test_should_not_be_empty
+ assert_equal 2, @transitions.length
+ end
+
+ def test_should_perform
+ assert_equal true, @transitions.perform
+ end
+end
+
diff --git a/test/unit/machine_collection/machine_collection_transitions_with_transition_test.rb b/test/unit/machine_collection/machine_collection_transitions_with_transition_test.rb
new file mode 100644
index 0000000..441a692
--- /dev/null
+++ b/test/unit/machine_collection/machine_collection_transitions_with_transition_test.rb
@@ -0,0 +1,26 @@
+require_relative '../../test_helper'
+
+class MachineCollectionTransitionsWithTransitionTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+
+ @machines = StateMachines::MachineCollection.new
+ @machines[:state] = @machine = StateMachines::Machine.new(@klass, :state, initial: :parked, action: :save)
+ @machine.event :ignite do
+ transition parked: :idling
+ end
+
+ @object = @klass.new
+ @object.state_event = 'ignite'
+ @transitions = @machines.transitions(@object, :save)
+ end
+
+ def test_should_not_be_empty
+ assert_equal 1, @transitions.length
+ end
+
+ def test_should_perform
+ assert_equal true, @transitions.perform
+ end
+end
+
diff --git a/test/unit/machine_collection/machine_collection_transitions_without_events_test.rb b/test/unit/machine_collection/machine_collection_transitions_without_events_test.rb
new file mode 100644
index 0000000..eb8d2ed
--- /dev/null
+++ b/test/unit/machine_collection/machine_collection_transitions_without_events_test.rb
@@ -0,0 +1,25 @@
+require_relative '../../test_helper'
+
+class MachineCollectionTransitionsWithoutEventsTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+
+ @machines = StateMachines::MachineCollection.new
+ @machines[:state] = @machine = StateMachines::Machine.new(@klass, :state, initial: :parked, action: :save)
+ @machine.event :ignite do
+ transition parked: :idling
+ end
+
+ @object = @klass.new
+ @object.state_event = nil
+ @transitions = @machines.transitions(@object, :save)
+ end
+
+ def test_should_be_empty
+ assert @transitions.empty?
+ end
+
+ def test_should_perform
+ assert_equal true, @transitions.perform
+ end
+end
diff --git a/test/unit/machine_collection/machine_collection_transitions_without_transition_test.rb b/test/unit/machine_collection/machine_collection_transitions_without_transition_test.rb
new file mode 100644
index 0000000..68d7dd9
--- /dev/null
+++ b/test/unit/machine_collection/machine_collection_transitions_without_transition_test.rb
@@ -0,0 +1,27 @@
+require_relative '../../test_helper'
+
+class MachineCollectionTransitionsWithoutTransitionTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+
+ @machines = StateMachines::MachineCollection.new
+ @machines[:state] = @machine = StateMachines::Machine.new(@klass, :state, initial: :parked, action: :save)
+ @machine.event :ignite do
+ transition parked: :idling
+ end
+
+ @object = @klass.new
+ @object.state = 'idling'
+ @object.state_event = 'ignite'
+ @transitions = @machines.transitions(@object, :save)
+ end
+
+ def test_should_be_empty
+ assert @transitions.empty?
+ end
+
+ def test_should_not_perform
+ assert_equal false, @transitions.perform
+ end
+end
+
diff --git a/test/unit/matcher/all_matcher_test.rb b/test/unit/matcher/all_matcher_test.rb
new file mode 100644
index 0000000..8dae644
--- /dev/null
+++ b/test/unit/matcher/all_matcher_test.rb
@@ -0,0 +1,29 @@
+require_relative '../../test_helper'
+
+class AllMatcherTest < StateMachinesTest
+ def setup
+ @matcher = StateMachines::AllMatcher.instance
+ end
+
+ def test_should_have_no_values
+ assert_equal [], @matcher.values
+ end
+
+ def test_should_always_match
+ [nil, :parked, :idling].each { |value| assert @matcher.matches?(value) }
+ end
+
+ def test_should_not_filter_any_values
+ assert_equal [:parked, :idling], @matcher.filter([:parked, :idling])
+ end
+
+ def test_should_generate_blacklist_matcher_after_subtraction
+ matcher = @matcher - [:parked, :idling]
+ assert_instance_of StateMachines::BlacklistMatcher, matcher
+ assert_equal [:parked, :idling], matcher.values
+ end
+
+ def test_should_have_a_description
+ assert_equal 'all', @matcher.description
+ end
+end
diff --git a/test/unit/matcher/blacklist_matcher_test.rb b/test/unit/matcher/blacklist_matcher_test.rb
new file mode 100644
index 0000000..b4dd31e
--- /dev/null
+++ b/test/unit/matcher/blacklist_matcher_test.rb
@@ -0,0 +1,30 @@
+require_relative '../../test_helper'
+
+class BlacklistMatcherTest < StateMachinesTest
+ def setup
+ @matcher = StateMachines::BlacklistMatcher.new([:parked, :idling])
+ end
+
+ def test_should_have_values
+ assert_equal [:parked, :idling], @matcher.values
+ end
+
+ def test_should_filter_known_values
+ assert_equal [:first_gear], @matcher.filter([:parked, :idling, :first_gear])
+ end
+
+ def test_should_match_unknown_values
+ assert @matcher.matches?(:first_gear)
+ end
+
+ def test_should_not_match_known_values
+ refute @matcher.matches?(:parked)
+ end
+
+ def test_should_have_a_description
+ assert_equal 'all - [:parked, :idling]', @matcher.description
+
+ matcher = StateMachines::BlacklistMatcher.new([:parked])
+ assert_equal 'all - :parked', matcher.description
+ end
+end
diff --git a/test/unit/matcher/loopback_matcher_test.rb b/test/unit/matcher/loopback_matcher_test.rb
new file mode 100644
index 0000000..1c424a5
--- /dev/null
+++ b/test/unit/matcher/loopback_matcher_test.rb
@@ -0,0 +1,27 @@
+require_relative '../../test_helper'
+
+class LoopbackMatcherTest < StateMachinesTest
+ def setup
+ @matcher = StateMachines::LoopbackMatcher.instance
+ end
+
+ def test_should_have_no_values
+ assert_equal [], @matcher.values
+ end
+
+ def test_should_filter_all_values
+ assert_equal [], @matcher.filter([:parked, :idling])
+ end
+
+ def test_should_match_if_from_context_is_same
+ assert @matcher.matches?(:parked, from: :parked)
+ end
+
+ def test_should_not_match_if_from_context_is_different
+ refute @matcher.matches?(:parked, from: :idling)
+ end
+
+ def test_should_have_a_description
+ assert_equal 'same', @matcher.description
+ end
+end
diff --git a/test/unit/matcher/matcher_by_default_test.rb b/test/unit/matcher/matcher_by_default_test.rb
new file mode 100644
index 0000000..9c7be05
--- /dev/null
+++ b/test/unit/matcher/matcher_by_default_test.rb
@@ -0,0 +1,15 @@
+require_relative '../../test_helper'
+
+class MatcherByDefaultTest < StateMachinesTest
+ def setup
+ @matcher = StateMachines::Matcher.new
+ end
+
+ def test_should_have_no_values
+ assert_equal [], @matcher.values
+ end
+
+ def test_should_filter_all_values
+ assert_equal [], @matcher.filter([:parked, :idling])
+ end
+end
diff --git a/test/unit/matcher/matcher_with_multiple_values_test.rb b/test/unit/matcher/matcher_with_multiple_values_test.rb
new file mode 100644
index 0000000..6069660
--- /dev/null
+++ b/test/unit/matcher/matcher_with_multiple_values_test.rb
@@ -0,0 +1,15 @@
+require_relative '../../test_helper'
+
+class MatcherWithMultipleValuesTest < StateMachinesTest
+ def setup
+ @matcher = StateMachines::Matcher.new([:parked, :idling])
+ end
+
+ def test_should_have_values
+ assert_equal [:parked, :idling], @matcher.values
+ end
+
+ def test_should_filter_unknown_values
+ assert_equal [:parked], @matcher.filter([:parked, :first_gear])
+ end
+end
diff --git a/test/unit/matcher/matcher_with_value_test.rb b/test/unit/matcher/matcher_with_value_test.rb
new file mode 100644
index 0000000..cfd1255
--- /dev/null
+++ b/test/unit/matcher/matcher_with_value_test.rb
@@ -0,0 +1,15 @@
+require_relative '../../test_helper'
+
+class MatcherWithValueTest < StateMachinesTest
+ def setup
+ @matcher = StateMachines::Matcher.new(nil)
+ end
+
+ def test_should_have_values
+ assert_equal [nil], @matcher.values
+ end
+
+ def test_should_filter_unknown_values
+ assert_equal [nil], @matcher.filter([nil, :parked])
+ end
+end
diff --git a/test/unit/matcher/whitelist_matcher_test.rb b/test/unit/matcher/whitelist_matcher_test.rb
new file mode 100644
index 0000000..11e537d
--- /dev/null
+++ b/test/unit/matcher/whitelist_matcher_test.rb
@@ -0,0 +1,30 @@
+require_relative '../../test_helper'
+
+class WhitelistMatcherTest < StateMachinesTest
+ def setup
+ @matcher = StateMachines::WhitelistMatcher.new([:parked, :idling])
+ end
+
+ def test_should_have_values
+ assert_equal [:parked, :idling], @matcher.values
+ end
+
+ def test_should_filter_unknown_values
+ assert_equal [:parked, :idling], @matcher.filter([:parked, :idling, :first_gear])
+ end
+
+ def test_should_match_known_values
+ assert @matcher.matches?(:parked)
+ end
+
+ def test_should_not_match_unknown_values
+ refute @matcher.matches?(:first_gear)
+ end
+
+ def test_should_have_a_description
+ assert_equal '[:parked, :idling]', @matcher.description
+
+ matcher = StateMachines::WhitelistMatcher.new([:parked])
+ assert_equal ':parked', matcher.description
+ end
+end
diff --git a/test/unit/matcher_helpers/matcher_helpers_all_test.rb b/test/unit/matcher_helpers/matcher_helpers_all_test.rb
new file mode 100644
index 0000000..b85dcfb
--- /dev/null
+++ b/test/unit/matcher_helpers/matcher_helpers_all_test.rb
@@ -0,0 +1,14 @@
+require_relative '../../test_helper'
+
+class MatcherHelpersAllTest < StateMachinesTest
+ include StateMachines::MatcherHelpers
+
+ def setup
+ @matcher = all
+ end
+
+ def test_should_build_an_all_matcher
+ assert_equal StateMachines::AllMatcher.instance, @matcher
+ end
+end
+
diff --git a/test/unit/matcher_helpers/matcher_helpers_any_test.rb b/test/unit/matcher_helpers/matcher_helpers_any_test.rb
new file mode 100644
index 0000000..7946e9f
--- /dev/null
+++ b/test/unit/matcher_helpers/matcher_helpers_any_test.rb
@@ -0,0 +1,14 @@
+require_relative '../../test_helper'
+
+class MatcherHelpersAnyTest < StateMachinesTest
+ include StateMachines::MatcherHelpers
+
+ def setup
+ @matcher = any
+ end
+
+ def test_should_build_an_all_matcher
+ assert_equal StateMachines::AllMatcher.instance, @matcher
+ end
+end
+
diff --git a/test/unit/matcher_helpers/matcher_helpers_same_test.rb b/test/unit/matcher_helpers/matcher_helpers_same_test.rb
new file mode 100644
index 0000000..7a50202
--- /dev/null
+++ b/test/unit/matcher_helpers/matcher_helpers_same_test.rb
@@ -0,0 +1,13 @@
+require_relative '../../test_helper'
+
+class MatcherHelpersSameTest < StateMachinesTest
+ include StateMachines::MatcherHelpers
+
+ def setup
+ @matcher = same
+ end
+
+ def test_should_build_a_loopback_matcher
+ assert_equal StateMachines::LoopbackMatcher.instance, @matcher
+ end
+end
diff --git a/test/unit/node_collection/node_collection_after_being_copied_test.rb b/test/unit/node_collection/node_collection_after_being_copied_test.rb
new file mode 100644
index 0000000..aaa8c3d
--- /dev/null
+++ b/test/unit/node_collection/node_collection_after_being_copied_test.rb
@@ -0,0 +1,46 @@
+require_relative '../../test_helper'
+require_relative '../../files/node'
+
+class NodeCollectionAfterBeingCopiedTest < StateMachinesTest
+ def setup
+ machine = StateMachines::Machine.new(Class.new)
+ @collection = StateMachines::NodeCollection.new(machine)
+ @collection << @parked = Node.new(:parked)
+
+ @contexts_run = contexts_run = []
+ @collection.context([:parked]) { contexts_run << :parked }
+ @contexts_run.clear
+
+ @copied_collection = @collection.dup
+ @copied_collection << @idling = Node.new(:idling)
+ @copied_collection.context([:first_gear]) { contexts_run << :first_gear }
+ end
+
+ def test_should_not_modify_the_original_list
+ assert_equal 1, @collection.length
+ assert_equal 2, @copied_collection.length
+ end
+
+ def test_should_not_modify_the_indices
+ assert_nil @collection[:idling]
+ assert_equal @idling, @copied_collection[:idling]
+ end
+
+ def test_should_copy_each_node
+ refute_same @parked, @copied_collection[:parked]
+ end
+
+ def test_should_not_run_contexts
+ assert_equal [], @contexts_run
+ end
+
+ def test_should_not_modify_contexts
+ @collection << Node.new(:first_gear)
+ assert_equal [], @contexts_run
+ end
+
+ def test_should_copy_contexts
+ @copied_collection << Node.new(:parked)
+ refute @contexts_run.empty?
+ end
+end
diff --git a/test/unit/node_collection/node_collection_after_update_test.rb b/test/unit/node_collection/node_collection_after_update_test.rb
new file mode 100644
index 0000000..088d75d
--- /dev/null
+++ b/test/unit/node_collection/node_collection_after_update_test.rb
@@ -0,0 +1,36 @@
+require_relative '../../test_helper'
+require_relative '../../files/node'
+
+class NodeCollectionAfterUpdateTest < StateMachinesTest
+ def setup
+ machine = StateMachines::Machine.new(Class.new)
+ @collection = StateMachines::NodeCollection.new(machine, index: [:name, :value])
+
+ @parked = Node.new(:parked, 1)
+ @idling = Node.new(:idling, 2)
+
+ @collection << @parked << @idling
+
+ @parked.name = :parking
+ @parked.value = 0
+ @collection.update(@parked)
+ end
+
+ def test_should_not_change_the_index
+ assert_equal @parked, @collection.at(0)
+ end
+
+ def test_should_not_duplicate_in_the_collection
+ assert_equal 2, @collection.length
+ end
+
+ def test_should_add_each_indexed_key
+ assert_equal @parked, @collection[:parking]
+ assert_equal @parked, @collection[0, :value]
+ end
+
+ def test_should_remove_each_old_indexed_key
+ assert_nil @collection[:parked]
+ assert_nil @collection[1, :value]
+ end
+end
diff --git a/test/unit/node_collection/node_collection_by_default_test.rb b/test/unit/node_collection/node_collection_by_default_test.rb
new file mode 100644
index 0000000..795aadd
--- /dev/null
+++ b/test/unit/node_collection/node_collection_by_default_test.rb
@@ -0,0 +1,22 @@
+require_relative '../../test_helper'
+require_relative '../../files/node'
+
+class NodeCollectionByDefaultTest < StateMachinesTest
+ def setup
+ @machine = StateMachines::Machine.new(Class.new)
+ @collection = StateMachines::NodeCollection.new(@machine)
+ end
+
+ def test_should_not_have_any_nodes
+ assert_equal 0, @collection.length
+ end
+
+ def test_should_have_a_machine
+ assert_equal @machine, @collection.machine
+ end
+
+ def test_should_index_by_name
+ @collection << object = Node.new(:parked)
+ assert_equal object, @collection[:parked]
+ end
+end
diff --git a/test/unit/node_collection/node_collection_test.rb b/test/unit/node_collection/node_collection_test.rb
new file mode 100644
index 0000000..d1784d5
--- /dev/null
+++ b/test/unit/node_collection/node_collection_test.rb
@@ -0,0 +1,23 @@
+require_relative '../../test_helper'
+
+class NodeCollectionTest < StateMachinesTest
+ def setup
+ @machine = StateMachines::Machine.new(Class.new)
+ @collection = StateMachines::NodeCollection.new(@machine)
+ end
+
+ def test_should_raise_exception_if_invalid_option_specified
+ exception = assert_raises(ArgumentError) { StateMachines::NodeCollection.new(@machine, invalid: true) }
+ assert_equal 'Unknown key: :invalid. Valid keys are: :index', exception.message
+ end
+
+ def test_should_raise_exception_on_lookup_if_invalid_index_specified
+ exception = assert_raises(ArgumentError) { @collection[:something, :invalid] }
+ assert_equal 'Invalid index: :invalid', exception.message
+ end
+
+ def test_should_raise_exception_on_fetch_if_invalid_index_specified
+ exception = assert_raises(ArgumentError) { @collection.fetch(:something, :invalid) }
+ assert_equal 'Invalid index: :invalid', exception.message
+ end
+end
diff --git a/test/unit/node_collection/node_collection_with_indices_test.rb b/test/unit/node_collection/node_collection_with_indices_test.rb
new file mode 100644
index 0000000..b15fe99
--- /dev/null
+++ b/test/unit/node_collection/node_collection_with_indices_test.rb
@@ -0,0 +1,42 @@
+require_relative '../../test_helper'
+require_relative '../../files/node'
+
+class NodeCollectionWithIndicesTest < StateMachinesTest
+ def setup
+ machine = StateMachines::Machine.new(Class.new)
+ @collection = StateMachines::NodeCollection.new(machine, index: [:name, :value])
+
+ @object = Node.new(:parked, 1)
+ @collection << @object
+ end
+
+ def test_should_use_first_index_by_default_on_key_retrieval
+ assert_equal [:parked], @collection.keys
+ end
+
+ def test_should_allow_customizing_index_for_key_retrieval
+ assert_equal [1], @collection.keys(:value)
+ end
+
+ def test_should_use_first_index_by_default_on_lookup
+ assert_equal @object, @collection[:parked]
+ assert_nil @collection[1]
+ end
+
+ def test_should_allow_customizing_index_on_lookup
+ assert_equal @object, @collection[1, :value]
+ assert_nil @collection[:parked, :value]
+ end
+
+ def test_should_use_first_index_by_default_on_fetch
+ assert_equal @object, @collection.fetch(:parked)
+ exception = assert_raises(IndexError) { @collection.fetch(1) }
+ assert_equal '1 is an invalid name', exception.message
+ end
+
+ def test_should_allow_customizing_index_on_fetch
+ assert_equal @object, @collection.fetch(1, :value)
+ exception = assert_raises(IndexError) { @collection.fetch(:parked, :value) }
+ assert_equal ':parked is an invalid value', exception.message
+ end
+end
diff --git a/test/unit/node_collection/node_collection_with_matcher_contexts_test.rb b/test/unit/node_collection/node_collection_with_matcher_contexts_test.rb
new file mode 100644
index 0000000..220c67c
--- /dev/null
+++ b/test/unit/node_collection/node_collection_with_matcher_contexts_test.rb
@@ -0,0 +1,25 @@
+require_relative '../../test_helper'
+require_relative '../../files/node'
+
+class NodeCollectionWithMatcherContextsTest < StateMachinesTest
+ def setup
+ machine = StateMachines::Machine.new(Class.new)
+ @collection = StateMachines::NodeCollection.new(machine)
+ @collection << Node.new(:parked)
+ end
+
+ def test_should_always_run_all_matcher_context
+ contexts_run = []
+ @collection.context([StateMachines::AllMatcher.instance]) { contexts_run << :all }
+ assert_equal [:all], contexts_run
+ end
+
+ def test_should_only_run_blacklist_matcher_if_not_matched
+ contexts_run = []
+ @collection.context([StateMachines::BlacklistMatcher.new([:parked])]) { contexts_run << :blacklist }
+ assert_equal [], contexts_run
+
+ @collection << Node.new(:idling)
+ assert_equal [:blacklist], contexts_run
+ end
+end
diff --git a/test/unit/node_collection/node_collection_with_nodes_test.rb b/test/unit/node_collection/node_collection_with_nodes_test.rb
new file mode 100644
index 0000000..22d32eb
--- /dev/null
+++ b/test/unit/node_collection/node_collection_with_nodes_test.rb
@@ -0,0 +1,46 @@
+require_relative '../../test_helper'
+require_relative '../../files/node'
+
+class NodeCollectionWithNodesTest < StateMachinesTest
+ def setup
+ @machine = StateMachines::Machine.new(Class.new)
+ @collection = StateMachines::NodeCollection.new(@machine)
+
+ @parked = Node.new(:parked, nil, @machine)
+ @idling = Node.new(:idling, nil, @machine)
+
+ @collection << @parked
+ @collection << @idling
+ end
+
+ def test_should_be_able_to_enumerate
+ order = []
+ @collection.each { |object| order << object }
+
+ assert_equal [@parked, @idling], order
+ end
+
+ def test_should_be_able_to_concatenate_multiple_nodes
+ @first_gear = Node.new(:first_gear, nil, @machine)
+ @second_gear = Node.new(:second_gear, nil, @machine)
+ @collection.concat([@first_gear, @second_gear])
+
+ order = []
+ @collection.each { |object| order << object }
+ assert_equal [@parked, @idling, @first_gear, @second_gear], order
+ end
+
+ def test_should_be_able_to_access_by_index
+ assert_equal @parked, @collection.at(0)
+ assert_equal @idling, @collection.at(1)
+ end
+
+ def test_should_deep_copy_machine_changes
+ new_machine = StateMachines::Machine.new(Class.new)
+ @collection.machine = new_machine
+
+ assert_equal new_machine, @collection.machine
+ assert_equal new_machine, @parked.machine
+ assert_equal new_machine, @idling.machine
+ end
+end
diff --git a/test/unit/node_collection/node_collection_with_numeric_index_test.rb b/test/unit/node_collection/node_collection_with_numeric_index_test.rb
new file mode 100644
index 0000000..1544e4c
--- /dev/null
+++ b/test/unit/node_collection/node_collection_with_numeric_index_test.rb
@@ -0,0 +1,24 @@
+require_relative '../../test_helper'
+require_relative '../../files/node'
+
+class NodeCollectionWithNumericIndexTest < StateMachinesTest
+ def setup
+ machine = StateMachines::Machine.new(Class.new)
+ @collection = StateMachines::NodeCollection.new(machine, index: [:name, :value])
+
+ @parked = Node.new(10, 1)
+ @collection << @parked
+ end
+
+ def test_should_index_by_name
+ assert_equal @parked, @collection[10]
+ end
+
+ def test_should_index_by_string_name
+ assert_equal @parked, @collection['10']
+ end
+
+ def test_should_index_by_symbol_name
+ assert_equal @parked, @collection[:'10']
+ end
+end
diff --git a/test/unit/node_collection/node_collection_with_postdefined_contexts_test.rb b/test/unit/node_collection/node_collection_with_postdefined_contexts_test.rb
new file mode 100644
index 0000000..55e6a4e
--- /dev/null
+++ b/test/unit/node_collection/node_collection_with_postdefined_contexts_test.rb
@@ -0,0 +1,22 @@
+require_relative '../../test_helper'
+require_relative '../../files/node'
+
+class NodeCollectionWithPostdefinedContextsTest < StateMachinesTest
+ def setup
+ machine = StateMachines::Machine.new(Class.new)
+ @collection = StateMachines::NodeCollection.new(machine)
+ @collection << Node.new(:parked)
+ end
+
+ def test_should_run_context_if_matched
+ contexts_run = []
+ @collection.context([:parked]) { contexts_run << :parked }
+ assert_equal [:parked], contexts_run
+ end
+
+ def test_should_not_run_contexts_if_not_matched
+ contexts_run = []
+ @collection.context([:idling]) { contexts_run << :idling }
+ assert_equal [], contexts_run
+ end
+end
diff --git a/test/unit/node_collection/node_collection_with_predefined_contexts_test.rb b/test/unit/node_collection/node_collection_with_predefined_contexts_test.rb
new file mode 100644
index 0000000..7cc9089
--- /dev/null
+++ b/test/unit/node_collection/node_collection_with_predefined_contexts_test.rb
@@ -0,0 +1,23 @@
+require_relative '../../test_helper'
+require_relative '../../files/node'
+
+class NodeCollectionWithPredefinedContextsTest < StateMachinesTest
+ def setup
+ machine = StateMachines::Machine.new(Class.new)
+ @collection = StateMachines::NodeCollection.new(machine)
+
+ @contexts_run = contexts_run = []
+ @collection.context([:parked]) { contexts_run << :parked }
+ @collection.context([:parked]) { contexts_run << :second_parked }
+ end
+
+ def test_should_run_contexts_in_the_order_defined
+ @collection << Node.new(:parked)
+ assert_equal [:parked, :second_parked], @contexts_run
+ end
+
+ def test_should_not_run_contexts_if_not_matched
+ @collection << Node.new(:idling)
+ assert_equal [], @contexts_run
+ end
+end
diff --git a/test/unit/node_collection/node_collection_with_string_index_test.rb b/test/unit/node_collection/node_collection_with_string_index_test.rb
new file mode 100644
index 0000000..77f7a38
--- /dev/null
+++ b/test/unit/node_collection/node_collection_with_string_index_test.rb
@@ -0,0 +1,20 @@
+require_relative '../../test_helper'
+require_relative '../../files/node'
+
+class NodeCollectionWithStringIndexTest < StateMachinesTest
+ def setup
+ machine = StateMachines::Machine.new(Class.new)
+ @collection = StateMachines::NodeCollection.new(machine, index: [:name, :value])
+
+ @parked = Node.new(:parked, 1)
+ @collection << @parked
+ end
+
+ def test_should_index_by_name
+ assert_equal @parked, @collection[:parked]
+ end
+
+ def test_should_index_by_string_name
+ assert_equal @parked, @collection['parked']
+ end
+end
diff --git a/test/unit/node_collection/node_collection_with_symbol_index_test.rb b/test/unit/node_collection/node_collection_with_symbol_index_test.rb
new file mode 100644
index 0000000..1d488f5
--- /dev/null
+++ b/test/unit/node_collection/node_collection_with_symbol_index_test.rb
@@ -0,0 +1,20 @@
+require_relative '../../test_helper'
+require_relative '../../files/node'
+
+class NodeCollectionWithSymbolIndexTest < StateMachinesTest
+ def setup
+ machine = StateMachines::Machine.new(Class.new)
+ @collection = StateMachines::NodeCollection.new(machine, index: [:name, :value])
+
+ @parked = Node.new('parked', 1)
+ @collection << @parked
+ end
+
+ def test_should_index_by_name
+ assert_equal @parked, @collection['parked']
+ end
+
+ def test_should_index_by_symbol_name
+ assert_equal @parked, @collection[:parked]
+ end
+end
diff --git a/test/unit/node_collection/node_collection_without_indices_test.rb b/test/unit/node_collection/node_collection_without_indices_test.rb
new file mode 100644
index 0000000..671b126
--- /dev/null
+++ b/test/unit/node_collection/node_collection_without_indices_test.rb
@@ -0,0 +1,30 @@
+require_relative '../../test_helper'
+
+class NodeCollectionWithoutIndicesTest < StateMachinesTest
+ def setup
+ machine = StateMachines::Machine.new(Class.new)
+ @collection = StateMachines::NodeCollection.new(machine, index: {})
+ end
+
+ def test_should_allow_adding_node
+ @collection << Object.new
+ assert_equal 1, @collection.length
+ end
+
+ def test_should_not_allow_keys_retrieval
+ exception = assert_raises(ArgumentError) { @collection.keys }
+ assert_equal 'No indices configured', exception.message
+ end
+
+ def test_should_not_allow_lookup
+ @collection << Object.new
+ exception = assert_raises(ArgumentError) { @collection[0] }
+ assert_equal 'No indices configured', exception.message
+ end
+
+ def test_should_not_allow_fetching
+ @collection << Object.new
+ exception = assert_raises(ArgumentError) { @collection.fetch(0) }
+ assert_equal 'No indices configured', exception.message
+ end
+end
diff --git a/test/unit/path/path_by_default_test.rb b/test/unit/path/path_by_default_test.rb
new file mode 100644
index 0000000..43f7a1e
--- /dev/null
+++ b/test/unit/path/path_by_default_test.rb
@@ -0,0 +1,54 @@
+require_relative '../../test_helper'
+
+class PathByDefaultTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @object = @klass.new
+
+ @path = StateMachines::Path.new(@object, @machine)
+ end
+
+ def test_should_have_an_object
+ assert_equal @object, @path.object
+ end
+
+ def test_should_have_a_machine
+ assert_equal @machine, @path.machine
+ end
+
+ def test_should_not_have_walked_anywhere
+ assert_equal [], @path
+ end
+
+ def test_should_not_have_a_from_name
+ assert_nil @path.from_name
+ end
+
+ def test_should_have_no_from_states
+ assert_equal [], @path.from_states
+ end
+
+ def test_should_not_have_a_to_name
+ assert_nil @path.to_name
+ end
+
+ def test_should_have_no_to_states
+ assert_equal [], @path.to_states
+ end
+
+ def test_should_have_no_events
+ assert_equal [], @path.events
+ end
+
+ def test_should_not_be_able_to_walk_anywhere
+ walked = false
+ @path.walk { walked = true }
+ assert_equal false, walked
+ end
+
+ def test_should_not_be_complete
+ assert_equal false, @path.complete?
+ end
+end
+
diff --git a/test/unit/path/path_test.rb b/test/unit/path/path_test.rb
new file mode 100644
index 0000000..983066e
--- /dev/null
+++ b/test/unit/path/path_test.rb
@@ -0,0 +1,14 @@
+require_relative '../../test_helper'
+
+class PathTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @object = @klass.new
+ end
+
+ def test_should_raise_exception_if_invalid_option_specified
+ exception = assert_raises(ArgumentError) { StateMachines::Path.new(@object, @machine, invalid: true) }
+ assert_equal 'Unknown key: :invalid. Valid keys are: :target, :guard', exception.message
+ end
+end
\ No newline at end of file
diff --git a/test/unit/path/path_with_available_transitions_after_reaching_target_test.rb b/test/unit/path/path_with_available_transitions_after_reaching_target_test.rb
new file mode 100644
index 0000000..589256d
--- /dev/null
+++ b/test/unit/path/path_with_available_transitions_after_reaching_target_test.rb
@@ -0,0 +1,40 @@
+require_relative '../../test_helper'
+
+class PathWithAvailableTransitionsAfterReachingTargetTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked, :idling
+ @machine.event :ignite do
+ transition parked: :idling
+ end
+ @machine.event :shift_up do
+ transition parked: :first_gear
+ end
+ @machine.event :park do
+ transition [:idling, :first_gear] => :parked
+ end
+
+ @object = @klass.new
+ @object.state = 'parked'
+
+ @path = StateMachines::Path.new(@object, @machine, target: :parked)
+ @path.concat([
+ @ignite_transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling),
+ @park_transition = StateMachines::Transition.new(@object, @machine, :park, :idling, :parked)
+ ])
+ end
+
+ def test_should_be_complete
+ assert_equal true, @path.complete?
+ end
+
+ def test_should_be_able_to_walk
+ paths = []
+ @path.walk { |path| paths << path }
+ assert_equal [
+ [@ignite_transition, @park_transition, StateMachines::Transition.new(@object, @machine, :shift_up, :parked, :first_gear)]
+ ], paths
+ end
+end
+
diff --git a/test/unit/path/path_with_available_transitions_test.rb b/test/unit/path/path_with_available_transitions_test.rb
new file mode 100644
index 0000000..72a4dcb
--- /dev/null
+++ b/test/unit/path/path_with_available_transitions_test.rb
@@ -0,0 +1,54 @@
+require_relative '../../test_helper'
+
+class PathWithAvailableTransitionsTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked, :idling, :first_gear
+ @machine.event :ignite
+ @machine.event :shift_up do
+ transition idling: :first_gear
+ end
+ @machine.event :park do
+ transition idling: :parked
+ end
+
+ @object = @klass.new
+ @object.state = 'parked'
+
+ @path = StateMachines::Path.new(@object, @machine)
+ @path.concat([
+ @ignite_transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ ])
+ end
+
+ def test_should_not_be_complete
+ refute @path.complete?
+ end
+
+ def test_should_walk_each_available_transition
+ paths = []
+ @path.walk { |path| paths << path }
+
+ assert_equal [
+ [@ignite_transition, StateMachines::Transition.new(@object, @machine, :shift_up, :idling, :first_gear)],
+ [@ignite_transition, StateMachines::Transition.new(@object, @machine, :park, :idling, :parked)]
+ ], paths
+ end
+
+ def test_should_yield_path_instances_when_walking
+ @path.walk do |path|
+ assert_instance_of StateMachines::Path, path
+ end
+ end
+
+ def test_should_not_modify_current_path_after_walking
+ @path.walk {}
+ assert_equal [@ignite_transition], @path
+ end
+
+ def test_should_not_modify_object_after_walking
+ @path.walk {}
+ assert_equal 'parked', @object.state
+ end
+end
diff --git a/test/unit/path/path_with_deep_target_reached_test.rb b/test/unit/path/path_with_deep_target_reached_test.rb
new file mode 100644
index 0000000..7c1ea6a
--- /dev/null
+++ b/test/unit/path/path_with_deep_target_reached_test.rb
@@ -0,0 +1,50 @@
+require_relative '../../test_helper'
+
+class PathWithDeepTargetReachedTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked, :idling
+ @machine.event :ignite do
+ transition parked: :idling
+ end
+ @machine.event :shift_up do
+ transition parked: :first_gear
+ end
+ @machine.event :park do
+ transition [:idling, :first_gear] => :parked
+ end
+
+ @object = @klass.new
+ @object.state = 'parked'
+
+ @path = StateMachines::Path.new(@object, @machine, target: :parked)
+ @path.concat([
+ @ignite_transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling),
+ @park_transition = StateMachines::Transition.new(@object, @machine, :park, :idling, :parked),
+ @shift_up_transition = StateMachines::Transition.new(@object, @machine, :shift_up, :parked, :first_gear),
+ @park_transition_2 = StateMachines::Transition.new(@object, @machine, :park, :first_gear, :parked)
+ ])
+ end
+
+ def test_should_be_complete
+ assert_equal true, @path.complete?
+ end
+
+ def test_should_not_be_able_to_walk
+ walked = false
+ @path.walk { walked = true }
+ assert_equal false, walked
+ end
+
+ def test_should_not_be_able_to_walk_with_available_transitions
+ @machine.event :park do
+ transition parked: same
+ end
+
+ walked = false
+ @path.walk { walked = true }
+ assert_equal false, walked
+ end
+end
+
diff --git a/test/unit/path/path_with_deep_target_test.rb b/test/unit/path/path_with_deep_target_test.rb
new file mode 100644
index 0000000..e0ae6b1
--- /dev/null
+++ b/test/unit/path/path_with_deep_target_test.rb
@@ -0,0 +1,40 @@
+require_relative '../../test_helper'
+
+class PathWithDeepTargetTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked, :idling
+ @machine.event :ignite do
+ transition parked: :idling
+ end
+ @machine.event :shift_up do
+ transition parked: :first_gear
+ end
+ @machine.event :park do
+ transition [:idling, :first_gear] => :parked
+ end
+
+ @object = @klass.new
+ @object.state = 'parked'
+
+ @path = StateMachines::Path.new(@object, @machine, target: :parked)
+ @path.concat([
+ @ignite_transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling),
+ @park_transition = StateMachines::Transition.new(@object, @machine, :park, :idling, :parked),
+ @shift_up_transition = StateMachines::Transition.new(@object, @machine, :shift_up, :parked, :first_gear)
+ ])
+ end
+
+ def test_should_not_be_complete
+ assert_equal false, @path.complete?
+ end
+
+ def test_should_be_able_to_walk
+ paths = []
+ @path.walk { |path| paths << path }
+ assert_equal [
+ [@ignite_transition, @park_transition, @shift_up_transition, StateMachines::Transition.new(@object, @machine, :park, :first_gear, :parked)]
+ ], paths
+ end
+end
diff --git a/test/unit/path/path_with_duplicates_test.rb b/test/unit/path/path_with_duplicates_test.rb
new file mode 100644
index 0000000..a651473
--- /dev/null
+++ b/test/unit/path/path_with_duplicates_test.rb
@@ -0,0 +1,32 @@
+require_relative '../../test_helper'
+
+class PathWithDuplicatesTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked, :idling
+ @machine.event :park, :ignite
+
+ @object = @klass.new
+ @object.state = 'parked'
+
+ @path = StateMachines::Path.new(@object, @machine)
+ @path.concat([
+ @ignite_transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling),
+ @park_transition = StateMachines::Transition.new(@object, @machine, :park, :idling, :parked),
+ @ignite_again_transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ ])
+ end
+
+ def test_should_not_include_duplicates_in_from_states
+ assert_equal [:parked, :idling], @path.from_states
+ end
+
+ def test_should_not_include_duplicates_in_to_states
+ assert_equal [:idling, :parked], @path.to_states
+ end
+
+ def test_should_not_include_duplicates_in_events
+ assert_equal [:ignite, :park], @path.events
+ end
+end
diff --git a/test/unit/path/path_with_encountered_transitions_test.rb b/test/unit/path/path_with_encountered_transitions_test.rb
new file mode 100644
index 0000000..88f6793
--- /dev/null
+++ b/test/unit/path/path_with_encountered_transitions_test.rb
@@ -0,0 +1,34 @@
+require_relative '../../test_helper'
+
+class PathWithEncounteredTransitionsTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked, :idling, :first_gear
+ @machine.event :ignite do
+ transition parked: :idling
+ end
+ @machine.event :park do
+ transition idling: :parked
+ end
+
+ @object = @klass.new
+ @object.state = 'parked'
+
+ @path = StateMachines::Path.new(@object, @machine)
+ @path.concat([
+ @ignite_transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling),
+ @park_transition = StateMachines::Transition.new(@object, @machine, :park, :idling, :parked)
+ ])
+ end
+
+ def test_should_be_complete
+ assert_equal true, @path.complete?
+ end
+
+ def test_should_not_be_able_to_walk
+ walked = false
+ @path.walk { walked = true }
+ assert_equal false, walked
+ end
+end
diff --git a/test/unit/path/path_with_guarded_transitions_test.rb b/test/unit/path/path_with_guarded_transitions_test.rb
new file mode 100644
index 0000000..4514530
--- /dev/null
+++ b/test/unit/path/path_with_guarded_transitions_test.rb
@@ -0,0 +1,42 @@
+require_relative '../../test_helper'
+
+class PathWithGuardedTransitionsTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked, :idling
+ @machine.event :ignite
+ @machine.event :shift_up do
+ transition idling: :first_gear, if: lambda { false }
+ end
+
+ @object = @klass.new
+ @object.state = 'parked'
+ end
+
+ def test_should_not_walk_transitions_if_guard_enabled
+ path = StateMachines::Path.new(@object, @machine)
+ path.concat([
+ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ ])
+
+ paths = []
+ path.walk { |next_path| paths << next_path }
+
+ assert_equal [], paths
+ end
+
+ def test_should_not_walk_transitions_if_guard_disabled
+ path = StateMachines::Path.new(@object, @machine, guard: false)
+ path.concat([
+ ignite_transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ ])
+
+ paths = []
+ path.walk { |next_path| paths << next_path }
+
+ assert_equal [
+ [ignite_transition, StateMachines::Transition.new(@object, @machine, :shift_up, :idling, :first_gear)]
+ ], paths
+ end
+end
diff --git a/test/unit/path/path_with_reached_target_test.rb b/test/unit/path/path_with_reached_target_test.rb
new file mode 100644
index 0000000..bcd7334
--- /dev/null
+++ b/test/unit/path/path_with_reached_target_test.rb
@@ -0,0 +1,35 @@
+require_relative '../../test_helper'
+
+class PathWithReachedTargetTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked, :idling
+ @machine.event :ignite do
+ transition parked: :idling
+ end
+ @machine.event :park do
+ transition idling: :parked
+ end
+
+ @object = @klass.new
+ @object.state = 'parked'
+
+ @path = StateMachines::Path.new(@object, @machine, target: :parked)
+ @path.concat([
+ @ignite_transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling),
+ @park_transition = StateMachines::Transition.new(@object, @machine, :park, :idling, :parked)
+ ])
+ end
+
+ def test_should_be_complete
+ assert_equal true, @path.complete?
+ end
+
+ def test_should_not_be_able_to_walk
+ walked = false
+ @path.walk { walked = true }
+ assert_equal false, walked
+ end
+end
+
diff --git a/test/unit/path/path_with_transitions_test.rb b/test/unit/path/path_with_transitions_test.rb
new file mode 100644
index 0000000..e747563
--- /dev/null
+++ b/test/unit/path/path_with_transitions_test.rb
@@ -0,0 +1,54 @@
+require_relative '../../test_helper'
+
+class PathWithTransitionsTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked, :idling, :first_gear
+ @machine.event :ignite, :shift_up
+
+ @object = @klass.new
+ @object.state = 'parked'
+
+ @path = StateMachines::Path.new(@object, @machine)
+ @path.concat([
+ @ignite_transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling),
+ @shift_up_transition = StateMachines::Transition.new(@object, @machine, :shift_up, :idling, :first_gear)
+ ])
+ end
+
+ def test_should_enumerate_transitions
+ assert_equal [@ignite_transition, @shift_up_transition], @path
+ end
+
+ def test_should_have_a_from_name
+ assert_equal :parked, @path.from_name
+ end
+
+ def test_should_have_from_states
+ assert_equal [:parked, :idling], @path.from_states
+ end
+
+ def test_should_have_a_to_name
+ assert_equal :first_gear, @path.to_name
+ end
+
+ def test_should_have_to_states
+ assert_equal [:idling, :first_gear], @path.to_states
+ end
+
+ def test_should_have_events
+ assert_equal [:ignite, :shift_up], @path.events
+ end
+
+ def test_should_not_be_able_to_walk_anywhere
+ walked = false
+ @path.walk { walked = true }
+ assert_equal false, walked
+ end
+
+ def test_should_be_complete
+ assert_equal true, @path.complete?
+ end
+end
+
diff --git a/test/unit/path/path_with_unreached_target_test.rb b/test/unit/path/path_with_unreached_target_test.rb
new file mode 100644
index 0000000..0c3b3db
--- /dev/null
+++ b/test/unit/path/path_with_unreached_target_test.rb
@@ -0,0 +1,31 @@
+require_relative '../../test_helper'
+
+class PathWithUnreachedTargetTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked, :idling
+ @machine.event :ignite do
+ transition parked: :idling
+ end
+
+ @object = @klass.new
+ @object.state = 'parked'
+
+ @path = StateMachines::Path.new(@object, @machine, target: :parked)
+ @path.concat([
+ @ignite_transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ ])
+ end
+
+ def test_should_not_be_complete
+ assert_equal false, @path.complete?
+ end
+
+ def test_should_not_be_able_to_walk
+ walked = false
+ @path.walk { walked = true }
+ assert_equal false, walked
+ end
+end
+
diff --git a/test/unit/path/path_without_transitions_test.rb b/test/unit/path/path_without_transitions_test.rb
new file mode 100644
index 0000000..8be5cd5
--- /dev/null
+++ b/test/unit/path/path_without_transitions_test.rb
@@ -0,0 +1,24 @@
+require_relative '../../test_helper'
+
+class PathWithoutTransitionsTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked, :idling
+ @machine.event :ignite
+
+ @object = @klass.new
+
+ @path = StateMachines::Path.new(@object, @machine)
+ @path.concat([
+ @ignite_transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ ])
+ end
+
+ def test_should_not_be_able_to_walk_anywhere
+ walked = false
+ @path.walk { walked = true }
+ assert_equal false, walked
+ end
+end
+
diff --git a/test/unit/path_collection/path_collection_by_default_test.rb b/test/unit/path_collection/path_collection_by_default_test.rb
new file mode 100644
index 0000000..1720166
--- /dev/null
+++ b/test/unit/path_collection/path_collection_by_default_test.rb
@@ -0,0 +1,46 @@
+require_relative '../../test_helper'
+
+class PathCollectionByDefaultTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked
+
+ @object = @klass.new
+ @object.state = 'parked'
+
+ @paths = StateMachines::PathCollection.new(@object, @machine)
+ end
+
+ def test_should_have_an_object
+ assert_equal @object, @paths.object
+ end
+
+ def test_should_have_a_machine
+ assert_equal @machine, @paths.machine
+ end
+
+ def test_should_have_a_from_name
+ assert_equal :parked, @paths.from_name
+ end
+
+ def test_should_not_have_a_to_name
+ assert_nil @paths.to_name
+ end
+
+ def test_should_have_no_from_states
+ assert_equal [], @paths.from_states
+ end
+
+ def test_should_have_no_to_states
+ assert_equal [], @paths.to_states
+ end
+
+ def test_should_have_no_events
+ assert_equal [], @paths.events
+ end
+
+ def test_should_have_no_paths
+ assert @paths.empty?
+ end
+end
diff --git a/test/unit/path_collection/path_collection_test.rb b/test/unit/path_collection/path_collection_test.rb
new file mode 100644
index 0000000..b875168
--- /dev/null
+++ b/test/unit/path_collection/path_collection_test.rb
@@ -0,0 +1,24 @@
+require_relative '../../test_helper'
+
+class PathCollectionTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @object = @klass.new
+ end
+
+ def test_should_raise_exception_if_invalid_option_specified
+ exception = assert_raises(ArgumentError) { StateMachines::PathCollection.new(@object, @machine, invalid: true) }
+ assert_equal 'Unknown key: :invalid. Valid keys are: :from, :to, :deep, :guard', exception.message
+ end
+
+ def test_should_raise_exception_if_invalid_from_state_specified
+ exception = assert_raises(IndexError) { StateMachines::PathCollection.new(@object, @machine, from: :invalid) }
+ assert_equal ':invalid is an invalid name', exception.message
+ end
+
+ def test_should_raise_exception_if_invalid_to_state_specified
+ exception = assert_raises(IndexError) { StateMachines::PathCollection.new(@object, @machine, to: :invalid) }
+ assert_equal ':invalid is an invalid name', exception.message
+ end
+end
diff --git a/test/unit/path_collection/path_collection_with_deep_paths_test.rb b/test/unit/path_collection/path_collection_with_deep_paths_test.rb
new file mode 100644
index 0000000..abbaf3c
--- /dev/null
+++ b/test/unit/path_collection/path_collection_with_deep_paths_test.rb
@@ -0,0 +1,43 @@
+require_relative '../../test_helper'
+
+class PathCollectionWithDeepPathsTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked, :idling
+ @machine.event :ignite do
+ transition parked: :idling
+ end
+ @machine.event :shift_up do
+ transition parked: :idling, idling: :first_gear
+ end
+ @machine.event :shift_down do
+ transition first_gear: :idling
+ end
+ @object = @klass.new
+ @object.state = 'parked'
+
+ @paths = StateMachines::PathCollection.new(@object, @machine, to: :idling, deep: true)
+ end
+
+ def test_should_allow_target_to_be_reached_more_than_once_per_path
+ assert_equal [
+ [
+ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ ],
+ [
+ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling),
+ StateMachines::Transition.new(@object, @machine, :shift_up, :idling, :first_gear),
+ StateMachines::Transition.new(@object, @machine, :shift_down, :first_gear, :idling)
+ ],
+ [
+ StateMachines::Transition.new(@object, @machine, :shift_up, :parked, :idling)
+ ],
+ [
+ StateMachines::Transition.new(@object, @machine, :shift_up, :parked, :idling),
+ StateMachines::Transition.new(@object, @machine, :shift_up, :idling, :first_gear),
+ StateMachines::Transition.new(@object, @machine, :shift_down, :first_gear, :idling)
+ ]
+ ], @paths
+ end
+end
diff --git a/test/unit/path_collection/path_collection_with_duplicate_nodes_test.rb b/test/unit/path_collection/path_collection_with_duplicate_nodes_test.rb
new file mode 100644
index 0000000..6639b7e
--- /dev/null
+++ b/test/unit/path_collection/path_collection_with_duplicate_nodes_test.rb
@@ -0,0 +1,31 @@
+require_relative '../../test_helper'
+
+class PathCollectionWithDuplicateNodesTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked, :idling
+ @machine.event :shift_up do
+ transition parked: :idling, idling: :first_gear
+ end
+ @machine.event :park do
+ transition first_gear: :idling
+ end
+ @object = @klass.new
+ @object.state = 'parked'
+
+ @paths = StateMachines::PathCollection.new(@object, @machine)
+ end
+
+ def test_should_not_include_duplicates_in_from_states
+ assert_equal [:parked, :idling, :first_gear], @paths.from_states
+ end
+
+ def test_should_not_include_duplicates_in_to_states
+ assert_equal [:idling, :first_gear], @paths.to_states
+ end
+
+ def test_should_not_include_duplicates_in_events
+ assert_equal [:shift_up, :park], @paths.events
+ end
+end
diff --git a/test/unit/path_collection/path_collection_with_from_state_test.rb b/test/unit/path_collection/path_collection_with_from_state_test.rb
new file mode 100644
index 0000000..e643dfc
--- /dev/null
+++ b/test/unit/path_collection/path_collection_with_from_state_test.rb
@@ -0,0 +1,27 @@
+require_relative '../../test_helper'
+
+class PathCollectionWithFromStateTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked, :idling, :first_gear
+ @machine.event :park do
+ transition idling: :parked
+ end
+
+ @object = @klass.new
+ @object.state = 'parked'
+
+ @paths = StateMachines::PathCollection.new(@object, @machine, from: :idling)
+ end
+
+ def test_should_generate_paths_from_custom_from_state
+ assert_equal [[
+ StateMachines::Transition.new(@object, @machine, :park, :idling, :parked)
+ ]], @paths
+ end
+
+ def test_should_have_a_from_name
+ assert_equal :idling, @paths.from_name
+ end
+end
diff --git a/test/unit/path_collection/path_collection_with_paths_test.rb b/test/unit/path_collection/path_collection_with_paths_test.rb
new file mode 100644
index 0000000..ac7c4ef
--- /dev/null
+++ b/test/unit/path_collection/path_collection_with_paths_test.rb
@@ -0,0 +1,47 @@
+require_relative '../../test_helper'
+
+class PathCollectionWithPathsTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked, :idling, :first_gear
+ @machine.event :ignite do
+ transition parked: :idling
+ end
+ @machine.event :shift_up do
+ transition idling: :first_gear
+ end
+
+ @object = @klass.new
+ @object.state = 'parked'
+
+ @paths = StateMachines::PathCollection.new(@object, @machine)
+ end
+
+ def test_should_enumerate_paths
+ assert_equal [[
+ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling),
+ StateMachines::Transition.new(@object, @machine, :shift_up, :idling, :first_gear)
+ ]], @paths
+ end
+
+ def test_should_have_a_from_name
+ assert_equal :parked, @paths.from_name
+ end
+
+ def test_should_not_have_a_to_name
+ assert_nil @paths.to_name
+ end
+
+ def test_should_have_from_states
+ assert_equal [:parked, :idling], @paths.from_states
+ end
+
+ def test_should_have_to_states
+ assert_equal [:idling, :first_gear], @paths.to_states
+ end
+
+ def test_should_have_no_events
+ assert_equal [:ignite, :shift_up], @paths.events
+ end
+end
diff --git a/test/unit/path_collection/path_collection_with_to_state_test.rb b/test/unit/path_collection/path_collection_with_to_state_test.rb
new file mode 100644
index 0000000..21d0599
--- /dev/null
+++ b/test/unit/path_collection/path_collection_with_to_state_test.rb
@@ -0,0 +1,29 @@
+require_relative '../../test_helper'
+
+class PathCollectionWithToStateTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked, :idling
+ @machine.event :ignite do
+ transition parked: :idling
+ end
+ @machine.event :shift_up do
+ transition parked: :idling, idling: :first_gear
+ end
+ @machine.event :shift_down do
+ transition first_gear: :idling
+ end
+ @object = @klass.new
+ @object.state = 'parked'
+
+ @paths = StateMachines::PathCollection.new(@object, @machine, to: :idling)
+ end
+
+ def test_should_stop_paths_once_target_state_reached
+ assert_equal [
+ [StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)],
+ [StateMachines::Transition.new(@object, @machine, :shift_up, :parked, :idling)]
+ ], @paths
+ end
+end
diff --git a/test/unit/path_collection/path_with_guarded_paths_test.rb b/test/unit/path_collection/path_with_guarded_paths_test.rb
new file mode 100644
index 0000000..98f6824
--- /dev/null
+++ b/test/unit/path_collection/path_with_guarded_paths_test.rb
@@ -0,0 +1,25 @@
+require_relative '../../test_helper'
+class PathWithGuardedPathsTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked, :idling, :first_gear
+ @machine.event :ignite do
+ transition parked: :idling, if: lambda { false }
+ end
+
+ @object = @klass.new
+ @object.state = 'parked'
+ end
+
+ def test_should_not_enumerate_paths_if_guard_enabled
+ assert_equal [], StateMachines::PathCollection.new(@object, @machine)
+ end
+
+ def test_should_enumerate_paths_if_guard_disabled
+ paths = StateMachines::PathCollection.new(@object, @machine, guard: false)
+ assert_equal [[
+ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ ]], paths
+ end
+end
diff --git a/test/unit/state/state_after_being_copied_test.rb b/test/unit/state/state_after_being_copied_test.rb
new file mode 100644
index 0000000..e8edfde
--- /dev/null
+++ b/test/unit/state/state_after_being_copied_test.rb
@@ -0,0 +1,19 @@
+require_relative '../../test_helper'
+
+class StateAfterBeingCopiedTest < StateMachinesTest
+ def setup
+ @machine = StateMachines::Machine.new(Class.new)
+ @machine.states << @state = StateMachines::State.new(@machine, :parked)
+ @copied_state = @state.dup
+ end
+
+ def test_should_not_have_the_context
+ state_context = nil
+ @state.context { state_context = self }
+
+ copied_state_context = nil
+ @copied_state.context { copied_state_context = self }
+
+ refute_same state_context, copied_state_context
+ end
+end
diff --git a/test/unit/state/state_by_default_test.rb b/test/unit/state/state_by_default_test.rb
new file mode 100644
index 0000000..0fa2e5a
--- /dev/null
+++ b/test/unit/state/state_by_default_test.rb
@@ -0,0 +1,41 @@
+require_relative '../../test_helper'
+
+class StateByDefaultTest < StateMachinesTest
+ def setup
+ @machine = StateMachines::Machine.new(Class.new)
+ @machine.states << @state = StateMachines::State.new(@machine, :parked)
+ end
+
+ def test_should_have_a_machine
+ assert_equal @machine, @state.machine
+ end
+
+ def test_should_have_a_name
+ assert_equal :parked, @state.name
+ end
+
+ def test_should_have_a_qualified_name
+ assert_equal :parked, @state.qualified_name
+ end
+
+ def test_should_have_a_human_name
+ assert_equal 'parked', @state.human_name
+ end
+
+ def test_should_use_stringify_the_name_as_the_value
+ assert_equal 'parked', @state.value
+ end
+
+ def test_should_not_be_initial
+ refute @state.initial
+ end
+
+ def test_should_not_have_a_matcher
+ assert_nil @state.matcher
+ end
+
+ def test_should_not_have_any_methods
+ expected = {}
+ assert_equal expected, @state.context_methods
+ end
+end
diff --git a/test/unit/state/state_final_test.rb b/test/unit/state/state_final_test.rb
new file mode 100644
index 0000000..12877b3
--- /dev/null
+++ b/test/unit/state/state_final_test.rb
@@ -0,0 +1,28 @@
+require_relative '../../test_helper'
+
+class StateFinalTest < StateMachinesTest
+ def setup
+ @machine = StateMachines::Machine.new(Class.new)
+ @machine.states << @state = StateMachines::State.new(@machine, :parked)
+ end
+
+ def test_should_be_final_without_input_transitions
+ assert @state.final?
+ end
+
+ def test_should_be_final_with_input_transitions
+ @machine.event :park do
+ transition idling: :parked
+ end
+
+ assert @state.final?
+ end
+
+ def test_should_be_final_with_loopback
+ @machine.event :ignite do
+ transition parked: same
+ end
+
+ assert @state.final?
+ end
+end
diff --git a/test/unit/state/state_initial_test.rb b/test/unit/state/state_initial_test.rb
new file mode 100644
index 0000000..ea3486d
--- /dev/null
+++ b/test/unit/state/state_initial_test.rb
@@ -0,0 +1,13 @@
+require_relative '../../test_helper'
+
+class StateInitialTest < StateMachinesTest
+ def setup
+ @machine = StateMachines::Machine.new(Class.new)
+ @machine.states << @state = StateMachines::State.new(@machine, :parked, initial: true)
+ end
+
+ def test_should_be_initial
+ assert @state.initial
+ assert @state.initial?
+ end
+end
diff --git a/test/unit/state/state_not_final_test.rb b/test/unit/state/state_not_final_test.rb
new file mode 100644
index 0000000..cf14f14
--- /dev/null
+++ b/test/unit/state/state_not_final_test.rb
@@ -0,0 +1,32 @@
+require_relative '../../test_helper'
+
+class StateNotFinalTest < StateMachinesTest
+ def setup
+ @machine = StateMachines::Machine.new(Class.new)
+ @machine.states << @state = StateMachines::State.new(@machine, :parked)
+ end
+
+ def test_should_not_be_final_with_outgoing_whitelist_transitions
+ @machine.event :ignite do
+ transition parked: :idling
+ end
+
+ refute @state.final?
+ end
+
+ def test_should_not_be_final_with_outgoing_all_transitions
+ @machine.event :ignite do
+ transition all => :idling
+ end
+
+ refute @state.final?
+ end
+
+ def test_should_not_be_final_with_outgoing_blacklist_transitions
+ @machine.event :ignite do
+ transition all - :first_gear => :idling
+ end
+
+ refute @state.final?
+ end
+end
diff --git a/test/unit/state/state_not_initial_test.rb b/test/unit/state/state_not_initial_test.rb
new file mode 100644
index 0000000..7ffbebf
--- /dev/null
+++ b/test/unit/state/state_not_initial_test.rb
@@ -0,0 +1,13 @@
+require_relative '../../test_helper'
+
+class StateNotInitialTest < StateMachinesTest
+ def setup
+ @machine = StateMachines::Machine.new(Class.new)
+ @machine.states << @state = StateMachines::State.new(@machine, :parked, initial: false)
+ end
+
+ def test_should_not_be_initial
+ refute @state.initial
+ refute @state.initial?
+ end
+end
diff --git a/test/unit/state/state_test.rb b/test/unit/state/state_test.rb
new file mode 100644
index 0000000..7f8a2d1
--- /dev/null
+++ b/test/unit/state/state_test.rb
@@ -0,0 +1,44 @@
+require_relative '../../test_helper'
+
+class StateTest < StateMachinesTest
+ def setup
+ @machine = StateMachines::Machine.new(Class.new)
+ @machine.states << @state = StateMachines::State.new(@machine, :parked)
+ end
+
+ def test_should_raise_exception_if_invalid_option_specified
+ exception = assert_raises(ArgumentError) { StateMachines::State.new(@machine, :parked, invalid: true) }
+ assert_equal 'Unknown key: :invalid. Valid keys are: :initial, :value, :cache, :if, :human_name', exception.message
+ end
+
+ def test_should_allow_changing_machine
+ new_machine = StateMachines::Machine.new(Class.new)
+ @state.machine = new_machine
+ assert_equal new_machine, @state.machine
+ end
+
+ def test_should_allow_changing_value
+ @state.value = 1
+ assert_equal 1, @state.value
+ end
+
+ def test_should_allow_changing_initial
+ @state.initial = true
+ assert @state.initial
+ end
+
+ def test_should_allow_changing_matcher
+ matcher = lambda {}
+ @state.matcher = matcher
+ assert_equal matcher, @state.matcher
+ end
+
+ def test_should_allow_changing_human_name
+ @state.human_name = 'stopped'
+ assert_equal 'stopped', @state.human_name
+ end
+
+ def test_should_use_pretty_inspect
+ assert_equal '#<StateMachines::State name=:parked value="parked" initial=false>', @state.inspect
+ end
+end
diff --git a/test/unit/state/state_with_cached_lambda_value_test.rb b/test/unit/state/state_with_cached_lambda_value_test.rb
new file mode 100644
index 0000000..12dbbec
--- /dev/null
+++ b/test/unit/state/state_with_cached_lambda_value_test.rb
@@ -0,0 +1,29 @@
+require_relative '../../test_helper'
+
+class StateWithCachedLambdaValueTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @dynamic_value = -> { 'value' }
+ @machine.states << @state = StateMachines::State.new(@machine, :parked, value: @dynamic_value, cache: true)
+ end
+
+ def test_should_be_caching
+ assert @state.cache
+ end
+
+ def test_should_evaluate_value
+ assert_equal 'value', @state.value
+ end
+
+ def test_should_only_evaluate_value_once
+ value = @state.value
+ assert_same value, @state.value
+ end
+
+ def test_should_update_value_index_for_state_collection
+ @state.value
+ assert_equal @state, @machine.states['value', :value]
+ assert_nil @machine.states[@dynamic_value, :value]
+ end
+end
diff --git a/test/unit/state/state_with_conflicting_helpers_after_definition_test.rb b/test/unit/state/state_with_conflicting_helpers_after_definition_test.rb
new file mode 100644
index 0000000..cd26a9f
--- /dev/null
+++ b/test/unit/state/state_with_conflicting_helpers_after_definition_test.rb
@@ -0,0 +1,38 @@
+require_relative '../../test_helper'
+
+class StateWithConflictingHelpersAfterDefinitionTest < StateMachinesTest
+ def setup
+ @original_stderr, $stderr = $stderr, StringIO.new
+
+ @klass = Class.new do
+ def parked?
+ 0
+ end
+ end
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked
+ @object = @klass.new
+ end
+
+ def test_should_not_override_state_predicate
+ assert_equal 0, @object.parked?
+ end
+
+ def test_should_still_allow_super_chaining
+ @klass.class_eval do
+ def parked?
+ super
+ end
+ end
+
+ assert_equal false, @object.parked?
+ end
+
+ def test_should_not_output_warning
+ assert_equal '', $stderr.string
+ end
+
+ def teardown
+ $stderr = @original_stderr
+ end
+end
diff --git a/test/unit/state/state_with_conflicting_helpers_before_definition_test.rb b/test/unit/state/state_with_conflicting_helpers_before_definition_test.rb
new file mode 100644
index 0000000..686e88b
--- /dev/null
+++ b/test/unit/state/state_with_conflicting_helpers_before_definition_test.rb
@@ -0,0 +1,29 @@
+require_relative '../../test_helper'
+
+class StateWithConflictingHelpersBeforeDefinitionTest < StateMachinesTest
+ def setup
+ @original_stderr, $stderr = $stderr, StringIO.new
+
+ @superclass = Class.new do
+ def parked?
+ 0
+ end
+ end
+ @klass = Class.new(@superclass)
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked
+ @object = @klass.new
+ end
+
+ def test_should_not_override_state_predicate
+ assert_equal 0, @object.parked?
+ end
+
+ def test_should_output_warning
+ assert_equal "Instance method \"parked?\" is already defined in #{@superclass}, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true.\n", $stderr.string
+ end
+
+ def teardown
+ $stderr = @original_stderr
+ end
+end
diff --git a/test/unit/state/state_with_conflicting_machine_name_test.rb b/test/unit/state/state_with_conflicting_machine_name_test.rb
new file mode 100644
index 0000000..2023739
--- /dev/null
+++ b/test/unit/state/state_with_conflicting_machine_name_test.rb
@@ -0,0 +1,20 @@
+require_relative '../../test_helper'
+require 'stringio'
+
+class StateWithConflictingMachineNameTest < StateMachinesTest
+ def setup
+ @original_stderr, $stderr = $stderr, StringIO.new
+
+ @klass = Class.new
+ @state_machine = StateMachines::Machine.new(@klass, :state)
+ end
+
+ def test_should_output_warning_if_name_conflicts
+ StateMachines::State.new(@state_machine, :state)
+ assert_equal "Instance method \"state?\" is already defined in #{@klass} :state instance helpers, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true.\n", $stderr.string
+ end
+
+ def teardown
+ $stderr = @original_stderr
+ end
+end
diff --git a/test/unit/state/state_with_conflicting_machine_test.rb b/test/unit/state/state_with_conflicting_machine_test.rb
new file mode 100644
index 0000000..11ec068
--- /dev/null
+++ b/test/unit/state/state_with_conflicting_machine_test.rb
@@ -0,0 +1,37 @@
+require_relative '../../test_helper'
+require 'stringio'
+
+class StateWithConflictingMachineTest < StateMachinesTest
+ def setup
+ @original_stderr, $stderr = $stderr, StringIO.new
+
+ @klass = Class.new
+ @state_machine = StateMachines::Machine.new(@klass, :state)
+ @state_machine.states << @state = StateMachines::State.new(@state_machine, :parked)
+ end
+
+ def test_should_output_warning_if_using_different_attribute
+ @status_machine = StateMachines::Machine.new(@klass, :status)
+ @status_machine.states << @state = StateMachines::State.new(@status_machine, :parked)
+
+ assert_equal "State :parked for :status is already defined in :state\n", $stderr.string
+ end
+
+ def test_should_not_output_warning_if_using_same_attribute
+ @status_machine = StateMachines::Machine.new(@klass, :status, attribute: :state)
+ @status_machine.states << @state = StateMachines::State.new(@status_machine, :parked)
+
+ assert_equal '', $stderr.string
+ end
+
+ def test_should_not_output_warning_if_using_different_namespace
+ @status_machine = StateMachines::Machine.new(@klass, :status, namespace: 'alarm')
+ @status_machine.states << @state = StateMachines::State.new(@status_machine, :parked)
+
+ assert_equal '', $stderr.string
+ end
+
+ def teardown
+ $stderr = @original_stderr
+ end
+end
diff --git a/test/unit/state/state_with_context_test.rb b/test/unit/state/state_with_context_test.rb
new file mode 100644
index 0000000..b042386
--- /dev/null
+++ b/test/unit/state/state_with_context_test.rb
@@ -0,0 +1,60 @@
+require_relative '../../test_helper'
+
+class StateWithContextTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @ancestors = @klass.ancestors
+ @machine.states << @state = StateMachines::State.new(@machine, :idling)
+
+ context = nil
+ speed_method = nil
+ rpm_method = nil
+ @result = @state.context do
+ context = self
+
+ def speed
+ 0
+ end
+
+ speed_method = instance_method(:speed)
+
+ def rpm
+ 1000
+ end
+
+ rpm_method = instance_method(:rpm)
+ end
+
+ @context = context
+ @speed_method = speed_method
+ @rpm_method = rpm_method
+ end
+
+ def test_should_return_true
+ assert_equal true, @result
+ end
+
+ def test_should_include_new_module_in_owner_class
+ refute_equal @ancestors, @klass.ancestors
+ assert_equal [@context], @klass.ancestors - @ancestors
+ end
+
+ def test_should_define_each_context_method_in_owner_class
+ %w(speed rpm).each { |method| assert @klass.method_defined?(method) }
+ end
+
+ def test_should_define_aliased_context_method_in_owner_class
+ %w(speed rpm).each { |method| assert @klass.method_defined?("__state_idling_#{method}_#{@context.object_id}__") }
+ end
+
+ def test_should_not_use_context_methods_as_owner_class_methods
+ refute_equal @speed_method, @state.context_methods[:speed]
+ refute_equal @rpm_method, @state.context_methods[:rpm]
+ end
+
+ def test_should_use_context_methods_as_aliased_owner_class_methods
+ assert_equal @speed_method, @state.context_methods[:"__state_idling_speed_#{@context.object_id}__"]
+ assert_equal @rpm_method, @state.context_methods[:"__state_idling_rpm_#{@context.object_id}__"]
+ end
+end
diff --git a/test/unit/state/state_with_dynamic_human_name_test.rb b/test/unit/state/state_with_dynamic_human_name_test.rb
new file mode 100644
index 0000000..d9e7298
--- /dev/null
+++ b/test/unit/state/state_with_dynamic_human_name_test.rb
@@ -0,0 +1,25 @@
+require_relative '../../test_helper'
+
+class StateWithDynamicHumanNameTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.states << @state = StateMachines::State.new(@machine, :parked, human_name: lambda { |_state, object| ['stopped', object] })
+ end
+
+ def test_should_use_custom_human_name
+ human_name, klass = @state.human_name
+ assert_equal 'stopped', human_name
+ assert_equal @klass, klass
+ end
+
+ def test_should_allow_custom_class_to_be_passed_through
+ human_name, klass = @state.human_name(1)
+ assert_equal 'stopped', human_name
+ assert_equal 1, klass
+ end
+
+ def test_should_not_cache_value
+ refute_same @state.human_name, @state.human_name
+ end
+end
diff --git a/test/unit/state/state_with_existing_context_method_test.rb b/test/unit/state/state_with_existing_context_method_test.rb
new file mode 100644
index 0000000..6caad15
--- /dev/null
+++ b/test/unit/state/state_with_existing_context_method_test.rb
@@ -0,0 +1,24 @@
+require_relative '../../test_helper'
+
+class StateWithExistingContextMethodTest < StateMachinesTest
+ def setup
+ @klass = Class.new do
+ def speed
+ 60
+ end
+ end
+ @original_speed_method = @klass.instance_method(:speed)
+
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.states << @state = StateMachines::State.new(@machine, :idling)
+ @state.context do
+ def speed
+ 0
+ end
+ end
+ end
+
+ def test_should_not_override_method
+ assert_equal @original_speed_method, @klass.instance_method(:speed)
+ end
+end
diff --git a/test/unit/state/state_with_human_name_test.rb b/test/unit/state/state_with_human_name_test.rb
new file mode 100644
index 0000000..7c0f32d
--- /dev/null
+++ b/test/unit/state/state_with_human_name_test.rb
@@ -0,0 +1,13 @@
+require_relative '../../test_helper'
+
+class StateWithHumanNameTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.states << @state = StateMachines::State.new(@machine, :parked, human_name: 'stopped')
+ end
+
+ def test_should_use_custom_human_name
+ assert_equal 'stopped', @state.human_name
+ end
+end
diff --git a/test/unit/state/state_with_integer_value_test.rb b/test/unit/state/state_with_integer_value_test.rb
new file mode 100644
index 0000000..01afadf
--- /dev/null
+++ b/test/unit/state/state_with_integer_value_test.rb
@@ -0,0 +1,32 @@
+require_relative '../../test_helper'
+
+class StateWithIntegerValueTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.states << @state = StateMachines::State.new(@machine, :parked, value: 1)
+ end
+
+ def test_should_use_custom_value
+ assert_equal 1, @state.value
+ end
+
+ def test_should_include_value_in_description
+ assert_equal 'parked (1)', @state.description
+ end
+
+ def test_should_allow_human_name_in_description
+ @state.human_name = 'Parked'
+ assert_equal 'Parked (1)', @state.description(human_name: true)
+ end
+
+ def test_should_match_integer_value
+ assert @state.matches?(1)
+ refute @state.matches?(2)
+ end
+
+ def test_should_define_predicate
+ object = @klass.new
+ assert object.respond_to?(:parked?)
+ end
+end
diff --git a/test/unit/state/state_with_invalid_method_call_test.rb b/test/unit/state/state_with_invalid_method_call_test.rb
new file mode 100644
index 0000000..2dc319f
--- /dev/null
+++ b/test/unit/state/state_with_invalid_method_call_test.rb
@@ -0,0 +1,21 @@
+require_relative '../../test_helper'
+
+class StateWithInvalidMethodCallTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @ancestors = @klass.ancestors
+ @machine.states << @state = StateMachines::State.new(@machine, :idling)
+ @state.context do
+ def speed
+ 0
+ end
+ end
+
+ @object = @klass.new
+ end
+
+ def test_should_call_method_missing_arg
+ assert_equal 1, @state.call(@object, :invalid, method_missing: -> { 1 })
+ end
+end
diff --git a/test/unit/state/state_with_lambda_value_test.rb b/test/unit/state/state_with_lambda_value_test.rb
new file mode 100644
index 0000000..2b4109e
--- /dev/null
+++ b/test/unit/state/state_with_lambda_value_test.rb
@@ -0,0 +1,37 @@
+require_relative '../../test_helper'
+
+class StateWithLambdaValueTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @args = nil
+ @machine = StateMachines::Machine.new(@klass)
+ @value = ->(*args) { @args = args; :parked }
+ @machine.states << @state = StateMachines::State.new(@machine, :parked, value: @value)
+ end
+
+ def test_should_use_evaluated_value_by_default
+ assert_equal :parked, @state.value
+ end
+
+ def test_should_allow_access_to_original_value
+ assert_equal @value, @state.value(false)
+ end
+
+ def test_should_include_masked_value_in_description
+ assert_equal 'parked (*)', @state.description
+ end
+
+ def test_should_not_pass_in_any_arguments
+ @state.value
+ assert_equal [], @args
+ end
+
+ def test_should_define_predicate
+ object = @klass.new
+ assert object.respond_to?(:parked?)
+ end
+
+ def test_should_match_evaluated_value
+ assert @state.matches?(:parked)
+ end
+end
diff --git a/test/unit/state/state_with_matcher_test.rb b/test/unit/state/state_with_matcher_test.rb
new file mode 100644
index 0000000..ed74146
--- /dev/null
+++ b/test/unit/state/state_with_matcher_test.rb
@@ -0,0 +1,18 @@
+require_relative '../../test_helper'
+
+class StateWithMatcherTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @args = nil
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.states << @state = StateMachines::State.new(@machine, :parked, if: lambda { |value| value == 1 })
+ end
+
+ def test_should_not_match_actual_value
+ refute @state.matches?('parked')
+ end
+
+ def test_should_match_evaluated_block
+ assert @state.matches?(1)
+ end
+end
diff --git a/test/unit/state/state_with_multiple_contexts_test.rb b/test/unit/state/state_with_multiple_contexts_test.rb
new file mode 100644
index 0000000..3cb0e46
--- /dev/null
+++ b/test/unit/state/state_with_multiple_contexts_test.rb
@@ -0,0 +1,57 @@
+require_relative '../../test_helper'
+
+class StateWithMultipleContextsTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @ancestors = @klass.ancestors
+ @machine.states << @state = StateMachines::State.new(@machine, :idling)
+
+ context = nil
+ speed_method = nil
+ @state.context do
+ context = self
+
+ def speed
+ 0
+ end
+
+ speed_method = instance_method(:speed)
+ end
+ @context = context
+ @speed_method = speed_method
+
+ rpm_method = nil
+ @state.context do
+ def rpm
+ 1000
+ end
+
+ rpm_method = instance_method(:rpm)
+ end
+ @rpm_method = rpm_method
+ end
+
+ def test_should_include_new_module_in_owner_class
+ refute_equal @ancestors, @klass.ancestors
+ assert_equal [@context], @klass.ancestors - @ancestors
+ end
+
+ def test_should_define_each_context_method_in_owner_class
+ %w(speed rpm).each { |method| assert @klass.method_defined?(method) }
+ end
+
+ def test_should_define_aliased_context_method_in_owner_class
+ %w(speed rpm).each { |method| assert @klass.method_defined?("__state_idling_#{method}_#{@context.object_id}__") }
+ end
+
+ def test_should_not_use_context_methods_as_owner_class_methods
+ refute_equal @speed_method, @state.context_methods[:speed]
+ refute_equal @rpm_method, @state.context_methods[:rpm]
+ end
+
+ def test_should_use_context_methods_as_aliased_owner_class_methods
+ assert_equal @speed_method, @state.context_methods[:"__state_idling_speed_#{@context.object_id}__"]
+ assert_equal @rpm_method, @state.context_methods[:"__state_idling_rpm_#{@context.object_id}__"]
+ end
+end
diff --git a/test/unit/state/state_with_name_test.rb b/test/unit/state/state_with_name_test.rb
new file mode 100644
index 0000000..1a1dba6
--- /dev/null
+++ b/test/unit/state/state_with_name_test.rb
@@ -0,0 +1,43 @@
+require_relative '../../test_helper'
+
+class StateWithNameTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.states << @state = StateMachines::State.new(@machine, :parked)
+ end
+
+ def test_should_have_a_name
+ assert_equal :parked, @state.name
+ end
+
+ def test_should_have_a_qualified_name
+ assert_equal :parked, @state.name
+ end
+
+ def test_should_have_a_human_name
+ assert_equal 'parked', @state.human_name
+ end
+
+ def test_should_use_stringify_the_name_as_the_value
+ assert_equal 'parked', @state.value
+ end
+
+ def test_should_match_stringified_name
+ assert @state.matches?('parked')
+ refute @state.matches?('idling')
+ end
+
+ def test_should_not_include_value_in_description
+ assert_equal 'parked', @state.description
+ end
+
+ def test_should_allow_using_human_name_in_description
+ @state.human_name = 'Parked'
+ assert_equal 'Parked', @state.description(human_name: true)
+ end
+
+ def test_should_define_predicate
+ assert @klass.new.respond_to?(:parked?)
+ end
+end
diff --git a/test/unit/state/state_with_namespace_test.rb b/test/unit/state/state_with_namespace_test.rb
new file mode 100644
index 0000000..7fc53a5
--- /dev/null
+++ b/test/unit/state/state_with_namespace_test.rb
@@ -0,0 +1,22 @@
+require_relative '../../test_helper'
+
+class StateWithNamespaceTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass, namespace: 'alarm')
+ @machine.states << @state = StateMachines::State.new(@machine, :active)
+ @object = @klass.new
+ end
+
+ def test_should_have_a_name
+ assert_equal :active, @state.name
+ end
+
+ def test_should_have_a_qualified_name
+ assert_equal :alarm_active, @state.qualified_name
+ end
+
+ def test_should_namespace_predicate
+ assert @object.respond_to?(:alarm_active?)
+ end
+end
diff --git a/test/unit/state/state_with_nil_value_test.rb b/test/unit/state/state_with_nil_value_test.rb
new file mode 100644
index 0000000..2dcc4c6
--- /dev/null
+++ b/test/unit/state/state_with_nil_value_test.rb
@@ -0,0 +1,35 @@
+require_relative '../../test_helper'
+
+class StateWithNilValueTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.states << @state = StateMachines::State.new(@machine, :parked, value: nil)
+ end
+
+ def test_should_have_a_name
+ assert_equal :parked, @state.name
+ end
+
+ def test_should_have_a_nil_value
+ assert_nil @state.value
+ end
+
+ def test_should_match_nil_values
+ assert @state.matches?(nil)
+ end
+
+ def test_should_have_a_description
+ assert_equal 'parked (nil)', @state.description
+ end
+
+ def test_should_have_a_description_with_human_name
+ @state.human_name = 'Parked'
+ assert_equal 'Parked (nil)', @state.description(human_name: true)
+ end
+
+ def test_should_define_predicate
+ object = @klass.new
+ assert object.respond_to?(:parked?)
+ end
+end
diff --git a/test/unit/state/state_with_redefined_context_method_test.rb b/test/unit/state/state_with_redefined_context_method_test.rb
new file mode 100644
index 0000000..53a526c
--- /dev/null
+++ b/test/unit/state/state_with_redefined_context_method_test.rb
@@ -0,0 +1,45 @@
+require_relative '../../test_helper'
+
+class StateWithRedefinedContextMethodTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.states << @state = StateMachines::State.new(@machine, 'on')
+
+ old_context = nil
+ old_speed_method = nil
+ @state.context do
+ old_context = self
+
+ def speed
+ 0
+ end
+
+ old_speed_method = instance_method(:speed)
+ end
+ @old_context = old_context
+ @old_speed_method = old_speed_method
+
+ current_context = nil
+ current_speed_method = nil
+ @state.context do
+ current_context = self
+
+ def speed
+ 'green'
+ end
+
+ current_speed_method = instance_method(:speed)
+ end
+ @current_context = current_context
+ @current_speed_method = current_speed_method
+ end
+
+ def test_should_track_latest_defined_method
+ assert_equal @current_speed_method, @state.context_methods[:"__state_on_speed_#{@current_context.object_id}__"]
+ end
+
+ def test_should_have_the_same_context
+ assert_equal @current_context, @old_context
+ end
+end
diff --git a/test/unit/state/state_with_symbolic_value_test.rb b/test/unit/state/state_with_symbolic_value_test.rb
new file mode 100644
index 0000000..eb747fc
--- /dev/null
+++ b/test/unit/state/state_with_symbolic_value_test.rb
@@ -0,0 +1,32 @@
+require_relative '../../test_helper'
+
+class StateWithSymbolicValueTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.states << @state = StateMachines::State.new(@machine, :parked, value: :parked)
+ end
+
+ def test_should_use_custom_value
+ assert_equal :parked, @state.value
+ end
+
+ def test_should_not_include_value_in_description
+ assert_equal 'parked', @state.description
+ end
+
+ def test_should_allow_human_name_in_description
+ @state.human_name = 'Parked'
+ assert_equal 'Parked', @state.description(human_name: true)
+ end
+
+ def test_should_match_symbolic_value
+ assert @state.matches?(:parked)
+ refute @state.matches?('parked')
+ end
+
+ def test_should_define_predicate
+ object = @klass.new
+ assert object.respond_to?(:parked?)
+ end
+end
diff --git a/test/unit/state/state_with_valid_inherited_method_call_for_current_state_test.rb b/test/unit/state/state_with_valid_inherited_method_call_for_current_state_test.rb
new file mode 100644
index 0000000..3f474d1
--- /dev/null
+++ b/test/unit/state/state_with_valid_inherited_method_call_for_current_state_test.rb
@@ -0,0 +1,40 @@
+require_relative '../../test_helper'
+
+class StateWithValidInheritedMethodCallForCurrentStateTest < StateMachinesTest
+ def setup
+ @superclass = Class.new do
+ def speed(arg = nil)
+ [arg]
+ end
+ end
+ @klass = Class.new(@superclass)
+ @machine = StateMachines::Machine.new(@klass, initial: :idling)
+ @ancestors = @klass.ancestors
+ @state = @machine.state(:idling)
+ @state.context do
+ def speed(arg = nil)
+ [arg] + super(2)
+ end
+ end
+
+ @object = @klass.new
+ end
+
+ def test_should_not_raise_an_exception
+ @state.call(@object, :speed, method_missing: lambda { fail })
+ end
+
+ def test_should_be_able_to_call_super
+ assert_equal [1, 2], @state.call(@object, :speed, 1)
+ end
+
+ def test_should_allow_redefinition
+ @state.context do
+ def speed(arg = nil)
+ [arg] + super(3)
+ end
+ end
+
+ assert_equal [1, 3], @state.call(@object, :speed, 1)
+ end
+end
diff --git a/test/unit/state/state_with_valid_method_call_for_current_state_test.rb b/test/unit/state/state_with_valid_method_call_for_current_state_test.rb
new file mode 100644
index 0000000..eb0febb
--- /dev/null
+++ b/test/unit/state/state_with_valid_method_call_for_current_state_test.rb
@@ -0,0 +1,33 @@
+require_relative '../../test_helper'
+
+class StateWithValidMethodCallForCurrentStateTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass, initial: :idling)
+ @ancestors = @klass.ancestors
+ @state = @machine.state(:idling)
+ @state.context do
+ def speed(arg = nil)
+ block_given? ? [arg, yield] : arg
+ end
+ end
+
+ @object = @klass.new
+ end
+
+ def test_should_not_raise_an_exception
+ @state.call(@object, :speed, method_missing: lambda { fail })
+ end
+
+ def test_should_pass_arguments_through
+ assert_equal 1, @state.call(@object, :speed, 1, method_missing: lambda {})
+ end
+
+ def test_should_pass_blocks_through
+ assert_equal [nil, 1], @state.call(@object, :speed) { 1 }
+ end
+
+ def test_should_pass_both_arguments_and_blocks_through
+ assert_equal [1, 2], @state.call(@object, :speed, 1, method_missing: lambda {}) { 2 }
+ end
+end
diff --git a/test/unit/state/state_with_valid_method_call_for_different_state_test.rb b/test/unit/state/state_with_valid_method_call_for_different_state_test.rb
new file mode 100644
index 0000000..08da917
--- /dev/null
+++ b/test/unit/state/state_with_valid_method_call_for_different_state_test.rb
@@ -0,0 +1,41 @@
+require_relative '../../test_helper'
+
+class StateWithValidMethodCallForDifferentStateTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @ancestors = @klass.ancestors
+ @machine.states << @state = StateMachines::State.new(@machine, :idling)
+ @state.context do
+ def speed
+ 0
+ end
+ end
+
+ @object = @klass.new
+ end
+
+ def test_should_call_method_missing_arg
+ assert_equal 1, @state.call(@object, :speed, method_missing: lambda { 1 })
+ end
+
+ def test_should_raise_invalid_context_on_no_method_error
+ exception = assert_raises(StateMachines::InvalidContext) do
+ @state.call(@object, :speed, method_missing: lambda { fail NoMethodError.new('Invalid', :speed, []) })
+ end
+ assert_equal @object, exception.object
+ assert_equal 'State nil for :state is not a valid context for calling #speed', exception.message
+ end
+
+ def test_should_raise_original_error_on_no_method_error_with_different_arguments
+ assert_raises(NoMethodError) do
+ @state.call(@object, :speed, method_missing: lambda { fail NoMethodError.new('Invalid', :speed, [1]) })
+ end
+ end
+
+ def test_should_raise_original_error_on_no_method_error_for_different_method
+ assert_raises(NoMethodError) do
+ @state.call(@object, :speed, method_missing: lambda { fail NoMethodError.new('Invalid', :rpm, []) })
+ end
+ end
+end
diff --git a/test/unit/state/state_without_cached_lambda_value_test.rb b/test/unit/state/state_without_cached_lambda_value_test.rb
new file mode 100644
index 0000000..75a2591
--- /dev/null
+++ b/test/unit/state/state_without_cached_lambda_value_test.rb
@@ -0,0 +1,25 @@
+require_relative '../../test_helper'
+
+class StateWithoutCachedLambdaValueTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @dynamic_value = -> { 'value' }
+ @machine.states << @state = StateMachines::State.new(@machine, :parked, value: @dynamic_value)
+ end
+
+ def test_should_not_be_caching
+ refute @state.cache
+ end
+
+ def test_should_evaluate_value_each_time
+ value = @state.value
+ refute_same value, @state.value
+ end
+
+ def test_should_not_update_value_index_for_state_collection
+ @state.value
+ assert_nil @machine.states['value', :value]
+ assert_equal @state, @machine.states[@dynamic_value, :value]
+ end
+end
diff --git a/test/unit/state/state_without_name_test.rb b/test/unit/state/state_without_name_test.rb
new file mode 100644
index 0000000..33e9b99
--- /dev/null
+++ b/test/unit/state/state_without_name_test.rb
@@ -0,0 +1,39 @@
+require_relative '../../test_helper'
+
+class StateWithoutNameTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.states << @state = StateMachines::State.new(@machine, nil)
+ end
+
+ def test_should_have_a_nil_name
+ assert_nil @state.name
+ end
+
+ def test_should_have_a_nil_qualified_name
+ assert_nil @state.qualified_name
+ end
+
+ def test_should_have_an_empty_human_name
+ assert_equal 'nil', @state.human_name
+ end
+
+ def test_should_have_a_nil_value
+ assert_nil @state.value
+ end
+
+ def test_should_not_redefine_nil_predicate
+ object = @klass.new
+ refute object.nil?
+ refute object.respond_to?('?')
+ end
+
+ def test_should_have_a_description
+ assert_equal 'nil', @state.description
+ end
+
+ def test_should_have_a_description_using_human_name
+ assert_equal 'nil', @state.description(human_name: true)
+ end
+end
diff --git a/test/unit/state_collection/state_collection_by_default_test.rb b/test/unit/state_collection/state_collection_by_default_test.rb
new file mode 100644
index 0000000..f1f4ea7
--- /dev/null
+++ b/test/unit/state_collection/state_collection_by_default_test.rb
@@ -0,0 +1,21 @@
+require_relative '../../test_helper'
+
+class StateCollectionByDefaultTest < StateMachinesTest
+ def setup
+ @machine = StateMachines::Machine.new(Class.new)
+ @states = StateMachines::StateCollection.new(@machine)
+ end
+
+ def test_should_not_have_any_nodes
+ assert_equal 0, @states.length
+ end
+
+ def test_should_have_a_machine
+ assert_equal @machine, @states.machine
+ end
+
+ def test_should_be_empty_by_priority
+ assert_equal [], @states.by_priority
+ end
+end
+
diff --git a/test/unit/state_collection/state_collection_string_test.rb b/test/unit/state_collection/state_collection_string_test.rb
new file mode 100644
index 0000000..86d0546
--- /dev/null
+++ b/test/unit/state_collection/state_collection_string_test.rb
@@ -0,0 +1,35 @@
+require_relative '../../test_helper'
+
+class StateCollectionStringTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @states = StateMachines::StateCollection.new(@machine)
+
+ @states << @nil = StateMachines::State.new(@machine, nil)
+ @states << @parked = StateMachines::State.new(@machine, 'parked')
+ @machine.states.concat(@states)
+
+ @object = @klass.new
+ end
+
+ def test_should_index_by_name
+ assert_equal @parked, @states['parked', :name]
+ end
+
+ def test_should_index_by_name_by_default
+ assert_equal @parked, @states['parked']
+ end
+
+ def test_should_index_by_symbol_name
+ assert_equal @parked, @states[:parked]
+ end
+
+ def test_should_index_by_qualified_name
+ assert_equal @parked, @states['parked', :qualified_name]
+ end
+
+ def test_should_index_by_symbol_qualified_name
+ assert_equal @parked, @states[:parked, :qualified_name]
+ end
+end
diff --git a/test/unit/state_collection/state_collection_test.rb b/test/unit/state_collection/state_collection_test.rb
new file mode 100644
index 0000000..f93ff34
--- /dev/null
+++ b/test/unit/state_collection/state_collection_test.rb
@@ -0,0 +1,74 @@
+require_relative '../../test_helper'
+
+class StateCollectionTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @states = StateMachines::StateCollection.new(@machine)
+
+ @states << @nil = StateMachines::State.new(@machine, nil)
+ @states << @parked = StateMachines::State.new(@machine, :parked)
+ @states << @idling = StateMachines::State.new(@machine, :idling)
+ @machine.states.concat(@states)
+
+ @object = @klass.new
+ end
+
+ def test_should_index_by_name
+ assert_equal @parked, @states[:parked, :name]
+ end
+
+ def test_should_index_by_name_by_default
+ assert_equal @parked, @states[:parked]
+ end
+
+ def test_should_index_by_string_name
+ assert_equal @parked, @states['parked']
+ end
+
+ def test_should_index_by_qualified_name
+ assert_equal @parked, @states[:parked, :qualified_name]
+ end
+
+ def test_should_index_by_string_qualified_name
+ assert_equal @parked, @states['parked', :qualified_name]
+ end
+
+ def test_should_index_by_value
+ assert_equal @parked, @states['parked', :value]
+ end
+
+ def test_should_not_match_if_value_does_not_match
+ refute @states.matches?(@object, :parked)
+ refute @states.matches?(@object, :idling)
+ end
+
+ def test_should_match_if_value_matches
+ assert @states.matches?(@object, nil)
+ end
+
+ def test_raise_exception_if_matching_invalid_state
+ assert_raises(IndexError) { @states.matches?(@object, :invalid) }
+ end
+
+ def test_should_find_state_for_object_if_value_is_known
+ @object.state = 'parked'
+ assert_equal @parked, @states.match(@object)
+ end
+
+ def test_should_find_bang_state_for_object_if_value_is_known
+ @object.state = 'parked'
+ assert_equal @parked, @states.match!(@object)
+ end
+
+ def test_should_not_find_state_for_object_with_unknown_value
+ @object.state = 'invalid'
+ assert_nil @states.match(@object)
+ end
+
+ def test_should_raise_exception_if_finding_bang_state_for_object_with_unknown_value
+ @object.state = 'invalid'
+ exception = assert_raises(ArgumentError) { @states.match!(@object) }
+ assert_equal '"invalid" is not a known state value', exception.message
+ end
+end
diff --git a/test/unit/state_collection/state_collection_with_custom_state_values_test.rb b/test/unit/state_collection/state_collection_with_custom_state_values_test.rb
new file mode 100644
index 0000000..ce6eda3
--- /dev/null
+++ b/test/unit/state_collection/state_collection_with_custom_state_values_test.rb
@@ -0,0 +1,29 @@
+require_relative '../../test_helper'
+
+class StateCollectionWithCustomStateValuesTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @states = StateMachines::StateCollection.new(@machine)
+
+ @states << @state = StateMachines::State.new(@machine, :parked, value: 1)
+ @machine.states.concat(@states)
+
+ @object = @klass.new
+ @object.state = 1
+ end
+
+ def test_should_match_if_value_matches
+ assert @states.matches?(@object, :parked)
+ end
+
+ def test_should_not_match_if_value_does_not_match
+ @object.state = 2
+ refute @states.matches?(@object, :parked)
+ end
+
+ def test_should_find_state_for_object_if_value_is_known
+ assert_equal @state, @states.match(@object)
+ end
+end
+
diff --git a/test/unit/state_collection/state_collection_with_event_transitions_test.rb b/test/unit/state_collection/state_collection_with_event_transitions_test.rb
new file mode 100644
index 0000000..611c6e2
--- /dev/null
+++ b/test/unit/state_collection/state_collection_with_event_transitions_test.rb
@@ -0,0 +1,39 @@
+require_relative '../../test_helper'
+
+class StateCollectionWithEventTransitionsTest < StateMachinesTest
+ def setup
+ @machine = StateMachines::Machine.new(Class.new)
+ @states = StateMachines::StateCollection.new(@machine)
+
+ @states << @parked = StateMachines::State.new(@machine, :parked)
+ @states << @idling = StateMachines::State.new(@machine, :idling)
+ @machine.states.concat(@states)
+
+ @machine.event :ignite do
+ transition to: :idling
+ end
+ end
+
+ def test_should_order_states_after_initial_state
+ @parked.initial = true
+ assert_equal [@parked, @idling], @states.by_priority
+ end
+
+ def test_should_order_states_before_states_with_behaviors
+ @parked.context do
+ def speed
+ 0
+ end
+ end
+ assert_equal [@idling, @parked], @states.by_priority
+ end
+
+ def test_should_order_states_before_other_states
+ assert_equal [@idling, @parked], @states.by_priority
+ end
+
+ def test_should_order_state_before_callback_states
+ @machine.before_transition from: :parked, do: lambda {}
+ assert_equal [@idling, @parked], @states.by_priority
+ end
+end
diff --git a/test/unit/state_collection/state_collection_with_initial_state_test.rb b/test/unit/state_collection/state_collection_with_initial_state_test.rb
new file mode 100644
index 0000000..aeb177a
--- /dev/null
+++ b/test/unit/state_collection/state_collection_with_initial_state_test.rb
@@ -0,0 +1,40 @@
+require_relative '../../test_helper'
+
+class StateCollectionWithInitialStateTest < StateMachinesTest
+ def setup
+ @machine = StateMachines::Machine.new(Class.new)
+ @states = StateMachines::StateCollection.new(@machine)
+
+ @states << @parked = StateMachines::State.new(@machine, :parked)
+ @states << @idling = StateMachines::State.new(@machine, :idling)
+ @machine.states.concat(@states)
+
+ @parked.initial = true
+ end
+
+ def test_should_order_state_before_transition_states
+ @machine.event :ignite do
+ transition to: :idling
+ end
+ assert_equal [@parked, @idling], @states.by_priority
+ end
+
+ def test_should_order_state_before_states_with_behaviors
+ @idling.context do
+ def speed
+ 0
+ end
+ end
+ assert_equal [@parked, @idling], @states.by_priority
+ end
+
+ def test_should_order_state_before_other_states
+ assert_equal [@parked, @idling], @states.by_priority
+ end
+
+ def test_should_order_state_before_callback_states
+ @machine.before_transition from: :idling, do: lambda {}
+ assert_equal [@parked, @idling], @states.by_priority
+ end
+end
+
diff --git a/test/unit/state_collection/state_collection_with_namespace_test.rb b/test/unit/state_collection/state_collection_with_namespace_test.rb
new file mode 100644
index 0000000..304a25d
--- /dev/null
+++ b/test/unit/state_collection/state_collection_with_namespace_test.rb
@@ -0,0 +1,21 @@
+require_relative '../../test_helper'
+
+class StateCollectionWithNamespaceTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass, namespace: 'vehicle')
+ @states = StateMachines::StateCollection.new(@machine)
+
+ @states << @state = StateMachines::State.new(@machine, :parked)
+ @machine.states.concat(@states)
+ end
+
+ def test_should_index_by_name
+ assert_equal @state, @states[:parked, :name]
+ end
+
+ def test_should_index_by_qualified_name
+ assert_equal @state, @states[:vehicle_parked, :qualified_name]
+ end
+end
+
diff --git a/test/unit/state_collection/state_collection_with_state_behaviors_test.rb b/test/unit/state_collection/state_collection_with_state_behaviors_test.rb
new file mode 100644
index 0000000..16f9830
--- /dev/null
+++ b/test/unit/state_collection/state_collection_with_state_behaviors_test.rb
@@ -0,0 +1,40 @@
+require_relative '../../test_helper'
+
+class StateCollectionWithStateBehaviorsTest < StateMachinesTest
+ def setup
+ @machine = StateMachines::Machine.new(Class.new)
+ @states = StateMachines::StateCollection.new(@machine)
+
+ @states << @parked = StateMachines::State.new(@machine, :parked)
+ @states << @idling = StateMachines::State.new(@machine, :idling)
+ @machine.states.concat(@states)
+
+ @idling.context do
+ def speed
+ 0
+ end
+ end
+ end
+
+ def test_should_order_states_after_initial_state
+ @parked.initial = true
+ assert_equal [@parked, @idling], @states.by_priority
+ end
+
+ def test_should_order_states_after_transition_states
+ @machine.event :ignite do
+ transition from: :parked
+ end
+ assert_equal [@parked, @idling], @states.by_priority
+ end
+
+ def test_should_order_states_before_other_states
+ assert_equal [@idling, @parked], @states.by_priority
+ end
+
+ def test_should_order_state_before_callback_states
+ @machine.before_transition from: :parked, do: lambda {}
+ assert_equal [@idling, @parked], @states.by_priority
+ end
+end
+
diff --git a/test/unit/state_collection/state_collection_with_state_matchers_test.rb b/test/unit/state_collection/state_collection_with_state_matchers_test.rb
new file mode 100644
index 0000000..41ccc94
--- /dev/null
+++ b/test/unit/state_collection/state_collection_with_state_matchers_test.rb
@@ -0,0 +1,29 @@
+require_relative '../../test_helper'
+
+class StateCollectionWithStateMatchersTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @states = StateMachines::StateCollection.new(@machine)
+
+ @states << @state = StateMachines::State.new(@machine, :parked, if: lambda { |value| !value.nil? })
+ @machine.states.concat(@states)
+
+ @object = @klass.new
+ @object.state = 1
+ end
+
+ def test_should_match_if_value_matches
+ assert @states.matches?(@object, :parked)
+ end
+
+ def test_should_not_match_if_value_does_not_match
+ @object.state = nil
+ refute @states.matches?(@object, :parked)
+ end
+
+ def test_should_find_state_for_object_if_value_is_known
+ assert_equal @state, @states.match(@object)
+ end
+end
+
diff --git a/test/unit/state_collection/state_collection_with_transition_callbacks_test.rb b/test/unit/state_collection/state_collection_with_transition_callbacks_test.rb
new file mode 100644
index 0000000..d112f3b
--- /dev/null
+++ b/test/unit/state_collection/state_collection_with_transition_callbacks_test.rb
@@ -0,0 +1,40 @@
+require_relative '../../test_helper'
+
+class StateCollectionWithTransitionCallbacksTest < StateMachinesTest
+ def setup
+ @machine = StateMachines::Machine.new(Class.new)
+ @states = StateMachines::StateCollection.new(@machine)
+
+ @states << @parked = StateMachines::State.new(@machine, :parked)
+ @states << @idling = StateMachines::State.new(@machine, :idling)
+ @machine.states.concat(@states)
+
+ @machine.before_transition to: :idling, do: lambda {}
+ end
+
+ def test_should_order_states_after_initial_state
+ @parked.initial = true
+ assert_equal [@parked, @idling], @states.by_priority
+ end
+
+ def test_should_order_states_after_transition_states
+ @machine.event :ignite do
+ transition from: :parked
+ end
+ assert_equal [@parked, @idling], @states.by_priority
+ end
+
+ def test_should_order_states_after_states_with_behaviors
+ @parked.context do
+ def speed
+ 0
+ end
+ end
+ assert_equal [@parked, @idling], @states.by_priority
+ end
+
+ def test_should_order_states_after_other_states
+ assert_equal [@parked, @idling], @states.by_priority
+ end
+end
+
diff --git a/test/unit/state_context/state_context_proxy_test.rb b/test/unit/state_context/state_context_proxy_test.rb
new file mode 100644
index 0000000..6075839
--- /dev/null
+++ b/test/unit/state_context/state_context_proxy_test.rb
@@ -0,0 +1,26 @@
+require_relative '../../test_helper'
+
+class StateContextProxyTest < StateMachinesTest
+ def setup
+ @klass = Class.new(Validateable)
+ machine = StateMachines::Machine.new(@klass, initial: :parked)
+ state = machine.state :parked
+
+ @state_context = StateMachines::StateContext.new(state)
+ end
+
+ def test_should_call_class_with_same_arguments
+ options = {}
+ validation = @state_context.validate(:name, options)
+
+ assert_equal [:name, options], validation
+ end
+
+ def test_should_pass_block_through_to_class
+ options = {}
+ proxy_block = lambda {}
+ validation = @state_context.validate(:name, options, &proxy_block)
+
+ assert_equal [:name, options, proxy_block], validation
+ end
+end
diff --git a/test/unit/state_context/state_context_proxy_with_if_and_unless_conditions_test.rb b/test/unit/state_context/state_context_proxy_with_if_and_unless_conditions_test.rb
new file mode 100644
index 0000000..16cf08e
--- /dev/null
+++ b/test/unit/state_context/state_context_proxy_with_if_and_unless_conditions_test.rb
@@ -0,0 +1,42 @@
+require_relative '../../test_helper'
+
+class StateContextProxyWithIfAndUnlessConditionsTest < StateMachinesTest
+ def setup
+ @klass = Class.new(Validateable)
+ machine = StateMachines::Machine.new(@klass, initial: :parked)
+ state = machine.state :parked
+
+ @state_context = StateMachines::StateContext.new(state)
+ @object = @klass.new
+
+ @if_condition_result = nil
+ @unless_condition_result = nil
+ @options = @state_context.validate(if: lambda { @if_condition_result }, unless: lambda { @unless_condition_result })[0]
+ end
+
+ def test_should_be_false_if_if_condition_is_false
+ @if_condition_result = false
+ @unless_condition_result = false
+ refute @options[:if].call(@object)
+
+ @if_condition_result = false
+ @unless_condition_result = true
+ refute @options[:if].call(@object)
+ end
+
+ def test_should_be_false_if_unless_condition_is_true
+ @if_condition_result = false
+ @unless_condition_result = true
+ refute @options[:if].call(@object)
+
+ @if_condition_result = true
+ @unless_condition_result = true
+ refute @options[:if].call(@object)
+ end
+
+ def test_should_be_true_if_if_condition_is_true_and_unless_condition_is_false
+ @if_condition_result = true
+ @unless_condition_result = false
+ assert @options[:if].call(@object)
+ end
+end
diff --git a/test/unit/state_context/state_context_proxy_with_if_condition_test.rb b/test/unit/state_context/state_context_proxy_with_if_condition_test.rb
new file mode 100644
index 0000000..ad6bbc3
--- /dev/null
+++ b/test/unit/state_context/state_context_proxy_with_if_condition_test.rb
@@ -0,0 +1,64 @@
+require_relative '../../test_helper'
+
+class StateContextProxyWithIfConditionTest < StateMachinesTest
+ def setup
+ @klass = Class.new(Validateable)
+ machine = StateMachines::Machine.new(@klass, initial: :parked)
+ state = machine.state :parked
+
+ @state_context = StateMachines::StateContext.new(state)
+ @object = @klass.new
+
+ @condition_result = nil
+ @options = @state_context.validate(if: lambda { @condition_result })[0]
+ end
+
+ def test_should_have_if_option
+ refute_nil @options[:if]
+ end
+
+ def test_should_be_false_if_state_is_different
+ @object.state = nil
+ refute @options[:if].call(@object)
+ end
+
+ def test_should_be_false_if_original_condition_is_false
+ @condition_result = false
+ refute @options[:if].call(@object)
+ end
+
+ def test_should_be_true_if_state_matches_and_original_condition_is_true
+ @condition_result = true
+ assert @options[:if].call(@object)
+ end
+
+ def test_should_evaluate_symbol_condition
+ @klass.class_eval do
+ attr_accessor :callback
+ end
+
+ options = @state_context.validate(if: :callback)[0]
+
+ object = @klass.new
+ object.callback = false
+ refute options[:if].call(object)
+
+ object.callback = true
+ assert options[:if].call(object)
+ end
+
+ def test_should_evaluate_string_condition
+ @klass.class_eval do
+ attr_accessor :callback
+ end
+
+ options = @state_context.validate(if: '@callback')[0]
+
+ object = @klass.new
+ object.callback = false
+ refute options[:if].call(object)
+
+ object.callback = true
+ assert options[:if].call(object)
+ end
+end
diff --git a/test/unit/state_context/state_context_proxy_with_multiple_if_conditions_test.rb b/test/unit/state_context/state_context_proxy_with_multiple_if_conditions_test.rb
new file mode 100644
index 0000000..16749a1
--- /dev/null
+++ b/test/unit/state_context/state_context_proxy_with_multiple_if_conditions_test.rb
@@ -0,0 +1,32 @@
+require_relative '../../test_helper'
+
+class StateContextProxyWithMultipleIfConditionsTest < StateMachinesTest
+ def setup
+ @klass = Class.new(Validateable)
+ machine = StateMachines::Machine.new(@klass, initial: :parked)
+ state = machine.state :parked
+
+ @state_context = StateMachines::StateContext.new(state)
+ @object = @klass.new
+
+ @first_condition_result = nil
+ @second_condition_result = nil
+ @options = @state_context.validate(if: [lambda { @first_condition_result }, lambda { @second_condition_result }])[0]
+ end
+
+ def test_should_be_true_if_all_conditions_are_true
+ @first_condition_result = true
+ @second_condition_result = true
+ assert @options[:if].call(@object)
+ end
+
+ def test_should_be_false_if_any_condition_is_false
+ @first_condition_result = true
+ @second_condition_result = false
+ refute @options[:if].call(@object)
+
+ @first_condition_result = false
+ @second_condition_result = true
+ refute @options[:if].call(@object)
+ end
+end
diff --git a/test/unit/state_context/state_context_proxy_with_multiple_unless_conditions_test.rb b/test/unit/state_context/state_context_proxy_with_multiple_unless_conditions_test.rb
new file mode 100644
index 0000000..47255c4
--- /dev/null
+++ b/test/unit/state_context/state_context_proxy_with_multiple_unless_conditions_test.rb
@@ -0,0 +1,32 @@
+require_relative '../../test_helper'
+
+class StateContextProxyWithMultipleUnlessConditionsTest < StateMachinesTest
+ def setup
+ @klass = Class.new(Validateable)
+ machine = StateMachines::Machine.new(@klass, initial: :parked)
+ state = machine.state :parked
+
+ @state_context = StateMachines::StateContext.new(state)
+ @object = @klass.new
+
+ @first_condition_result = nil
+ @second_condition_result = nil
+ @options = @state_context.validate(unless: [-> { @first_condition_result }, lambda { @second_condition_result }])[0]
+ end
+
+ def test_should_be_true_if_all_conditions_are_false
+ @first_condition_result = false
+ @second_condition_result = false
+ assert @options[:if].call(@object)
+ end
+
+ def test_should_be_false_if_any_condition_is_true
+ @first_condition_result = true
+ @second_condition_result = false
+ refute @options[:if].call(@object)
+
+ @first_condition_result = false
+ @second_condition_result = true
+ refute @options[:if].call(@object)
+ end
+end
diff --git a/test/unit/state_context/state_context_proxy_with_unless_condition_test.rb b/test/unit/state_context/state_context_proxy_with_unless_condition_test.rb
new file mode 100644
index 0000000..780c9e4
--- /dev/null
+++ b/test/unit/state_context/state_context_proxy_with_unless_condition_test.rb
@@ -0,0 +1,64 @@
+require_relative '../../test_helper'
+
+class StateContextProxyWithUnlessConditionTest < StateMachinesTest
+ def setup
+ @klass = Class.new(Validateable)
+ machine = StateMachines::Machine.new(@klass, initial: :parked)
+ state = machine.state :parked
+
+ @state_context = StateMachines::StateContext.new(state)
+ @object = @klass.new
+
+ @condition_result = nil
+ @options = @state_context.validate(unless: lambda { @condition_result })[0]
+ end
+
+ def test_should_have_if_option
+ refute_nil @options[:if]
+ end
+
+ def test_should_be_false_if_state_is_different
+ @object.state = nil
+ refute @options[:if].call(@object)
+ end
+
+ def test_should_be_false_if_original_condition_is_true
+ @condition_result = true
+ refute @options[:if].call(@object)
+ end
+
+ def test_should_be_true_if_state_matches_and_original_condition_is_false
+ @condition_result = false
+ assert @options[:if].call(@object)
+ end
+
+ def test_should_evaluate_symbol_condition
+ @klass.class_eval do
+ attr_accessor :callback
+ end
+
+ options = @state_context.validate(unless: :callback)[0]
+
+ object = @klass.new
+ object.callback = true
+ refute options[:if].call(object)
+
+ object.callback = false
+ assert options[:if].call(object)
+ end
+
+ def test_should_evaluate_string_condition
+ @klass.class_eval do
+ attr_accessor :callback
+ end
+
+ options = @state_context.validate(unless: '@callback')[0]
+
+ object = @klass.new
+ object.callback = true
+ refute options[:if].call(object)
+
+ object.callback = false
+ assert options[:if].call(object)
+ end
+end
diff --git a/test/unit/state_context/state_context_proxy_without_conditions_test.rb b/test/unit/state_context/state_context_proxy_without_conditions_test.rb
new file mode 100644
index 0000000..6b3ca04
--- /dev/null
+++ b/test/unit/state_context/state_context_proxy_without_conditions_test.rb
@@ -0,0 +1,31 @@
+require_relative '../../test_helper'
+
+class StateContextProxyWithoutConditionsTest < StateMachinesTest
+ def setup
+ @klass = Class.new(Validateable)
+ machine = StateMachines::Machine.new(@klass, initial: :parked)
+ state = machine.state :parked
+
+ @state_context = StateMachines::StateContext.new(state)
+ @object = @klass.new
+
+ @options = @state_context.validate[0]
+ end
+
+ def test_should_have_options_configuration
+ assert_instance_of Hash, @options
+ end
+
+ def test_should_have_if_option
+ refute_nil @options[:if]
+ end
+
+ def test_should_be_false_if_state_is_different
+ @object.state = nil
+ refute @options[:if].call(@object)
+ end
+
+ def test_should_be_true_if_state_matches
+ assert @options[:if].call(@object)
+ end
+end
diff --git a/test/unit/state_context/state_context_test.rb b/test/unit/state_context/state_context_test.rb
new file mode 100644
index 0000000..8f056c7
--- /dev/null
+++ b/test/unit/state_context/state_context_test.rb
@@ -0,0 +1,28 @@
+require_relative '../../test_helper'
+
+class Validateable
+ class << self
+ def validate(*args, &block)
+ args << block if block_given?
+ args
+ end
+ end
+end
+
+class StateContextTest < StateMachinesTest
+ def setup
+ @klass = Class.new(Validateable)
+ @machine = StateMachines::Machine.new(@klass, initial: :parked)
+ @state = @machine.state :parked
+
+ @state_context = StateMachines::StateContext.new(@state)
+ end
+
+ def test_should_have_a_machine
+ assert_equal @machine, @state_context.machine
+ end
+
+ def test_should_have_a_state
+ assert_equal @state, @state_context.state
+ end
+end
diff --git a/test/unit/state_context/state_context_transition_test.rb b/test/unit/state_context/state_context_transition_test.rb
new file mode 100644
index 0000000..7a0e93f
--- /dev/null
+++ b/test/unit/state_context/state_context_transition_test.rb
@@ -0,0 +1,104 @@
+require_relative '../../test_helper'
+
+class StateContextTransitionTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass, initial: :parked)
+ @state = @machine.state :parked
+
+ @state_context = StateMachines::StateContext.new(@state)
+ end
+
+ def test_should_not_allow_except_to
+ exception = assert_raises(ArgumentError) { @state_context.transition(except_to: :idling) }
+ assert_equal 'Unknown key: :except_to. Valid keys are: :from, :to, :on, :if, :unless', exception.message
+ end
+
+ def test_should_not_allow_except_from
+ exception = assert_raises(ArgumentError) { @state_context.transition(except_from: :idling) }
+ assert_equal 'Unknown key: :except_from. Valid keys are: :from, :to, :on, :if, :unless', exception.message
+ end
+
+ def test_should_not_allow_implicit_transitions
+ exception = assert_raises(ArgumentError) { @state_context.transition(parked: :idling) }
+ assert_equal 'Unknown key: :parked. Valid keys are: :from, :to, :on, :if, :unless', exception.message
+ end
+
+ def test_should_not_allow_except_on
+ exception = assert_raises(ArgumentError) { @state_context.transition(except_on: :park) }
+ assert_equal 'Unknown key: :except_on. Valid keys are: :from, :to, :on, :if, :unless', exception.message
+ end
+
+ def test_should_require_on_event
+ exception = assert_raises(ArgumentError) { @state_context.transition(to: :idling) }
+ assert_equal 'Must specify :on event', exception.message
+ end
+
+ def test_should_not_allow_missing_from_and_to
+ exception = assert_raises(ArgumentError) { @state_context.transition(on: :ignite) }
+ assert_equal 'Must specify either :to or :from state', exception.message
+ end
+
+ def test_should_not_allow_from_and_to
+ exception = assert_raises(ArgumentError) { @state_context.transition(on: :ignite, from: :parked, to: :idling) }
+ assert_equal 'Must specify either :to or :from state', exception.message
+ end
+
+ def test_should_allow_to_state_if_missing_from_state
+ @state_context.transition(on: :park, from: :parked)
+ end
+
+ def test_should_allow_from_state_if_missing_to_state
+ @state_context.transition(on: :ignite, to: :idling)
+ end
+
+ def test_should_automatically_set_to_option_with_from_state
+ branch = @state_context.transition(from: :idling, on: :park)
+ assert_instance_of StateMachines::Branch, branch
+
+ state_requirements = branch.state_requirements
+ assert_equal 1, state_requirements.length
+
+ from_requirement = state_requirements[0][:to]
+ assert_instance_of StateMachines::WhitelistMatcher, from_requirement
+ assert_equal [:parked], from_requirement.values
+ end
+
+ def test_should_automatically_set_from_option_with_to_state
+ branch = @state_context.transition(to: :idling, on: :ignite)
+ assert_instance_of StateMachines::Branch, branch
+
+ state_requirements = branch.state_requirements
+ assert_equal 1, state_requirements.length
+
+ from_requirement = state_requirements[0][:from]
+ assert_instance_of StateMachines::WhitelistMatcher, from_requirement
+ assert_equal [:parked], from_requirement.values
+ end
+
+ def test_should_allow_if_condition
+ @state_context.transition(to: :idling, on: :park, if: :seatbelt_on?)
+ end
+
+ def test_should_allow_unless_condition
+ @state_context.transition(to: :idling, on: :park, unless: :seatbelt_off?)
+ end
+
+ def test_should_include_all_transition_states_in_machine_states
+ @state_context.transition(to: :idling, on: :ignite)
+
+ assert_equal [:parked, :idling], @machine.states.map { |state| state.name }
+ end
+
+ def test_should_include_all_transition_events_in_machine_events
+ @state_context.transition(to: :idling, on: :ignite)
+
+ assert_equal [:ignite], @machine.events.map { |event| event.name }
+ end
+
+ def test_should_allow_multiple_events
+ @state_context.transition(to: :idling, on: [:ignite, :shift_up])
+
+ assert_equal [:ignite, :shift_up], @machine.events.map { |event| event.name }
+ end
+end
diff --git a/test/unit/state_context/state_context_with_matching_transition_test.rb b/test/unit/state_context/state_context_with_matching_transition_test.rb
new file mode 100644
index 0000000..d959b46
--- /dev/null
+++ b/test/unit/state_context/state_context_with_matching_transition_test.rb
@@ -0,0 +1,27 @@
+require_relative '../../test_helper'
+
+class StateContextWithMatchingTransitionTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass, initial: :parked)
+ @state = @machine.state :parked
+
+ @state_context = StateMachines::StateContext.new(@state)
+ @state_context.transition(to: :idling, on: :ignite)
+
+ @event = @machine.event(:ignite)
+ @object = @klass.new
+ end
+
+ def test_should_be_able_to_fire
+ assert @event.can_fire?(@object)
+ end
+
+ def test_should_have_a_transition
+ transition = @event.transition_for(@object)
+ refute_nil transition
+ assert_equal 'parked', transition.from
+ assert_equal 'idling', transition.to
+ assert_equal :ignite, transition.event
+ end
+end
diff --git a/test/unit/state_machine/state_machine_by_default_test.rb b/test/unit/state_machine/state_machine_by_default_test.rb
new file mode 100644
index 0000000..0e2d34d
--- /dev/null
+++ b/test/unit/state_machine/state_machine_by_default_test.rb
@@ -0,0 +1,12 @@
+require_relative '../../test_helper'
+
+class StateMachineByDefaultTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = @klass.state_machine
+ end
+
+ def test_should_use_state_attribute
+ assert_equal :state, @machine.attribute
+ end
+end
diff --git a/test/unit/state_machine/state_machine_test.rb b/test/unit/state_machine/state_machine_test.rb
new file mode 100644
index 0000000..38a4240
--- /dev/null
+++ b/test/unit/state_machine/state_machine_test.rb
@@ -0,0 +1,20 @@
+require_relative '../../test_helper'
+
+class StateMachineTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ end
+
+ def test_should_allow_state_machines_on_any_class
+ assert @klass.respond_to?(:state_machine)
+ end
+
+ def test_should_evaluate_block_within_machine_context
+ responded = false
+ @klass.state_machine(:state) do
+ responded = respond_to?(:event)
+ end
+
+ assert responded
+ end
+end
diff --git a/test/unit/transition/transition_after_being_performed_test.rb b/test/unit/transition/transition_after_being_performed_test.rb
new file mode 100644
index 0000000..5f73614
--- /dev/null
+++ b/test/unit/transition/transition_after_being_performed_test.rb
@@ -0,0 +1,48 @@
+require_relative '../../test_helper'
+
+class TransitionAfterBeingPerformedTest < StateMachinesTest
+ def setup
+ @klass = Class.new do
+ attr_reader :saved, :save_state
+
+ def save
+ @save_state = state
+ @saved = true
+ 1
+ end
+ end
+
+ @machine = StateMachines::Machine.new(@klass, action: :save)
+ @machine.state :parked, :idling
+ @machine.event :ignite
+
+ @object = @klass.new
+ @object.state = 'parked'
+ @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ @result = @transition.perform
+ end
+
+ def test_should_have_empty_args
+ assert_equal [], @transition.args
+ end
+
+ def test_should_have_a_result
+ assert_equal 1, @transition.result
+ end
+
+ def test_should_be_successful
+ assert_equal true, @result
+ end
+
+ def test_should_change_the_current_state
+ assert_equal 'idling', @object.state
+ end
+
+ def test_should_run_the_action
+ assert @object.saved
+ end
+
+ def test_should_run_the_action_after_saving_the_state
+ assert_equal 'idling', @object.save_state
+ end
+end
diff --git a/test/unit/transition/transition_after_being_persisted_test.rb b/test/unit/transition/transition_after_being_persisted_test.rb
new file mode 100644
index 0000000..91c678f
--- /dev/null
+++ b/test/unit/transition/transition_after_being_persisted_test.rb
@@ -0,0 +1,46 @@
+require_relative '../../test_helper'
+
+class TransitionAfterBeingPersistedTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass, action: :save)
+ @machine.state :parked, :idling
+ @machine.event :ignite
+
+ @object = @klass.new
+ @object.state = 'parked'
+
+ @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ @transition.persist
+ end
+
+ def test_should_update_state_value
+ assert_equal 'idling', @object.state
+ end
+
+ def test_should_not_change_from_state
+ assert_equal 'parked', @transition.from
+ end
+
+ def test_should_not_change_to_state
+ assert_equal 'idling', @transition.to
+ end
+
+ def test_should_not_be_able_to_persist_twice
+ @object.state = 'parked'
+ @transition.persist
+ assert_equal 'parked', @object.state
+ end
+
+ def test_should_be_able_to_persist_again_after_resetting
+ @object.state = 'parked'
+ @transition.reset
+ @transition.persist
+ assert_equal 'idling', @object.state
+ end
+
+ def test_should_revert_to_from_state_on_rollback
+ @transition.rollback
+ assert_equal 'parked', @object.state
+ end
+end
diff --git a/test/unit/transition/transition_after_being_rolled_back_test.rb b/test/unit/transition/transition_after_being_rolled_back_test.rb
new file mode 100644
index 0000000..099dc52
--- /dev/null
+++ b/test/unit/transition/transition_after_being_rolled_back_test.rb
@@ -0,0 +1,35 @@
+require_relative '../../test_helper'
+
+class TransitionAfterBeingRolledBackTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass, action: :save)
+ @machine.state :parked, :idling
+ @machine.event :ignite
+
+ @object = @klass.new
+ @object.state = 'parked'
+
+ @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ @object.state = 'idling'
+
+ @transition.rollback
+ end
+
+ def test_should_update_state_value_to_from_state
+ assert_equal 'parked', @object.state
+ end
+
+ def test_should_not_change_from_state
+ assert_equal 'parked', @transition.from
+ end
+
+ def test_should_not_change_to_state
+ assert_equal 'idling', @transition.to
+ end
+
+ def test_should_still_be_able_to_persist
+ @transition.persist
+ assert_equal 'idling', @object.state
+ end
+end
diff --git a/test/unit/transition/transition_equality_test.rb b/test/unit/transition/transition_equality_test.rb
new file mode 100644
index 0000000..3751bc7
--- /dev/null
+++ b/test/unit/transition/transition_equality_test.rb
@@ -0,0 +1,52 @@
+require_relative '../../test_helper'
+
+class TransitionEqualityTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked, :idling
+ @machine.event :ignite
+
+ @object = @klass.new
+ @object.state = 'parked'
+ @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ end
+
+ def test_should_be_equal_with_same_properties
+ transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ assert_equal transition, @transition
+ end
+
+ def test_should_not_be_equal_with_different_machines
+ machine = StateMachines::Machine.new(@klass, :status, namespace: :other)
+ machine.state :parked, :idling
+ machine.event :ignite
+ transition = StateMachines::Transition.new(@object, machine, :ignite, :parked, :idling)
+
+ refute_equal transition, @transition
+ end
+
+ def test_should_not_be_equal_with_different_objects
+ transition = StateMachines::Transition.new(@klass.new, @machine, :ignite, :parked, :idling)
+ refute_equal transition, @transition
+ end
+
+ def test_should_not_be_equal_with_different_event_names
+ @machine.event :park
+ transition = StateMachines::Transition.new(@object, @machine, :park, :parked, :idling)
+ refute_equal transition, @transition
+ end
+
+ def test_should_not_be_equal_with_different_from_state_names
+ @machine.state :first_gear
+ transition = StateMachines::Transition.new(@object, @machine, :ignite, :first_gear, :idling)
+ refute_equal transition, @transition
+ end
+
+ def test_should_not_be_equal_with_different_to_state_names
+ @machine.state :first_gear
+ transition = StateMachines::Transition.new(@object, @machine, :ignite, :idling, :first_gear)
+ refute_equal transition, @transition
+ end
+end
diff --git a/test/unit/transition/transition_loopback_test.rb b/test/unit/transition/transition_loopback_test.rb
new file mode 100644
index 0000000..5ed4301
--- /dev/null
+++ b/test/unit/transition/transition_loopback_test.rb
@@ -0,0 +1,18 @@
+require_relative '../../test_helper'
+
+class TransitionLoopbackTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked
+ @machine.event :park
+
+ @object = @klass.new
+ @object.state = 'parked'
+ @transition = StateMachines::Transition.new(@object, @machine, :park, :parked, :parked)
+ end
+
+ def test_should_be_loopback
+ assert @transition.loopback?
+ end
+end
diff --git a/test/unit/transition/transition_test.rb b/test/unit/transition/transition_test.rb
new file mode 100644
index 0000000..b62739e
--- /dev/null
+++ b/test/unit/transition/transition_test.rb
@@ -0,0 +1,96 @@
+require_relative '../../test_helper'
+
+class TransitionTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked, :idling
+ @machine.event :ignite
+
+ @object = @klass.new
+ @object.state = 'parked'
+
+ @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ end
+
+ def test_should_have_an_object
+ assert_equal @object, @transition.object
+ end
+
+ def test_should_have_a_machine
+ assert_equal @machine, @transition.machine
+ end
+
+ def test_should_have_an_event
+ assert_equal :ignite, @transition.event
+ end
+
+ def test_should_have_a_qualified_event
+ assert_equal :ignite, @transition.qualified_event
+ end
+
+ def test_should_have_a_human_event
+ assert_equal 'ignite', @transition.human_event
+ end
+
+ def test_should_have_a_from_value
+ assert_equal 'parked', @transition.from
+ end
+
+ def test_should_have_a_from_name
+ assert_equal :parked, @transition.from_name
+ end
+
+ def test_should_have_a_qualified_from_name
+ assert_equal :parked, @transition.qualified_from_name
+ end
+
+ def test_should_have_a_human_from_name
+ assert_equal 'parked', @transition.human_from_name
+ end
+
+ def test_should_have_a_to_value
+ assert_equal 'idling', @transition.to
+ end
+
+ def test_should_have_a_to_name
+ assert_equal :idling, @transition.to_name
+ end
+
+ def test_should_have_a_qualified_to_name
+ assert_equal :idling, @transition.qualified_to_name
+ end
+
+ def test_should_have_a_human_to_name
+ assert_equal 'idling', @transition.human_to_name
+ end
+
+ def test_should_have_an_attribute
+ assert_equal :state, @transition.attribute
+ end
+
+ def test_should_not_have_an_action
+ assert_nil @transition.action
+ end
+
+ def test_should_not_be_transient
+ assert_equal false, @transition.transient?
+ end
+
+ def test_should_generate_attributes
+ expected = { object: @object, attribute: :state, event: :ignite, from: 'parked', to: 'idling' }
+ assert_equal expected, @transition.attributes
+ end
+
+ def test_should_have_empty_args
+ assert_equal [], @transition.args
+ end
+
+ def test_should_not_have_a_result
+ assert_nil @transition.result
+ end
+
+ def test_should_use_pretty_inspect
+ assert_equal '#<StateMachines::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>', @transition.inspect
+ end
+end
diff --git a/test/unit/transition/transition_transient_test.rb b/test/unit/transition/transition_transient_test.rb
new file mode 100644
index 0000000..e8a5be3
--- /dev/null
+++ b/test/unit/transition/transition_transient_test.rb
@@ -0,0 +1,20 @@
+require_relative '../../test_helper'
+
+class TransitionTransientTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked, :idling
+ @machine.event :ignite
+
+ @object = @klass.new
+ @object.state = 'parked'
+ @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ @transition.transient = true
+ end
+
+ def test_should_be_transient
+ assert @transition.transient?
+ end
+end
diff --git a/test/unit/transition/transition_with_action_test.rb b/test/unit/transition/transition_with_action_test.rb
new file mode 100644
index 0000000..e42a2f1
--- /dev/null
+++ b/test/unit/transition/transition_with_action_test.rb
@@ -0,0 +1,27 @@
+require_relative '../../test_helper'
+
+class TransitionWithActionTest < StateMachinesTest
+ def setup
+ @klass = Class.new do
+ def save
+ end
+ end
+
+ @machine = StateMachines::Machine.new(@klass, action: :save)
+ @machine.state :parked, :idling
+ @machine.event :ignite
+
+ @object = @klass.new
+ @object.state = 'parked'
+
+ @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ end
+
+ def test_should_have_an_action
+ assert_equal :save, @transition.action
+ end
+
+ def test_should_not_have_a_result
+ assert_nil @transition.result
+ end
+end
diff --git a/test/unit/transition/transition_with_after_callbacks_skipped_test.rb b/test/unit/transition/transition_with_after_callbacks_skipped_test.rb
new file mode 100644
index 0000000..f071c78
--- /dev/null
+++ b/test/unit/transition/transition_with_after_callbacks_skipped_test.rb
@@ -0,0 +1,127 @@
+require_relative '../../test_helper'
+
+class TransitionWithAfterCallbacksSkippedTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked, :idling
+ @machine.event :ignite
+
+ @object = @klass.new
+ @object.state = 'parked'
+ @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ end
+
+ def test_should_run_before_callbacks
+ @machine.before_transition { @run = true }
+
+ assert_equal true, @transition.run_callbacks(after: false)
+ assert @run
+ end
+
+ def test_should_not_run_after_callbacks
+ @run = false
+ @machine.after_transition { @run = true }
+
+ assert_equal true, @transition.run_callbacks(after: false)
+ refute @run
+ end
+
+ if StateMachines::Transition.pause_supported?
+ def test_should_run_around_callbacks_before_yield
+ @machine.around_transition { |block| @run = true; block.call }
+
+ assert_equal true, @transition.run_callbacks(after: false)
+ assert @run
+ end
+
+ def test_should_not_run_around_callbacks_after_yield
+ @run = false
+ @machine.around_transition { |block| block.call; @run = true }
+
+ assert_equal true, @transition.run_callbacks(after: false)
+ refute @run
+ end
+
+ def test_should_continue_around_transition_execution_on_second_call
+ @callbacks = []
+ @machine.around_transition { |block| @callbacks << :before_around_1; block.call; @callbacks << :after_around_1 }
+ @machine.around_transition { |block| @callbacks << :before_around_2; block.call; @callbacks << :after_around_2 }
+ @machine.after_transition { @callbacks << :after }
+
+ assert_equal true, @transition.run_callbacks(after: false)
+ assert_equal [:before_around_1, :before_around_2], @callbacks
+
+ assert_equal true, @transition.run_callbacks
+ assert_equal [:before_around_1, :before_around_2, :after_around_2, :after_around_1, :after], @callbacks
+ end
+
+ def test_should_not_run_further_callbacks_if_halted_during_continue_around_transition
+ @callbacks = []
+ @machine.around_transition { |block| @callbacks << :before_around_1; block.call; @callbacks << :after_around_1 }
+ @machine.around_transition { |block| @callbacks << :before_around_2; block.call; @callbacks << :after_around_2; throw :halt }
+ @machine.after_transition { @callbacks << :after }
+
+ assert_equal true, @transition.run_callbacks(after: false)
+ assert_equal [:before_around_1, :before_around_2], @callbacks
+
+ assert_equal true, @transition.run_callbacks
+ assert_equal [:before_around_1, :before_around_2, :after_around_2], @callbacks
+ end
+
+ def test_should_not_be_able_to_continue_twice
+ @count = 0
+ @machine.around_transition { |block| block.call; @count += 1 }
+ @machine.after_transition { @count += 1 }
+
+ @transition.run_callbacks(after: false)
+
+ 2.times do
+ assert_equal true, @transition.run_callbacks
+ assert_equal 2, @count
+ end
+ end
+
+ def test_should_not_be_able_to_continue_again_after_halted
+ @count = 0
+ @machine.around_transition { |block| block.call; @count += 1; throw :halt }
+ @machine.after_transition { @count += 1 }
+
+ @transition.run_callbacks(after: false)
+
+ 2.times do
+ assert_equal true, @transition.run_callbacks
+ assert_equal 1, @count
+ end
+ end
+
+ def test_should_have_access_to_result_after_continued
+ @machine.around_transition { |block| @around_before_result = @transition.result; block.call; @around_after_result = @transition.result }
+ @machine.after_transition { @after_result = @transition.result }
+
+ @transition.run_callbacks(after: false)
+ @transition.run_callbacks { { result: 1 } }
+
+ assert_nil @around_before_result
+ assert_equal 1, @around_after_result
+ assert_equal 1, @after_result
+ end
+
+ def test_should_raise_exceptions_during_around_callbacks_after_yield_in_second_execution
+ @machine.around_transition { |block| block.call; fail ArgumentError }
+
+ @transition.run_callbacks(after: false)
+ assert_raises(ArgumentError) { @transition.run_callbacks }
+ end
+ else
+ def test_should_raise_exception_on_second_call
+ @callbacks = []
+ @machine.around_transition { |block| @callbacks << :before_around_1; block.call; @callbacks << :after_around_1 }
+ @machine.around_transition { |block| @callbacks << :before_around_2; block.call; @callbacks << :after_around_2 }
+ @machine.after_transition { @callbacks << :after }
+
+ assert_raises(ArgumentError) { @transition.run_callbacks(after: false) }
+ end
+ end
+end
diff --git a/test/unit/transition/transition_with_after_callbacks_test.rb b/test/unit/transition/transition_with_after_callbacks_test.rb
new file mode 100644
index 0000000..07c2d18
--- /dev/null
+++ b/test/unit/transition/transition_with_after_callbacks_test.rb
@@ -0,0 +1,93 @@
+require_relative '../../test_helper'
+
+class TransitionWithAfterCallbacksTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked, :idling
+ @machine.event :ignite
+
+ @object = @klass.new
+ @object.state = 'parked'
+ @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ end
+
+ def test_should_run_after_callbacks
+ @machine.after_transition { |_object| @run = true }
+ result = @transition.run_callbacks
+
+ assert_equal true, result
+ assert_equal true, @run
+ end
+
+ def test_should_only_run_those_that_match_transition_context
+ @count = 0
+ callback = lambda { @count += 1 }
+
+ @machine.after_transition from: :parked, to: :idling, on: :park, do: callback
+ @machine.after_transition from: :parked, to: :parked, on: :park, do: callback
+ @machine.after_transition from: :parked, to: :idling, on: :ignite, do: callback
+ @machine.after_transition from: :idling, to: :idling, on: :park, do: callback
+ @transition.run_callbacks
+
+ assert_equal 1, @count
+ end
+
+ def test_should_not_run_if_not_successful
+ @run = false
+ @machine.after_transition { |_object| @run = true }
+ @transition.run_callbacks { { success: false } }
+ refute @run
+ end
+
+ def test_should_run_if_successful
+ @machine.after_transition { |_object| @run = true }
+ @transition.run_callbacks { { success: true } }
+ assert @run
+ end
+
+ def test_should_pass_transition_as_argument
+ @machine.after_transition { |*args| @args = args }
+
+ @transition.run_callbacks
+ assert_equal [@object, @transition], @args
+ end
+
+ def test_should_catch_halts
+ @machine.after_transition { throw :halt }
+
+ result = @transition.run_callbacks
+ assert_equal true, result
+ end
+
+ def test_should_not_catch_exceptions
+ @machine.after_transition { fail ArgumentError }
+ assert_raises(ArgumentError) { @transition.run_callbacks }
+ end
+
+ def test_should_not_be_able_to_run_twice
+ @count = 0
+ @machine.after_transition { @count += 1 }
+ @transition.run_callbacks
+ @transition.run_callbacks
+ assert_equal 1, @count
+ end
+
+ def test_should_not_be_able_to_run_twice_if_halted
+ @count = 0
+ @machine.after_transition { @count += 1; throw :halt }
+ @transition.run_callbacks
+ @transition.run_callbacks
+ assert_equal 1, @count
+ end
+
+ def test_should_be_able_to_run_again_after_resetting
+ @count = 0
+ @machine.after_transition { @count += 1 }
+ @transition.run_callbacks
+ @transition.reset
+ @transition.run_callbacks
+ assert_equal 2, @count
+ end
+end
diff --git a/test/unit/transition/transition_with_around_callbacks_test.rb b/test/unit/transition/transition_with_around_callbacks_test.rb
new file mode 100644
index 0000000..872d9d8
--- /dev/null
+++ b/test/unit/transition/transition_with_around_callbacks_test.rb
@@ -0,0 +1,141 @@
+require_relative '../../test_helper'
+
+class TransitionWithAroundCallbacksTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked, :idling
+ @machine.event :ignite
+
+ @object = @klass.new
+ @object.state = 'parked'
+ @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ end
+
+ def test_should_run_around_callbacks
+ @machine.around_transition { |_object, _transition, block| @run_before = true; block.call; @run_after = true }
+ result = @transition.run_callbacks
+
+ assert_equal true, result
+ assert_equal true, @run_before
+ assert_equal true, @run_after
+ end
+
+ def test_should_only_run_those_that_match_transition_context
+ @count = 0
+ callback = lambda { |_object, _transition, block| @count += 1; block.call }
+
+ @machine.around_transition from: :parked, to: :idling, on: :park, do: callback
+ @machine.around_transition from: :parked, to: :parked, on: :park, do: callback
+ @machine.around_transition from: :parked, to: :idling, on: :ignite, do: callback
+ @machine.around_transition from: :idling, to: :idling, on: :park, do: callback
+ @transition.run_callbacks
+
+ assert_equal 1, @count
+ end
+
+ def test_should_pass_transition_as_argument
+ @machine.around_transition { |*args| block = args.pop; @args = args; block.call }
+ @transition.run_callbacks
+
+ assert_equal [@object, @transition], @args
+ end
+
+ def test_should_run_block_between_callback
+ @callbacks = []
+ @machine.around_transition { |block| @callbacks << :before; block.call; @callbacks << :after }
+ @transition.run_callbacks { @callbacks << :within; { success: true } }
+
+ assert_equal [:before, :within, :after], @callbacks
+ end
+
+ def test_should_have_access_to_result_after_yield
+ @machine.around_transition { |block| @before_result = @transition.result; block.call; @after_result = @transition.result }
+ @transition.run_callbacks { { result: 1, success: true } }
+
+ assert_nil @before_result
+ assert_equal 1, @after_result
+ end
+
+ def test_should_catch_before_yield_halts
+ @machine.around_transition { throw :halt }
+
+ result = @transition.run_callbacks
+ assert_equal false, result
+ end
+
+ def test_should_catch_after_yield_halts
+ @machine.around_transition { |block| block.call; throw :halt }
+
+ result = @transition.run_callbacks
+ assert_equal true, result
+ end
+
+ def test_should_not_catch_before_yield
+ @machine.around_transition { fail ArgumentError }
+ assert_raises(ArgumentError) { @transition.run_callbacks }
+ end
+
+ def test_should_not_catch_after_yield
+ @machine.around_transition { |block| block.call; fail ArgumentError }
+ assert_raises(ArgumentError) { @transition.run_callbacks }
+ end
+
+ def test_should_fail_if_not_yielded
+ @machine.around_transition {}
+
+ result = @transition.run_callbacks
+ assert_equal false, result
+ end
+
+ def test_should_not_be_able_to_run_twice
+ @before_count = 0
+ @after_count = 0
+ @machine.around_transition { |block| @before_count += 1; block.call; @after_count += 1 }
+ @transition.run_callbacks
+ @transition.run_callbacks
+ assert_equal 1, @before_count
+ assert_equal 1, @after_count
+ end
+
+ def test_should_be_able_to_run_again_after_resetting
+ @before_count = 0
+ @after_count = 0
+ @machine.around_transition { |block| @before_count += 1; block.call; @after_count += 1 }
+ @transition.run_callbacks
+ @transition.reset
+ @transition.run_callbacks
+ assert_equal 2, @before_count
+ assert_equal 2, @after_count
+ end
+
+ def test_should_succeed_if_block_result_is_false
+ @machine.around_transition { |block| @before_run = true; block.call; @after_run = true }
+ assert_equal true, @transition.run_callbacks { { success: true, result: false } }
+ assert @before_run
+ assert @after_run
+ end
+
+ def test_should_succeed_if_block_result_is_true
+ @machine.around_transition { |block| @before_run = true; block.call; @after_run = true }
+ assert_equal true, @transition.run_callbacks { { success: true, result: true } }
+ assert @before_run
+ assert @after_run
+ end
+
+ def test_should_only_run_before_if_block_success_is_false
+ @after_run = false
+ @machine.around_transition { |block| @before_run = true; block.call; @after_run = true }
+ assert_equal true, @transition.run_callbacks { { success: false } }
+ assert @before_run
+ refute @after_run
+ end
+
+ def test_should_succeed_if_block_success_is_false
+ @machine.around_transition { |block| @before_run = true; block.call; @after_run = true }
+ assert_equal true, @transition.run_callbacks { { success: true } }
+ assert @before_run
+ assert @after_run
+ end
+end
diff --git a/test/unit/transition/transition_with_before_callbacks_skipped_test.rb b/test/unit/transition/transition_with_before_callbacks_skipped_test.rb
new file mode 100644
index 0000000..f66f36c
--- /dev/null
+++ b/test/unit/transition/transition_with_before_callbacks_skipped_test.rb
@@ -0,0 +1,30 @@
+require_relative '../../test_helper'
+
+class TransitionWithBeforeCallbacksSkippedTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked, :idling
+ @machine.event :ignite
+
+ @object = @klass.new
+ @object.state = 'parked'
+ @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ end
+
+ def test_should_not_run_before_callbacks
+ @run = false
+ @machine.before_transition { @run = true }
+
+ assert_equal false, @transition.run_callbacks(before: false)
+ refute @run
+ end
+
+ def test_should_run_failure_callbacks
+ @machine.after_failure { @run = true }
+
+ assert_equal false, @transition.run_callbacks(before: false)
+ assert @run
+ end
+end
diff --git a/test/unit/transition/transition_with_before_callbacks_test.rb b/test/unit/transition/transition_with_before_callbacks_test.rb
new file mode 100644
index 0000000..d749ac5
--- /dev/null
+++ b/test/unit/transition/transition_with_before_callbacks_test.rb
@@ -0,0 +1,104 @@
+require_relative '../../test_helper'
+
+class TransitionWithBeforeCallbacksTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked, :idling
+ @machine.event :ignite
+
+ @object = @klass.new
+ @object.state = 'parked'
+ @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ end
+
+ def test_should_run_before_callbacks
+ @machine.before_transition { @run = true }
+ result = @transition.run_callbacks
+
+ assert_equal true, result
+ assert_equal true, @run
+ end
+
+ def test_should_only_run_those_that_match_transition_context
+ @count = 0
+ callback = lambda { @count += 1 }
+
+ @machine.before_transition from: :parked, to: :idling, on: :park, do: callback
+ @machine.before_transition from: :parked, to: :parked, on: :park, do: callback
+ @machine.before_transition from: :parked, to: :idling, on: :ignite, do: callback
+ @machine.before_transition from: :idling, to: :idling, on: :park, do: callback
+ @transition.run_callbacks
+
+ assert_equal 1, @count
+ end
+
+ def test_should_pass_transition_as_argument
+ @machine.before_transition { |*args| @args = args }
+ @transition.run_callbacks
+
+ assert_equal [@object, @transition], @args
+ end
+
+ def test_should_catch_halts
+ @machine.before_transition { throw :halt }
+
+ result = @transition.run_callbacks
+ assert_equal false, result
+ end
+
+ def test_should_not_catch_exceptions
+ @machine.before_transition { fail ArgumentError }
+ assert_raises(ArgumentError) { @transition.run_callbacks }
+ end
+
+ def test_should_not_be_able_to_run_twice
+ @count = 0
+ @machine.before_transition { @count += 1 }
+ @transition.run_callbacks
+ @transition.run_callbacks
+ assert_equal 1, @count
+ end
+
+ def test_should_be_able_to_run_again_after_halt
+ @count = 0
+ @machine.before_transition { @count += 1; throw :halt }
+ @transition.run_callbacks
+ @transition.run_callbacks
+ assert_equal 2, @count
+ end
+
+ def test_should_be_able_to_run_again_after_resetting
+ @count = 0
+ @machine.before_transition { @count += 1 }
+ @transition.run_callbacks
+ @transition.reset
+ @transition.run_callbacks
+ assert_equal 2, @count
+ end
+
+ def test_should_succeed_if_block_result_is_false
+ @machine.before_transition { @run = true }
+ assert_equal true, @transition.run_callbacks { { result: false } }
+ assert @run
+ end
+
+ def test_should_succeed_if_block_result_is_true
+ @machine.before_transition { @run = true }
+ assert_equal true, @transition.run_callbacks { { result: true } }
+ assert @run
+ end
+
+ def test_should_succeed_if_block_success_is_false
+ @machine.before_transition { @run = true }
+ assert_equal true, @transition.run_callbacks { { success: false } }
+ assert @run
+ end
+
+ def test_should_succeed_if_block_success_is_true
+ @machine.before_transition { @run = true }
+ assert_equal true, @transition.run_callbacks { { success: true } }
+ assert @run
+ end
+end
diff --git a/test/unit/transition/transition_with_custom_machine_attribute_test.rb b/test/unit/transition/transition_with_custom_machine_attribute_test.rb
new file mode 100644
index 0000000..507ee50
--- /dev/null
+++ b/test/unit/transition/transition_with_custom_machine_attribute_test.rb
@@ -0,0 +1,28 @@
+require_relative '../../test_helper'
+
+class TransitionWithCustomMachineAttributeTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass, :state, attribute: :state_id)
+ @machine.state :off, value: 1
+ @machine.state :active, value: 2
+ @machine.event :activate
+
+ @object = @klass.new
+ @object.state_id = 1
+
+ @transition = StateMachines::Transition.new(@object, @machine, :activate, :off, :active)
+ end
+
+ def test_should_persist
+ @transition.persist
+ assert_equal 2, @object.state_id
+ end
+
+ def test_should_rollback
+ @object.state_id = 2
+ @transition.rollback
+
+ assert_equal 1, @object.state_id
+ end
+end
diff --git a/test/unit/transition/transition_with_different_states_test.rb b/test/unit/transition/transition_with_different_states_test.rb
new file mode 100644
index 0000000..a0ee2d2
--- /dev/null
+++ b/test/unit/transition/transition_with_different_states_test.rb
@@ -0,0 +1,18 @@
+require_relative '../../test_helper'
+
+class TransitionWithDifferentStatesTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked, :idling
+ @machine.event :ignite
+
+ @object = @klass.new
+ @object.state = 'parked'
+ @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ end
+
+ def test_should_not_be_loopback
+ refute @transition.loopback?
+ end
+end
diff --git a/test/unit/transition/transition_with_dynamic_to_value_test.rb b/test/unit/transition/transition_with_dynamic_to_value_test.rb
new file mode 100644
index 0000000..b1c4abc
--- /dev/null
+++ b/test/unit/transition/transition_with_dynamic_to_value_test.rb
@@ -0,0 +1,19 @@
+require_relative '../../test_helper'
+
+class TransitionWithDynamicToValueTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked
+ @machine.state :idling, value: lambda { 1 }
+ @machine.event :ignite
+
+ @object = @klass.new
+ @object.state = 'parked'
+ @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ end
+
+ def test_should_evaluate_to_value
+ assert_equal 1, @transition.to
+ end
+end
diff --git a/test/unit/transition/transition_with_failure_callbacks_test.rb b/test/unit/transition/transition_with_failure_callbacks_test.rb
new file mode 100644
index 0000000..9a19a84
--- /dev/null
+++ b/test/unit/transition/transition_with_failure_callbacks_test.rb
@@ -0,0 +1,84 @@
+require_relative '../../test_helper'
+
+class TransitionWithFailureCallbacksTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked, :idling
+ @machine.event :ignite
+
+ @object = @klass.new
+ @object.state = 'parked'
+ @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ end
+
+ def test_should_only_run_those_that_match_transition_context
+ @count = 0
+ callback = lambda { @count += 1 }
+
+ @machine.after_failure do: callback
+ @machine.after_failure on: :park, do: callback
+ @machine.after_failure on: :ignite, do: callback
+ @transition.run_callbacks { { success: false } }
+
+ assert_equal 2, @count
+ end
+
+ def test_should_run_if_not_successful
+ @machine.after_failure { |_object| @run = true }
+ @transition.run_callbacks { { success: false } }
+ assert @run
+ end
+
+ def test_should_not_run_if_successful
+ @run = false
+ @machine.after_failure { |_object| @run = true }
+ @transition.run_callbacks { { success: true } }
+ refute @run
+ end
+
+ def test_should_pass_transition_as_argument
+ @machine.after_failure { |*args| @args = args }
+
+ @transition.run_callbacks { { success: false } }
+ assert_equal [@object, @transition], @args
+ end
+
+ def test_should_catch_halts
+ @machine.after_failure { throw :halt }
+
+ result = @transition.run_callbacks { { success: false } }
+ assert_equal true, result
+ end
+
+ def test_should_not_catch_exceptions
+ @machine.after_failure { fail ArgumentError }
+ assert_raises(ArgumentError) { @transition.run_callbacks { { success: false } } }
+ end
+
+ def test_should_not_be_able_to_run_twice
+ @count = 0
+ @machine.after_failure { @count += 1 }
+ @transition.run_callbacks { { success: false } }
+ @transition.run_callbacks { { success: false } }
+ assert_equal 1, @count
+ end
+
+ def test_should_not_be_able_to_run_twice_if_halted
+ @count = 0
+ @machine.after_failure { @count += 1; throw :halt }
+ @transition.run_callbacks { { success: false } }
+ @transition.run_callbacks { { success: false } }
+ assert_equal 1, @count
+ end
+
+ def test_should_be_able_to_run_again_after_resetting
+ @count = 0
+ @machine.after_failure { @count += 1 }
+ @transition.run_callbacks { { success: false } }
+ @transition.reset
+ @transition.run_callbacks { { success: false } }
+ assert_equal 2, @count
+ end
+end
diff --git a/test/unit/transition/transition_with_invalid_nodes_test.rb b/test/unit/transition/transition_with_invalid_nodes_test.rb
new file mode 100644
index 0000000..f1dd9f8
--- /dev/null
+++ b/test/unit/transition/transition_with_invalid_nodes_test.rb
@@ -0,0 +1,29 @@
+require_relative '../../test_helper'
+
+class TransitionWithInvalidNodesTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked, :idling
+ @machine.event :ignite
+
+ @object = @klass.new
+ @object.state = 'parked'
+ end
+
+ def test_should_raise_exception_without_event
+ assert_raises(IndexError) { StateMachines::Transition.new(@object, @machine, nil, :parked, :idling) }
+ end
+
+ def test_should_raise_exception_with_invalid_event
+ assert_raises(IndexError) { StateMachines::Transition.new(@object, @machine, :invalid, :parked, :idling) }
+ end
+
+ def test_should_raise_exception_with_invalid_from_state
+ assert_raises(IndexError) { StateMachines::Transition.new(@object, @machine, :ignite, :invalid, :idling) }
+ end
+
+ def test_should_raise_exception_with_invalid_to_state
+ assert_raises(IndexError) { StateMachines::Transition.new(@object, @machine, :ignite, :parked, :invalid) }
+ end
+end
diff --git a/test/unit/transition/transition_with_mixed_callbacks_test.rb b/test/unit/transition/transition_with_mixed_callbacks_test.rb
new file mode 100644
index 0000000..588d9c4
--- /dev/null
+++ b/test/unit/transition/transition_with_mixed_callbacks_test.rb
@@ -0,0 +1,105 @@
+require_relative '../../test_helper'
+
+class TransitionWithMixedCallbacksTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked, :idling
+ @machine.event :ignite
+
+ @object = @klass.new
+ @object.state = 'parked'
+ @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ end
+
+ def test_should_before_and_around_callbacks_in_order_defined
+ @callbacks = []
+ @machine.before_transition { @callbacks << :before_1 }
+ @machine.around_transition { |block| @callbacks << :around; block.call }
+ @machine.before_transition { @callbacks << :before_2 }
+
+ assert_equal true, @transition.run_callbacks
+ assert_equal [:before_1, :around, :before_2], @callbacks
+ end
+
+ def test_should_run_around_callbacks_before_after_callbacks
+ @callbacks = []
+ @machine.after_transition { @callbacks << :after_1 }
+ @machine.around_transition { |block| block.call; @callbacks << :after_2 }
+ @machine.after_transition { @callbacks << :after_3 }
+
+ assert_equal true, @transition.run_callbacks
+ assert_equal [:after_2, :after_1, :after_3], @callbacks
+ end
+
+ def test_should_have_access_to_result_for_both_after_and_around_callbacks
+ @machine.after_transition { @after_result = @transition.result }
+ @machine.around_transition { |block| block.call; @around_result = @transition.result }
+
+ @transition.run_callbacks { { result: 1, success: true } }
+ assert_equal 1, @after_result
+ assert_equal 1, @around_result
+ end
+
+ def test_should_not_run_further_callbacks_if_before_callback_halts
+ @callbacks = []
+ @machine.before_transition { @callbacks << :before_1 }
+ @machine.around_transition { |block| @callbacks << :before_around_1; block.call; @callbacks << :after_around_1 }
+ @machine.before_transition { @callbacks << :before_2; throw :halt }
+ @machine.around_transition { |block| @callbacks << :before_around_2; block.call; @callbacks << :after_around_2 }
+ @machine.after_transition { @callbacks << :after }
+
+ assert_equal false, @transition.run_callbacks
+ assert_equal [:before_1, :before_around_1, :before_2], @callbacks
+ end
+
+ def test_should_not_run_further_callbacks_if_before_yield_halts
+ @callbacks = []
+ @machine.before_transition { @callbacks << :before_1 }
+ @machine.around_transition { |_block| @callbacks << :before_around_1; throw :halt }
+ @machine.before_transition { @callbacks << :before_2; throw :halt }
+ @machine.around_transition { |block| @callbacks << :before_around_2; block.call; @callbacks << :after_around_2 }
+ @machine.after_transition { @callbacks << :after }
+
+ assert_equal false, @transition.run_callbacks
+ assert_equal [:before_1, :before_around_1], @callbacks
+ end
+
+ def test_should_not_run_further_callbacks_if_around_callback_fails_to_yield
+ @callbacks = []
+ @machine.before_transition { @callbacks << :before_1 }
+ @machine.around_transition { |_block| @callbacks << :before_around_1 }
+ @machine.before_transition { @callbacks << :before_2; throw :halt }
+ @machine.around_transition { |block| @callbacks << :before_around_2; block.call; @callbacks << :after_around_2 }
+ @machine.after_transition { @callbacks << :after }
+
+ assert_equal false, @transition.run_callbacks
+ assert_equal [:before_1, :before_around_1], @callbacks
+ end
+
+ def test_should_not_run_further_callbacks_if_after_yield_halts
+ @callbacks = []
+ @machine.before_transition { @callbacks << :before_1 }
+ @machine.around_transition { |block| @callbacks << :before_around_1; block.call; @callbacks << :after_around_1; throw :halt }
+ @machine.before_transition { @callbacks << :before_2 }
+ @machine.around_transition { |block| @callbacks << :before_around_2; block.call; @callbacks << :after_around_2 }
+ @machine.after_transition { @callbacks << :after }
+
+ assert_equal true, @transition.run_callbacks
+ assert_equal [:before_1, :before_around_1, :before_2, :before_around_2, :after_around_2, :after_around_1], @callbacks
+ end
+
+ def test_should_not_run_further_callbacks_if_after_callback_halts
+ @callbacks = []
+ @machine.before_transition { @callbacks << :before_1 }
+ @machine.around_transition { |block| @callbacks << :before_around_1; block.call; @callbacks << :after_around_1 }
+ @machine.before_transition { @callbacks << :before_2 }
+ @machine.around_transition { |block| @callbacks << :before_around_2; block.call; @callbacks << :after_around_2 }
+ @machine.after_transition { @callbacks << :after_1; throw :halt }
+ @machine.after_transition { @callbacks << :after_2 }
+
+ assert_equal true, @transition.run_callbacks
+ assert_equal [:before_1, :before_around_1, :before_2, :before_around_2, :after_around_2, :after_around_1, :after_1], @callbacks
+ end
+end
diff --git a/test/unit/transition/transition_with_multiple_after_callbacks_test.rb b/test/unit/transition/transition_with_multiple_after_callbacks_test.rb
new file mode 100644
index 0000000..2ad5da0
--- /dev/null
+++ b/test/unit/transition/transition_with_multiple_after_callbacks_test.rb
@@ -0,0 +1,40 @@
+require_relative '../../test_helper'
+
+class TransitionWithMultipleAfterCallbacksTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked, :idling
+ @machine.event :ignite
+
+ @object = @klass.new
+ @object.state = 'parked'
+ @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ end
+
+ def test_should_run_in_the_order_they_were_defined
+ @callbacks = []
+ @machine.after_transition { @callbacks << 1 }
+ @machine.after_transition { @callbacks << 2 }
+ @transition.run_callbacks
+
+ assert_equal [1, 2], @callbacks
+ end
+
+ def test_should_not_run_further_callbacks_if_halted
+ @callbacks = []
+ @machine.after_transition { @callbacks << 1; throw :halt }
+ @machine.after_transition { @callbacks << 2 }
+
+ assert_equal true, @transition.run_callbacks
+ assert_equal [1], @callbacks
+ end
+
+ def test_should_fail_if_any_callback_halted
+ @machine.after_transition { true }
+ @machine.after_transition { throw :halt }
+
+ assert_equal true, @transition.run_callbacks
+ end
+end
diff --git a/test/unit/transition/transition_with_multiple_around_callbacks_test.rb b/test/unit/transition/transition_with_multiple_around_callbacks_test.rb
new file mode 100644
index 0000000..13df26d
--- /dev/null
+++ b/test/unit/transition/transition_with_multiple_around_callbacks_test.rb
@@ -0,0 +1,114 @@
+require_relative '../../test_helper'
+
+class TransitionWithMultipleAroundCallbacksTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked, :idling
+ @machine.event :ignite
+
+ @object = @klass.new
+ @object.state = 'parked'
+ @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ end
+
+ def test_should_before_yield_in_the_order_they_were_defined
+ @callbacks = []
+ @machine.around_transition { |block| @callbacks << 1; block.call }
+ @machine.around_transition { |block| @callbacks << 2; block.call }
+ @transition.run_callbacks
+
+ assert_equal [1, 2], @callbacks
+ end
+
+ def test_should_before_yield_multiple_methods_in_the_order_they_were_defined
+ @callbacks = []
+ @machine.around_transition(lambda { |block| @callbacks << 1; block.call }, lambda { |block| @callbacks << 2; block.call })
+ @machine.around_transition(lambda { |block| @callbacks << 3; block.call }, lambda { |block| @callbacks << 4; block.call })
+ @transition.run_callbacks
+
+ assert_equal [1, 2, 3, 4], @callbacks
+ end
+
+ def test_should_after_yield_in_the_reverse_order_they_were_defined
+ @callbacks = []
+ @machine.around_transition { |block| block.call; @callbacks << 1 }
+ @machine.around_transition { |block| block.call; @callbacks << 2 }
+ @transition.run_callbacks
+
+ assert_equal [2, 1], @callbacks
+ end
+
+ def test_should_after_yield_multiple_methods_in_the_reverse_order_they_were_defined
+ @callbacks = []
+ @machine.around_transition(lambda { |block| block.call; @callbacks << 1 }) { |block| block.call; @callbacks << 2 }
+ @machine.around_transition(lambda { |block| block.call; @callbacks << 3 }) { |block| block.call; @callbacks << 4 }
+ @transition.run_callbacks
+
+ assert_equal [4, 3, 2, 1], @callbacks
+ end
+
+ def test_should_run_block_between_callback
+ @callbacks = []
+ @machine.around_transition { |block| @callbacks << :before_1; block.call; @callbacks << :after_1 }
+ @machine.around_transition { |block| @callbacks << :before_2; block.call; @callbacks << :after_2 }
+ @transition.run_callbacks { @callbacks << :within; { success: true } }
+
+ assert_equal [:before_1, :before_2, :within, :after_2, :after_1], @callbacks
+ end
+
+ def test_should_have_access_to_result_after_yield
+ @machine.around_transition { |block| @before_result_1 = @transition.result; block.call; @after_result_1 = @transition.result }
+ @machine.around_transition { |block| @before_result_2 = @transition.result; block.call; @after_result_2 = @transition.result }
+ @transition.run_callbacks { { result: 1, success: true } }
+
+ assert_nil @before_result_1
+ assert_nil @before_result_2
+ assert_equal 1, @after_result_1
+ assert_equal 1, @after_result_2
+ end
+
+ def test_should_fail_if_any_before_yield_halted
+ @machine.around_transition { |block| block.call }
+ @machine.around_transition { throw :halt }
+
+ assert_equal false, @transition.run_callbacks
+ end
+
+ def test_should_not_continue_around_callbacks_if_before_yield_halted
+ @callbacks = []
+ @machine.around_transition { @callbacks << 1; throw :halt }
+ @machine.around_transition { |block| @callbacks << 2; block.call; @callbacks << 3 }
+
+ assert_equal false, @transition.run_callbacks
+ assert_equal [1], @callbacks
+ end
+
+ def test_should_not_continue_around_callbacks_if_later_before_yield_halted
+ @callbacks = []
+ @machine.around_transition { |block| block.call; @callbacks << 1 }
+ @machine.around_transition { throw :halt }
+
+ @transition.run_callbacks
+ assert_equal [], @callbacks
+ end
+
+ def test_should_not_run_further_callbacks_if_after_yield_halted
+ @callbacks = []
+ @machine.around_transition { |block| block.call; @callbacks << 1 }
+ @machine.around_transition { |block| block.call; throw :halt }
+
+ assert_equal true, @transition.run_callbacks
+ assert_equal [], @callbacks
+ end
+
+ def test_should_fail_if_any_fail_to_yield
+ @callbacks = []
+ @machine.around_transition { @callbacks << 1 }
+ @machine.around_transition { |block| @callbacks << 2; block.call; @callbacks << 3 }
+
+ assert_equal false, @transition.run_callbacks
+ assert_equal [1], @callbacks
+ end
+end
diff --git a/test/unit/transition/transition_with_multiple_before_callbacks_test.rb b/test/unit/transition/transition_with_multiple_before_callbacks_test.rb
new file mode 100644
index 0000000..8b8d098
--- /dev/null
+++ b/test/unit/transition/transition_with_multiple_before_callbacks_test.rb
@@ -0,0 +1,40 @@
+require_relative '../../test_helper'
+
+class TransitionWithMultipleBeforeCallbacksTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked, :idling
+ @machine.event :ignite
+
+ @object = @klass.new
+ @object.state = 'parked'
+ @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ end
+
+ def test_should_run_in_the_order_they_were_defined
+ @callbacks = []
+ @machine.before_transition { @callbacks << 1 }
+ @machine.before_transition { @callbacks << 2 }
+ @transition.run_callbacks
+
+ assert_equal [1, 2], @callbacks
+ end
+
+ def test_should_not_run_further_callbacks_if_halted
+ @callbacks = []
+ @machine.before_transition { @callbacks << 1; throw :halt }
+ @machine.before_transition { @callbacks << 2 }
+
+ assert_equal false, @transition.run_callbacks
+ assert_equal [1], @callbacks
+ end
+
+ def test_should_fail_if_any_callback_halted
+ @machine.before_transition { true }
+ @machine.before_transition { throw :halt }
+
+ assert_equal false, @transition.run_callbacks
+ end
+end
diff --git a/test/unit/transition/transition_with_multiple_failure_callbacks_test.rb b/test/unit/transition/transition_with_multiple_failure_callbacks_test.rb
new file mode 100644
index 0000000..272f878
--- /dev/null
+++ b/test/unit/transition/transition_with_multiple_failure_callbacks_test.rb
@@ -0,0 +1,40 @@
+require_relative '../../test_helper'
+
+class TransitionWithMultipleFailureCallbacksTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked, :idling
+ @machine.event :ignite
+
+ @object = @klass.new
+ @object.state = 'parked'
+ @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ end
+
+ def test_should_run_in_the_order_they_were_defined
+ @callbacks = []
+ @machine.after_failure { @callbacks << 1 }
+ @machine.after_failure { @callbacks << 2 }
+ @transition.run_callbacks { { success: false } }
+
+ assert_equal [1, 2], @callbacks
+ end
+
+ def test_should_not_run_further_callbacks_if_halted
+ @callbacks = []
+ @machine.after_failure { @callbacks << 1; throw :halt }
+ @machine.after_failure { @callbacks << 2 }
+
+ assert_equal true, @transition.run_callbacks { { success: false } }
+ assert_equal [1], @callbacks
+ end
+
+ def test_should_fail_if_any_callback_halted
+ @machine.after_failure { true }
+ @machine.after_failure { throw :halt }
+
+ assert_equal true, @transition.run_callbacks { { success: false } }
+ end
+end
diff --git a/test/unit/transition/transition_with_namespace_test.rb b/test/unit/transition/transition_with_namespace_test.rb
new file mode 100644
index 0000000..f4e6b33
--- /dev/null
+++ b/test/unit/transition/transition_with_namespace_test.rb
@@ -0,0 +1,47 @@
+require_relative '../../test_helper'
+
+class TransitionWithNamespaceTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass, namespace: 'alarm')
+ @machine.state :off, :active
+ @machine.event :activate
+
+ @object = @klass.new
+ @object.state = 'off'
+
+ @transition = StateMachines::Transition.new(@object, @machine, :activate, :off, :active)
+ end
+
+ def test_should_have_an_event
+ assert_equal :activate, @transition.event
+ end
+
+ def test_should_have_a_qualified_event
+ assert_equal :activate_alarm, @transition.qualified_event
+ end
+
+ def test_should_have_a_from_name
+ assert_equal :off, @transition.from_name
+ end
+
+ def test_should_have_a_qualified_from_name
+ assert_equal :alarm_off, @transition.qualified_from_name
+ end
+
+ def test_should_have_a_human_from_name
+ assert_equal 'off', @transition.human_from_name
+ end
+
+ def test_should_have_a_to_name
+ assert_equal :active, @transition.to_name
+ end
+
+ def test_should_have_a_qualified_to_name
+ assert_equal :alarm_active, @transition.qualified_to_name
+ end
+
+ def test_should_have_a_human_to_name
+ assert_equal 'active', @transition.human_to_name
+ end
+end
diff --git a/test/unit/transition/transition_with_perform_arguments_test.rb b/test/unit/transition/transition_with_perform_arguments_test.rb
new file mode 100644
index 0000000..fee4ffa
--- /dev/null
+++ b/test/unit/transition/transition_with_perform_arguments_test.rb
@@ -0,0 +1,35 @@
+require_relative '../../test_helper'
+
+class TransitionWithPerformArgumentsTest < StateMachinesTest
+ def setup
+ @klass = Class.new do
+ attr_reader :saved
+
+ def save
+ @saved = true
+ end
+ end
+
+ @machine = StateMachines::Machine.new(@klass, action: :save)
+ @machine.state :parked, :idling
+ @machine.event :ignite
+
+ @object = @klass.new
+ @object.state = 'parked'
+ @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ end
+
+ def test_should_have_arguments
+ @transition.perform(1, 2)
+
+ assert_equal [1, 2], @transition.args
+ assert @object.saved
+ end
+
+ def test_should_not_include_run_action_in_arguments
+ @transition.perform(1, 2, false)
+
+ assert_equal [1, 2], @transition.args
+ refute @object.saved
+ end
+end
diff --git a/test/unit/transition/transition_with_transactions_test.rb b/test/unit/transition/transition_with_transactions_test.rb
new file mode 100644
index 0000000..95df3f8
--- /dev/null
+++ b/test/unit/transition/transition_with_transactions_test.rb
@@ -0,0 +1,42 @@
+require_relative '../../test_helper'
+
+class TransitionWithTransactionsTest < StateMachinesTest
+ def setup
+ @klass = Class.new do
+ class << self
+ attr_accessor :running_transaction
+ end
+
+ attr_accessor :result
+
+ def save
+ @result = self.class.running_transaction
+ true
+ end
+ end
+
+ @machine = StateMachines::Machine.new(@klass, action: :save)
+ @machine.state :parked, :idling
+ @machine.event :ignite
+
+ @object = @klass.new
+ @object.state = 'parked'
+ @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+
+ class << @machine
+ def within_transaction(object)
+ owner_class.running_transaction = object
+ yield
+ owner_class.running_transaction = false
+ end
+ end
+ end
+
+ def test_should_run_blocks_within_transaction_for_object
+ @transition.within_transaction do
+ @result = @klass.running_transaction
+ end
+
+ assert_equal @object, @result
+ end
+end
diff --git a/test/unit/transition/transition_without_callbacks_test.rb b/test/unit/transition/transition_without_callbacks_test.rb
new file mode 100644
index 0000000..7d7306f
--- /dev/null
+++ b/test/unit/transition/transition_without_callbacks_test.rb
@@ -0,0 +1,33 @@
+require_relative '../../test_helper'
+
+class TransitionWithoutCallbacksTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked, :idling
+ @machine.event :ignite
+
+ @object = @klass.new
+ @object.state = 'parked'
+ @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ end
+
+ def test_should_succeed
+ assert_equal true, @transition.run_callbacks
+ end
+
+ def test_should_succeed_if_after_callbacks_skipped
+ assert_equal true, @transition.run_callbacks(after: false)
+ end
+
+ def test_should_call_block_if_provided
+ @transition.run_callbacks { @ran_block = true; {} }
+ assert @ran_block
+ end
+
+ def test_should_track_block_result
+ @transition.run_callbacks { { result: 1 } }
+ assert_equal 1, @transition.result
+ end
+end
diff --git a/test/unit/transition/transition_without_reading_state_test.rb b/test/unit/transition/transition_without_reading_state_test.rb
new file mode 100644
index 0000000..98f68d2
--- /dev/null
+++ b/test/unit/transition/transition_without_reading_state_test.rb
@@ -0,0 +1,22 @@
+require_relative '../../test_helper'
+
+class TransitionWithoutReadingStateTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ @machine = StateMachines::Machine.new(@klass)
+ @machine.state :parked, :idling
+ @machine.event :ignite
+
+ @object = @klass.new
+ @object.state = 'idling'
+ @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling, false)
+ end
+
+ def test_should_not_read_from_value_from_object
+ assert_equal 'parked', @transition.from
+ end
+
+ def test_should_have_to_value
+ assert_equal 'idling', @transition.to
+ end
+end
diff --git a/test/unit/transition/transition_without_running_action_test.rb b/test/unit/transition/transition_without_running_action_test.rb
new file mode 100644
index 0000000..77bae96
--- /dev/null
+++ b/test/unit/transition/transition_without_running_action_test.rb
@@ -0,0 +1,47 @@
+require_relative '../../test_helper'
+
+class TransitionWithoutRunningActionTest < StateMachinesTest
+ def setup
+ @klass = Class.new do
+ attr_reader :saved
+
+ def save
+ @saved = true
+ end
+ end
+
+ @machine = StateMachines::Machine.new(@klass, action: :save)
+ @machine.state :parked, :idling
+ @machine.event :ignite
+ @machine.after_transition { |_object| @run_after = true }
+
+ @object = @klass.new
+ @object.state = 'parked'
+ @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ @result = @transition.perform(false)
+ end
+
+ def test_should_have_empty_args
+ assert_equal [], @transition.args
+ end
+
+ def test_should_not_have_a_result
+ assert_nil @transition.result
+ end
+
+ def test_should_be_successful
+ assert_equal true, @result
+ end
+
+ def test_should_change_the_current_state
+ assert_equal 'idling', @object.state
+ end
+
+ def test_should_not_run_the_action
+ refute @object.saved
+ end
+
+ def test_should_run_after_callbacks
+ assert @run_after
+ end
+end
diff --git a/test/unit/transition_collection/attribute_transition_collection_by_default_test.rb b/test/unit/transition_collection/attribute_transition_collection_by_default_test.rb
new file mode 100644
index 0000000..a22b6fc
--- /dev/null
+++ b/test/unit/transition_collection/attribute_transition_collection_by_default_test.rb
@@ -0,0 +1,23 @@
+require_relative '../../test_helper'
+
+class AttributeTransitionCollectionByDefaultTest < StateMachinesTest
+ def setup
+ @transitions = StateMachines::AttributeTransitionCollection.new
+ end
+
+ def test_should_skip_actions
+ assert @transitions.skip_actions
+ end
+
+ def test_should_not_skip_after
+ refute @transitions.skip_after
+ end
+
+ def test_should_not_use_transaction
+ refute @transitions.use_transactions
+ end
+
+ def test_should_be_empty
+ assert @transitions.empty?
+ end
+end
diff --git a/test/unit/transition_collection/attribute_transition_collection_marshalling_test.rb b/test/unit/transition_collection/attribute_transition_collection_marshalling_test.rb
new file mode 100644
index 0000000..846732a
--- /dev/null
+++ b/test/unit/transition_collection/attribute_transition_collection_marshalling_test.rb
@@ -0,0 +1,64 @@
+require_relative '../../test_helper'
+
+class AttributeTransitionCollectionMarshallingTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+ self.class.const_set('Example', @klass)
+
+ @machine = StateMachines::Machine.new(@klass, initial: :parked, action: :save)
+ @machine.state :idling
+ @machine.event :ignite
+
+ @object = @klass.new
+ @object.state_event = 'ignite'
+ end
+
+ def test_should_marshal_during_before_callbacks
+ @machine.before_transition { |object, _transition| Marshal.dump(object) }
+ transitions(after: false).perform { true }
+ transitions.perform { true }
+ end
+
+ def test_should_marshal_during_action
+ transitions(after: false).perform do
+ Marshal.dump(@object)
+ true
+ end
+
+ transitions.perform do
+ Marshal.dump(@object)
+ true
+ end
+ end
+
+ def test_should_marshal_during_after_callbacks
+ @machine.after_transition { |object, _transition| Marshal.dump(object) }
+ transitions(after: false).perform { true }
+ transitions.perform { true }
+ end
+
+ if StateMachines::Transition.pause_supported?
+ def test_should_marshal_during_around_callbacks_before_yield
+ @machine.around_transition { |object, _transition, block| Marshal.dump(object); block.call }
+ transitions(after: false).perform { true }
+ transitions.perform { true }
+ end
+
+ def test_should_marshal_during_around_callbacks_after_yield
+ @machine.around_transition { |object, _transition, block| block.call; Marshal.dump(object) }
+ transitions(after: false).perform { true }
+ transitions.perform { true }
+ end
+ end
+
+ def teardown
+ self.class.send(:remove_const, 'Example')
+ end
+
+ private
+ def transitions(options = {})
+ StateMachines::AttributeTransitionCollection.new([
+ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ ], options)
+ end
+end
diff --git a/test/unit/transition_collection/attribute_transition_collection_with_action_error_test.rb b/test/unit/transition_collection/attribute_transition_collection_with_action_error_test.rb
new file mode 100644
index 0000000..0439792
--- /dev/null
+++ b/test/unit/transition_collection/attribute_transition_collection_with_action_error_test.rb
@@ -0,0 +1,44 @@
+require_relative '../../test_helper'
+
+class AttributeTransitionCollectionWithActionErrorTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+
+ @state = StateMachines::Machine.new(@klass, initial: :parked, action: :save)
+ @state.state :idling
+ @state.event :ignite
+
+ @status = StateMachines::Machine.new(@klass, :status, initial: :first_gear, action: :save)
+ @status.state :second_gear
+ @status.event :shift_up
+
+ @object = @klass.new
+ @object.state_event = 'ignite'
+ @object.status_event = 'shift_up'
+
+ @transitions = StateMachines::AttributeTransitionCollection.new([
+ @state_transition = StateMachines::Transition.new(@object, @state, :ignite, :parked, :idling),
+ @status_transition = StateMachines::Transition.new(@object, @status, :shift_up, :first_gear, :second_gear)
+ ])
+
+ begin
+ ; @transitions.perform { fail ArgumentError }
+ rescue
+ end
+ end
+
+ def test_should_not_persist_states
+ assert_equal 'parked', @object.state
+ assert_equal 'first_gear', @object.status
+ end
+
+ def test_should_not_clear_events
+ assert_equal :ignite, @object.state_event
+ assert_equal :shift_up, @object.status_event
+ end
+
+ def test_should_not_write_event_transitions
+ assert_nil @object.send(:state_event_transition)
+ assert_nil @object.send(:status_event_transition)
+ end
+end
diff --git a/test/unit/transition_collection/attribute_transition_collection_with_action_failed_test.rb b/test/unit/transition_collection/attribute_transition_collection_with_action_failed_test.rb
new file mode 100644
index 0000000..238a49c
--- /dev/null
+++ b/test/unit/transition_collection/attribute_transition_collection_with_action_failed_test.rb
@@ -0,0 +1,44 @@
+require_relative '../../test_helper'
+
+class AttributeTransitionCollectionWithActionFailedTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+
+ @state = StateMachines::Machine.new(@klass, initial: :parked, action: :save)
+ @state.state :idling
+ @state.event :ignite
+
+ @status = StateMachines::Machine.new(@klass, :status, initial: :first_gear, action: :save)
+ @status.state :second_gear
+ @status.event :shift_up
+
+ @object = @klass.new
+ @object.state_event = 'ignite'
+ @object.status_event = 'shift_up'
+
+ @transitions = StateMachines::AttributeTransitionCollection.new([
+ @state_transition = StateMachines::Transition.new(@object, @state, :ignite, :parked, :idling),
+ @status_transition = StateMachines::Transition.new(@object, @status, :shift_up, :first_gear, :second_gear)
+ ])
+ @result = @transitions.perform { false }
+ end
+
+ def test_should_not_succeed
+ assert_equal false, @result
+ end
+
+ def test_should_not_persist_states
+ assert_equal 'parked', @object.state
+ assert_equal 'first_gear', @object.status
+ end
+
+ def test_should_not_clear_events
+ assert_equal :ignite, @object.state_event
+ assert_equal :shift_up, @object.status_event
+ end
+
+ def test_should_not_write_event_transitions
+ assert_nil @object.send(:state_event_transition)
+ assert_nil @object.send(:status_event_transition)
+ end
+end
diff --git a/test/unit/transition_collection/attribute_transition_collection_with_after_callback_error_test.rb b/test/unit/transition_collection/attribute_transition_collection_with_after_callback_error_test.rb
new file mode 100644
index 0000000..eef418d
--- /dev/null
+++ b/test/unit/transition_collection/attribute_transition_collection_with_after_callback_error_test.rb
@@ -0,0 +1,32 @@
+require_relative '../../test_helper'
+
+class AttributeTransitionCollectionWithBeforeCallbackErrorTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+
+ @machine = StateMachines::Machine.new(@klass, initial: :parked, action: :save)
+ @machine.state :idling
+ @machine.event :ignite
+
+ @machine.before_transition { fail ArgumentError }
+
+ @object = @klass.new
+ @object.state_event = 'ignite'
+
+ @transitions = StateMachines::AttributeTransitionCollection.new([
+ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ ])
+ begin
+ ; @transitions.perform
+ rescue
+ end
+ end
+
+ def test_should_not_clear_event
+ assert_equal :ignite, @object.state_event
+ end
+
+ def test_should_not_write_event_transition
+ assert_nil @object.send(:state_event_transition)
+ end
+end
diff --git a/test/unit/transition_collection/attribute_transition_collection_with_after_callback_halt_test.rb b/test/unit/transition_collection/attribute_transition_collection_with_after_callback_halt_test.rb
new file mode 100644
index 0000000..a22cb4e
--- /dev/null
+++ b/test/unit/transition_collection/attribute_transition_collection_with_after_callback_halt_test.rb
@@ -0,0 +1,33 @@
+require_relative '../../test_helper'
+
+class AttributeTransitionCollectionWithBeforeCallbackHaltTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+
+ @machine = StateMachines::Machine.new(@klass, initial: :parked, action: :save)
+ @machine.state :idling
+ @machine.event :ignite
+
+ @machine.before_transition { throw :halt }
+
+ @object = @klass.new
+ @object.state_event = 'ignite'
+
+ @transitions = StateMachines::AttributeTransitionCollection.new([
+ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ ])
+ @result = @transitions.perform
+ end
+
+ def test_should_not_succeed
+ assert_equal false, @result
+ end
+
+ def test_should_not_clear_event
+ assert_equal :ignite, @object.state_event
+ end
+
+ def test_should_not_write_event_transition
+ assert_nil @object.send(:state_event_transition)
+ end
+end
diff --git a/test/unit/transition_collection/attribute_transition_collection_with_around_after_yield_callback_error_test.rb b/test/unit/transition_collection/attribute_transition_collection_with_around_after_yield_callback_error_test.rb
new file mode 100644
index 0000000..c1cb5bd
--- /dev/null
+++ b/test/unit/transition_collection/attribute_transition_collection_with_around_after_yield_callback_error_test.rb
@@ -0,0 +1,32 @@
+require_relative '../../test_helper'
+
+class AttributeTransitionCollectionWithAroundAfterYieldCallbackErrorTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+
+ @machine = StateMachines::Machine.new(@klass, initial: :parked, action: :save)
+ @machine.state :idling
+ @machine.event :ignite
+
+ @machine.before_transition { fail ArgumentError }
+
+ @object = @klass.new
+ @object.state_event = 'ignite'
+
+ @transitions = StateMachines::AttributeTransitionCollection.new([
+ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ ])
+ begin
+ ; @transitions.perform
+ rescue
+ end
+ end
+
+ def test_should_not_clear_event
+ assert_equal :ignite, @object.state_event
+ end
+
+ def test_should_not_write_event_transition
+ assert_nil @object.send(:state_event_transition)
+ end
+end
diff --git a/test/unit/transition_collection/attribute_transition_collection_with_around_callback_after_yield_error_test.rb b/test/unit/transition_collection/attribute_transition_collection_with_around_callback_after_yield_error_test.rb
new file mode 100644
index 0000000..a27f38a
--- /dev/null
+++ b/test/unit/transition_collection/attribute_transition_collection_with_around_callback_after_yield_error_test.rb
@@ -0,0 +1,32 @@
+require_relative '../../test_helper'
+
+class AttributeTransitionCollectionWithAroundCallbackAfterYieldErrorTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+
+ @machine = StateMachines::Machine.new(@klass, initial: :parked, action: :save)
+ @machine.state :idling
+ @machine.event :ignite
+
+ @machine.around_transition { |block| block.call; fail ArgumentError }
+
+ @object = @klass.new
+ @object.state_event = 'ignite'
+
+ @transitions = StateMachines::AttributeTransitionCollection.new([
+ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ ])
+ begin
+ ; @transitions.perform
+ rescue
+ end
+ end
+
+ def test_should_clear_event
+ assert_nil @object.state_event
+ end
+
+ def test_should_not_write_event_transition
+ assert_nil @object.send(:state_event_transition)
+ end
+end
diff --git a/test/unit/transition_collection/attribute_transition_collection_with_around_callback_after_yield_halt_test.rb b/test/unit/transition_collection/attribute_transition_collection_with_around_callback_after_yield_halt_test.rb
new file mode 100644
index 0000000..6dcc4ba
--- /dev/null
+++ b/test/unit/transition_collection/attribute_transition_collection_with_around_callback_after_yield_halt_test.rb
@@ -0,0 +1,33 @@
+require_relative '../../test_helper'
+
+class AttributeTransitionCollectionWithAroundCallbackAfterYieldHaltTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+
+ @machine = StateMachines::Machine.new(@klass, initial: :parked, action: :save)
+ @machine.state :idling
+ @machine.event :ignite
+
+ @machine.around_transition { |block| block.call; throw :halt }
+
+ @object = @klass.new
+ @object.state_event = 'ignite'
+
+ @transitions = StateMachines::AttributeTransitionCollection.new([
+ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ ])
+ @result = @transitions.perform
+ end
+
+ def test_should_succeed
+ assert_equal true, @result
+ end
+
+ def test_should_clear_event
+ assert_nil @object.state_event
+ end
+
+ def test_should_not_write_event_transition
+ assert_nil @object.send(:state_event_transition)
+ end
+end
diff --git a/test/unit/transition_collection/attribute_transition_collection_with_around_callback_before_yield_halt_test.rb b/test/unit/transition_collection/attribute_transition_collection_with_around_callback_before_yield_halt_test.rb
new file mode 100644
index 0000000..416e551
--- /dev/null
+++ b/test/unit/transition_collection/attribute_transition_collection_with_around_callback_before_yield_halt_test.rb
@@ -0,0 +1,33 @@
+require_relative '../../test_helper'
+
+class AttributeTransitionCollectionWithAroundCallbackBeforeYieldHaltTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+
+ @machine = StateMachines::Machine.new(@klass, initial: :parked, action: :save)
+ @machine.state :idling
+ @machine.event :ignite
+
+ @machine.around_transition { throw :halt }
+
+ @object = @klass.new
+ @object.state_event = 'ignite'
+
+ @transitions = StateMachines::AttributeTransitionCollection.new([
+ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ ])
+ @result = @transitions.perform
+ end
+
+ def test_should_not_succeed
+ assert_equal false, @result
+ end
+
+ def test_should_not_clear_event
+ assert_equal :ignite, @object.state_event
+ end
+
+ def test_should_not_write_event_transition
+ assert_nil @object.send(:state_event_transition)
+ end
+end
diff --git a/test/unit/transition_collection/attribute_transition_collection_with_before_callback_error_test.rb b/test/unit/transition_collection/attribute_transition_collection_with_before_callback_error_test.rb
new file mode 100644
index 0000000..22dca8c
--- /dev/null
+++ b/test/unit/transition_collection/attribute_transition_collection_with_before_callback_error_test.rb
@@ -0,0 +1,32 @@
+require_relative '../../test_helper'
+
+class AttributeTransitionCollectionWithAfterCallbackErrorTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+
+ @machine = StateMachines::Machine.new(@klass, initial: :parked, action: :save)
+ @machine.state :idling
+ @machine.event :ignite
+
+ @machine.after_transition { fail ArgumentError }
+
+ @object = @klass.new
+ @object.state_event = 'ignite'
+
+ @transitions = StateMachines::AttributeTransitionCollection.new([
+ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ ])
+ begin
+ ; @transitions.perform
+ rescue
+ end
+ end
+
+ def test_should_clear_event
+ assert_nil @object.state_event
+ end
+
+ def test_should_not_write_event_transition
+ assert_nil @object.send(:state_event_transition)
+ end
+end
diff --git a/test/unit/transition_collection/attribute_transition_collection_with_before_callback_halt_test.rb b/test/unit/transition_collection/attribute_transition_collection_with_before_callback_halt_test.rb
new file mode 100644
index 0000000..aa034a7
--- /dev/null
+++ b/test/unit/transition_collection/attribute_transition_collection_with_before_callback_halt_test.rb
@@ -0,0 +1,33 @@
+require_relative '../../test_helper'
+
+class AttributeTransitionCollectionWithAfterCallbackHaltTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+
+ @machine = StateMachines::Machine.new(@klass, initial: :parked, action: :save)
+ @machine.state :idling
+ @machine.event :ignite
+
+ @machine.after_transition { throw :halt }
+
+ @object = @klass.new
+ @object.state_event = 'ignite'
+
+ @transitions = StateMachines::AttributeTransitionCollection.new([
+ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ ])
+ @result = @transitions.perform
+ end
+
+ def test_should_succeed
+ assert_equal true, @result
+ end
+
+ def test_should_clear_event
+ assert_nil @object.state_event
+ end
+
+ def test_should_not_write_event_transition
+ assert_nil @object.send(:state_event_transition)
+ end
+end
diff --git a/test/unit/transition_collection/attribute_transition_collection_with_callbacks_test.rb b/test/unit/transition_collection/attribute_transition_collection_with_callbacks_test.rb
new file mode 100644
index 0000000..7728c2e
--- /dev/null
+++ b/test/unit/transition_collection/attribute_transition_collection_with_callbacks_test.rb
@@ -0,0 +1,68 @@
+require_relative '../../test_helper'
+
+class AttributeTransitionCollectionWithCallbacksTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+
+ @state = StateMachines::Machine.new(@klass, initial: :parked, action: :save)
+ @state.state :idling
+ @state.event :ignite
+
+ @status = StateMachines::Machine.new(@klass, :status, initial: :first_gear, action: :save)
+ @status.state :second_gear
+ @status.event :shift_up
+
+ @object = @klass.new
+
+ @transitions = StateMachines::AttributeTransitionCollection.new([
+ @state_transition = StateMachines::Transition.new(@object, @state, :ignite, :parked, :idling),
+ @status_transition = StateMachines::Transition.new(@object, @status, :shift_up, :first_gear, :second_gear)
+ ])
+ end
+
+ def test_should_not_have_events_during_before_callbacks
+ @state.before_transition { |object, _transition| @before_state_event = object.state_event }
+ @state.around_transition { |object, _transition, block| @around_state_event = object.state_event; block.call }
+ @transitions.perform
+
+ assert_nil @before_state_event
+ assert_nil @around_state_event
+ end
+
+ def test_should_not_have_events_during_action
+ @transitions.perform { @state_event = @object.state_event }
+
+ assert_nil @state_event
+ end
+
+ def test_should_not_have_events_during_after_callbacks
+ @state.after_transition { |object, _transition| @after_state_event = object.state_event }
+ @state.around_transition { |object, _transition, block| block.call; @around_state_event = object.state_event }
+ @transitions.perform
+
+ assert_nil @after_state_event
+ assert_nil @around_state_event
+ end
+
+ def test_should_not_have_event_transitions_during_before_callbacks
+ @state.before_transition { |object, _transition| @state_event_transition = object.send(:state_event_transition) }
+ @transitions.perform
+
+ assert_nil @state_event_transition
+ end
+
+ def test_should_not_have_event_transitions_during_action
+ @transitions.perform { @state_event_transition = @object.send(:state_event_transition) }
+
+ assert_nil @state_event_transition
+ end
+
+ def test_should_not_have_event_transitions_during_after_callbacks
+ @state.after_transition { |object, _transition| @after_state_event_transition = object.send(:state_event_transition) }
+ @state.around_transition { |object, _transition, block| block.call; @around_state_event_transition = object.send(:state_event_transition) }
+ @transitions.perform
+
+ assert_nil @after_state_event_transition
+ assert_nil @around_state_event_transition
+ end
+end
diff --git a/test/unit/transition_collection/attribute_transition_collection_with_event_transitions_test.rb b/test/unit/transition_collection/attribute_transition_collection_with_event_transitions_test.rb
new file mode 100644
index 0000000..90bead4
--- /dev/null
+++ b/test/unit/transition_collection/attribute_transition_collection_with_event_transitions_test.rb
@@ -0,0 +1,41 @@
+require_relative '../../test_helper'
+
+class AttributeTransitionCollectionWithEventTransitionsTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+
+ @state = StateMachines::Machine.new(@klass, initial: :parked, action: :save)
+ @state.state :idling
+ @state.event :ignite
+
+ @status = StateMachines::Machine.new(@klass, :status, initial: :first_gear, action: :save)
+ @status.state :second_gear
+ @status.event :shift_up
+
+ @object = @klass.new
+ @object.send(:state_event_transition=, @state_transition = StateMachines::Transition.new(@object, @state, :ignite, :parked, :idling))
+ @object.send(:status_event_transition=, @status_transition = StateMachines::Transition.new(@object, @status, :shift_up, :first_gear, :second_gear))
+
+ @transitions = StateMachines::AttributeTransitionCollection.new([@state_transition, @status_transition])
+ @result = @transitions.perform
+ end
+
+ def test_should_succeed
+ assert_equal true, @result
+ end
+
+ def test_should_persist_states
+ assert_equal 'idling', @object.state
+ assert_equal 'second_gear', @object.status
+ end
+
+ def test_should_not_write_events
+ assert_nil @object.state_event
+ assert_nil @object.status_event
+ end
+
+ def test_should_clear_event_transitions
+ assert_nil @object.send(:state_event_transition)
+ assert_nil @object.send(:status_event_transition)
+ end
+end
diff --git a/test/unit/transition_collection/attribute_transition_collection_with_events_test.rb b/test/unit/transition_collection/attribute_transition_collection_with_events_test.rb
new file mode 100644
index 0000000..8068afd
--- /dev/null
+++ b/test/unit/transition_collection/attribute_transition_collection_with_events_test.rb
@@ -0,0 +1,44 @@
+require_relative '../../test_helper'
+
+class AttributeTransitionCollectionWithEventsTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+
+ @state = StateMachines::Machine.new(@klass, initial: :parked, action: :save)
+ @state.state :idling
+ @state.event :ignite
+
+ @status = StateMachines::Machine.new(@klass, :status, initial: :first_gear, action: :save)
+ @status.state :second_gear
+ @status.event :shift_up
+
+ @object = @klass.new
+ @object.state_event = 'ignite'
+ @object.status_event = 'shift_up'
+
+ @transitions = StateMachines::AttributeTransitionCollection.new([
+ @state_transition = StateMachines::Transition.new(@object, @state, :ignite, :parked, :idling),
+ @status_transition = StateMachines::Transition.new(@object, @status, :shift_up, :first_gear, :second_gear)
+ ])
+ @result = @transitions.perform
+ end
+
+ def test_should_succeed
+ assert_equal true, @result
+ end
+
+ def test_should_persist_states
+ assert_equal 'idling', @object.state
+ assert_equal 'second_gear', @object.status
+ end
+
+ def test_should_clear_events
+ assert_nil @object.state_event
+ assert_nil @object.status_event
+ end
+
+ def test_should_not_write_event_transitions
+ assert_nil @object.send(:state_event_transition)
+ assert_nil @object.send(:status_event_transition)
+ end
+end
diff --git a/test/unit/transition_collection/attribute_transition_collection_with_skipped_after_callbacks_test.rb b/test/unit/transition_collection/attribute_transition_collection_with_skipped_after_callbacks_test.rb
new file mode 100644
index 0000000..0ac5178
--- /dev/null
+++ b/test/unit/transition_collection/attribute_transition_collection_with_skipped_after_callbacks_test.rb
@@ -0,0 +1,42 @@
+require_relative '../../test_helper'
+
+class AttributeTransitionCollectionWithSkippedAfterCallbacksTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+
+ @state = StateMachines::Machine.new(@klass, initial: :parked, action: :save)
+ @state.state :idling
+ @state.event :ignite
+
+ @status = StateMachines::Machine.new(@klass, :status, initial: :first_gear, action: :save)
+ @status.state :second_gear
+ @status.event :shift_up
+
+ @object = @klass.new
+ @object.state_event = 'ignite'
+ @object.status_event = 'shift_up'
+
+ @transitions = StateMachines::AttributeTransitionCollection.new([
+ @state_transition = StateMachines::Transition.new(@object, @state, :ignite, :parked, :idling),
+ @status_transition = StateMachines::Transition.new(@object, @status, :shift_up, :first_gear, :second_gear)
+ ], after: false)
+ end
+
+ def test_should_clear_events
+ @transitions.perform
+ assert_nil @object.state_event
+ assert_nil @object.status_event
+ end
+
+ def test_should_write_event_transitions_if_success
+ @transitions.perform { true }
+ assert_equal @state_transition, @object.send(:state_event_transition)
+ assert_equal @status_transition, @object.send(:status_event_transition)
+ end
+
+ def test_should_not_write_event_transitions_if_failed
+ @transitions.perform { false }
+ assert_nil @object.send(:state_event_transition)
+ assert_nil @object.send(:status_event_transition)
+ end
+end
diff --git a/test/unit/transition_collection/transition_collection_by_default_test.rb b/test/unit/transition_collection/transition_collection_by_default_test.rb
new file mode 100644
index 0000000..5fb69bd
--- /dev/null
+++ b/test/unit/transition_collection/transition_collection_by_default_test.rb
@@ -0,0 +1,23 @@
+require_relative '../../test_helper'
+
+class TransitionCollectionByDefaultTest < StateMachinesTest
+ def setup
+ @transitions = StateMachines::TransitionCollection.new
+ end
+
+ def test_should_not_skip_actions
+ refute @transitions.skip_actions
+ end
+
+ def test_should_not_skip_after
+ refute @transitions.skip_after
+ end
+
+ def test_should_use_transaction
+ assert @transitions.use_transactions
+ end
+
+ def test_should_be_empty
+ assert @transitions.empty?
+ end
+end
diff --git a/test/unit/transition_collection/transition_collection_empty_with_block_test.rb b/test/unit/transition_collection/transition_collection_empty_with_block_test.rb
new file mode 100644
index 0000000..6b2fc25
--- /dev/null
+++ b/test/unit/transition_collection/transition_collection_empty_with_block_test.rb
@@ -0,0 +1,23 @@
+require_relative '../../test_helper'
+
+class TransitionCollectionEmptyWithBlockTest < StateMachinesTest
+ def setup
+ @transitions = StateMachines::TransitionCollection.new
+ end
+
+ def test_should_raise_exception_if_perform_raises_exception
+ assert_raises(ArgumentError) { @transitions.perform { fail ArgumentError } }
+ end
+
+ def test_should_use_block_result_if_non_boolean
+ assert_equal 1, @transitions.perform { 1 }
+ end
+
+ def test_should_use_block_result_if_false
+ assert_equal false, @transitions.perform { false }
+ end
+
+ def test_should_use_block_reslut_if_nil
+ assert_equal nil, @transitions.perform { nil }
+ end
+end
diff --git a/test/unit/transition_collection/transition_collection_empty_without_block_test.rb b/test/unit/transition_collection/transition_collection_empty_without_block_test.rb
new file mode 100644
index 0000000..0467a9a
--- /dev/null
+++ b/test/unit/transition_collection/transition_collection_empty_without_block_test.rb
@@ -0,0 +1,12 @@
+require_relative '../../test_helper'
+
+class TransitionCollectionEmptyWithoutBlockTest < StateMachinesTest
+ def setup
+ @transitions = StateMachines::TransitionCollection.new
+ @result = @transitions.perform
+ end
+
+ def test_should_succeed
+ assert_equal true, @result
+ end
+end
diff --git a/test/unit/transition_collection/transition_collection_invalid_test.rb b/test/unit/transition_collection/transition_collection_invalid_test.rb
new file mode 100644
index 0000000..2cc27d3
--- /dev/null
+++ b/test/unit/transition_collection/transition_collection_invalid_test.rb
@@ -0,0 +1,21 @@
+require_relative '../../test_helper'
+
+class TransitionCollectionInvalidTest < StateMachinesTest
+ def setup
+ @transitions = StateMachines::TransitionCollection.new([false])
+ end
+
+ def test_should_be_empty
+ assert @transitions.empty?
+ end
+
+ def test_should_not_succeed
+ assert_equal false, @transitions.perform
+ end
+
+ def test_should_not_run_perform_block
+ ran_block = false
+ @transitions.perform { ran_block = true }
+ refute ran_block
+ end
+end
diff --git a/test/unit/transition_collection/transition_collection_partial_invalid_test.rb b/test/unit/transition_collection/transition_collection_partial_invalid_test.rb
new file mode 100644
index 0000000..83eaf49
--- /dev/null
+++ b/test/unit/transition_collection/transition_collection_partial_invalid_test.rb
@@ -0,0 +1,69 @@
+require_relative '../../test_helper'
+
+class TransitionCollectionPartialInvalidTest < StateMachinesTest
+ def setup
+ @klass = Class.new do
+ attr_accessor :ran_transaction
+ end
+
+ @callbacks = []
+
+ @machine = StateMachines::Machine.new(@klass, initial: :parked)
+ @machine.state :idling
+ @machine.event :ignite
+ @machine.before_transition { @callbacks << :before }
+ @machine.after_transition { @callbacks << :after }
+ @machine.around_transition { |block| @callbacks << :around_before; block.call; @callbacks << :around_after }
+
+ class << @machine
+ def within_transaction(object)
+ object.ran_transaction = true
+ end
+ end
+
+ @object = @klass.new
+
+ @transitions = StateMachines::TransitionCollection.new([
+ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling),
+ false
+ ])
+ end
+
+ def test_should_not_store_invalid_values
+ assert_equal 1, @transitions.length
+ end
+
+ def test_should_not_succeed
+ assert_equal false, @transitions.perform
+ end
+
+ def test_should_not_start_transaction
+ refute @object.ran_transaction
+ end
+
+ def test_should_not_run_perform_block
+ ran_block = false
+ @transitions.perform { ran_block = true }
+ refute ran_block
+ end
+
+ def test_should_not_run_before_callbacks
+ refute @callbacks.include?(:before)
+ end
+
+ def test_should_not_persist_states
+ assert_equal 'parked', @object.state
+ end
+
+ def test_should_not_run_after_callbacks
+ refute @callbacks.include?(:after)
+ end
+
+ def test_should_not_run_around_callbacks_before_yield
+ refute @callbacks.include?(:around_before)
+ end
+
+ def test_should_not_run_around_callbacks_after_yield
+ refute @callbacks.include?(:around_after)
+ end
+end
diff --git a/test/unit/transition_collection/transition_collection_test.rb b/test/unit/transition_collection/transition_collection_test.rb
new file mode 100644
index 0000000..29bad02
--- /dev/null
+++ b/test/unit/transition_collection/transition_collection_test.rb
@@ -0,0 +1,26 @@
+require_relative '../../test_helper'
+
+class TransitionCollectionTest < StateMachinesTest
+ def test_should_raise_exception_if_invalid_option_specified
+ exception = assert_raises(ArgumentError) { StateMachines::TransitionCollection.new([], invalid: true) }
+ assert_equal 'Unknown key: :invalid. Valid keys are: :actions, :after, :use_transactions', exception.message
+ end
+
+ def test_should_raise_exception_if_multiple_transitions_for_same_attribute_specified
+ @klass = Class.new
+
+ @machine = StateMachines::Machine.new(@klass, initial: :parked)
+ @machine.state :parked, :idling
+ @machine.event :ignite
+
+ @object = @klass.new
+
+ exception = assert_raises(ArgumentError) do
+ StateMachines::TransitionCollection.new([
+ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling),
+ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ ])
+ end
+ assert_equal 'Cannot perform multiple transitions in parallel for the same state machine attribute', exception.message
+ end
+end
diff --git a/test/unit/transition_collection/transition_collection_valid_test.rb b/test/unit/transition_collection/transition_collection_valid_test.rb
new file mode 100644
index 0000000..c127be8
--- /dev/null
+++ b/test/unit/transition_collection/transition_collection_valid_test.rb
@@ -0,0 +1,57 @@
+require_relative '../../test_helper'
+
+class TransitionCollectionValidTest < StateMachinesTest
+ def setup
+ @klass = Class.new do
+ attr_reader :persisted
+
+ def initialize
+ @persisted = nil
+ super
+ @persisted = []
+ end
+
+ def state=(value)
+ @persisted << 'state' if @persisted
+ @state = value
+ end
+
+ def status=(value)
+ @persisted << 'status' if @persisted
+ @status = value
+ end
+ end
+
+ @state = StateMachines::Machine.new(@klass, initial: :parked)
+ @state.state :idling
+ @state.event :ignite
+ @status = StateMachines::Machine.new(@klass, :status, initial: :first_gear)
+ @status.state :second_gear
+ @status.event :shift_up
+
+ @object = @klass.new
+
+ @result = StateMachines::TransitionCollection.new([
+ @state_transition = StateMachines::Transition.new(@object, @state, :ignite, :parked, :idling),
+ @status_transition = StateMachines::Transition.new(@object, @status, :shift_up, :first_gear, :second_gear)
+ ]).perform
+ end
+
+ def test_should_succeed
+ assert_equal true, @result
+ end
+
+ def test_should_persist_each_state
+ assert_equal 'idling', @object.state
+ assert_equal 'second_gear', @object.status
+ end
+
+ def test_should_persist_in_order
+ assert_equal %w(state status), @object.persisted
+ end
+
+ def test_should_store_results_in_transitions
+ assert_nil @state_transition.result
+ assert_nil @status_transition.result
+ end
+end
diff --git a/test/unit/transition_collection/transition_collection_with_action_error_test.rb b/test/unit/transition_collection/transition_collection_with_action_error_test.rb
new file mode 100644
index 0000000..516d5e0
--- /dev/null
+++ b/test/unit/transition_collection/transition_collection_with_action_error_test.rb
@@ -0,0 +1,66 @@
+require_relative '../../test_helper'
+
+class TransitionCollectionWithActionErrorTest < StateMachinesTest
+ def setup
+ @klass = Class.new do
+ def save
+ fail ArgumentError
+ end
+ end
+ @before_count = 0
+ @around_before_count = 0
+ @after_count = 0
+ @around_after_count = 0
+ @failure_count = 0
+
+ @machine = StateMachines::Machine.new(@klass, initial: :parked, action: :save)
+ @machine.state :idling
+ @machine.event :ignite
+
+ @machine.before_transition { @before_count += 1 }
+ @machine.after_transition { @after_count += 1 }
+ @machine.around_transition { |block| @around_before_count += 1; block.call; @around_after_count += 1 }
+ @machine.after_failure { @failure_count += 1 }
+
+ @object = @klass.new
+
+ @transitions = StateMachines::TransitionCollection.new([
+ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ ])
+
+ @raised = true
+ begin
+ @transitions.perform
+ @raised = false
+ rescue ArgumentError
+ end
+ end
+
+ def test_should_not_catch_exception
+ assert @raised
+ end
+
+ def test_should_not_persist_state
+ assert_equal 'parked', @object.state
+ end
+
+ def test_should_run_before_callbacks
+ assert_equal 1, @before_count
+ end
+
+ def test_should_run_around_callbacks_before_yield
+ assert_equal 1, @around_before_count
+ end
+
+ def test_should_not_run_after_callbacks
+ assert_equal 0, @after_count
+ end
+
+ def test_should_not_run_around_callbacks_after_yield
+ assert_equal 0, @around_after_count
+ end
+
+ def test_should_not_run_failure_callbacks
+ assert_equal 0, @failure_count
+ end
+end
diff --git a/test/unit/transition_collection/transition_collection_with_action_failed_test.rb b/test/unit/transition_collection/transition_collection_with_action_failed_test.rb
new file mode 100644
index 0000000..fe8f36b
--- /dev/null
+++ b/test/unit/transition_collection/transition_collection_with_action_failed_test.rb
@@ -0,0 +1,60 @@
+require_relative '../../test_helper'
+
+class TransitionCollectionWithActionFailedTest < StateMachinesTest
+ def setup
+ @klass = Class.new do
+ def save
+ false
+ end
+ end
+ @before_count = 0
+ @around_before_count = 0
+ @after_count = 0
+ @around_after_count = 0
+ @failure_count = 0
+
+ @machine = StateMachines::Machine.new(@klass, initial: :parked, action: :save)
+ @machine.state :idling
+ @machine.event :ignite
+
+ @machine.before_transition { @before_count += 1 }
+ @machine.after_transition { @after_count += 1 }
+ @machine.around_transition { |block| @around_before_count += 1; block.call; @around_after_count += 1 }
+ @machine.after_failure { @failure_count += 1 }
+
+ @object = @klass.new
+
+ @transitions = StateMachines::TransitionCollection.new([
+ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ ])
+ @result = @transitions.perform
+ end
+
+ def test_should_not_succeed
+ assert_equal false, @result
+ end
+
+ def test_should_not_persist_state
+ assert_equal 'parked', @object.state
+ end
+
+ def test_should_run_before_callbacks
+ assert_equal 1, @before_count
+ end
+
+ def test_should_run_around_callbacks_before_yield
+ assert_equal 1, @around_before_count
+ end
+
+ def test_should_not_run_after_callbacks
+ assert_equal 0, @after_count
+ end
+
+ def test_should_not_run_around_callbacks
+ assert_equal 0, @around_after_count
+ end
+
+ def test_should_run_failure_callbacks
+ assert_equal 1, @failure_count
+ end
+end
diff --git a/test/unit/transition_collection/transition_collection_with_action_hook_and_block_test.rb b/test/unit/transition_collection/transition_collection_with_action_hook_and_block_test.rb
new file mode 100644
index 0000000..c12014a
--- /dev/null
+++ b/test/unit/transition_collection/transition_collection_with_action_hook_and_block_test.rb
@@ -0,0 +1,17 @@
+require_relative '../../test_helper'
+require_relative 'transition_collection_with_action_hook_base_test'
+
+class TransitionCollectionWithActionHookAndBlockTest < TransitionCollectionWithActionHookBaseTest
+ def setup
+ super
+ @result = StateMachines::TransitionCollection.new([@transition]).perform { true }
+ end
+
+ def test_should_succeed
+ assert_equal true, @result
+ end
+
+ def test_should_not_run_action
+ refute @object.saved
+ end
+end
diff --git a/test/unit/transition_collection/transition_collection_with_action_hook_and_skipped_action_test.rb b/test/unit/transition_collection/transition_collection_with_action_hook_and_skipped_action_test.rb
new file mode 100644
index 0000000..3a8e690
--- /dev/null
+++ b/test/unit/transition_collection/transition_collection_with_action_hook_and_skipped_action_test.rb
@@ -0,0 +1,17 @@
+require_relative '../../test_helper'
+require_relative 'transition_collection_with_action_hook_base_test.rb'
+
+class TransitionCollectionWithActionHookAndSkippedActionTest < TransitionCollectionWithActionHookBaseTest
+ def setup
+ super
+ @result = StateMachines::TransitionCollection.new([@transition], actions: false).perform
+ end
+
+ def test_should_succeed
+ assert_equal true, @result
+ end
+
+ def test_should_not_run_action
+ refute @object.saved
+ end
+end
diff --git a/test/unit/transition_collection/transition_collection_with_action_hook_and_skipped_after_callbacks_test.rb b/test/unit/transition_collection/transition_collection_with_action_hook_and_skipped_after_callbacks_test.rb
new file mode 100644
index 0000000..2ba7bb2
--- /dev/null
+++ b/test/unit/transition_collection/transition_collection_with_action_hook_and_skipped_after_callbacks_test.rb
@@ -0,0 +1,37 @@
+require_relative '../../test_helper'
+require_relative 'transition_collection_with_action_hook_base_test.rb'
+
+class TransitionCollectionWithActionHookAndSkippedAfterCallbacksTest < TransitionCollectionWithActionHookBaseTest
+ def setup
+ super
+ @result = StateMachines::TransitionCollection.new([@transition], after: false).perform
+ end
+
+ def test_should_succeed
+ assert_equal true, @result
+ end
+
+ def test_should_run_action
+ assert @object.saved
+ end
+
+ def test_should_have_already_persisted_when_running_action
+ assert_equal 'idling', @object.state_on_save
+ end
+
+ def test_should_not_have_event_during_action
+ assert_nil @object.state_event_on_save
+ end
+
+ def test_should_not_write_event
+ assert_nil @object.state_event
+ end
+
+ def test_should_not_have_event_transition_during_save
+ assert_nil @object.state_event_transition_on_save
+ end
+
+ def test_should_not_write_event_attribute
+ assert_nil @object.send(:state_event_transition)
+ end
+end
diff --git a/test/unit/transition_collection/transition_collection_with_action_hook_base_test.rb b/test/unit/transition_collection/transition_collection_with_action_hook_base_test.rb
new file mode 100644
index 0000000..c115122
--- /dev/null
+++ b/test/unit/transition_collection/transition_collection_with_action_hook_base_test.rb
@@ -0,0 +1,34 @@
+require_relative '../../test_helper'
+
+class TransitionCollectionWithActionHookBaseTest < StateMachinesTest
+ def setup
+ @superclass = Class.new do
+ def save
+ true
+ end
+ end
+
+ @klass = Class.new(@superclass) do
+ attr_reader :saved, :state_on_save, :state_event_on_save, :state_event_transition_on_save
+
+ def save
+ @saved = true
+ @state_on_save = state
+ @state_event_on_save = state_event
+ @state_event_transition_on_save = state_event_transition
+ super
+ end
+ end
+
+ @machine = StateMachines::Machine.new(@klass, initial: :parked, action: :save)
+ @machine.state :idling
+ @machine.event :ignite
+
+ @object = @klass.new
+
+ @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ end
+
+ def default_test
+ end
+end
diff --git a/test/unit/transition_collection/transition_collection_with_action_hook_error_test.rb b/test/unit/transition_collection/transition_collection_with_action_hook_error_test.rb
new file mode 100644
index 0000000..6c35c79
--- /dev/null
+++ b/test/unit/transition_collection/transition_collection_with_action_hook_error_test.rb
@@ -0,0 +1,29 @@
+require_relative '../../test_helper'
+require_relative 'transition_collection_with_action_hook_base_test.rb'
+
+class TransitionCollectionWithActionHookErrorTest < TransitionCollectionWithActionHookBaseTest
+ def setup
+ super
+
+ @superclass.class_eval do
+ remove_method :save
+
+ def save
+ fail ArgumentError
+ end
+ end
+
+ begin
+ ; StateMachines::TransitionCollection.new([@transition]).perform
+ rescue
+ end
+ end
+
+ def test_should_not_write_event
+ assert_nil @object.state_event
+ end
+
+ def test_should_not_write_event_transition
+ assert_nil @object.send(:state_event_transition)
+ end
+end
diff --git a/test/unit/transition_collection/transition_collection_with_action_hook_invalid_test.rb b/test/unit/transition_collection/transition_collection_with_action_hook_invalid_test.rb
new file mode 100644
index 0000000..a6befab
--- /dev/null
+++ b/test/unit/transition_collection/transition_collection_with_action_hook_invalid_test.rb
@@ -0,0 +1,17 @@
+require_relative '../../test_helper'
+require_relative 'transition_collection_with_action_hook_base_test.rb'
+
+class TransitionCollectionWithActionHookInvalidTest < TransitionCollectionWithActionHookBaseTest
+ def setup
+ super
+ @result = StateMachines::TransitionCollection.new([@transition, nil]).perform
+ end
+
+ def test_should_not_succeed
+ assert_equal false, @result
+ end
+
+ def test_should_not_run_action
+ refute @object.saved
+ end
+end
diff --git a/test/unit/transition_collection/transition_collection_with_action_hook_multiple_test.rb b/test/unit/transition_collection/transition_collection_with_action_hook_multiple_test.rb
new file mode 100644
index 0000000..de1d9b4
--- /dev/null
+++ b/test/unit/transition_collection/transition_collection_with_action_hook_multiple_test.rb
@@ -0,0 +1,79 @@
+require_relative '../../test_helper'
+require_relative 'transition_collection_with_action_hook_base_test.rb'
+
+class TransitionCollectionWithActionHookMultipleTest < TransitionCollectionWithActionHookBaseTest
+ def setup
+ super
+
+ @status_machine = StateMachines::Machine.new(@klass, :status, initial: :first_gear, action: :save)
+ @status_machine.state :second_gear
+ @status_machine.event :shift_up
+
+ @klass.class_eval do
+ attr_reader :status_on_save, :status_event_on_save, :status_event_transition_on_save
+
+ remove_method :save
+
+ def save
+ @saved = true
+ @state_on_save = state
+ @state_event_on_save = state_event
+ @state_event_transition_on_save = state_event_transition
+ @status_on_save = status
+ @status_event_on_save = status_event
+ @status_event_transition_on_save = status_event_transition
+ super
+ 1
+ end
+ end
+
+ @object = @klass.new
+ @state_transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ @status_transition = StateMachines::Transition.new(@object, @status_machine, :shift_up, :first_gear, :second_gear)
+
+ @result = StateMachines::TransitionCollection.new([@state_transition, @status_transition]).perform
+ end
+
+ def test_should_succeed
+ assert_equal 1, @result
+ end
+
+ def test_should_run_action
+ assert @object.saved
+ end
+
+ def test_should_not_have_already_persisted_when_running_action
+ assert_equal 'parked', @object.state_on_save
+ assert_equal 'first_gear', @object.status_on_save
+ end
+
+ def test_should_persist
+ assert_equal 'idling', @object.state
+ assert_equal 'second_gear', @object.status
+ end
+
+ def test_should_not_have_events_during_action
+ assert_nil @object.state_event_on_save
+ assert_nil @object.status_event_on_save
+ end
+
+ def test_should_not_write_events
+ assert_nil @object.state_event
+ assert_nil @object.status_event
+ end
+
+ def test_should_have_event_transitions_during_action
+ assert_equal @state_transition, @object.state_event_transition_on_save
+ assert_equal @status_transition, @object.status_event_transition_on_save
+ end
+
+ def test_should_not_write_event_transitions
+ assert_nil @object.send(:state_event_transition)
+ assert_nil @object.send(:status_event_transition)
+ end
+
+ def test_should_mark_event_transitions_as_transient
+ assert @state_transition.transient?
+ assert @status_transition.transient?
+ end
+end
diff --git a/test/unit/transition_collection/transition_collection_with_action_hook_test.rb b/test/unit/transition_collection/transition_collection_with_action_hook_test.rb
new file mode 100644
index 0000000..433ef3b
--- /dev/null
+++ b/test/unit/transition_collection/transition_collection_with_action_hook_test.rb
@@ -0,0 +1,45 @@
+require_relative '../../test_helper'
+require_relative 'transition_collection_with_action_hook_base_test.rb'
+
+class TransitionCollectionWithActionHookTest < TransitionCollectionWithActionHookBaseTest
+ def setup
+ super
+ @result = StateMachines::TransitionCollection.new([@transition]).perform
+ end
+
+ def test_should_succeed
+ assert_equal true, @result
+ end
+
+ def test_should_run_action
+ assert @object.saved
+ end
+
+ def test_should_not_have_already_persisted_when_running_action
+ assert_equal 'parked', @object.state_on_save
+ end
+
+ def test_should_persist
+ assert_equal 'idling', @object.state
+ end
+
+ def test_should_not_have_event_during_action
+ assert_nil @object.state_event_on_save
+ end
+
+ def test_should_not_write_event
+ assert_nil @object.state_event
+ end
+
+ def test_should_have_event_transition_during_action
+ assert_equal @transition, @object.state_event_transition_on_save
+ end
+
+ def test_should_not_write_event_transition
+ assert_nil @object.send(:state_event_transition)
+ end
+
+ def test_should_mark_event_transition_as_transient
+ assert @transition.transient?
+ end
+end
diff --git a/test/unit/transition_collection/transition_collection_with_action_hook_with_different_actions_test.rb b/test/unit/transition_collection/transition_collection_with_action_hook_with_different_actions_test.rb
new file mode 100644
index 0000000..e5114c8
--- /dev/null
+++ b/test/unit/transition_collection/transition_collection_with_action_hook_with_different_actions_test.rb
@@ -0,0 +1,48 @@
+require_relative '../../test_helper'
+require_relative 'transition_collection_with_action_hook_base_test.rb'
+
+class TransitionCollectionWithActionHookWithDifferentActionsTest < TransitionCollectionWithActionHookBaseTest
+ def setup
+ super
+
+ @klass.class_eval do
+ def save_status
+ true
+ end
+ end
+
+ @machine = StateMachines::Machine.new(@klass, :status, initial: :first_gear, action: :save_status)
+ @machine.state :second_gear
+ @machine.event :shift_up
+
+ @result = StateMachines::TransitionCollection.new([@transition, StateMachines::Transition.new(@object, @machine, :shift_up, :first_gear, :second_gear)]).perform
+ end
+
+ def test_should_succeed
+ assert_equal true, @result
+ end
+
+ def test_should_run_action
+ assert @object.saved
+ end
+
+ def test_should_have_already_persisted_when_running_action
+ assert_equal 'idling', @object.state_on_save
+ end
+
+ def test_should_not_have_event_during_action
+ assert_nil @object.state_event_on_save
+ end
+
+ def test_should_not_write_event
+ assert_nil @object.state_event
+ end
+
+ def test_should_not_have_event_transition_during_save
+ assert_nil @object.state_event_transition_on_save
+ end
+
+ def test_should_not_write_event_attribute
+ assert_nil @object.send(:state_event_transition)
+ end
+end
diff --git a/test/unit/transition_collection/transition_collection_with_action_hook_with_nil_action_test.rb b/test/unit/transition_collection/transition_collection_with_action_hook_with_nil_action_test.rb
new file mode 100644
index 0000000..e8b9126
--- /dev/null
+++ b/test/unit/transition_collection/transition_collection_with_action_hook_with_nil_action_test.rb
@@ -0,0 +1,42 @@
+require_relative '../../test_helper'
+require_relative 'transition_collection_with_action_hook_base_test.rb'
+
+class TransitionCollectionWithActionHookWithNilActionTest < TransitionCollectionWithActionHookBaseTest
+ def setup
+ super
+
+ @machine = StateMachines::Machine.new(@klass, :status, initial: :first_gear)
+ @machine.state :second_gear
+ @machine.event :shift_up
+
+ @result = StateMachines::TransitionCollection.new([@transition, StateMachines::Transition.new(@object, @machine, :shift_up, :first_gear, :second_gear)]).perform
+ end
+
+ def test_should_succeed
+ assert_equal true, @result
+ end
+
+ def test_should_run_action
+ assert @object.saved
+ end
+
+ def test_should_have_already_persisted_when_running_action
+ assert_equal 'idling', @object.state_on_save
+ end
+
+ def test_should_not_have_event_during_action
+ assert_nil @object.state_event_on_save
+ end
+
+ def test_should_not_write_event
+ assert_nil @object.state_event
+ end
+
+ def test_should_not_have_event_transition_during_save
+ assert_nil @object.state_event_transition_on_save
+ end
+
+ def test_should_not_write_event_attribute
+ assert_nil @object.send(:state_event_transition)
+ end
+end
diff --git a/test/unit/transition_collection/transition_collection_with_after_callback_halt_test.rb b/test/unit/transition_collection/transition_collection_with_after_callback_halt_test.rb
new file mode 100644
index 0000000..8f6cc23
--- /dev/null
+++ b/test/unit/transition_collection/transition_collection_with_after_callback_halt_test.rb
@@ -0,0 +1,51 @@
+require_relative '../../test_helper'
+
+class TransitionCollectionWithBeforeCallbackHaltTest < StateMachinesTest
+ def setup
+ @klass = Class.new do
+ attr_reader :saved
+
+ def save
+ @saved = true
+ end
+ end
+ @before_count = 0
+ @after_count = 0
+
+ @machine = StateMachines::Machine.new(@klass, initial: :parked, action: :save)
+ @machine.state :idling
+ @machine.event :ignite
+
+ @machine.before_transition { @before_count += 1; throw :halt }
+ @machine.before_transition { @before_count += 1 }
+ @machine.after_transition { @after_count += 1 }
+ @machine.around_transition { |block| @before_count += 1; block.call; @after_count += 1 }
+
+ @object = @klass.new
+
+ @transitions = StateMachines::TransitionCollection.new([
+ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ ])
+ @result = @transitions.perform
+ end
+
+ def test_should_not_succeed
+ assert_equal false, @result
+ end
+
+ def test_should_not_persist_state
+ assert_equal 'parked', @object.state
+ end
+
+ def test_should_not_run_action
+ refute @object.saved
+ end
+
+ def test_should_not_run_further_before_callbacks
+ assert_equal 1, @before_count
+ end
+
+ def test_should_not_run_after_callbacks
+ assert_equal 0, @after_count
+ end
+end
diff --git a/test/unit/transition_collection/transition_collection_with_before_callback_halt_test.rb b/test/unit/transition_collection/transition_collection_with_before_callback_halt_test.rb
new file mode 100644
index 0000000..f1718ac
--- /dev/null
+++ b/test/unit/transition_collection/transition_collection_with_before_callback_halt_test.rb
@@ -0,0 +1,47 @@
+require_relative '../../test_helper'
+
+class TransitionCollectionWithAfterCallbackHaltTest < StateMachinesTest
+ def setup
+ @klass = Class.new do
+ attr_reader :saved
+
+ def save
+ @saved = true
+ end
+ end
+ @before_count = 0
+ @after_count = 0
+
+ @machine = StateMachines::Machine.new(@klass, initial: :parked, action: :save)
+ @machine.state :idling
+ @machine.event :ignite
+
+ @machine.before_transition { @before_count += 1 }
+ @machine.after_transition { @after_count += 1; throw :halt }
+ @machine.after_transition { @after_count += 1 }
+ @machine.around_transition { |block| @before_count += 1; block.call; @after_count += 1 }
+
+ @object = @klass.new
+
+ @transitions = StateMachines::TransitionCollection.new([
+ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ ])
+ @result = @transitions.perform
+ end
+
+ def test_should_succeed
+ assert_equal true, @result
+ end
+
+ def test_should_persist_state
+ assert_equal 'idling', @object.state
+ end
+
+ def test_should_run_before_callbacks
+ assert_equal 2, @before_count
+ end
+
+ def test_should_not_run_further_after_callbacks
+ assert_equal 2, @after_count
+ end
+end
diff --git a/test/unit/transition_collection/transition_collection_with_block_test.rb b/test/unit/transition_collection/transition_collection_with_block_test.rb
new file mode 100644
index 0000000..8179b4e
--- /dev/null
+++ b/test/unit/transition_collection/transition_collection_with_block_test.rb
@@ -0,0 +1,46 @@
+require_relative '../../test_helper'
+
+class TransitionCollectionWithBlockTest < StateMachinesTest
+ def setup
+ @klass = Class.new do
+ attr_reader :actions
+
+ def save
+ (@actions ||= []) << :save
+ end
+ end
+
+ @state = StateMachines::Machine.new(@klass, :state, initial: :parked, action: :save)
+ @state.state :idling
+ @state.event :ignite
+
+ @status = StateMachines::Machine.new(@klass, :status, initial: :first_gear, action: :save)
+ @status.state :second_gear
+ @status.event :shift_up
+
+ @object = @klass.new
+ @transitions = StateMachines::TransitionCollection.new([
+ @state_transition = StateMachines::Transition.new(@object, @state, :ignite, :parked, :idling),
+ @status_transition = StateMachines::Transition.new(@object, @status, :shift_up, :first_gear, :second_gear)
+ ])
+ @result = @transitions.perform { 1 }
+ end
+
+ def test_should_succeed
+ assert_equal 1, @result
+ end
+
+ def test_should_persist_states
+ assert_equal 'idling', @object.state
+ assert_equal 'second_gear', @object.status
+ end
+
+ def test_should_not_run_machine_actions
+ assert_nil @object.actions
+ end
+
+ def test_should_use_result_as_transition_result
+ assert_equal 1, @state_transition.result
+ assert_equal 1, @status_transition.result
+ end
+end
diff --git a/test/unit/transition_collection/transition_collection_with_callbacks_test.rb b/test/unit/transition_collection/transition_collection_with_callbacks_test.rb
new file mode 100644
index 0000000..b0c5295
--- /dev/null
+++ b/test/unit/transition_collection/transition_collection_with_callbacks_test.rb
@@ -0,0 +1,135 @@
+require_relative '../../test_helper'
+
+class TransitionCollectionWithCallbacksTest < StateMachinesTest
+ def setup
+ @klass = Class.new do
+ attr_reader :saved
+
+ def save
+ @saved = true
+ end
+ end
+
+ @before_callbacks = []
+ @after_callbacks = []
+
+ @state = StateMachines::Machine.new(@klass, initial: :parked, action: :save)
+ @state.state :idling
+ @state.event :ignite
+ @state.before_transition { @before_callbacks << :state_before }
+ @state.after_transition { @after_callbacks << :state_after }
+ @state.around_transition { |block| @before_callbacks << :state_around; block.call; @after_callbacks << :state_around }
+
+ @status = StateMachines::Machine.new(@klass, :status, initial: :first_gear, action: :save)
+ @status.state :second_gear
+ @status.event :shift_up
+ @status.before_transition { @before_callbacks << :status_before }
+ @status.after_transition { @after_callbacks << :status_after }
+ @status.around_transition { |block| @before_callbacks << :status_around; block.call; @after_callbacks << :status_around }
+
+ @object = @klass.new
+ @transitions = StateMachines::TransitionCollection.new([
+ StateMachines::Transition.new(@object, @state, :ignite, :parked, :idling),
+ StateMachines::Transition.new(@object, @status, :shift_up, :first_gear, :second_gear)
+ ])
+ end
+
+ def test_should_run_before_callbacks_in_order
+ @transitions.perform
+ assert_equal [:state_before, :state_around, :status_before, :status_around], @before_callbacks
+ end
+
+ def test_should_halt_if_before_callback_halted_for_first_transition
+ @state.before_transition { throw :halt }
+
+ assert_equal false, @transitions.perform
+ assert_equal [:state_before, :state_around], @before_callbacks
+ end
+
+ def test_should_halt_if_before_callback_halted_for_second_transition
+ @status.before_transition { throw :halt }
+
+ assert_equal false, @transitions.perform
+ assert_equal [:state_before, :state_around, :status_before, :status_around], @before_callbacks
+ end
+
+ def test_should_halt_if_around_callback_halted_before_yield_for_first_transition
+ @state.around_transition { throw :halt }
+
+ assert_equal false, @transitions.perform
+ assert_equal [:state_before, :state_around], @before_callbacks
+ end
+
+ def test_should_halt_if_around_callback_halted_before_yield_for_second_transition
+ @status.around_transition { throw :halt }
+
+ assert_equal false, @transitions.perform
+ assert_equal [:state_before, :state_around, :status_before, :status_around], @before_callbacks
+ end
+
+ def test_should_run_after_callbacks_in_reverse_order
+ @transitions.perform
+ assert_equal [:status_around, :status_after, :state_around, :state_after], @after_callbacks
+ end
+
+ def test_should_not_halt_if_after_callback_halted_for_first_transition
+ @state.after_transition { throw :halt }
+
+ assert_equal true, @transitions.perform
+ assert_equal [:status_around, :status_after, :state_around, :state_after], @after_callbacks
+ end
+
+ def test_should_not_halt_if_around_callback_halted_for_second_transition
+ @status.around_transition { |block| block.call; throw :halt }
+
+ assert_equal true, @transitions.perform
+ assert_equal [:state_around, :state_after], @after_callbacks
+ end
+
+ def test_should_run_before_callbacks_before_persisting_the_state
+ @state.before_transition { |object| @before_state = object.state }
+ @state.around_transition { |object, _transition, block| @around_state = object.state; block.call }
+ @transitions.perform
+
+ assert_equal 'parked', @before_state
+ assert_equal 'parked', @around_state
+ end
+
+ def test_should_persist_state_before_running_action
+ @klass.class_eval do
+ attr_reader :saved_on_persist
+
+ def state=(value)
+ @state = value
+ @saved_on_persist = saved
+ end
+ end
+
+ @transitions.perform
+ refute @object.saved_on_persist
+ end
+
+ def test_should_persist_state_before_running_action_block
+ @klass.class_eval do
+ attr_writer :saved
+ attr_reader :saved_on_persist
+
+ def state=(value)
+ @state = value
+ @saved_on_persist = saved
+ end
+ end
+
+ @transitions.perform { @object.saved = true }
+ refute @object.saved_on_persist
+ end
+
+ def test_should_run_after_callbacks_after_running_the_action
+ @state.after_transition { |object| @after_saved = object.saved }
+ @state.around_transition { |object, _transition, block| block.call; @around_saved = object.saved }
+ @transitions.perform
+
+ assert @after_saved
+ assert @around_saved
+ end
+end
diff --git a/test/unit/transition_collection/transition_collection_with_different_actions_test.rb b/test/unit/transition_collection/transition_collection_with_different_actions_test.rb
new file mode 100644
index 0000000..98b62b1
--- /dev/null
+++ b/test/unit/transition_collection/transition_collection_with_different_actions_test.rb
@@ -0,0 +1,189 @@
+require_relative '../../test_helper'
+
+class TransitionCollectionWithDifferentActionsTest < StateMachinesTest
+ def setup
+ @klass = Class.new do
+ attr_reader :actions
+
+ def save_state
+ (@actions ||= []) << :save_state
+ :save_state
+ end
+
+ def save_status
+ (@actions ||= []) << :save_status
+ :save_status
+ end
+ end
+
+ @state = StateMachines::Machine.new(@klass, initial: :parked, action: :save_state)
+ @state.state :idling
+ @state.event :ignite
+
+ @status = StateMachines::Machine.new(@klass, :status, initial: :first_gear, action: :save_status)
+ @status.state :second_gear
+ @status.event :shift_up
+
+ @object = @klass.new
+
+ @transitions = StateMachines::TransitionCollection.new([
+ @state_transition = StateMachines::Transition.new(@object, @state, :ignite, :parked, :idling),
+ @status_transition = StateMachines::Transition.new(@object, @status, :shift_up, :first_gear, :second_gear)
+ ])
+ end
+
+ def test_should_succeed
+ assert_equal true, @transitions.perform
+ end
+
+ def test_should_persist_states
+ @transitions.perform
+ assert_equal 'idling', @object.state
+ assert_equal 'second_gear', @object.status
+ end
+
+ def test_should_run_actions_in_order
+ @transitions.perform
+ assert_equal [:save_state, :save_status], @object.actions
+ end
+
+ def test_should_store_results_in_transitions
+ @transitions.perform
+ assert_equal :save_state, @state_transition.result
+ assert_equal :save_status, @status_transition.result
+ end
+
+ def test_should_not_halt_if_action_fails_for_first_transition
+ @klass.class_eval do
+ remove_method :save_state
+
+ def save_state
+ (@actions ||= []) << :save_state
+ false
+ end
+ end
+
+ assert_equal false, @transitions.perform
+ assert_equal [:save_state, :save_status], @object.actions
+ end
+
+ def test_should_halt_if_action_fails_for_second_transition
+ @klass.class_eval do
+ remove_method :save_status
+
+ def save_status
+ (@actions ||= []) << :save_status
+ false
+ end
+ end
+
+ assert_equal false, @transitions.perform
+ assert_equal [:save_state, :save_status], @object.actions
+ end
+
+ def test_should_rollback_if_action_errors_for_first_transition
+ @klass.class_eval do
+ remove_method :save_state
+
+ def save_state
+ fail ArgumentError
+ end
+ end
+
+ begin
+ ; @transitions.perform
+ rescue
+ end
+ assert_equal 'parked', @object.state
+ assert_equal 'first_gear', @object.status
+ end
+
+ def test_should_rollback_if_action_errors_for_second_transition
+ @klass.class_eval do
+ remove_method :save_status
+
+ def save_status
+ fail ArgumentError
+ end
+ end
+
+ begin
+ ; @transitions.perform
+ rescue
+ end
+ assert_equal 'parked', @object.state
+ assert_equal 'first_gear', @object.status
+ end
+
+ def test_should_not_run_after_callbacks_if_action_fails_for_first_transition
+ @klass.class_eval do
+ remove_method :save_state
+
+ def save_state
+ false
+ end
+ end
+
+ @callbacks = []
+ @state.after_transition { @callbacks << :state_after }
+ @state.around_transition { |block| block.call; @callbacks << :state_around }
+ @status.after_transition { @callbacks << :status_after }
+ @status.around_transition { |block| block.call; @callbacks << :status_around }
+
+ @transitions.perform
+ assert_equal [], @callbacks
+ end
+
+ def test_should_not_run_after_callbacks_if_action_fails_for_second_transition
+ @klass.class_eval do
+ remove_method :save_status
+
+ def save_status
+ false
+ end
+ end
+
+ @callbacks = []
+ @state.after_transition { @callbacks << :state_after }
+ @state.around_transition { |block| block.call; @callbacks << :state_around }
+ @status.after_transition { @callbacks << :status_after }
+ @status.around_transition { |block| block.call; @callbacks << :status_around }
+
+ @transitions.perform
+ assert_equal [], @callbacks
+ end
+
+ def test_should_run_after_failure_callbacks_if_action_fails_for_first_transition
+ @klass.class_eval do
+ remove_method :save_state
+
+ def save_state
+ false
+ end
+ end
+
+ @callbacks = []
+ @state.after_failure { @callbacks << :state_after }
+ @status.after_failure { @callbacks << :status_after }
+
+ @transitions.perform
+ assert_equal [:status_after, :state_after], @callbacks
+ end
+
+ def test_should_run_after_failure_callbacks_if_action_fails_for_second_transition
+ @klass.class_eval do
+ remove_method :save_status
+
+ def save_status
+ false
+ end
+ end
+
+ @callbacks = []
+ @state.after_failure { @callbacks << :state_after }
+ @status.after_failure { @callbacks << :status_after }
+
+ @transitions.perform
+ assert_equal [:status_after, :state_after], @callbacks
+ end
+end
diff --git a/test/unit/transition_collection/transition_collection_with_duplicate_actions_test.rb b/test/unit/transition_collection/transition_collection_with_duplicate_actions_test.rb
new file mode 100644
index 0000000..72130fc
--- /dev/null
+++ b/test/unit/transition_collection/transition_collection_with_duplicate_actions_test.rb
@@ -0,0 +1,48 @@
+require_relative '../../test_helper'
+
+class TransitionCollectionWithDuplicateActionsTest < StateMachinesTest
+ def setup
+ @klass = Class.new do
+ attr_reader :actions
+
+ def save
+ (@actions ||= []) << :save
+ :save
+ end
+ end
+
+ @state = StateMachines::Machine.new(@klass, initial: :parked, action: :save)
+ @state.state :idling
+ @state.event :ignite
+
+ @status = StateMachines::Machine.new(@klass, :status, initial: :first_gear, action: :save)
+ @status.state :second_gear
+ @status.event :shift_up
+
+ @object = @klass.new
+
+ @transitions = StateMachines::TransitionCollection.new([
+ @state_transition = StateMachines::Transition.new(@object, @state, :ignite, :parked, :idling),
+ @status_transition = StateMachines::Transition.new(@object, @status, :shift_up, :first_gear, :second_gear)
+ ])
+ @result = @transitions.perform
+ end
+
+ def test_should_succeed
+ assert_equal :save, @result
+ end
+
+ def test_should_persist_states
+ assert_equal 'idling', @object.state
+ assert_equal 'second_gear', @object.status
+ end
+
+ def test_should_run_action_once
+ assert_equal [:save], @object.actions
+ end
+
+ def test_should_store_results_in_transitions
+ assert_equal :save, @state_transition.result
+ assert_equal :save, @status_transition.result
+ end
+end
diff --git a/test/unit/transition_collection/transition_collection_with_empty_actions_test.rb b/test/unit/transition_collection/transition_collection_with_empty_actions_test.rb
new file mode 100644
index 0000000..bb4176b
--- /dev/null
+++ b/test/unit/transition_collection/transition_collection_with_empty_actions_test.rb
@@ -0,0 +1,41 @@
+require_relative '../../test_helper'
+
+class TransitionCollectionWithEmptyActionsTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+
+ @state = StateMachines::Machine.new(@klass, initial: :parked)
+ @state.state :idling
+ @state.event :ignite
+
+ @status = StateMachines::Machine.new(@klass, :status, initial: :first_gear)
+ @status.state :second_gear
+ @status.event :shift_up
+
+ @object = @klass.new
+
+ @transitions = StateMachines::TransitionCollection.new([
+ @state_transition = StateMachines::Transition.new(@object, @state, :ignite, :parked, :idling),
+ @status_transition = StateMachines::Transition.new(@object, @status, :shift_up, :first_gear, :second_gear)
+ ])
+
+ @object.state = 'idling'
+ @object.status = 'second_gear'
+
+ @result = @transitions.perform
+ end
+
+ def test_should_succeed
+ assert_equal true, @result
+ end
+
+ def test_should_persist_states
+ assert_equal 'idling', @object.state
+ assert_equal 'second_gear', @object.status
+ end
+
+ def test_should_store_results_in_transitions
+ assert_nil @state_transition.result
+ assert_nil @status_transition.result
+ end
+end
diff --git a/test/unit/transition_collection/transition_collection_with_mixed_actions_test.rb b/test/unit/transition_collection/transition_collection_with_mixed_actions_test.rb
new file mode 100644
index 0000000..c2c17ab
--- /dev/null
+++ b/test/unit/transition_collection/transition_collection_with_mixed_actions_test.rb
@@ -0,0 +1,41 @@
+require_relative '../../test_helper'
+
+class TransitionCollectionWithMixedActionsTest < StateMachinesTest
+ def setup
+ @klass = Class.new do
+ def save
+ true
+ end
+ end
+
+ @state = StateMachines::Machine.new(@klass, initial: :parked, action: :save)
+ @state.state :idling
+ @state.event :ignite
+
+ @status = StateMachines::Machine.new(@klass, :status, initial: :first_gear)
+ @status.state :second_gear
+ @status.event :shift_up
+
+ @object = @klass.new
+
+ @transitions = StateMachines::TransitionCollection.new([
+ @state_transition = StateMachines::Transition.new(@object, @state, :ignite, :parked, :idling),
+ @status_transition = StateMachines::Transition.new(@object, @status, :shift_up, :first_gear, :second_gear)
+ ])
+ @result = @transitions.perform
+ end
+
+ def test_should_succeed
+ assert_equal true, @result
+ end
+
+ def test_should_persist_states
+ assert_equal 'idling', @object.state
+ assert_equal 'second_gear', @object.status
+ end
+
+ def test_should_store_results_in_transitions
+ assert_equal true, @state_transition.result
+ assert_nil @status_transition.result
+ end
+end
diff --git a/test/unit/transition_collection/transition_collection_with_skipped_actions_and_block_test.rb b/test/unit/transition_collection/transition_collection_with_skipped_actions_and_block_test.rb
new file mode 100644
index 0000000..3976322
--- /dev/null
+++ b/test/unit/transition_collection/transition_collection_with_skipped_actions_and_block_test.rb
@@ -0,0 +1,34 @@
+require_relative '../../test_helper'
+
+class TransitionCollectionWithSkippedActionsAndBlockTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+
+ @machine = StateMachines::Machine.new(@klass, initial: :parked, action: :save_state)
+ @machine.state :idling
+ @machine.event :ignite
+
+ @object = @klass.new
+
+ @transitions = StateMachines::TransitionCollection.new([
+ @state_transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ ], actions: false)
+ @result = @transitions.perform { @ran_block = true; 1 }
+ end
+
+ def test_should_succeed
+ assert_equal 1, @result
+ end
+
+ def test_should_persist_states
+ assert_equal 'idling', @object.state
+ end
+
+ def test_should_run_block
+ assert @ran_block
+ end
+
+ def test_should_store_results_in_transitions
+ assert_equal 1, @state_transition.result
+ end
+end
diff --git a/test/unit/transition_collection/transition_collection_with_skipped_actions_test.rb b/test/unit/transition_collection/transition_collection_with_skipped_actions_test.rb
new file mode 100644
index 0000000..b87aec9
--- /dev/null
+++ b/test/unit/transition_collection/transition_collection_with_skipped_actions_test.rb
@@ -0,0 +1,69 @@
+require_relative '../../test_helper'
+
+class TransitionCollectionWithSkippedActionsTest < StateMachinesTest
+ def setup
+ @klass = Class.new do
+ attr_reader :actions
+
+ def save_state
+ (@actions ||= []) << :save_state
+ :save_state
+ end
+
+ def save_status
+ (@actions ||= []) << :save_status
+ :save_status
+ end
+ end
+
+ @callbacks = []
+
+ @state = StateMachines::Machine.new(@klass, initial: :parked, action: :save_state)
+ @state.state :idling
+ @state.event :ignite
+ @state.before_transition { @callbacks << :state_before }
+ @state.after_transition { @callbacks << :state_after }
+ @state.around_transition { |block| @callbacks << :state_around_before; block.call; @callbacks << :state_around_after }
+
+ @status = StateMachines::Machine.new(@klass, :status, initial: :first_gear, action: :save_status)
+ @status.state :second_gear
+ @status.event :shift_up
+ @status.before_transition { @callbacks << :status_before }
+ @status.after_transition { @callbacks << :status_after }
+ @status.around_transition { |block| @callbacks << :status_around_before; block.call; @callbacks << :status_around_after }
+
+ @object = @klass.new
+
+ @transitions = StateMachines::TransitionCollection.new([
+ @state_transition = StateMachines::Transition.new(@object, @state, :ignite, :parked, :idling),
+ @status_transition = StateMachines::Transition.new(@object, @status, :shift_up, :first_gear, :second_gear)
+ ], actions: false)
+ @result = @transitions.perform
+ end
+
+ def test_should_skip_actions
+ assert_equal true, @transitions.skip_actions
+ end
+
+ def test_should_succeed
+ assert_equal true, @result
+ end
+
+ def test_should_persist_states
+ assert_equal 'idling', @object.state
+ assert_equal 'second_gear', @object.status
+ end
+
+ def test_should_not_run_actions
+ assert_nil @object.actions
+ end
+
+ def test_should_store_results_in_transitions
+ assert_nil @state_transition.result
+ assert_nil @status_transition.result
+ end
+
+ def test_should_run_all_callbacks
+ assert_equal [:state_before, :state_around_before, :status_before, :status_around_before, :status_around_after, :status_after, :state_around_after, :state_after], @callbacks
+ end
+end
diff --git a/test/unit/transition_collection/transition_collection_with_skipped_after_callbacks_and_around_callbacks_test.rb b/test/unit/transition_collection/transition_collection_with_skipped_after_callbacks_and_around_callbacks_test.rb
new file mode 100644
index 0000000..3ec7244
--- /dev/null
+++ b/test/unit/transition_collection/transition_collection_with_skipped_after_callbacks_and_around_callbacks_test.rb
@@ -0,0 +1,53 @@
+require_relative '../../test_helper'
+
+class TransitionCollectionWithSkippedAfterCallbacksAndAroundCallbacksTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+
+ @callbacks = []
+
+ @machine = StateMachines::Machine.new(@klass, initial: :parked)
+ @machine.state :idling
+ @machine.event :ignite
+ @machine.around_transition { |block| @callbacks << :around_before; block.call; @callbacks << :around_after }
+
+ @object = @klass.new
+
+ @transitions = StateMachines::TransitionCollection.new([
+ @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ ], after: false)
+ @result = @transitions.perform
+ end
+
+ def test_should_raise_exception
+ skip('Not supported') if StateMachines::Transition.pause_supported?
+ assert_raises(ArgumentError) { @transitions.perform }
+ end
+
+ def test_should_succeed
+ skip unless StateMachines::Transition.pause_supported?
+
+ assert_equal true, @result
+ end
+
+ def test_should_not_run_around_callbacks_after_yield
+ skip unless StateMachines::Transition.pause_supported?
+
+ refute @callbacks.include?(:around_after)
+ end
+
+ def test_should_run_around_callbacks_after_yield_on_subsequent_perform
+ skip unless StateMachines::Transition.pause_supported?
+
+ StateMachines::TransitionCollection.new([@transition]).perform
+ assert @callbacks.include?(:around_after)
+ end
+
+ def test_should_not_rerun_around_callbacks_before_yield_on_subsequent_perform
+ skip unless StateMachines::Transition.pause_supported?
+
+ @callbacks = []
+ StateMachines::TransitionCollection.new([@transition]).perform
+ refute @callbacks.include?(:around_before)
+ end
+end
diff --git a/test/unit/transition_collection/transition_collection_with_skipped_after_callbacks_test.rb b/test/unit/transition_collection/transition_collection_with_skipped_after_callbacks_test.rb
new file mode 100644
index 0000000..feac90c
--- /dev/null
+++ b/test/unit/transition_collection/transition_collection_with_skipped_after_callbacks_test.rb
@@ -0,0 +1,34 @@
+require_relative '../../test_helper'
+
+class TransitionCollectionWithSkippedAfterCallbacksTest < StateMachinesTest
+ def setup
+ @klass = Class.new
+
+ @callbacks = []
+
+ @machine = StateMachines::Machine.new(@klass, initial: :parked)
+ @machine.state :idling
+ @machine.event :ignite
+ @machine.after_transition { @callbacks << :after }
+
+ @object = @klass.new
+
+ @transitions = StateMachines::TransitionCollection.new([
+ @transition = StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ ], after: false)
+ @result = @transitions.perform
+ end
+
+ def test_should_succeed
+ assert_equal true, @result
+ end
+
+ def test_should_not_run_after_callbacks
+ refute @callbacks.include?(:after)
+ end
+
+ def test_should_run_after_callbacks_on_subsequent_perform
+ StateMachines::TransitionCollection.new([@transition]).perform
+ assert @callbacks.include?(:after)
+ end
+end
diff --git a/test/unit/transition_collection/transition_collection_with_transactions_test.rb b/test/unit/transition_collection/transition_collection_with_transactions_test.rb
new file mode 100644
index 0000000..dd1e64d
--- /dev/null
+++ b/test/unit/transition_collection/transition_collection_with_transactions_test.rb
@@ -0,0 +1,65 @@
+require_relative '../../test_helper'
+
+class TransitionCollectionWithTransactionsTest < StateMachinesTest
+ def setup
+ @klass = Class.new do
+ attr_accessor :running_transaction, :cancelled_transaction
+ end
+
+ @machine = StateMachines::Machine.new(@klass, initial: :parked)
+ @machine.state :idling
+ @machine.event :ignite
+
+ class << @machine
+ def within_transaction(object)
+ object.running_transaction = true
+ object.cancelled_transaction = yield == false
+ object.running_transaction = false
+ end
+ end
+
+ @object = @klass.new
+ @transitions = StateMachines::TransitionCollection.new([
+ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ ], use_transactions: true)
+ end
+
+ def test_should_run_before_callbacks_within_transaction
+ @machine.before_transition { |object| @in_transaction = object.running_transaction }
+ @transitions.perform
+
+ assert @in_transaction
+ end
+
+ def test_should_run_action_within_transaction
+ @transitions.perform { @in_transaction = @object.running_transaction }
+
+ assert @in_transaction
+ end
+
+ def test_should_run_after_callbacks_within_transaction
+ @machine.after_transition { |object| @in_transaction = object.running_transaction }
+ @transitions.perform
+
+ assert @in_transaction
+ end
+
+ def test_should_cancel_the_transaction_on_before_halt
+ @machine.before_transition { throw :halt }
+
+ @transitions.perform
+ assert @object.cancelled_transaction
+ end
+
+ def test_should_cancel_the_transaction_on_action_failure
+ @transitions.perform { false }
+ assert @object.cancelled_transaction
+ end
+
+ def test_should_not_cancel_the_transaction_on_after_halt
+ @machine.after_transition { throw :halt }
+
+ @transitions.perform
+ refute @object.cancelled_transaction
+ end
+end
diff --git a/test/unit/transition_collection/transition_collection_without_transactions_test.rb b/test/unit/transition_collection/transition_collection_without_transactions_test.rb
new file mode 100644
index 0000000..9b20e1a
--- /dev/null
+++ b/test/unit/transition_collection/transition_collection_without_transactions_test.rb
@@ -0,0 +1,29 @@
+require_relative '../../test_helper'
+
+class TransitionCollectionWithoutTransactionsTest < StateMachinesTest
+ def setup
+ @klass = Class.new do
+ attr_accessor :ran_transaction
+ end
+
+ @machine = StateMachines::Machine.new(@klass, initial: :parked)
+ @machine.state :idling
+ @machine.event :ignite
+
+ class << @machine
+ def within_transaction(object)
+ object.ran_transaction = true
+ end
+ end
+
+ @object = @klass.new
+ @transitions = StateMachines::TransitionCollection.new([
+ StateMachines::Transition.new(@object, @machine, :ignite, :parked, :idling)
+ ], use_transactions: false)
+ @transitions.perform
+ end
+
+ def test_should_not_run_within_transaction
+ refute @object.ran_transaction
+ end
+end
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-ruby-extras/ruby-state-machines.git
More information about the Pkg-ruby-extras-commits
mailing list