<commit_before># -*- coding: utf-8 -*-

# FOGLAMP_BEGIN
# See: http://foglamp.readthedocs.io/
# FOGLAMP_END

import asyncio
import datetime
import uuid
import pytest
import time
from unittest.mock import MagicMock, call, Mock
from foglamp.services.core.scheduler.scheduler import Scheduler, AuditLogger, ConfigurationManager
from foglamp.services.core.scheduler.entities import Task, Schedule
from foglamp.services.core.scheduler.exceptions import *
from foglamp.common.storage_client.storage_client import StorageClient

__author__ = "<NAME>"
__copyright__ = "Copyright (c) 2017 OSIsoft, LLC"
__license__ = "Apache 2.0"
__version__ = "${VERSION}"


@asyncio.coroutine
def mock_process():
    m = MagicMock()
    m.pid = 9999
    m.terminate = lambda: True
    return m


@pytest.allure.feature("unit")
@pytest.allure.story("scheduler")
class TestScheduler:
    async def scheduler_fixture(self, mocker):
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)
        mocker.patch.object(scheduler, '_schedule_first_task')
        mocker.patch.object(scheduler, '_ready', True)
        mocker.patch.object(scheduler, '_paused', False)
        mocker.patch.object(scheduler, '_process_scripts', return_value="North Readings to PI")
        mocker.patch.object(scheduler, '_wait_for_task_completion', return_value=mock_process())
        mocker.patch.object(scheduler, '_terminate_child_processes')
        mocker.patch.object(asyncio, 'create_subprocess_exec', return_value=mock_process())

        await scheduler._get_schedules()

        schedule = scheduler._ScheduleRow(
            id=uuid.UUID("2b614d26-760f-11e7-b5a5-be2e44b06b34"),
            process_name="North Readings to PI",
            name="OMF to PI north",
            type=Schedule.Type.INTERVAL,
            repeat=datetime.timedelta(seconds=30),
            repeat_seconds=30,
            time=None,
            day=None,
            exclusive=True,
            enabled=True)

        log_exception = mocker.patch.object(scheduler._logger, "exception")
        log_error = mocker.patch.object(scheduler._logger, "error")
        log_debug = mocker.patch.object(scheduler._logger, "debug")
        log_info = mocker.patch.object(scheduler._logger, "info")

        return scheduler, schedule, log_info, log_exception, log_error, log_debug

    @pytest.mark.asyncio
    async def test__resume_check_schedules(self, mocker):
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)

        # WHEN
        # Check IF part
        mocker.patch.object(scheduler, '_scheduler_loop_sleep_task', asyncio.Task(asyncio.sleep(5)))
        scheduler._resume_check_schedules()

        # THEN
        assert scheduler._check_processes_pending is False

        # WHEN
        # Check ELSE part
        mocker.patch.object(scheduler, '_scheduler_loop_sleep_task', None)
        scheduler._resume_check_schedules()

        # THEN
        assert scheduler._check_processes_pending is True

    @pytest.mark.asyncio
    async def test__wait_for_task_completion(self, mocker):
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)

        mock_schedules = dict()
        mock_schedule = scheduler._ScheduleRow(
            id=uuid.UUID("2b614d26-760f-11e7-b5a5-be2e44b06b34"),
            process_name="North Readings to PI",
            name="OMF to PI north",
            type=Schedule.Type.INTERVAL,
            repeat=datetime.timedelta(seconds=30),
            repeat_seconds=30,
            time=None,
            day=None,
            exclusive=True,
            enabled=True)
        mock_schedules[mock_schedule.id] = mock_schedule

        mock_task_process = scheduler._TaskProcess()
        mock_task_processes = dict()
        mock_task_process.process = await asyncio.create_subprocess_exec("sleep", ".1")
        mock_task_process.schedule = mock_schedule
        mock_task_id = uuid.uuid4()
        mock_task_process.task_id = mock_task_id
        mock_task_processes[mock_task_process.task_id] = mock_task_process

        mock_schedule_executions = dict()
        mock_schedule_execution = scheduler._ScheduleExecution()
        mock_schedule_executions[mock_schedule.id] = mock_schedule_execution
        mock_schedule_executions[mock_schedule.id].task_processes[mock_task_id] = mock_task_process

        mocker.patch.object(scheduler, '_resume_check_schedules')
        mocker.patch.object(scheduler, '_schedule_next_task')
        mocker.patch.multiple(scheduler, _schedules=mock_schedules,
                              _task_processes=mock_task_processes,
                              _schedule_executions=mock_schedule_executions)
        mocker.patch.object(scheduler, '_process_scripts', return_value="North Readings to PI")

        # WHEN
        await scheduler._wait_for_task_completion(mock_task_process)

        # THEN
        # After task completion, sleep above, no task processes should be left pending
        assert 0 == len(scheduler._task_processes)
        assert 0 == len(scheduler._schedule_executions[mock_schedule.id].task_processes)

    @pytest.mark.asyncio
    async def test__start_task(self, mocker):
        # TODO: Mandatory - Add negative tests for full code coverage
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)
        mocker.patch.object(scheduler, '_schedule_first_task')
        await scheduler._get_schedules()

        schedule = scheduler._ScheduleRow(
            id=uuid.UUID("2b614d26-760f-11e7-b5a5-be2e44b06b34"),
            process_name="North Readings to PI",
            name="OMF to PI north",
            type=Schedule.Type.INTERVAL,
            repeat=datetime.timedelta(seconds=30),
            repeat_seconds=30,
            time=None,
            day=None,
            exclusive=True,
            enabled=True)

        mocker.patch.object(scheduler, '_ready', True)
        mocker.patch.object(scheduler, '_resume_check_schedules')

        # Assert that there is no task queued for mock_schedule
        with pytest.raises(KeyError) as excinfo:
            assert scheduler._schedule_executions[schedule.id] is True
        # Now queue task and assert that the task has been queued
        await scheduler.queue_task(schedule.id)
        assert isinstance(scheduler._schedule_executions[schedule.id], scheduler._ScheduleExecution)

        mocker.patch.object(asyncio, 'create_subprocess_exec', return_value=mock_process())
        mocker.patch.object(asyncio, 'ensure_future', return_value=mock_process())
        mocker.patch.object(scheduler, '_resume_check_schedules')
        mocker.patch.object(scheduler, '_process_scripts', return_value="North Readings to PI")
        mocker.patch.object(scheduler, '_wait_for_task_completion')

        # Confirm that task has not started yet
        assert 0 == len(scheduler._schedule_executions[schedule.id].task_processes)

        # WHEN
        await scheduler._start_task(schedule)

        # THEN
        # Confirm that task has started
        assert 1 == len(scheduler._schedule_executions[schedule.id].task_processes)

    @pytest.mark.asyncio
    async def test_purge_tasks(self, mocker):
        # TODO: Mandatory - Add negative tests for full code coverage
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)
        mocker.patch.multiple(scheduler, _ready=True, _paused=False)
        mocker.patch.object(scheduler, '_max_completed_task_age', datetime.datetime.now())

        # WHEN
        await scheduler.purge_tasks()

        # THEN
        assert scheduler._purge_tasks_task is None
        assert scheduler._last_task_purge_time is not None

    def test__check_purge_tasks(self, mocker):
        # TODO: Mandatory - Add negative tests for full code coverage
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)
        mocker.patch.multiple(scheduler, _purge_tasks_task=None,
                              _last_task_purge_time=None)
        mocker.patch.object(scheduler, 'purge_tasks', return_value=mock_process())

        # WHEN
        scheduler._check_purge_tasks()

        # THEN
        assert scheduler._purge_tasks_task is not None

    @pytest.mark.asyncio
    async def test__check_schedules(self, mocker):
        # TODO: Mandatory - Add negative tests for full code coverage
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)
        current_time = time.time()
        mocker.patch.multiple(scheduler, _max_running_tasks=10,
                              _start_time=current_time - 3600)
        await scheduler._get_schedules()
        mocker.patch.object(scheduler, '_start_task', return_value=mock_process())

        # WHEN
        earliest_start_time = await scheduler._check_schedules()

        # THEN
        assert earliest_start_time is not None

    @pytest.mark.asyncio
    @pytest.mark.skip("_scheduler_loop() not suitable for unit testing. Will be tested during System tests.")
    async def test__scheduler_loop(self, mocker):
        pass

    @pytest.mark.asyncio
    async def test__schedule_next_timed_task(self, mocker):
        # TODO: Mandatory - Add negative tests for full code coverage
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)
        current_time = time.time()
        mocker.patch.multiple(scheduler, _max_running_tasks=10,
                              _start_time=current_time - 3600)
        await scheduler._get_schedules()

        sch_id = uuid.UUID("2176eb68-7303-11e7-8cf7-a6006ad3dba0")  # stat collector
        sch = scheduler._schedules[sch_id]
        sch_execution = scheduler._schedule_executions[sch_id]
        time_before_call = sch_execution.next_start_time

        # WHEN
        next_dt = datetime.datetime.fromtimestamp(sch_execution.next_start_time)
        next_dt += datetime.timedelta(seconds=sch.repeat_seconds)
        scheduler._schedule_next_timed_task(sch, sch_execution, next_dt)
        time_after_call = sch_execution.next_start_time

        # THEN
        assert time_after_call > time_before_call

    @pytest.mark.asyncio
    async def test__schedule_next_task(self, mocker):
        # TODO: Mandatory - Add negative tests for full code coverage
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)
        current_time = time.time()
        mocker.patch.multiple(scheduler, _max_running_tasks=10,
                              _start_time=current_time - 3600)
        await scheduler._get_schedules()

        sch_id = uuid.UUID("2176eb68-7303-11e7-8cf7-a6006ad3dba0")  # stat collector
        sch = scheduler._schedules[sch_id]
        sch_execution = scheduler._schedule_executions[sch_id]
        time_before_call = sch_execution.next_start_time

        # WHEN
        scheduler._schedule_next_task(sch)
        time_after_call = sch_execution.next_start_time

        # THEN
        assert time_after_call > time_before_call

    async def test__schedule_first_task(self, mocker):
        # TODO: Mandatory - Add negative tests for full code coverage
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)
        current_time = time.time()
        curr_time = datetime.datetime.fromtimestamp(current_time)
        mocker.patch.multiple(scheduler, _max_running_tasks=10,
                              _start_time=current_time - 3600)
        await scheduler._get_schedules()

        sch_id = uuid.UUID("2176eb68-7303-11e7-8cf7-a6006ad3dba0")  # stat collector
        sch = scheduler._schedules[sch_id]
        sch_execution = scheduler._schedule_executions[sch_id]

        # WHEN
        scheduler._schedule_first_task(sch, current_time)
        time_after_call = sch_execution.next_start_time

        # THEN
        assert time_after_call > time.mktime(curr_time.timetuple())

    @pytest.mark.asyncio
    async def test__get_process_scripts(self, mocker):
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)

        # WHEN
        await scheduler._get_process_scripts()

        # THEN
        assert len(scheduler._storage.scheduled_processes) == len(scheduler._process_scripts)

    @pytest.mark.asyncio
    async def test__get_process_scripts_exception(self, mocker):
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)
        log_debug = mocker.patch.object(scheduler._logger, "debug", side_effect=Exception())

        # WHEN
        # THEN
        with pytest.raises(Exception):
            await scheduler._get_process_scripts()

    @pytest.mark.asyncio
    async def test__get_schedules(self, mocker):
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)
        mocker.patch.object(scheduler, '_schedule_first_task')

        # WHEN
        await scheduler._get_schedules()

        # THEN
        assert len(scheduler._storage.schedules) == len(scheduler._schedules)

    @pytest.mark.asyncio
    async def test__get_schedules_exception(self, mocker):
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)
        mocker.patch.object(scheduler, '_schedule_first_task', side_effect=Exception())

        # WHEN
        # THEN
        with pytest.raises(Exception):
            await scheduler._get_schedules()

    @pytest.mark.asyncio
    async def test__read_storage(self, mocker):
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)
        mocker.patch.object(scheduler, '_schedule_first_task')

        # WHEN
        await scheduler._read_storage()

        # THEN
        assert len(scheduler._storage.scheduled_processes) == len(scheduler._process_scripts)
        assert len(scheduler._storage.schedules) == len(scheduler._schedules)

    @pytest.mark.asyncio
    @pytest.mark.skip("_mark_tasks_interrupted() not implemented in main Scheduler class.")
    async def test__mark_tasks_interrupted(self, mocker):
        pass

    @pytest.mark.asyncio
    async def test__read_config(self, mocker):
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)
        cr_cat = mocker.patch.object(ConfigurationManager, "create_category", return_value=mock_process())

        # WHEN
        assert scheduler._max_running_tasks is None
        assert scheduler._max_completed_task_age is None
        await scheduler._read_config()

        # THEN
        assert 1 == cr_cat.call_count
        assert scheduler._max_running_tasks is not None
        assert scheduler._max_completed_task_age is not None

    @pytest.mark.asyncio
    async def test_start(self, mocker):
        # TODO: Mandatory - Add negative tests for full code coverage
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)
        current_time = time.time()
        mocker.patch.object(scheduler, '_schedule_first_task')
        mocker.patch.object(scheduler, '_scheduler_loop', return_value=mock_process())
        mocker.patch.multiple(scheduler, _core_management_port=9999,
                              _core_management_host="0.0.0.0",
                              current_time=current_time - 3600)

        # TODO: Remove after implementation of above test test__read_config()
        mocker.patch.object(scheduler, '_read_config', return_value=mock_process())

        assert scheduler._ready is False

        # WHEN
        await scheduler.start()

        # THEN
        assert scheduler._ready is True
        assert len(scheduler._storage.scheduled_processes) == len(scheduler._process_scripts)
        assert len(scheduler._storage.schedules) == len(scheduler._schedules)

    @pytest.mark.asyncio
    async def test_stop(self, mocker):
        # TODO: Mandatory - Add negative tests for full code coverage
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)
        mocker.patch.object(scheduler, '_scheduler_loop', return_value=mock_process())
        mocker.patch.object(scheduler, '_resume_check_schedules', return_value=mock_process())
        mocker.patch.object(scheduler, '_purge_tasks_task', return_value=mock_process())
        mocker.patch.object(scheduler, '_scheduler_loop_task', return_value=mock_process())
        current_time = time.time()
        mocker.patch.multiple(scheduler, _core_management_port=9999,
                              _core_management_host="0.0.0.0",
                              _start_time=current_time - 3600,
                              _paused=False,
                              _task_processes={})

        # WHEN
        retval = await scheduler.stop()

        # THEN
        assert retval is True
        assert scheduler._schedule_executions is None
        assert scheduler._task_processes is None
        assert scheduler._schedules is None
        assert scheduler._process_scripts is None
        assert scheduler._ready is False
        assert scheduler._paused is False
        assert scheduler._start_time is None

    @pytest.mark.asyncio
    async def test_get_scheduled_processes(self, mocker):
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)
        await scheduler._get_process_scripts()
        mocker.patch.object(scheduler, '_ready', True)

        # WHEN
        processes = await scheduler.get_scheduled_processes()

        # THEN
        assert len(scheduler._storage.scheduled_processes) == len(processes)

    def test_schedule_row_to_schedule(self, mocker):
        # GIVEN
        scheduler = Scheduler()
        schedule_id = uuid.uuid4()
        schedule_row = scheduler._ScheduleRow(
            id=schedule_id,
            name='Test Schedule',
            type=Schedule.Type.INTERVAL,
            day=0,
            time=0,
            repeat=10,
            repeat_seconds=10,
            exclusive=False,
            enabled=True,
            process_name='TestProcess')

        # WHEN
        schedule = scheduler._schedule_row_to_schedule(schedule_id, schedule_row)

        # THEN
        assert isinstance(schedule, Schedule)
        assert schedule.schedule_id == schedule_row[0]
        assert schedule.name == schedule_row[1]
        assert schedule.schedule_type == schedule_row[2]
        assert schedule.repeat == schedule_row[5]
        assert schedule.exclusive == schedule_row[7]
        assert schedule.enabled == schedule_row[8]
        assert schedule.process_name == schedule_row[9]

    @pytest.mark.asyncio
    async def test_get_schedules(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)

        # WHEN
        schedules = await scheduler.get_schedules()

        # THEN
        assert len(scheduler._storage.schedules) == len(schedules)

    @pytest.mark.asyncio
    async def test_get_schedule(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)
        schedule_id = uuid.UUID("cea17db8-6ccc-11e7-907b-a6006ad3dba0")  # purge schedule

        # WHEN
        schedule = await scheduler.get_schedule(schedule_id)

        # THEN
        assert isinstance(schedule, Schedule)
        assert schedule.schedule_id == schedule_id
        assert schedule.name == "purge"
        assert schedule.schedule_type == Schedule.Type.INTERVAL
        assert schedule.repeat == datetime.timedelta(0, 3600)
        assert schedule.exclusive is True
        assert schedule.enabled is True
        assert schedule.process_name == "purge"

    @pytest.mark.asyncio
    async def test_get_schedule_exception(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)
        schedule_id = uuid.uuid4()

        # WHEN
        # THEN
        with pytest.raises(ScheduleNotFoundError):
            schedule = await scheduler.get_schedule(schedule_id)

    @pytest.mark.asyncio
    async def test_save_schedule_new(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)
        audit_logger = mocker.patch.object(AuditLogger, 'information', return_value=mock_process())
        first_task = mocker.patch.object(scheduler, '_schedule_first_task')
        resume_sch = mocker.patch.object(scheduler, '_resume_check_schedules')
        log_info = mocker.patch.object(scheduler._logger, "info")
        schedule_id = uuid.uuid4()
        schedule_row = scheduler._ScheduleRow(
            id=schedule_id,
            name='Test Schedule',
            type=Schedule.Type.INTERVAL,
            day=0,
            time=0,
            repeat=datetime.timedelta(seconds=30),
            repeat_seconds=30,
            exclusive=False,
            enabled=True,
            process_name='TestProcess')
        schedule = scheduler._schedule_row_to_schedule(schedule_id, schedule_row)

        # WHEN
        await scheduler.save_schedule(schedule)

        # THEN
        assert len(scheduler._storage.schedules) + 1 == len(scheduler._schedules)
        assert 1 == audit_logger.call_count
        calls =[call('SCHAD', {'schedule': {'name': 'Test Schedule', 'processName': 'TestProcess',
                                            'type': Schedule.Type.INTERVAL, 'repeat': 30.0, 'enabled': True,
                                            'exclusive': False}})]
        audit_logger.assert_has_calls(calls, any_order=True)
        assert 1 == first_task.call_count
        assert 1 == resume_sch.call_count

    @pytest.mark.asyncio
    async def test_save_schedule_update(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)
        audit_logger = mocker.patch.object(AuditLogger, 'information', return_value=mock_process())
        first_task = mocker.patch.object(scheduler, '_schedule_first_task')
        resume_sch = mocker.patch.object(scheduler, '_resume_check_schedules')
        log_info = mocker.patch.object(scheduler._logger, "info")
        schedule_id = uuid.UUID("2b614d26-760f-11e7-b5a5-be2e44b06b34")  # OMF to PI North
        schedule_row = scheduler._ScheduleRow(
            id=schedule_id,
            name='Test Schedule',
            type=Schedule.Type.TIMED,
            day=1,
            time=datetime.time(),
            repeat=datetime.timedelta(seconds=30),
            repeat_seconds=30,
            exclusive=False,
            enabled=True,
            process_name='TestProcess')
        schedule = scheduler._schedule_row_to_schedule(schedule_id, schedule_row)

        # WHEN
        await scheduler.save_schedule(schedule)

        # THEN
        assert len(scheduler._storage.schedules) == len(scheduler._schedules)
        assert 1 == audit_logger.call_count
        calls = [call('SCHCH', {'schedule': {'name': 'Test Schedule', 'enabled': True, 'repeat': 30.0,
                                             'exclusive': False, 'day': 1, 'time': '0:0:0',
                                             'processName': 'TestProcess', 'type': Schedule.Type.TIMED}})]
        audit_logger.assert_has_calls(calls, any_order=True)
        assert 1 == first_task.call_count
        assert 1 == resume_sch.call_count

    @pytest.mark.asyncio
    async def test_save_schedule_exception(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)
        schedule_id = uuid.uuid4()
        schedule_row = scheduler._ScheduleRow(
            id=schedule_id,
            name='Test Schedule',
            type=Schedule.Type.TIMED,
            day=0,
            time=0,
            repeat=datetime.timedelta(seconds=30),
            repeat_seconds=30,
            exclusive=False,
            enabled=True,
            process_name='TestProcess')

        # WHEN
        # THEN
        with pytest.raises(ValueError) as ex:
            temp_schedule = scheduler._schedule_row_to_schedule(schedule_id, schedule_row)
            temp_schedule.name = None
            await scheduler.save_schedule(temp_schedule)
            del temp_schedule
        assert str(ex).endswith("name can not be empty")

        with pytest.raises(ValueError) as ex:
            temp_schedule = scheduler._schedule_row_to_schedule(schedule_id, schedule_row)
            temp_schedule.name = ""
            await scheduler.save_schedule(temp_schedule)
            del temp_schedule
        assert str(ex).endswith("name can not be empty")

        with pytest.raises(ValueError) as ex:
            temp_schedule = scheduler._schedule_row_to_schedule(schedule_id, schedule_row)
            temp_schedule.repeat = 1234
            await scheduler.save_schedule(temp_schedule)
            del temp_schedule
        assert str(ex).endswith('repeat must be of type datetime.timedelta')

        with pytest.raises(ValueError) as ex:
            temp_schedule = scheduler._schedule_row_to_schedule(schedule_id, schedule_row)
            temp_schedule.exclusive = None
            await scheduler.save_schedule(temp_schedule)
            del temp_schedule
        assert str(ex).endswith('exclusive can not be None')

        with pytest.raises(ValueError) as ex:
            temp_schedule = scheduler._schedule_row_to_schedule(schedule_id, schedule_row)
            temp_schedule.time = 1234
            await scheduler.save_schedule(temp_schedule)
            del temp_schedule
        assert str(ex).endswith('time must be of type datetime.time')

        with pytest.raises(ValueError) as ex:
            temp_schedule = scheduler._schedule_row_to_schedule(schedule_id, schedule_row)
            temp_schedule.day = 0
            temp_schedule.time = datetime.time()
            await scheduler.save_schedule(temp_schedule)
            del temp_schedule
        assert str(ex).endswith('day must be between 1 and 7')

    @pytest.mark.asyncio
    async def test_disable_schedule(self, mocker):
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)
        mocker.patch.object(scheduler, '_schedule_first_task')
        await scheduler._get_schedules()
        mocker.patch.object(scheduler, '_ready', True)
        mocker.patch.object(scheduler, '_task_processes')
        audit_logger = mocker.patch.object(AuditLogger, 'information', return_value=mock_process())
        log_info = mocker.patch.object(scheduler._logger, "info")
        sch_id = uuid.UUID("2b614d26-760f-11e7-b5a5-be2e44b06b34")  # OMF to PI North

        # WHEN
        status, message = await scheduler.disable_schedule(sch_id)

        # THEN
        assert status is True
        assert message == "Schedule successfully disabled"
        assert (scheduler._schedules[sch_id]).id == sch_id
        assert (scheduler._schedules[sch_id]).enabled is False
        assert 2 == log_info.call_count
        calls = [call('No Task running for Schedule %s', '2b614d26-760f-11e7-b5a5-be2e44b06b34'),
                 call("Disabled Schedule '%s/%s' process '%s'\n", 'OMF to PI north',
                      '2b614d26-760f-11e7-b5a5-be2e44b06b34', 'North Readings to PI')]
        log_info.assert_has_calls(calls)
        assert 1 == audit_logger.call_count
        calls = [call('SCHCH', {'schedule': {'name': 'OMF to PI north', 'repeat': 30.0, 'enabled': False,
                                             'type': Schedule.Type.INTERVAL, 'exclusive': True,
                                             'processName': 'North Readings to PI'}})]
        audit_logger.assert_has_calls(calls, any_order=True)

    @pytest.mark.asyncio
    async def test_disable_schedule_wrong_schedule_id(self, mocker):
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)
        mocker.patch.object(scheduler, '_schedule_first_task')
        await scheduler._get_schedules()
        mocker.patch.object(scheduler, '_ready', True)
        mocker.patch.object(scheduler, '_task_processes')
        log_exception = mocker.patch.object(scheduler._logger, "exception")
        random_schedule_id = uuid.uuid4()

        # WHEN
        await scheduler.disable_schedule(random_schedule_id)

        # THEN
        assert 1 == log_exception.call_count
        log_params = "No such Schedule %s", str(random_schedule_id)
        log_exception.assert_called_with(*log_params)

    @pytest.mark.asyncio
    async def test_disable_schedule_already_disabled(self, mocker):
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)
        mocker.patch.object(scheduler, '_schedule_first_task')
        await scheduler._get_schedules()
        mocker.patch.object(scheduler, '_ready', True)
        mocker.patch.object(scheduler, '_task_processes')
        log_info = mocker.patch.object(scheduler._logger, "info")
        sch_id = uuid.UUID("d1631422-9ec6-11e7-abc4-cec278b6b50a")  # backup

        # WHEN
        status, message = await scheduler.disable_schedule(sch_id)

        # THEN
        assert status is True
        assert message == "Schedule {} already disabled".format(str(sch_id))
        assert (scheduler._schedules[sch_id]).id == sch_id
        assert (scheduler._schedules[sch_id]).enabled is False
        assert 1 == log_info.call_count
        log_params = "Schedule %s already disabled", str(sch_id)
        log_info.assert_called_with(*log_params)

    @pytest.mark.asyncio
    async def test_enable_schedule(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)
        sch_id = uuid.UUID("d1631422-9ec6-11e7-abc4-cec278b6b50a")  # backup
        queue_task = mocker.patch.object(scheduler, 'queue_task', return_value=mock_process())
        audit_logger = mocker.patch.object(AuditLogger, 'information', return_value=mock_process())

        # WHEN
        status, message = await scheduler.enable_schedule(sch_id)

        # THEN
        assert status is True
        assert message == "Schedule successfully enabled"
        assert (scheduler._schedules[sch_id]).id == sch_id
        assert (scheduler._schedules[sch_id]).enabled is True
        assert 1 == queue_task.call_count
        calls = [call("Enabled Schedule '%s/%s' process '%s'\n", 'backup hourly', 'd1631422-9ec6-11e7-abc4-cec278b6b50a', 'backup')]
        log_info.assert_has_calls(calls, any_order=True)
        assert 1 == audit_logger.call_count
        calls = [call('SCHCH', {'schedule': {'name': 'backup hourly', 'type': Schedule.Type.INTERVAL, 'processName': 'backup', 'exclusive': True, 'repeat': 3600.0, 'enabled': True}})]
        audit_logger.assert_has_calls(calls, any_order=True)

    @pytest.mark.asyncio
    async def test_enable_schedule_already_enabled(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)
        sch_id = uuid.UUID("ada12840-68d3-11e7-907b-a6006ad3dba0")  #Coap
        mocker.patch.object(scheduler, 'queue_task', return_value=mock_process())

        # WHEN
        status, message = await scheduler.enable_schedule(sch_id)

        # THEN
        assert status is True
        assert message == "Schedule is already enabled"
        assert (scheduler._schedules[sch_id]).id == sch_id
        assert (scheduler._schedules[sch_id]).enabled is True
        assert 1 == log_info.call_count
        log_params = "Schedule %s already enabled", str(sch_id)
        log_info.assert_called_with(*log_params)

    @pytest.mark.asyncio
    async def test_enable_schedule_wrong_schedule_id(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)
        random_schedule_id = uuid.uuid4()

        # WHEN
        await scheduler.enable_schedule(random_schedule_id)

        # THEN
        assert 1 == log_exception.call_count
        log_params = "No such Schedule %s", str(random_schedule_id)
        log_exception.assert_called_with(*log_params)

    @pytest.mark.asyncio
    async def test_queue_task(self, mocker):
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)
        mocker.patch.object(scheduler, '_schedule_first_task')
        log_info = mocker.patch.object(scheduler._logger, "info")
        await scheduler._get_schedules()
        sch_id = uuid.UUID("d1631422-9ec6-11e7-abc4-cec278b6b50a")  # backup

        mocker.patch.object(scheduler, '_ready', True)
        mocker.patch.object(scheduler, '_resume_check_schedules')

        # Assert that there is no task queued for this schedule at first
        with pytest.raises(KeyError) as excinfo:
            assert scheduler._schedule_executions[sch_id] is True

        # WHEN
        await scheduler.queue_task(sch_id)

        # THEN
        assert isinstance(scheduler._schedule_executions[sch_id], scheduler._ScheduleExecution)
        assert 1 == log_info.call_count
        log_params = "Queued schedule '%s' for execution", 'backup hourly'
        log_info.assert_called_with(*log_params)

    @pytest.mark.asyncio
    async def test_queue_task_schedule_not_found(self, mocker):
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)
        mocker.patch.object(scheduler, '_schedule_first_task')
        mocker.patch.object(scheduler, '_ready', True)
        mocker.patch.object(scheduler, '_resume_check_schedules')

        # WHEN
        # THEN
        with pytest.raises(ScheduleNotFoundError) as excinfo:
            await scheduler.queue_task(uuid.uuid4())

    @pytest.mark.asyncio
    async def test_delete_schedule(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)
        sch_id = uuid.UUID("d1631422-9ec6-11e7-abc4-cec278b6b50a")  # backup
        await scheduler._get_schedules()

        # Confirm no. of schedules
        assert len(scheduler._storage.schedules) == len(scheduler._schedules)

        mocker.patch.object(scheduler, '_ready', True)

        # WHEN
        # Now delete schedule
        await scheduler.delete_schedule(sch_id)

        # THEN
        # Now confirm there is one schedule less
        assert len(scheduler._storage.schedules) - 1 == len(scheduler._schedules)

    @pytest.mark.asyncio
    async def test_delete_schedule_enabled_schedule(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)
        sch_id = uuid.UUID("ada12840-68d3-11e7-907b-a6006ad3dba0")  #Coap
        await scheduler._get_schedules()
        mocker.patch.object(scheduler, '_ready', True)

        # Confirm there are 14 schedules
        assert len(scheduler._storage.schedules) == len(scheduler._schedules)

        # WHEN
        # Now delete schedule
        await scheduler.delete_schedule(sch_id)

        # THEN
        # Now confirm no schedule is deleted
        assert len(scheduler._storage.schedules) == len(scheduler._schedules)
        assert 1 == log_exception.call_count
        log_params = 'Attempt to delete an enabled Schedule %s. Not deleted.', str(sch_id)
        log_exception.assert_called_with(*log_params)

    @pytest.mark.asyncio
    async def test_delete_schedule_exception(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)
        log_debug = mocker.patch.object(scheduler._logger, 'debug', side_effect=Exception())
        sch_id = uuid.UUID("d1631422-9ec6-11e7-abc4-cec278b6b50a")  # backup

        # WHEN
        # THEN
        with pytest.raises(ScheduleNotFoundError) as excinfo:
            await scheduler.delete_schedule(uuid.uuid4())

    @pytest.mark.asyncio
    async def test_delete_schedule_not_found(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)

        # WHEN
        # THEN
        with pytest.raises(ScheduleNotFoundError) as excinfo:
            await scheduler.delete_schedule(uuid.uuid4())

    @pytest.mark.asyncio
    async def test_get_running_tasks(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)

        # Assert that there is no task queued for schedule
        with pytest.raises(KeyError) as excinfo:
            assert scheduler._schedule_executions[schedule.id] is True

        # Now queue task and assert that the task has been queued
        await scheduler.queue_task(schedule.id)
        assert isinstance(scheduler._schedule_executions[schedule.id], scheduler._ScheduleExecution)

        # Confirm that no task has started yet
        assert 0 == len(scheduler._schedule_executions[schedule.id].task_processes)

        await scheduler._start_task(schedule)

        # Confirm that task has started
        assert 1 == len(scheduler._schedule_executions[schedule.id].task_processes)

        # WHEN
        tasks = await scheduler.get_running_tasks()

        # THEN
        assert 1 == len(tasks)
        assert schedule.process_name == tasks[0].process_name
        assert tasks[0].reason is None
        assert tasks[0].state == Task.State.RUNNING
        assert tasks[0].cancel_requested is None
        assert tasks[0].start_time is not None
        assert tasks[0].end_time is None
        assert tasks[0].exit_code is None

    @pytest.mark.asyncio
    async def test_get_task(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)

        # Assert that there is no North task queued for schedule
        with pytest.raises(KeyError) as excinfo:
            assert scheduler._schedule_executions[schedule.id] is True

        # Now queue task and assert that the North task has been queued
        await scheduler.queue_task(schedule.id)
        assert isinstance(scheduler._schedule_executions[schedule.id], scheduler._ScheduleExecution)

        # Confirm that no task has started yet
        assert 0 == len(scheduler._schedule_executions[schedule.id].task_processes)

        await scheduler._start_task(schedule)

        # Confirm that task has started
        assert 1 == len(scheduler._schedule_executions[schedule.id].task_processes)
        task_id = list(scheduler._schedule_executions[schedule.id].task_processes.keys())[0]

        # WHEN
        task = await scheduler.get_task(task_id)

        # THEN
        assert schedule.process_name == task.process_name
        assert task.reason is ''
        assert task.state is not None
        assert task.cancel_requested is None
        assert task.start_time is not None
        assert task.end_time is not None
        assert task.exit_code is '0'

    @pytest.mark.skip("Need a suitable fixture")
    @pytest.mark.asyncio
    async def test_get_task_not_found(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)

        # WHEN
        # THEN
        with pytest.raises(TaskNotFoundError) as excinfo:
            tasks = await scheduler.get_task(uuid.uuid4())

    @pytest.mark.asyncio
    async def test_get_task_exception(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)
        log_debug = mocker.patch.object(scheduler._logger, 'debug', side_effect=Exception())

        # WHEN
        # THEN
        with pytest.raises(Exception) as excinfo:
            tasks = await scheduler.get_task(uuid.uuid4())

        # THEN
        assert 1 == log_exception.call_count

    @pytest.mark.asyncio
    async def test_get_tasks(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)

        # Assert that there is no North task queued for schedule
        with pytest.raises(KeyError) as excinfo:
            assert scheduler._schedule_executions[schedule.id] is True

        # Now queue task and assert that the North task has been queued
        await scheduler.queue_task(schedule.id)
        assert isinstance(scheduler._schedule_executions[schedule.id], scheduler._ScheduleExecution)

        # Confirm that no task has started yet
        assert 0 == len(scheduler._schedule_executions[schedule.id].task_processes)

        await scheduler._start_task(schedule)

        # Confirm that task has started
        assert 1 == len(scheduler._schedule_executions[schedule.id].task_processes)
        task_id = list(scheduler._schedule_executions[schedule.id].task_processes.keys())[0]

        # WHEN
        tasks = await scheduler.get_tasks()

        # THEN
        assert schedule.process_name == tasks[0].process_name
        assert tasks[0].reason is ''
        assert tasks[0].state is not None
        assert tasks[0].cancel_requested is None
        assert tasks[0].start_time is not None
        assert tasks[0].end_time is not None
        assert tasks[0].exit_code is '0'

    async def test_get_tasks_exception(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)
        log_debug = mocker.patch.object(scheduler._logger, 'debug', side_effect=Exception())

        # WHEN
        # THEN
        with pytest.raises(Exception) as excinfo:
            tasks = await scheduler.get_tasks()

        # THEN
        assert 1 == log_exception.call_count

    @pytest.mark.asyncio
    async def test_cancel_task_all_ok(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)

        # Assert that there is no task queued for schedule
        with pytest.raises(KeyError) as excinfo:
            assert scheduler._schedule_executions[schedule.id] is True

        # Now queue task and assert that the task has been queued
        await scheduler.queue_task(schedule.id)
        assert isinstance(scheduler._schedule_executions[schedule.id], scheduler._ScheduleExecution)

        # Confirm that no task has started yet
        assert 0 == len(scheduler._schedule_executions[schedule.id].task_processes)
        await scheduler._start_task(schedule)

        # Confirm that task has started
        assert 1 == len(scheduler._schedule_executions[schedule.id].task_processes)
        task_id = list(scheduler._schedule_executions[schedule.id].task_processes.keys())[0]

        # Confirm that cancel request has not been made
        assert scheduler._schedule_executions[schedule.id].task_processes[task_id].cancel_requested is None

        # WHEN
        await scheduler.cancel_task(task_id)

        # THEN
        assert scheduler._schedule_executions[schedule.id].task_processes[task_id].cancel_requested is not None
        assert 3 == log_info.call_count

    @pytest.mark.asyncio
    async def test_cancel_task_exception(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)

        # WHEN
        # THEN
        with pytest.raises(TaskNotRunningError) as excinfo:
            await scheduler.cancel_task(uuid.uuid4())

    @pytest.mark.asyncio
    async def test_not_ready_and_paused(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)
        mocker.patch.object(scheduler, '_ready', False)
        mocker.patch.object(scheduler, '_paused', True)

        # WHEN
        # THEN
        with pytest.raises(NotReadyError) as excinfo:
            await scheduler.start()

        with pytest.raises(NotReadyError) as excinfo:
            await scheduler.get_scheduled_processes()

        with pytest.raises(NotReadyError) as excinfo:
            await scheduler.get_schedules()

        with pytest.raises(NotReadyError) as excinfo:
            await scheduler.get_schedule(uuid.uuid4())

        with pytest.raises(NotReadyError) as excinfo:
            await scheduler.save_schedule(Schedule(Schedule.Type.INTERVAL))

        with pytest.raises(NotReadyError) as excinfo:
            await scheduler.disable_schedule(uuid.uuid4())

        with pytest.raises(NotReadyError) as excinfo:
            await scheduler.enable_schedule(uuid.uuid4())

        with pytest.raises(NotReadyError) as excinfo:
            await scheduler.queue_task(uuid.uuid4())

        with pytest.raises(NotReadyError) as excinfo:
            await scheduler.delete_schedule(uuid.uuid4())

        with pytest.raises(NotReadyError) as excinfo:
            await scheduler.get_running_tasks()

        with pytest.raises(NotReadyError) as excinfo:
            await scheduler.cancel_task(uuid.uuid4())

    @pytest.mark.skip("_terminate_child_processes() not fit for unit test.")
    def test__terminate_child_processes(self, mocker):
        pass


