# -*- coding: utf-8; -*- import os import shutil import time import tempfile import queue from unittest import TestCase from mock import Mock, patch, call from rattail.config import make_config, RattailConfig, ConfigProfileAction from rattail.filemon import actions from rattail.filemon.config_ import Profile class TestAction(TestCase): def test_callable_must_be_implemented_in_subclass(self): config = make_config([], extend=False) action = actions.Action(config) self.assertRaises(NotImplementedError, action) @patch(u'rattail.filemon.actions.noop') class TestPerformActions(TestCase): def setUp(self): self.tempdir = tempfile.mkdtemp() self.config = self.make_config() # Must delay creating the profile since doing it here would bypass our mock of noop. def tearDown(self): shutil.rmtree(self.tempdir) def make_config(self, foo_actions=True): cfg = make_config([], extend=False) cfg.setdefault('rattail.filemon', 'monitor', 'foo') cfg.setdefault('rattail.filemon', 'foo.dirs', self.tempdir) if foo_actions: cfg.setdefault('rattail.filemon', 'foo.actions', 'noop') cfg.setdefault('rattail.filemon', 'foo.action.noop.func', 'rattail.filemon.actions:noop') return cfg def write_file(self, fname, content): path = os.path.join(self.tempdir, fname) with open(path, 'wt') as f: f.write(content) return path def get_profile(self, stop_on_error=False): profile = Profile(self.config, u'foo') profile.stop_on_error = stop_on_error profile.queue = Mock() profile.queue.get_nowait.side_effect = [ queue.Empty, # for coverage sake; will be effectively skipped self.write_file('file1', ''), self.write_file('file2', ''), self.write_file('file3', ''), actions.StopProcessing, ] return profile def test_action_is_invoked_for_each_file_in_queue(self, noop): profile = self.get_profile() actions.perform_actions(profile) self.assertEqual(noop.call_count, 3) noop.assert_has_calls([ call(os.path.join(self.tempdir, 'file1')), call(os.path.join(self.tempdir, 'file2')), call(os.path.join(self.tempdir, 'file3')), ]) def test_action_is_skipped_for_nonexistent_file(self, noop): profile = self.get_profile() os.remove(os.path.join(self.tempdir, 'file2')) actions.perform_actions(profile) self.assertEqual(noop.call_count, 2) # no call for file2 noop.assert_has_calls([ call(os.path.join(self.tempdir, 'file1')), call(os.path.join(self.tempdir, 'file3')), ]) def test_action_which_raises_error_causes_subsequent_actions_to_be_skipped_for_same_file(self, noop): self.config = self.make_config(foo_actions=False) self.config.setdefault('rattail.filemon', 'foo.actions', 'noop, delete') self.config.setdefault('rattail.filemon', 'foo.action.delete.func', 'os:remove') profile = self.get_profile() # processing second file fails, so it shouldn't be deleted noop.side_effect = [None, RuntimeError, None] actions.perform_actions(profile) self.assertFalse(os.path.exists(os.path.join(self.tempdir, 'file1'))) self.assertTrue(os.path.exists(os.path.join(self.tempdir, 'file2'))) self.assertFalse(os.path.exists(os.path.join(self.tempdir, 'file3'))) def test_action_which_raises_error_causes_all_processing_to_stop_if_so_configured(self, noop): self.config = self.make_config(foo_actions=False) self.config.setdefault('rattail.filemon', 'foo.actions', 'noop, delete') self.config.setdefault('rattail.filemon', 'foo.action.delete.func', 'os:remove') profile = self.get_profile(stop_on_error=True) # processing second file fails; third file shouldn't be processed at all noop.side_effect = [None, RuntimeError, None] actions.perform_actions(profile) self.assertEqual(noop.call_count, 2) noop.assert_has_calls([ call(os.path.join(self.tempdir, 'file1')), call(os.path.join(self.tempdir, 'file2')), ]) self.assertFalse(os.path.exists(os.path.join(self.tempdir, 'file1'))) self.assertTrue(os.path.exists(os.path.join(self.tempdir, 'file2'))) self.assertTrue(os.path.exists(os.path.join(self.tempdir, 'file3'))) class TestInvokeAction(TestCase): def setUp(self): self.action = ConfigProfileAction() self.action.config = RattailConfig() self.action.action = Mock(return_value=None) self.action.retry_attempts = 6 self.tempdir = tempfile.mkdtemp() self.file = self.write_file('file', '') def tearDown(self): shutil.rmtree(self.tempdir) def write_file(self, fname, content): path = os.path.join(self.tempdir, fname) with open(path, 'wt') as f: f.write(content) return path def test_action_which_succeeds_is_only_called_once(self): actions.invoke_action(self.action, self.file) self.assertEqual(self.action.action.call_count, 1) def test_action_with_no_delay_does_not_pause_between_attempts(self): self.action.retry_attempts = 3 self.action.action.side_effect = [RuntimeError, RuntimeError, None] start = time.time() actions.invoke_action(self.action, self.file) self.assertEqual(self.action.action.call_count, 3) self.assertTrue(time.time() - start < 1.0) def test_action_with_delay_pauses_between_attempts(self): self.action.retry_attempts = 3 self.action.retry_delay = 1 self.action.action.side_effect = [RuntimeError, RuntimeError, None] start = time.time() actions.invoke_action(self.action, self.file) self.assertEqual(self.action.action.call_count, 3) self.assertTrue(time.time() - start >= 2.0) # TODO: must mock out AppHandler.send_email() instead # def test_action_which_fails_is_only_attempted_the_specified_number_of_times(self): # self.action.action.side_effect = RuntimeError # # Last attempt will not handle the exception; assert that as well. # with patch('rattail.filemon.actions.send_email') as send_email: # self.assertRaises(RuntimeError, actions.invoke_action, self.action, self.file) # self.assertEqual(self.action.action.call_count, 6) def test_action_which_fails_then_succeeds_stops_retrying(self): # First 2 attempts fail, third succeeds. self.action.action.side_effect = [RuntimeError, RuntimeError, None] actions.invoke_action(self.action, self.file) self.assertEqual(self.action.action.call_count, 3) def test_action_which_fails_with_different_errors_stops_retrying(self): self.action.action.side_effect = [ValueError, TypeError, None] # Second attempt will not handle the exception; assert that as well. self.assertRaises(TypeError, actions.invoke_action, self.action, self.file) self.assertEqual(self.action.action.call_count, 2) class TestRaiseException(TestCase): def test_exception_is_raised(self): # this hardly deserves a test, but what the hell self.assertRaises(Exception, actions.raise_exception, '/dev/null')