def setUp(self): yield super(UnitRelationLifecycleTest, self).setUp() yield self.setup_default_test_relation() self.relation_name = self.states["service_relation"].relation_name self.lifecycle = UnitRelationLifecycle( self.client, self.states["unit"].unit_name, self.states["unit_relation"], self.states["service_relation"].relation_name, self.unit_directory, self.executor) self.log_stream = self.capture_logging("unit.relation.lifecycle", logging.DEBUG)
def get_unit_relation_workflow(self, states): state_dir = os.path.join(self.juju_directory, "state") lifecycle = UnitRelationLifecycle( self.client, states["unit_relation"], states["service_relation"].relation_name, self.unit_directory, self.executor) workflow = RelationWorkflowState(self.client, states["unit_relation"], lifecycle, state_dir) return (workflow, lifecycle)
def setUp(self): yield super(UnitRelationLifecycleTest, self).setUp() yield self.setup_default_test_relation() self.relation_name = self.states["service_relation"].relation_name self.lifecycle = UnitRelationLifecycle( self.client, self.states["unit"].unit_name, self.states["unit_relation"], self.states["service_relation"].relation_name, self.unit_directory, self.executor) self.log_stream = self.capture_logging("unit.relation.lifecycle", logging.DEBUG)
def setup_unit_relations(self, service_relation, *units): """ Given a service relation and set of unit tuples in the form unit_state, unit_relation_workflow_state, will add unit relations for these units and update their workflow state to the desired/given state. """ for unit, state in units: unit_relation = yield service_relation.add_unit_state(unit) lifecycle = UnitRelationLifecycle(self.client, unit.unit_name, unit_relation, service_relation.relation_name, self.makeDir(), self.executor) workflow_state = RelationWorkflowState(self.client, unit_relation, lifecycle, self.makeDir()) yield workflow_state.set_state(state)
def setUp(self): yield super(UnitRelationWorkflowTest, self).setUp() yield self.setup_default_test_relation() self.relation_name = self.states["service_relation"].relation_name self.juju_directory = self.makeDir() self.log_stream = self.capture_logging("unit.relation.lifecycle", logging.DEBUG) self.lifecycle = UnitRelationLifecycle(self.client, self.states["unit"].unit_name, self.states["unit_relation"], self.relation_name, self.unit_directory, self.executor) self.state_directory = self.makeDir( path=os.path.join(self.juju_directory, "state")) self.workflow = RelationWorkflowState(self.client, self.states["unit_relation"], self.lifecycle, self.state_directory)
class UnitRelationWorkflowTest(WorkflowTestBase): @inlineCallbacks def setUp(self): yield super(UnitRelationWorkflowTest, self).setUp() yield self.setup_default_test_relation() self.relation_name = self.states["service_relation"].relation_name self.relation_ident = self.states["service_relation"].relation_ident self.log_stream = self.capture_logging( "unit.relation.lifecycle", logging.DEBUG) self.lifecycle = UnitRelationLifecycle( self.client, self.states["unit"].unit_name, self.states["unit_relation"], self.relation_ident, self.unit_directory, self.state_directory, self.executor) self.workflow = RelationWorkflowState( self.client, self.states["unit_relation"], self.states["unit"].unit_name, self.lifecycle, self.state_directory) @inlineCallbacks def tearDown(self): yield self.lifecycle.stop() yield super(UnitRelationWorkflowTest, self).tearDown() @inlineCallbacks def test_is_relation_running(self): """The unit relation's workflow state can be categorized as a boolean. """ with (yield self.workflow.lock()): running, state = yield is_relation_running( self.client, self.states["unit_relation"]) self.assertIdentical(running, False) self.assertIdentical(state, None) relation_state = self.workflow.get_relation_info() self.assertEquals(relation_state, {"relation-0000000000": {"relation_name": "mysql/0", "relation_scope": "global"}}) yield self.workflow.fire_transition("start") running, state = yield is_relation_running( self.client, self.states["unit_relation"]) self.assertIdentical(running, True) self.assertEqual(state, "up") relation_state = self.workflow.get_relation_info() self.assertEquals(relation_state, {"relation-0000000000": {"relation_name": "mysql/0", "relation_scope": "global"}}) yield self.workflow.fire_transition("stop") running, state = yield is_relation_running( self.client, self.states["unit_relation"]) self.assertIdentical(running, False) self.assertEqual(state, "down") relation_state = self.workflow.get_relation_info() self.assertEquals(relation_state, {"relation-0000000000": {"relation_name": "mysql/0", "relation_scope": "global"}}) @inlineCallbacks def test_up_down_cycle(self): """The workflow can be transition from up to down, and back. """ self.write_hook("%s-relation-changed" % self.relation_name, "#!/bin/bash\nexit 0\n") with (yield self.workflow.lock()): yield self.workflow.fire_transition("start") yield self.assertState(self.workflow, "up") hook_executed = self.wait_on_hook("app-relation-changed") # Add a new unit, while we're stopped. with (yield self.workflow.lock()): yield self.workflow.fire_transition("stop") yield self.add_opposite_service_unit(self.states) yield self.assertState(self.workflow, "down") self.assertFalse(hook_executed.called) # Come back up; check unit add detected. with (yield self.workflow.lock()): yield self.workflow.fire_transition("restart") yield self.assertState(self.workflow, "up") yield hook_executed self.assert_history_concise( ("start", "up"), ("stop", "down"), ("restart", "up"), history_id=self.workflow.zk_state_id) @inlineCallbacks def test_join_hook_with_error(self): """A join hook error stops execution of synthetic change hooks. """ self.capture_logging("unit.relation.lifecycle", logging.DEBUG) self.write_hook("%s-relation-joined" % self.relation_name, "#!/bin/bash\nexit 1\n") self.write_hook("%s-relation-changed" % self.relation_name, "#!/bin/bash\nexit 1\n") with (yield self.workflow.lock()): yield self.workflow.fire_transition("start") yield self.assertState(self.workflow, "up") # Add a new unit, and wait for the broken hook to result in # the transition to the down state. yield self.add_opposite_service_unit(self.states) yield self.wait_on_state(self.workflow, "error") f_state, history, zk_state = yield self.read_persistent_state( history_id=self.workflow.zk_state_id) self.assertEqual(f_state, zk_state) error = "Error processing '%s': exit code 1." % ( os.path.join(self.unit_directory, "charm", "hooks", "app-relation-joined")) self.assertEqual(f_state, {"state": "error", "state_variables": { "change_type": "joined", "error_message": error}}) @inlineCallbacks def test_change_hook_with_error(self): """An error while processing a change hook, results in the workflow transitioning to the down state. """ self.capture_logging("unit.relation.lifecycle", logging.DEBUG) self.write_hook("%s-relation-joined" % self.relation_name, "#!/bin/bash\nexit 0\n") self.write_hook("%s-relation-changed" % self.relation_name, "#!/bin/bash\nexit 1\n") with (yield self.workflow.lock()): yield self.workflow.fire_transition("start") yield self.assertState(self.workflow, "up") # Add a new unit, and wait for the broken hook to result in # the transition to the down state. yield self.add_opposite_service_unit(self.states) yield self.wait_on_state(self.workflow, "error") f_state, history, zk_state = yield self.read_persistent_state( history_id=self.workflow.zk_state_id) self.assertEqual(f_state, zk_state) error = "Error processing '%s': exit code 1." % ( os.path.join(self.unit_directory, "charm", "hooks", "app-relation-changed")) self.assertEqual(f_state, {"state": "error", "state_variables": { "change_type": "modified", "error_message": error}}) @inlineCallbacks def test_depart(self): """When the workflow is transition to the down state, a relation broken hook is executed, and the unit stops responding to relation changes. """ with (yield self.workflow.lock()): yield self.workflow.fire_transition("start") yield self.assertState(self.workflow, "up") wait_on_hook = self.wait_on_hook("app-relation-changed") states = yield self.add_opposite_service_unit(self.states) yield wait_on_hook wait_on_hook = self.wait_on_hook("app-relation-broken") wait_on_state = self.wait_on_state(self.workflow, "departed") with (yield self.workflow.lock()): yield self.workflow.fire_transition("depart") yield wait_on_hook yield wait_on_state # verify further changes to the related unit don't result in # hook executions. results = [] def collect_executions(*args): results.append(args) self.executor.set_observer(collect_executions) yield states["unit_relation"].set_data(dict(a=1)) # Sleep to give errors a chance. yield self.sleep(0.1) self.assertFalse(results) def test_lifecycle_attribute(self): """The workflow lifecycle is accessible from the workflow.""" self.assertIdentical(self.workflow.lifecycle, self.lifecycle) @inlineCallbacks def test_client_read_none(self): workflow = WorkflowStateClient( self.client, self.states["unit_relation"]) self.assertEqual(None, (yield workflow.get_state())) @inlineCallbacks def test_client_read_state(self): """The relation workflow client can read the state of a unit relation.""" with (yield self.workflow.lock()): yield self.workflow.fire_transition("start") yield self.assertState(self.workflow, "up") self.write_hook("%s-relation-changed" % self.relation_name, "#!/bin/bash\necho hello\n") wait_on_hook = self.wait_on_hook("app-relation-changed") yield self.add_opposite_service_unit(self.states) yield wait_on_hook workflow = WorkflowStateClient( self.client, self.states["unit_relation"]) self.assertEqual("up", (yield workflow.get_state())) @inlineCallbacks def test_client_read_only(self): workflow_client = WorkflowStateClient( self.client, self.states["unit_relation"]) with (yield workflow_client.lock()): yield self.assertFailure( workflow_client.set_state("up"), NotImplementedError) @inlineCallbacks def assert_synchronize(self, start_state, state, watches, scheduler, sync_watches=None, sync_scheduler=None, start_inflight=None): # Handle cases where we expect to be in a different state pre-sync # to the final state post-sync. if sync_watches is None: sync_watches = watches if sync_scheduler is None: sync_scheduler = scheduler super_sync = WorkflowState.synchronize @inlineCallbacks def check_sync(obj): self.assertEquals( self.workflow.lifecycle.watching, sync_watches) self.assertEquals( self.workflow.lifecycle.executing, sync_scheduler) yield super_sync(obj) start_states = itertools.product((True, False), (True, False)) for (initial_watches, initial_scheduler) in start_states: yield self.workflow.lifecycle.stop() yield self.workflow.lifecycle.start( start_watches=initial_watches, start_scheduler=initial_scheduler) self.assertEquals( self.workflow.lifecycle.watching, initial_watches) self.assertEquals( self.workflow.lifecycle.executing, initial_scheduler) with (yield self.workflow.lock()): yield self.workflow.set_state(start_state) yield self.workflow.set_inflight(start_inflight) # self.patch is not suitable because we can't unpatch until # the end of the test, and we don't really want 13 distinct # one-line test_synchronize_foo methods. WorkflowState.synchronize = check_sync try: yield self.workflow.synchronize() finally: WorkflowState.synchronize = super_sync new_inflight = yield self.workflow.get_inflight() self.assertEquals(new_inflight, None) new_state = yield self.workflow.get_state() self.assertEquals(new_state, state) self.assertEquals(self.workflow.lifecycle.watching, watches) self.assertEquals(self.workflow.lifecycle.executing, scheduler) @inlineCallbacks def test_synchronize(self): # No transition in flight yield self.assert_synchronize(None, "up", True, True, False, False) yield self.assert_synchronize("down", "down", False, False) yield self.assert_synchronize("departed", "departed", False, False) yield self.assert_synchronize("error", "error", True, False) yield self.assert_synchronize("up", "up", True, True) # With transition inflight yield self.assert_synchronize( None, "up", True, True, False, False, "start") yield self.assert_synchronize( "up", "down", False, False, True, True, "stop") yield self.assert_synchronize( "down", "up", True, True, False, False, "restart") yield self.assert_synchronize( "up", "error", True, False, True, True, "error") yield self.assert_synchronize( "error", "up", True, True, True, False, "reset") yield self.assert_synchronize( "up", "departed", False, False, True, True, "depart") yield self.assert_synchronize( "down", "departed", False, False, False, False, "down_depart") yield self.assert_synchronize( "error", "departed", False, False, True, False, "error_depart") @inlineCallbacks def test_depart_hook_error(self): """A depart hook error, still results in a transition to the departed state with a state variable noting the error.""" self.write_hook("%s-relation-broken" % self.relation_name, "#!/bin/bash\nexit 1\n") error_output = self.capture_logging("unit.relation.workflow") with (yield self.workflow.lock()): yield self.workflow.fire_transition("start") yield self.assertState(self.workflow, "up") wait_on_hook = self.wait_on_hook("app-relation-changed") states = yield self.add_opposite_service_unit(self.states) yield wait_on_hook wait_on_hook = self.wait_on_hook("app-relation-broken") wait_on_state = self.wait_on_state(self.workflow, "departed") with (yield self.workflow.lock()): yield self.workflow.fire_transition("depart") yield wait_on_hook yield wait_on_state # verify further changes to the related unit don't result in # hook executions. results = [] def collect_executions(*args): results.append(args) self.executor.set_observer(collect_executions) yield states["unit_relation"].set_data(dict(a=1)) # Sleep to give errors a chance. yield self.sleep(0.1) self.assertFalse(results) # Verify final state and log output. msg = "Depart hook error, ignoring: " error_msg = "Error processing " error_msg += repr(os.path.join( self.unit_directory, "charm", "hooks", "app-relation-broken")) error_msg += ": exit code 1." self.assertEqual( error_output.getvalue(), (msg + error_msg + "\n")) current_state = yield self.workflow.get_state() self.assertEqual(current_state, "departed") f_state, history, zk_state = yield self.read_persistent_state( history_id=self.workflow.zk_state_id) self.assertEqual(f_state, zk_state) self.assertEqual(f_state, {"state": "departed", "state_variables": { "change_type": "depart", "error_message": error_msg}}) def test_depart_down(self): """When the workflow is transitioned from down to departed, a relation broken hook is executed, and the unit stops responding to relation changes. """ with (yield self.workflow.lock()): yield self.workflow.fire_transition("start") yield self.assertState(self.workflow, "up") yield self.workflow.fire_transition("stop") yield self.assertState(self.workflow, "down") states = yield self.add_opposite_service_unit(self.states) wait_on_hook = self.wait_on_hook("app-relation-broken") wait_on_state = self.wait_on_state(self.workflow, "departed") with (yield self.workflow.lock()): yield self.workflow.fire_transition("depart") yield wait_on_hook yield wait_on_state # Verify further changes to the related unit don't result in # hook executions. results = [] def collect_executions(*args): results.append(args) self.executor.set_observer(collect_executions) yield states["unit_relation"].set_data(dict(a=1)) # Sleep to give errors a chance. yield self.sleep(0.1) self.assertFalse(results) def test_depart_error(self): with (yield self.workflow.lock()): yield self.workflow.fire_transition("start") yield self.assertState(self.workflow, "up") yield self.workflow.fire_transition("error") yield self.assertState(self.workflow, "error") states = yield self.add_opposite_service_unit(self.states) wait_on_hook = self.wait_on_hook("app-relation-broken") wait_on_state = self.wait_on_state(self.workflow, "departed") with (yield self.workflow.lock()): yield self.workflow.fire_transition("depart") yield wait_on_hook yield wait_on_state # Verify further changes to the related unit don't result in # hook executions. results = [] def collect_executions(*args): results.append(args) self.executor.set_observer(collect_executions) yield states["unit_relation"].set_data(dict(a=1)) # Sleep to give errors a chance. yield self.sleep(0.1) self.assertFalse(results)
class UnitRelationLifecycleTest(LifecycleTestBase): hook_template = ( "#!/bin/bash\n" "echo %(change_type)s >> %(file_path)s\n" "echo JUJU_RELATION=$JUJU_RELATION >> %(file_path)s\n" "echo JUJU_REMOTE_UNIT=$JUJU_REMOTE_UNIT >> %(file_path)s") @inlineCallbacks def setUp(self): yield super(UnitRelationLifecycleTest, self).setUp() yield self.setup_default_test_relation() self.relation_name = self.states["service_relation"].relation_name self.lifecycle = UnitRelationLifecycle( self.client, self.states["unit"].unit_name, self.states["unit_relation"], self.states["service_relation"].relation_name, self.unit_directory, self.executor) self.log_stream = self.capture_logging("unit.relation.lifecycle", logging.DEBUG) @inlineCallbacks def test_initial_start_lifecycle_no_related_no_exec(self): """ If there are no related units on startup, the relation joined hook is not invoked. """ file_path = self.makeFile() self.write_hook( "%s-relation-changed" % self.relation_name, ("/bin/bash\n" "echo executed >> %s\n" % file_path)) yield self.lifecycle.start() self.assertFalse(os.path.exists(file_path)) @inlineCallbacks def test_stop_can_continue_watching(self): """ """ file_path = self.makeFile() self.write_hook( "%s-relation-changed" % self.relation_name, ("#!/bin/bash\n" "echo executed >> %s\n" % file_path)) rel_states = yield self.add_opposite_service_unit(self.states) yield self.lifecycle.start() yield self.wait_on_hook( sequence=["app-relation-joined", "app-relation-changed"]) changed_executed = self.wait_on_hook("app-relation-changed") yield self.lifecycle.stop(watches=False) rel_states["unit_relation"].set_data(yaml.dump(dict(hello="world"))) # Sleep to give an error a chance. yield self.sleep(0.1) self.assertFalse(changed_executed.called) yield self.lifecycle.start(watches=False) yield changed_executed @inlineCallbacks def test_initial_start_lifecycle_with_related(self): """ If there are related units on startup, the relation changed hook is invoked. """ yield self.add_opposite_service_unit(self.states) file_path = self.makeFile() self.write_hook("%s-relation-joined" % self.relation_name, self.hook_template % dict(change_type="joined", file_path=file_path)) self.write_hook("%s-relation-changed" % self.relation_name, self.hook_template % dict(change_type="changed", file_path=file_path)) yield self.lifecycle.start() yield self.wait_on_hook( sequence=["app-relation-joined", "app-relation-changed"]) self.assertTrue(os.path.exists(file_path)) contents = open(file_path).read() self.assertEqual(contents, ("joined\n" "JUJU_RELATION=app\n" "JUJU_REMOTE_UNIT=wordpress/0\n" "changed\n" "JUJU_RELATION=app\n" "JUJU_REMOTE_UNIT=wordpress/0\n")) @inlineCallbacks def test_hooks_executed_during_lifecycle_start_stop_start(self): """If the unit relation lifecycle is stopped, hooks will no longer be executed.""" file_path = self.makeFile() self.write_hook("%s-relation-joined" % self.relation_name, self.hook_template % dict(change_type="joined", file_path=file_path)) self.write_hook("%s-relation-changed" % self.relation_name, self.hook_template % dict(change_type="changed", file_path=file_path)) # starting is async yield self.lifecycle.start() # stopping is sync. self.lifecycle.stop() # Add a related unit. yield self.add_opposite_service_unit(self.states) # Give a chance for things to go bad yield self.sleep(0.1) self.assertFalse(os.path.exists(file_path)) # Now start again yield self.lifecycle.start() # Verify we get our join event. yield self.wait_on_hook("app-relation-changed") self.assertTrue(os.path.exists(file_path)) @inlineCallbacks def test_hook_error_handler(self): # use an error handler that completes async. self.write_hook("app-relation-joined", "#!/bin/bash\nexit 0\n") self.write_hook("app-relation-changed", "#!/bin/bash\nexit 1\n") results = [] finish_callback = Deferred() @inlineCallbacks def error_handler(change, e): yield self.client.create( "/errors", str(e), flags=zookeeper.EPHEMERAL | zookeeper.SEQUENCE) results.append((change.change_type, e)) yield self.lifecycle.stop() finish_callback.callback(True) self.lifecycle.set_hook_error_handler(error_handler) # Add a related unit. yield self.add_opposite_service_unit(self.states) yield self.lifecycle.start() yield finish_callback self.assertEqual(len(results), 1) self.assertTrue(results[0][0], "joined") self.assertTrue(isinstance(results[0][1], CharmInvocationError)) hook_relative_path = "charm/hooks/app-relation-changed" output = ( "started relation:app lifecycle", "Executing hook app-relation-joined", "Executing hook app-relation-changed", "Error in app-relation-changed hook: %s '%s/%s': exit code 1." % ( "Error processing", self.unit_directory, hook_relative_path), "Invoked error handler for app-relation-changed hook", "stopped relation:app lifecycle\n") self.assertEqual(self.log_stream.getvalue(), "\n".join(output)) @inlineCallbacks def test_depart(self): """If a relation is departed, the depart hook is executed. """ file_path = self.makeFile() self.write_hook("%s-relation-joined" % self.relation_name, "#!/bin/bash\n echo joined") self.write_hook("%s-relation-changed" % self.relation_name, "#!/bin/bash\n echo hello") self.write_hook("%s-relation-broken" % self.relation_name, self.hook_template % dict(change_type="broken", file_path=file_path)) yield self.lifecycle.start() wordpress_states = yield self.add_opposite_service_unit(self.states) yield self.wait_on_hook( sequence=["app-relation-joined", "app-relation-changed"]) yield self.lifecycle.stop() yield self.relation_manager.remove_relation_state( wordpress_states["relation"]) hook_complete = self.wait_on_hook("app-relation-broken") yield self.lifecycle.depart() yield hook_complete self.assertTrue(os.path.exists(file_path)) @inlineCallbacks def test_lock_start_stop(self): """ The relation lifecycle, internally uses a lock when its interacting with zk, and acquires the lock to protct its internal data structures. """ original_method = ClientServerUnitWatcher.start watcher = self.mocker.patch(ClientServerUnitWatcher) finish_callback = Deferred() @inlineCallbacks def long_op(*args): yield finish_callback yield original_method(*args) watcher.start() self.mocker.call(long_op, with_object=True) self.mocker.replay() start_complete = self.lifecycle.start() stop_complete = self.lifecycle.stop() # Sadly this sleeping is the easiest way to verify that # the stop hasn't procesed prior to the start. yield self.sleep(0.1) self.assertFalse(start_complete.called) self.assertFalse(stop_complete.called) finish_callback.callback(True) yield start_complete self.assertTrue(stop_complete.called)
class UnitRelationLifecycleTest(LifecycleTestBase): hook_template = ( "#!/bin/bash\n" "echo %(change_type)s >> %(file_path)s\n" "echo JUJU_RELATION=$JUJU_RELATION >> %(file_path)s\n" "echo JUJU_REMOTE_UNIT=$JUJU_REMOTE_UNIT >> %(file_path)s") @inlineCallbacks def setUp(self): yield super(UnitRelationLifecycleTest, self).setUp() yield self.setup_default_test_relation() self.relation_name = self.states["service_relation"].relation_name self.lifecycle = UnitRelationLifecycle( self.client, self.states["unit"].unit_name, self.states["unit_relation"], self.states["service_relation"].relation_name, self.unit_directory, self.executor) self.log_stream = self.capture_logging("unit.relation.lifecycle", logging.DEBUG) @inlineCallbacks def test_initial_start_lifecycle_no_related_no_exec(self): """ If there are no related units on startup, the relation joined hook is not invoked. """ file_path = self.makeFile() self.write_hook("%s-relation-changed" % self.relation_name, ("/bin/bash\n" "echo executed >> %s\n" % file_path)) yield self.lifecycle.start() self.assertFalse(os.path.exists(file_path)) @inlineCallbacks def test_stop_can_continue_watching(self): """ """ file_path = self.makeFile() self.write_hook("%s-relation-changed" % self.relation_name, ("#!/bin/bash\n" "echo executed >> %s\n" % file_path)) rel_states = yield self.add_opposite_service_unit(self.states) yield self.lifecycle.start() yield self.wait_on_hook( sequence=["app-relation-joined", "app-relation-changed"]) changed_executed = self.wait_on_hook("app-relation-changed") yield self.lifecycle.stop(watches=False) rel_states["unit_relation"].set_data(yaml.dump(dict(hello="world"))) # Sleep to give an error a chance. yield self.sleep(0.1) self.assertFalse(changed_executed.called) yield self.lifecycle.start(watches=False) yield changed_executed @inlineCallbacks def test_initial_start_lifecycle_with_related(self): """ If there are related units on startup, the relation changed hook is invoked. """ yield self.add_opposite_service_unit(self.states) file_path = self.makeFile() self.write_hook( "%s-relation-joined" % self.relation_name, self.hook_template % dict(change_type="joined", file_path=file_path)) self.write_hook( "%s-relation-changed" % self.relation_name, self.hook_template % dict(change_type="changed", file_path=file_path)) yield self.lifecycle.start() yield self.wait_on_hook( sequence=["app-relation-joined", "app-relation-changed"]) self.assertTrue(os.path.exists(file_path)) contents = open(file_path).read() self.assertEqual(contents, ("joined\n" "JUJU_RELATION=app\n" "JUJU_REMOTE_UNIT=wordpress/0\n" "changed\n" "JUJU_RELATION=app\n" "JUJU_REMOTE_UNIT=wordpress/0\n")) @inlineCallbacks def test_hooks_executed_during_lifecycle_start_stop_start(self): """If the unit relation lifecycle is stopped, hooks will no longer be executed.""" file_path = self.makeFile() self.write_hook( "%s-relation-joined" % self.relation_name, self.hook_template % dict(change_type="joined", file_path=file_path)) self.write_hook( "%s-relation-changed" % self.relation_name, self.hook_template % dict(change_type="changed", file_path=file_path)) # starting is async yield self.lifecycle.start() # stopping is sync. self.lifecycle.stop() # Add a related unit. yield self.add_opposite_service_unit(self.states) # Give a chance for things to go bad yield self.sleep(0.1) self.assertFalse(os.path.exists(file_path)) # Now start again yield self.lifecycle.start() # Verify we get our join event. yield self.wait_on_hook("app-relation-changed") self.assertTrue(os.path.exists(file_path)) @inlineCallbacks def test_hook_error_handler(self): # use an error handler that completes async. self.write_hook("app-relation-joined", "#!/bin/bash\nexit 0\n") self.write_hook("app-relation-changed", "#!/bin/bash\nexit 1\n") results = [] finish_callback = Deferred() @inlineCallbacks def error_handler(change, e): yield self.client.create("/errors", str(e), flags=zookeeper.EPHEMERAL | zookeeper.SEQUENCE) results.append((change.change_type, e)) yield self.lifecycle.stop() finish_callback.callback(True) self.lifecycle.set_hook_error_handler(error_handler) # Add a related unit. yield self.add_opposite_service_unit(self.states) yield self.lifecycle.start() yield finish_callback self.assertEqual(len(results), 1) self.assertTrue(results[0][0], "joined") self.assertTrue(isinstance(results[0][1], CharmInvocationError)) hook_relative_path = "charm/hooks/app-relation-changed" output = ( "started relation:app lifecycle", "Executing hook app-relation-joined", "Executing hook app-relation-changed", "Error in app-relation-changed hook: %s '%s/%s': exit code 1." % ("Error processing", self.unit_directory, hook_relative_path), "Invoked error handler for app-relation-changed hook", "stopped relation:app lifecycle\n") self.assertEqual(self.log_stream.getvalue(), "\n".join(output)) @inlineCallbacks def test_depart(self): """If a relation is departed, the depart hook is executed. """ file_path = self.makeFile() self.write_hook("%s-relation-joined" % self.relation_name, "#!/bin/bash\n echo joined") self.write_hook("%s-relation-changed" % self.relation_name, "#!/bin/bash\n echo hello") self.write_hook( "%s-relation-broken" % self.relation_name, self.hook_template % dict(change_type="broken", file_path=file_path)) yield self.lifecycle.start() wordpress_states = yield self.add_opposite_service_unit(self.states) yield self.wait_on_hook( sequence=["app-relation-joined", "app-relation-changed"]) yield self.lifecycle.stop() yield self.relation_manager.remove_relation_state( wordpress_states["relation"]) hook_complete = self.wait_on_hook("app-relation-broken") yield self.lifecycle.depart() yield hook_complete self.assertTrue(os.path.exists(file_path)) @inlineCallbacks def test_lock_start_stop(self): """ The relation lifecycle, internally uses a lock when its interacting with zk, and acquires the lock to protct its internal data structures. """ original_method = ClientServerUnitWatcher.start watcher = self.mocker.patch(ClientServerUnitWatcher) finish_callback = Deferred() @inlineCallbacks def long_op(*args): yield finish_callback yield original_method(*args) watcher.start() self.mocker.call(long_op, with_object=True) self.mocker.replay() start_complete = self.lifecycle.start() stop_complete = self.lifecycle.stop() # Sadly this sleeping is the easiest way to verify that # the stop hasn't procesed prior to the start. yield self.sleep(0.1) self.assertFalse(start_complete.called) self.assertFalse(stop_complete.called) finish_callback.callback(True) yield start_complete self.assertTrue(stop_complete.called)