class Storage(StorageClient):
    schedules = [
        {
            "id": "cea17db8-6ccc-11e7-907b-a6006ad3dba0",
            "process_name": "purge",
            "schedule_name": "purge",
            "schedule_type": "3",
            "schedule_interval": "01:00:00",
            "schedule_time": "",
            "schedule_day": "",
            "exclusive": "t",
            "enabled": "t"<commit_msg>FOGL-1017 - few refactoring<commit_after># -*- coding: utf-8 -*-

# FOGLAMP_BEGIN
# See: http://foglamp.readthedocs.io/
# FOGLAMP_END

import asyncio
import datetime
import uuid
import pytest
import time
from unittest.mock import MagicMock, call, Mock
from foglamp.services.core.scheduler.scheduler import Scheduler, AuditLogger, ConfigurationManager
from foglamp.services.core.scheduler.entities import Task, Schedule
from foglamp.services.core.scheduler.exceptions import *
from foglamp.common.storage_client.storage_client import StorageClient

__author__ = "<NAME>"
__copyright__ = "Copyright (c) 2017 OSIsoft, LLC"
__license__ = "Apache 2.0"
__version__ = "${VERSION}"


async def mock_task():
    return ""

async def mock_process():
    m = MagicMock()
    m.pid = 9999
    m.terminate = lambda: True
    return m


@pytest.allure.feature("unit")
@pytest.allure.story("scheduler")
class TestScheduler:
    async def scheduler_fixture(self, mocker):
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)
        mocker.patch.object(scheduler, '_schedule_first_task')
        mocker.patch.object(scheduler, '_ready', True)
        mocker.patch.object(scheduler, '_paused', False)
        mocker.patch.object(scheduler, '_process_scripts', return_value="North Readings to PI")
        mocker.patch.object(scheduler, '_wait_for_task_completion', return_value=asyncio.ensure_future(mock_task()))
        mocker.patch.object(scheduler, '_terminate_child_processes')
        mocker.patch.object(asyncio, 'create_subprocess_exec', return_value=asyncio.ensure_future(mock_process()))

        await scheduler._get_schedules()

        schedule = scheduler._ScheduleRow(
            id=uuid.UUID("2b614d26-760f-11e7-b5a5-be2e44b06b34"),
            process_name="North Readings to PI",
            name="OMF to PI north",
            type=Schedule.Type.INTERVAL,
            repeat=datetime.timedelta(seconds=30),
            repeat_seconds=30,
            time=None,
            day=None,
            exclusive=True,
            enabled=True)

        log_exception = mocker.patch.object(scheduler._logger, "exception")
        log_error = mocker.patch.object(scheduler._logger, "error")
        log_debug = mocker.patch.object(scheduler._logger, "debug")
        log_info = mocker.patch.object(scheduler._logger, "info")

        return scheduler, schedule, log_info, log_exception, log_error, log_debug

    @pytest.mark.asyncio
    async def test__resume_check_schedules(self, mocker):
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)

        # WHEN
        # Check IF part
        mocker.patch.object(scheduler, '_scheduler_loop_sleep_task', asyncio.Task(asyncio.sleep(5)))
        scheduler._resume_check_schedules()

        # THEN
        assert scheduler._check_processes_pending is False

        # WHEN
        # Check ELSE part
        mocker.patch.object(scheduler, '_scheduler_loop_sleep_task', None)
        scheduler._resume_check_schedules()

        # THEN
        assert scheduler._check_processes_pending is True

    @pytest.mark.asyncio
    async def test__wait_for_task_completion(self, mocker):
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)

        mock_schedules = dict()
        mock_schedule = scheduler._ScheduleRow(
            id=uuid.UUID("2b614d26-760f-11e7-b5a5-be2e44b06b34"),
            process_name="North Readings to PI",
            name="OMF to PI north",
            type=Schedule.Type.INTERVAL,
            repeat=datetime.timedelta(seconds=30),
            repeat_seconds=30,
            time=None,
            day=None,
            exclusive=True,
            enabled=True)
        mock_schedules[mock_schedule.id] = mock_schedule

        mock_task_process = scheduler._TaskProcess()
        mock_task_processes = dict()
        mock_task_process.process = await asyncio.create_subprocess_exec("sleep", ".1")
        mock_task_process.schedule = mock_schedule
        mock_task_id = uuid.uuid4()
        mock_task_process.task_id = mock_task_id
        mock_task_processes[mock_task_process.task_id] = mock_task_process

        mock_schedule_executions = dict()
        mock_schedule_execution = scheduler._ScheduleExecution()
        mock_schedule_executions[mock_schedule.id] = mock_schedule_execution
        mock_schedule_executions[mock_schedule.id].task_processes[mock_task_id] = mock_task_process

        mocker.patch.object(scheduler, '_resume_check_schedules')
        mocker.patch.object(scheduler, '_schedule_next_task')
        mocker.patch.multiple(scheduler, _schedules=mock_schedules,
                              _task_processes=mock_task_processes,
                              _schedule_executions=mock_schedule_executions)
        mocker.patch.object(scheduler, '_process_scripts', return_value="North Readings to PI")

        # WHEN
        await scheduler._wait_for_task_completion(mock_task_process)

        # THEN
        # After task completion, sleep above, no task processes should be left pending
        assert 0 == len(scheduler._task_processes)
        assert 0 == len(scheduler._schedule_executions[mock_schedule.id].task_processes)

    @pytest.mark.asyncio
    async def test__start_task(self, mocker):
        # TODO: Mandatory - Add negative tests for full code coverage
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)
        mocker.patch.object(scheduler, '_schedule_first_task')
        await scheduler._get_schedules()

        schedule = scheduler._ScheduleRow(
            id=uuid.UUID("2b614d26-760f-11e7-b5a5-be2e44b06b34"),
            process_name="North Readings to PI",
            name="OMF to PI north",
            type=Schedule.Type.INTERVAL,
            repeat=datetime.timedelta(seconds=30),
            repeat_seconds=30,
            time=None,
            day=None,
            exclusive=True,
            enabled=True)

        mocker.patch.object(scheduler, '_ready', True)
        mocker.patch.object(scheduler, '_resume_check_schedules')

        # Assert that there is no task queued for mock_schedule
        with pytest.raises(KeyError) as excinfo:
            assert scheduler._schedule_executions[schedule.id] is True
        # Now queue task and assert that the task has been queued
        await scheduler.queue_task(schedule.id)
        assert isinstance(scheduler._schedule_executions[schedule.id], scheduler._ScheduleExecution)

        mocker.patch.object(asyncio, 'create_subprocess_exec', return_value=asyncio.ensure_future(mock_process()))
        mocker.patch.object(asyncio, 'ensure_future', return_value=asyncio.ensure_future(mock_task()))
        mocker.patch.object(scheduler, '_resume_check_schedules')
        mocker.patch.object(scheduler, '_process_scripts', return_value="North Readings to PI")
        mocker.patch.object(scheduler, '_wait_for_task_completion')

        # Confirm that task has not started yet
        assert 0 == len(scheduler._schedule_executions[schedule.id].task_processes)

        # WHEN
        await scheduler._start_task(schedule)

        # THEN
        # Confirm that task has started
        assert 1 == len(scheduler._schedule_executions[schedule.id].task_processes)

    @pytest.mark.asyncio
    async def test_purge_tasks(self, mocker):
        # TODO: Mandatory - Add negative tests for full code coverage
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)
        mocker.patch.multiple(scheduler, _ready=True, _paused=False)
        mocker.patch.object(scheduler, '_max_completed_task_age', datetime.datetime.now())

        # WHEN
        await scheduler.purge_tasks()

        # THEN
        assert scheduler._purge_tasks_task is None
        assert scheduler._last_task_purge_time is not None

    @pytest.mark.asyncio
    async def test__check_purge_tasks(self, mocker):
        # TODO: Mandatory - Add negative tests for full code coverage
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)
        mocker.patch.multiple(scheduler, _purge_tasks_task=None,
                              _last_task_purge_time=None)
        mocker.patch.object(scheduler, 'purge_tasks', return_value=asyncio.ensure_future(mock_task()))

        # WHEN
        scheduler._check_purge_tasks()

        # THEN
        assert scheduler._purge_tasks_task is not None

    @pytest.mark.asyncio
    async def test__check_schedules(self, mocker):
        # TODO: Mandatory - Add negative tests for full code coverage
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)
        current_time = time.time()
        mocker.patch.multiple(scheduler, _max_running_tasks=10,
                              _start_time=current_time - 3600)
        await scheduler._get_schedules()
        mocker.patch.object(scheduler, '_start_task', return_value=asyncio.ensure_future(mock_task()))

        # WHEN
        earliest_start_time = await scheduler._check_schedules()

        # THEN
        assert earliest_start_time is not None

    @pytest.mark.asyncio
    @pytest.mark.skip("_scheduler_loop() not suitable for unit testing. Will be tested during System tests.")
    async def test__scheduler_loop(self, mocker):
        pass

    @pytest.mark.asyncio
    async def test__schedule_next_timed_task(self, mocker):
        # TODO: Mandatory - Add negative tests for full code coverage
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)
        current_time = time.time()
        mocker.patch.multiple(scheduler, _max_running_tasks=10,
                              _start_time=current_time - 3600)
        await scheduler._get_schedules()

        sch_id = uuid.UUID("2176eb68-7303-11e7-8cf7-a6006ad3dba0")  # stat collector
        sch = scheduler._schedules[sch_id]
        sch_execution = scheduler._schedule_executions[sch_id]
        time_before_call = sch_execution.next_start_time

        # WHEN
        next_dt = datetime.datetime.fromtimestamp(sch_execution.next_start_time)
        next_dt += datetime.timedelta(seconds=sch.repeat_seconds)
        scheduler._schedule_next_timed_task(sch, sch_execution, next_dt)
        time_after_call = sch_execution.next_start_time

        # THEN
        assert time_after_call > time_before_call

    @pytest.mark.asyncio
    async def test__schedule_next_task(self, mocker):
        # TODO: Mandatory - Add negative tests for full code coverage
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)
        current_time = time.time()
        mocker.patch.multiple(scheduler, _max_running_tasks=10,
                              _start_time=current_time - 3600)
        await scheduler._get_schedules()

        sch_id = uuid.UUID("2176eb68-7303-11e7-8cf7-a6006ad3dba0")  # stat collector
        sch = scheduler._schedules[sch_id]
        sch_execution = scheduler._schedule_executions[sch_id]
        time_before_call = sch_execution.next_start_time

        # WHEN
        scheduler._schedule_next_task(sch)
        time_after_call = sch_execution.next_start_time

        # THEN
        assert time_after_call > time_before_call

    @pytest.mark.asyncio
    async def test__schedule_first_task(self, mocker):
        # TODO: Mandatory - Add negative tests for full code coverage
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)
        current_time = time.time()
        curr_time = datetime.datetime.fromtimestamp(current_time)
        mocker.patch.multiple(scheduler, _max_running_tasks=10,
                              _start_time=current_time - 3600)
        await scheduler._get_schedules()

        sch_id = uuid.UUID("2176eb68-7303-11e7-8cf7-a6006ad3dba0")  # stat collector
        sch = scheduler._schedules[sch_id]
        sch_execution = scheduler._schedule_executions[sch_id]

        # WHEN
        scheduler._schedule_first_task(sch, current_time)
        time_after_call = sch_execution.next_start_time

        # THEN
        assert time_after_call > time.mktime(curr_time.timetuple())

    @pytest.mark.asyncio
    async def test__get_process_scripts(self, mocker):
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)

        # WHEN
        await scheduler._get_process_scripts()

        # THEN
        assert len(scheduler._storage.scheduled_processes) == len(scheduler._process_scripts)

    @pytest.mark.asyncio
    async def test__get_process_scripts_exception(self, mocker):
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)
        log_debug = mocker.patch.object(scheduler._logger, "debug", side_effect=Exception())

        # WHEN
        # THEN
        with pytest.raises(Exception):
            await scheduler._get_process_scripts()

    @pytest.mark.asyncio
    async def test__get_schedules(self, mocker):
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)
        mocker.patch.object(scheduler, '_schedule_first_task')

        # WHEN
        await scheduler._get_schedules()

        # THEN
        assert len(scheduler._storage.schedules) == len(scheduler._schedules)

    @pytest.mark.asyncio
    async def test__get_schedules_exception(self, mocker):
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)
        mocker.patch.object(scheduler, '_schedule_first_task', side_effect=Exception())

        # WHEN
        # THEN
        with pytest.raises(Exception):
            await scheduler._get_schedules()

    @pytest.mark.asyncio
    async def test__read_storage(self, mocker):
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)
        mocker.patch.object(scheduler, '_schedule_first_task')

        # WHEN
        await scheduler._read_storage()

        # THEN
        assert len(scheduler._storage.scheduled_processes) == len(scheduler._process_scripts)
        assert len(scheduler._storage.schedules) == len(scheduler._schedules)

    @pytest.mark.asyncio
    @pytest.mark.skip("_mark_tasks_interrupted() not implemented in main Scheduler class.")
    async def test__mark_tasks_interrupted(self, mocker):
        pass

    @pytest.mark.asyncio
    async def test__read_config(self, mocker):
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)
        cr_cat = mocker.patch.object(ConfigurationManager, "create_category", return_value=asyncio.ensure_future(mock_task()))

        # WHEN
        assert scheduler._max_running_tasks is None
        assert scheduler._max_completed_task_age is None
        await scheduler._read_config()

        # THEN
        assert 1 == cr_cat.call_count
        assert scheduler._max_running_tasks is not None
        assert scheduler._max_completed_task_age is not None

    @pytest.mark.asyncio
    async def test_start(self, mocker):
        # TODO: Mandatory - Add negative tests for full code coverage
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)
        current_time = time.time()
        mocker.patch.object(scheduler, '_schedule_first_task')
        mocker.patch.object(scheduler, '_scheduler_loop', return_value=asyncio.ensure_future(mock_task()))
        mocker.patch.multiple(scheduler, _core_management_port=9999,
                              _core_management_host="0.0.0.0",
                              current_time=current_time - 3600)

        # TODO: Remove after implementation of above test test__read_config()
        mocker.patch.object(scheduler, '_read_config', return_value=asyncio.ensure_future(mock_task()))

        assert scheduler._ready is False

        # WHEN
        await scheduler.start()

        # THEN
        assert scheduler._ready is True
        assert len(scheduler._storage.scheduled_processes) == len(scheduler._process_scripts)
        assert len(scheduler._storage.schedules) == len(scheduler._schedules)

    @pytest.mark.asyncio
    async def test_stop(self, mocker):
        # TODO: Mandatory - Add negative tests for full code coverage
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)
        mocker.patch.object(scheduler, '_scheduler_loop', return_value=asyncio.ensure_future(mock_task()))
        mocker.patch.object(scheduler, '_resume_check_schedules', return_value=asyncio.ensure_future(mock_task()))
        mocker.patch.object(scheduler, '_purge_tasks_task', return_value=asyncio.ensure_future(mock_task()))
        mocker.patch.object(scheduler, '_scheduler_loop_task', return_value=asyncio.ensure_future(mock_task()))
        current_time = time.time()
        mocker.patch.multiple(scheduler, _core_management_port=9999,
                              _core_management_host="0.0.0.0",
                              _start_time=current_time - 3600,
                              _paused=False,
                              _task_processes={})

        # WHEN
        retval = await scheduler.stop()

        # THEN
        assert retval is True
        assert scheduler._schedule_executions is None
        assert scheduler._task_processes is None
        assert scheduler._schedules is None
        assert scheduler._process_scripts is None
        assert scheduler._ready is False
        assert scheduler._paused is False
        assert scheduler._start_time is None

    @pytest.mark.asyncio
    async def test_get_scheduled_processes(self, mocker):
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)
        await scheduler._get_process_scripts()
        mocker.patch.object(scheduler, '_ready', True)

        # WHEN
        processes = await scheduler.get_scheduled_processes()

        # THEN
        assert len(scheduler._storage.scheduled_processes) == len(processes)

    @pytest.mark.asyncio
    async def test_schedule_row_to_schedule(self, mocker):
        # GIVEN
        scheduler = Scheduler()
        schedule_id = uuid.uuid4()
        schedule_row = scheduler._ScheduleRow(
            id=schedule_id,
            name='Test Schedule',
            type=Schedule.Type.INTERVAL,
            day=0,
            time=0,
            repeat=10,
            repeat_seconds=10,
            exclusive=False,
            enabled=True,
            process_name='TestProcess')

        # WHEN
        schedule = scheduler._schedule_row_to_schedule(schedule_id, schedule_row)

        # THEN
        assert isinstance(schedule, Schedule)
        assert schedule.schedule_id == schedule_row[0]
        assert schedule.name == schedule_row[1]
        assert schedule.schedule_type == schedule_row[2]
        assert schedule.repeat == schedule_row[5]
        assert schedule.exclusive == schedule_row[7]
        assert schedule.enabled == schedule_row[8]
        assert schedule.process_name == schedule_row[9]

    @pytest.mark.asyncio
    async def test_get_schedules(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)

        # WHEN
        schedules = await scheduler.get_schedules()

        # THEN
        assert len(scheduler._storage.schedules) == len(schedules)

    @pytest.mark.asyncio
    async def test_get_schedule(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)
        schedule_id = uuid.UUID("cea17db8-6ccc-11e7-907b-a6006ad3dba0")  # purge schedule

        # WHEN
        schedule = await scheduler.get_schedule(schedule_id)

        # THEN
        assert isinstance(schedule, Schedule)
        assert schedule.schedule_id == schedule_id
        assert schedule.name == "purge"
        assert schedule.schedule_type == Schedule.Type.MANUAL
        assert schedule.repeat == datetime.timedelta(0, 3600)
        assert schedule.exclusive is True
        assert schedule.enabled is True
        assert schedule.process_name == "purge"

    @pytest.mark.asyncio
    async def test_get_schedule_exception(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)
        schedule_id = uuid.uuid4()

        # WHEN
        # THEN
        with pytest.raises(ScheduleNotFoundError):
            schedule = await scheduler.get_schedule(schedule_id)

    @pytest.mark.asyncio
    async def test_save_schedule_new(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)
        audit_logger = mocker.patch.object(AuditLogger, 'information', return_value=asyncio.ensure_future(mock_task()))
        first_task = mocker.patch.object(scheduler, '_schedule_first_task')
        resume_sch = mocker.patch.object(scheduler, '_resume_check_schedules')
        log_info = mocker.patch.object(scheduler._logger, "info")
        schedule_id = uuid.uuid4()
        schedule_row = scheduler._ScheduleRow(
            id=schedule_id,
            name='Test Schedule',
            type=Schedule.Type.INTERVAL,
            day=0,
            time=0,
            repeat=datetime.timedelta(seconds=30),
            repeat_seconds=30,
            exclusive=False,
            enabled=True,
            process_name='TestProcess')
        schedule = scheduler._schedule_row_to_schedule(schedule_id, schedule_row)

        # WHEN
        await scheduler.save_schedule(schedule)

        # THEN
        assert len(scheduler._storage.schedules) + 1 == len(scheduler._schedules)
        assert 1 == audit_logger.call_count
        calls =[call('SCHAD', {'schedule': {'name': 'Test Schedule', 'processName': 'TestProcess',
                                            'type': Schedule.Type.INTERVAL, 'repeat': 30.0, 'enabled': True,
                                            'exclusive': False}})]
        audit_logger.assert_has_calls(calls, any_order=True)
        assert 1 == first_task.call_count
        assert 1 == resume_sch.call_count

    @pytest.mark.asyncio
    async def test_save_schedule_update(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)
        audit_logger = mocker.patch.object(AuditLogger, 'information', return_value=asyncio.ensure_future(mock_task()))
        first_task = mocker.patch.object(scheduler, '_schedule_first_task')
        resume_sch = mocker.patch.object(scheduler, '_resume_check_schedules')
        log_info = mocker.patch.object(scheduler._logger, "info")
        schedule_id = uuid.UUID("2b614d26-760f-11e7-b5a5-be2e44b06b34")  # OMF to PI North
        schedule_row = scheduler._ScheduleRow(
            id=schedule_id,
            name='Test Schedule',
            type=Schedule.Type.TIMED,
            day=1,
            time=datetime.time(),
            repeat=datetime.timedelta(seconds=30),
            repeat_seconds=30,
            exclusive=False,
            enabled=True,
            process_name='TestProcess')
        schedule = scheduler._schedule_row_to_schedule(schedule_id, schedule_row)

        # WHEN
        await scheduler.save_schedule(schedule)

        # THEN
        assert len(scheduler._storage.schedules) == len(scheduler._schedules)
        assert 1 == audit_logger.call_count
        calls = [call('SCHCH', {'schedule': {'name': 'Test Schedule', 'enabled': True, 'repeat': 30.0,
                                             'exclusive': False, 'day': 1, 'time': '0:0:0',
                                             'processName': 'TestProcess', 'type': Schedule.Type.TIMED}})]
        audit_logger.assert_has_calls(calls, any_order=True)
        assert 1 == first_task.call_count
        assert 1 == resume_sch.call_count

    @pytest.mark.asyncio
    async def test_save_schedule_exception(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)
        schedule_id = uuid.uuid4()
        schedule_row = scheduler._ScheduleRow(
            id=schedule_id,
            name='<NAME>',
            type=Schedule.Type.TIMED,
            day=0,
            time=0,
            repeat=datetime.timedelta(seconds=30),
            repeat_seconds=30,
            exclusive=False,
            enabled=True,
            process_name='TestProcess')

        # WHEN
        # THEN
        with pytest.raises(ValueError) as ex:
            temp_schedule = scheduler._schedule_row_to_schedule(schedule_id, schedule_row)
            temp_schedule.name = None
            await scheduler.save_schedule(temp_schedule)
            del temp_schedule
        assert str(ex).endswith("name can not be empty")

        with pytest.raises(ValueError) as ex:
            temp_schedule = scheduler._schedule_row_to_schedule(schedule_id, schedule_row)
            temp_schedule.name = ""
            await scheduler.save_schedule(temp_schedule)
            del temp_schedule
        assert str(ex).endswith("name can not be empty")

        with pytest.raises(ValueError) as ex:
            temp_schedule = scheduler._schedule_row_to_schedule(schedule_id, schedule_row)
            temp_schedule.repeat = 1234
            await scheduler.save_schedule(temp_schedule)
            del temp_schedule
        assert str(ex).endswith('repeat must be of type datetime.timedelta')

        with pytest.raises(ValueError) as ex:
            temp_schedule = scheduler._schedule_row_to_schedule(schedule_id, schedule_row)
            temp_schedule.exclusive = None
            await scheduler.save_schedule(temp_schedule)
            del temp_schedule
        assert str(ex).endswith('exclusive can not be None')

        with pytest.raises(ValueError) as ex:
            temp_schedule = scheduler._schedule_row_to_schedule(schedule_id, schedule_row)
            temp_schedule.time = 1234
            await scheduler.save_schedule(temp_schedule)
            del temp_schedule
        assert str(ex).endswith('time must be of type datetime.time')

        with pytest.raises(ValueError) as ex:
            temp_schedule = scheduler._schedule_row_to_schedule(schedule_id, schedule_row)
            temp_schedule.day = 0
            temp_schedule.time = datetime.time()
            await scheduler.save_schedule(temp_schedule)
            del temp_schedule
        assert str(ex).endswith('day must be between 1 and 7')

    @pytest.mark.asyncio
    async def test_disable_schedule(self, mocker):
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)
        mocker.patch.object(scheduler, '_schedule_first_task')
        await scheduler._get_schedules()
        mocker.patch.object(scheduler, '_ready', True)
        mocker.patch.object(scheduler, '_task_processes')
        audit_logger = mocker.patch.object(AuditLogger, 'information', return_value=asyncio.ensure_future(mock_task()))
        log_info = mocker.patch.object(scheduler._logger, "info")
        sch_id = uuid.UUID("2b614d26-760f-11e7-b5a5-be2e44b06b34")  # OMF to PI North

        # WHEN
        status, message = await scheduler.disable_schedule(sch_id)

        # THEN
        assert status is True
        assert message == "Schedule successfully disabled"
        assert (scheduler._schedules[sch_id]).id == sch_id
        assert (scheduler._schedules[sch_id]).enabled is False
        assert 2 == log_info.call_count
        calls = [call('No Task running for Schedule %s', '2b614d26-760f-11e7-b5a5-be2e44b06b34'),
                 call("Disabled Schedule '%s/%s' process '%s'\n", 'OMF to PI north',
                      '2b614d26-760f-11e7-b5a5-be2e44b06b34', 'North Readings to PI')]
        log_info.assert_has_calls(calls)
        assert 1 == audit_logger.call_count
        calls = [call('SCHCH', {'schedule': {'name': 'OMF to PI north', 'repeat': 30.0, 'enabled': False,
                                             'type': Schedule.Type.INTERVAL, 'exclusive': True,
                                             'processName': 'North Readings to PI'}})]
        audit_logger.assert_has_calls(calls, any_order=True)

    @pytest.mark.asyncio
    async def test_disable_schedule_wrong_schedule_id(self, mocker):
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)
        mocker.patch.object(scheduler, '_schedule_first_task')
        await scheduler._get_schedules()
        mocker.patch.object(scheduler, '_ready', True)
        mocker.patch.object(scheduler, '_task_processes')
        log_exception = mocker.patch.object(scheduler._logger, "exception")
        random_schedule_id = uuid.uuid4()

        # WHEN
        await scheduler.disable_schedule(random_schedule_id)

        # THEN
        assert 1 == log_exception.call_count
        log_params = "No such Schedule %s", str(random_schedule_id)
        log_exception.assert_called_with(*log_params)

    @pytest.mark.asyncio
    async def test_disable_schedule_already_disabled(self, mocker):
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)
        mocker.patch.object(scheduler, '_schedule_first_task')
        await scheduler._get_schedules()
        mocker.patch.object(scheduler, '_ready', True)
        mocker.patch.object(scheduler, '_task_processes')
        log_info = mocker.patch.object(scheduler._logger, "info")
        sch_id = uuid.UUID("d1631422-9ec6-11e7-abc4-cec278b6b50a")  # backup

        # WHEN
        status, message = await scheduler.disable_schedule(sch_id)

        # THEN
        assert status is True
        assert message == "Schedule {} already disabled".format(str(sch_id))
        assert (scheduler._schedules[sch_id]).id == sch_id
        assert (scheduler._schedules[sch_id]).enabled is False
        assert 1 == log_info.call_count
        log_params = "Schedule %s already disabled", str(sch_id)
        log_info.assert_called_with(*log_params)

    @pytest.mark.asyncio
    async def test_enable_schedule(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)
        sch_id = uuid.UUID("d1631422-9ec6-11e7-abc4-cec278b6b50a")  # backup
        queue_task = mocker.patch.object(scheduler, 'queue_task', return_value=asyncio.ensure_future(mock_task()))
        audit_logger = mocker.patch.object(AuditLogger, 'information', return_value=asyncio.ensure_future(mock_task()))

        # WHEN
        status, message = await scheduler.enable_schedule(sch_id)

        # THEN
        assert status is True
        assert message == "Schedule successfully enabled"
        assert (scheduler._schedules[sch_id]).id == sch_id
        assert (scheduler._schedules[sch_id]).enabled is True
        assert 1 == queue_task.call_count
        calls = [call("Enabled Schedule '%s/%s' process '%s'\n", 'backup hourly', 'd1631422-9ec6-11e7-abc4-cec278b6b50a', 'backup')]
        log_info.assert_has_calls(calls, any_order=True)
        assert 1 == audit_logger.call_count
        calls = [call('SCHCH', {'schedule': {'name': 'backup hourly', 'type': Schedule.Type.INTERVAL, 'processName': 'backup', 'exclusive': True, 'repeat': 3600.0, 'enabled': True}})]
        audit_logger.assert_has_calls(calls, any_order=True)

    @pytest.mark.asyncio
    async def test_enable_schedule_already_enabled(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)
        sch_id = uuid.UUID("ada12840-68d3-11e7-907b-a6006ad3dba0")  #Coap
        mocker.patch.object(scheduler, 'queue_task', return_value=asyncio.ensure_future(mock_task()))

        # WHEN
        status, message = await scheduler.enable_schedule(sch_id)

        # THEN
        assert status is True
        assert message == "Schedule is already enabled"
        assert (scheduler._schedules[sch_id]).id == sch_id
        assert (scheduler._schedules[sch_id]).enabled is True
        assert 1 == log_info.call_count
        log_params = "Schedule %s already enabled", str(sch_id)
        log_info.assert_called_with(*log_params)

    @pytest.mark.asyncio
    async def test_enable_schedule_wrong_schedule_id(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)
        random_schedule_id = uuid.uuid4()

        # WHEN
        await scheduler.enable_schedule(random_schedule_id)

        # THEN
        assert 1 == log_exception.call_count
        log_params = "No such Schedule %s", str(random_schedule_id)
        log_exception.assert_called_with(*log_params)

    @pytest.mark.asyncio
    async def test_queue_task(self, mocker):
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)
        mocker.patch.object(scheduler, '_schedule_first_task')
        log_info = mocker.patch.object(scheduler._logger, "info")
        await scheduler._get_schedules()
        sch_id = uuid.UUID("cea17db8-6ccc-11e7-907b-a6006ad3dba0")  # backup

        mocker.patch.object(scheduler, '_ready', True)
        mocker.patch.object(scheduler, '_resume_check_schedules')

        # Assert that there is no task queued for this schedule at first
        with pytest.raises(KeyError) as excinfo:
            assert scheduler._schedule_executions[sch_id] is True

        # WHEN
        await scheduler.queue_task(sch_id)

        # THEN
        assert isinstance(scheduler._schedule_executions[sch_id], scheduler._ScheduleExecution)
        assert 1 == log_info.call_count
        log_params = "Queued schedule '%s' for execution", 'purge'
        log_info.assert_called_with(*log_params)

    @pytest.mark.asyncio
    async def test_queue_task_schedule_not_found(self, mocker):
        # GIVEN
        scheduler = Scheduler()
        scheduler._storage = Storage(core_management_host=None, core_management_port=None)
        mocker.patch.object(scheduler, '_schedule_first_task')
        mocker.patch.object(scheduler, '_ready', True)
        mocker.patch.object(scheduler, '_resume_check_schedules')

        # WHEN
        # THEN
        with pytest.raises(ScheduleNotFoundError) as excinfo:
            await scheduler.queue_task(uuid.uuid4())

    @pytest.mark.asyncio
    async def test_delete_schedule(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)
        sch_id = uuid.UUID("d1631422-9ec6-11e7-abc4-cec278b6b50a")  # backup
        await scheduler._get_schedules()

        # Confirm no. of schedules
        assert len(scheduler._storage.schedules) == len(scheduler._schedules)

        mocker.patch.object(scheduler, '_ready', True)

        # WHEN
        # Now delete schedule
        await scheduler.delete_schedule(sch_id)

        # THEN
        # Now confirm there is one schedule less
        assert len(scheduler._storage.schedules) - 1 == len(scheduler._schedules)

    @pytest.mark.asyncio
    async def test_delete_schedule_enabled_schedule(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)
        sch_id = uuid.UUID("ada12840-68d3-11e7-907b-a6006ad3dba0")  #Coap
        await scheduler._get_schedules()
        mocker.patch.object(scheduler, '_ready', True)

        # Confirm there are 14 schedules
        assert len(scheduler._storage.schedules) == len(scheduler._schedules)

        # WHEN
        # Now delete schedule
        await scheduler.delete_schedule(sch_id)

        # THEN
        # Now confirm no schedule is deleted
        assert len(scheduler._storage.schedules) == len(scheduler._schedules)
        assert 1 == log_exception.call_count
        log_params = 'Attempt to delete an enabled Schedule %s. Not deleted.', str(sch_id)
        log_exception.assert_called_with(*log_params)

    @pytest.mark.asyncio
    async def test_delete_schedule_exception(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)
        log_debug = mocker.patch.object(scheduler._logger, 'debug', side_effect=Exception())
        sch_id = uuid.UUID("d1631422-9ec6-11e7-abc4-cec278b6b50a")  # backup

        # WHEN
        # THEN
        with pytest.raises(ScheduleNotFoundError) as excinfo:
            await scheduler.delete_schedule(uuid.uuid4())

    @pytest.mark.asyncio
    async def test_delete_schedule_not_found(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)

        # WHEN
        # THEN
        with pytest.raises(ScheduleNotFoundError) as excinfo:
            await scheduler.delete_schedule(uuid.uuid4())

    @pytest.mark.asyncio
    async def test_get_running_tasks(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)

        # Assert that there is no task queued for schedule
        with pytest.raises(KeyError) as excinfo:
            assert scheduler._schedule_executions[schedule.id] is True

        # Now queue task and assert that the task has been queued
        await scheduler.queue_task(schedule.id)
        assert isinstance(scheduler._schedule_executions[schedule.id], scheduler._ScheduleExecution)

        # Confirm that no task has started yet
        assert 0 == len(scheduler._schedule_executions[schedule.id].task_processes)

        await scheduler._start_task(schedule)

        # Confirm that task has started
        assert 1 == len(scheduler._schedule_executions[schedule.id].task_processes)

        # WHEN
        tasks = await scheduler.get_running_tasks()

        # THEN
        assert 1 == len(tasks)
        assert schedule.process_name == tasks[0].process_name
        assert tasks[0].reason is None
        assert tasks[0].state == Task.State.RUNNING
        assert tasks[0].cancel_requested is None
        assert tasks[0].start_time is not None
        assert tasks[0].end_time is None
        assert tasks[0].exit_code is None

    @pytest.mark.asyncio
    async def test_get_task(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)

        # Assert that there is no North task queued for schedule
        with pytest.raises(KeyError) as excinfo:
            assert scheduler._schedule_executions[schedule.id] is True

        # Now queue task and assert that the North task has been queued
        await scheduler.queue_task(schedule.id)
        assert isinstance(scheduler._schedule_executions[schedule.id], scheduler._ScheduleExecution)

        # Confirm that no task has started yet
        assert 0 == len(scheduler._schedule_executions[schedule.id].task_processes)

        await scheduler._start_task(schedule)

        # Confirm that task has started
        assert 1 == len(scheduler._schedule_executions[schedule.id].task_processes)
        task_id = list(scheduler._schedule_executions[schedule.id].task_processes.keys())[0]

        # WHEN
        task = await scheduler.get_task(task_id)

        # THEN
        assert schedule.process_name == task.process_name
        assert task.reason is ''
        assert task.state is not None
        assert task.cancel_requested is None
        assert task.start_time is not None
        assert task.end_time is not None
        assert task.exit_code is '0'

    @pytest.mark.skip("Need a suitable fixture")
    @pytest.mark.asyncio
    async def test_get_task_not_found(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)

        # WHEN
        # THEN
        with pytest.raises(TaskNotFoundError) as excinfo:
            tasks = await scheduler.get_task(uuid.uuid4())

    @pytest.mark.asyncio
    async def test_get_task_exception(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)
        log_debug = mocker.patch.object(scheduler._logger, 'debug', side_effect=Exception())

        # WHEN
        # THEN
        with pytest.raises(Exception) as excinfo:
            tasks = await scheduler.get_task(uuid.uuid4())

        # THEN
        assert 1 == log_exception.call_count

    @pytest.mark.asyncio
    async def test_get_tasks(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)

        # Assert that there is no North task queued for schedule
        with pytest.raises(KeyError) as excinfo:
            assert scheduler._schedule_executions[schedule.id] is True

        # Now queue task and assert that the North task has been queued
        await scheduler.queue_task(schedule.id)
        assert isinstance(scheduler._schedule_executions[schedule.id], scheduler._ScheduleExecution)

        # Confirm that no task has started yet
        assert 0 == len(scheduler._schedule_executions[schedule.id].task_processes)

        await scheduler._start_task(schedule)

        # Confirm that task has started
        assert 1 == len(scheduler._schedule_executions[schedule.id].task_processes)
        task_id = list(scheduler._schedule_executions[schedule.id].task_processes.keys())[0]

        # WHEN
        tasks = await scheduler.get_tasks()

        # THEN
        assert schedule.process_name == tasks[0].process_name
        assert tasks[0].reason is ''
        assert tasks[0].state is not None
        assert tasks[0].cancel_requested is None
        assert tasks[0].start_time is not None
        assert tasks[0].end_time is not None
        assert tasks[0].exit_code is '0'

    @pytest.mark.asyncio
    async def test_get_tasks_exception(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)
        log_debug = mocker.patch.object(scheduler._logger, 'debug', side_effect=Exception())

        # WHEN
        # THEN
        with pytest.raises(Exception) as excinfo:
            tasks = await scheduler.get_tasks()

        # THEN
        assert 1 == log_exception.call_count

    @pytest.mark.asyncio
    async def test_cancel_task_all_ok(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)

        # Assert that there is no task queued for schedule
        with pytest.raises(KeyError) as excinfo:
            assert scheduler._schedule_executions[schedule.id] is True

        # Now queue task and assert that the task has been queued
        await scheduler.queue_task(schedule.id)
        assert isinstance(scheduler._schedule_executions[schedule.id], scheduler._ScheduleExecution)

        # Confirm that no task has started yet
        assert 0 == len(scheduler._schedule_executions[schedule.id].task_processes)
        await scheduler._start_task(schedule)

        # Confirm that task has started
        assert 1 == len(scheduler._schedule_executions[schedule.id].task_processes)
        task_id = list(scheduler._schedule_executions[schedule.id].task_processes.keys())[0]

        # Confirm that cancel request has not been made
        assert scheduler._schedule_executions[schedule.id].task_processes[task_id].cancel_requested is None

        # WHEN
        await scheduler.cancel_task(task_id)

        # THEN
        assert scheduler._schedule_executions[schedule.id].task_processes[task_id].cancel_requested is not None
        assert 3 == log_info.call_count

    @pytest.mark.asyncio
    async def test_cancel_task_exception(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)

        # WHEN
        # THEN
        with pytest.raises(TaskNotRunningError) as excinfo:
            await scheduler.cancel_task(uuid.uuid4())

    @pytest.mark.asyncio
    async def test_not_ready_and_paused(self, mocker):
        # GIVEN
        scheduler, schedule, log_info, log_exception, log_error, log_debug = await self.scheduler_fixture(mocker)
        mocker.patch.object(scheduler, '_ready', False)
        mocker.patch.object(scheduler, '_paused', True)

        # WHEN
        # THEN
        with pytest.raises(NotReadyError) as excinfo:
            await scheduler.start()

        with pytest.raises(NotReadyError) as excinfo:
            await scheduler.get_scheduled_processes()

        with pytest.raises(NotReadyError) as excinfo:
            await scheduler.get_schedules()

        with pytest.raises(NotReadyError) as excinfo:
            await scheduler.get_schedule(uuid.uuid4())

        with pytest.raises(NotReadyError) as excinfo:
            await scheduler.save_schedule(Schedule(Schedule.Type.INTERVAL))

        with pytest.raises(NotReadyError) as excinfo:
            await scheduler.disable_schedule(uuid.uuid4())

        with pytest.raises(NotReadyError) as excinfo:
            await scheduler.enable_schedule(uuid.uuid4())

        with pytest.raises(NotReadyError) as excinfo:
            await scheduler.queue_task(uuid.uuid4())

        with pytest.raises(NotReadyError) as excinfo:
            await scheduler.delete_schedule(uuid.uuid4())

        with pytest.raises(NotReadyError) as excinfo:
            await scheduler.get_running_tasks()

        with pytest.raises(NotReadyError) as excinfo:
            await scheduler.cancel_task(uuid.uuid4())

    @pytest.mark.skip("_terminate_child_processes() not fit for unit test.")
    @pytest.mark.asyncio
    async def test__terminate_child_processes(self, mocker):
        pass


class Storage(StorageClient):
    schedules = [
        {
            "id": "cea17db8-6ccc-11e7-907b-a6006ad3dba0",
            "process_name": "purge",
            "schedule_name": "purge",
            "schedule_type": "4",
            "schedule_interval": "01:00:00",
            "schedule_time": "",
            "schedule_day": "",
            "exclusive": "t",
            "enabled": "t"
{'fraction_non_alphanumeric': 0.0883314465874586, 'fraction_numerical': 0.01302383647732202, 'mean_word_length': 2.8140585909127593, 'pattern_counts': {'":': 18, '<': 6, '<?xml version=': 0, '>': 12, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 4, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 24, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1373490', 'n_tokens_mistral': 28626, 'n_tokens_neox': 27836, 'n_words': 5399}