# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Tests for glazier.lib.config.runner."""

from absl.testing import absltest
from pyfakefs import fake_filesystem
from pyfakefs import fake_filesystem_shutil
from glazier.lib import buildinfo
from glazier.lib import constants
from glazier.lib.config import runner

import mock


class ConfigRunnerTest(absltest.TestCase):

  def setUp(self):
    super(ConfigRunnerTest, self).setUp()
    self.buildinfo = buildinfo.BuildInfo()
    constants.FLAGS.verify_urls = None
    # filesystem
    self.filesystem = fake_filesystem.FakeFilesystem()
    runner.os = fake_filesystem.FakeOsModule(self.filesystem)
    runner.open = fake_filesystem.FakeFileOpen(self.filesystem)
    runner.shutil = fake_filesystem_shutil.FakeShutilModule(self.filesystem)
    self.cr = runner.ConfigRunner(self.buildinfo)
    self.cr._task_list_path = '/tmp/task_list.yaml'

  @mock.patch.object(runner.files, 'Remove', autospec=True)
  @mock.patch.object(runner.base.actions, 'pull', autospec=True)
  @mock.patch.object(runner.files, 'Dump', autospec=True)
  def testIteration(self, dump, pull, remove):
    conf = [{
        'data': {
            'pull': 'val1'
        },
        'path': ['/path1'],
        'server': 'https://glazier.example.com'
    }, {
        'data': {
            'pull': 'val2'
        },
        'path': ['/path2'],
        'server': 'https://glazier.example.com'
    }, {
        'data': {
            'pull': 'val3'
        },
        'path': ['/path3'],
        'server': 'https://glazier.example.com'
    }]
    self.cr._ProcessTasks(conf)
    dump.assert_has_calls([
        mock.call(self.cr._task_list_path, conf[1:], mode='w'),
        mock.call(self.cr._task_list_path, conf[2:], mode='w'),
        mock.call(self.cr._task_list_path, [], mode='w')
    ])
    pull.assert_has_calls([])
    self.assertTrue(remove.called)

  @mock.patch.object(runner.files, 'Dump', autospec=True)
  def testPopTask(self, dump):
    self.cr._PopTask([1, 2, 3])
    dump.assert_called_with('/tmp/task_list.yaml', [2, 3], mode='w')
    dump.side_effect = runner.files.Error
    with self.assertRaises(runner.ConfigRunnerError):
      self.cr._PopTask([1, 2])

  @mock.patch.object(runner.files, 'Remove', autospec=True)
  @mock.patch.object(runner.files, 'Dump', autospec=True)
  def testPopLastTask(self, dump, remove):
    self.cr._PopTask([1])
    dump.assert_called_with('/tmp/task_list.yaml', [], mode='w')
    remove.assert_called_with('/tmp/task_list.yaml')

  @mock.patch.object(runner.power, 'Restart', autospec=True)
  @mock.patch.object(runner.ConfigRunner, '_ProcessAction', autospec=True)
  @mock.patch.object(runner.ConfigRunner, '_PopTask', autospec=True)
  def testRestartEvents(self, pop, action, restart):
    conf = [{
        'data': {
            'Shutdown': ['25', 'Reason']
        },
        'path': ['path1'],
        'server': 'https://glazier.example.com'
    }]
    event = runner.base.actions.RestartEvent('Some reason', timeout=25)
    action.side_effect = event
    self.assertRaises(SystemExit, self.cr._ProcessTasks, conf)
    restart.assert_called_with(25, 'Some reason')
    self.assertTrue(pop.called)
    pop.reset_mock()

    # with retry
    event = runner.base.actions.RestartEvent(
        'Some other reason', timeout=10, retry_on_restart=True)
    action.side_effect = event
    self.assertRaises(SystemExit, self.cr._ProcessTasks, conf)
    restart.assert_called_with(10, 'Some other reason')
    self.assertFalse(pop.called)

    # with pop
    event = runner.base.actions.RestartEvent(
        'Some other reason', timeout=10, pop_next=True)
    action.side_effect = event
    self.assertRaises(SystemExit, self.cr._ProcessTasks, conf)
    restart.assert_called_with(10, 'Some other reason')
    self.assertTrue(pop.called)

  @mock.patch.object(runner.power, 'Shutdown', autospec=True)
  @mock.patch.object(runner.ConfigRunner, '_ProcessAction', autospec=True)
  @mock.patch.object(runner.ConfigRunner, '_PopTask', autospec=True)
  def testShutdownEvents(self, pop, action, shutdown):
    conf = [{
        'data': {
            'Restart': ['25', 'Reason']
        },
        'path': ['path1'],
        'server': 'https://glazier.example.com'
    }]
    event = runner.base.actions.ShutdownEvent('Some reason', timeout=25)
    action.side_effect = event
    self.assertRaises(SystemExit, self.cr._ProcessTasks, conf)
    shutdown.assert_called_with(25, 'Some reason')
    self.assertTrue(pop.called)
    pop.reset_mock()

    # with retry
    event = runner.base.actions.ShutdownEvent(
        'Some other reason', timeout=10, retry_on_restart=True)
    action.side_effect = event
    self.assertRaises(SystemExit, self.cr._ProcessTasks, conf)
    shutdown.assert_called_with(10, 'Some other reason')
    self.assertFalse(pop.called)

    # with pop
    event = runner.base.actions.ShutdownEvent(
        'Some other reason', timeout=10, pop_next=True)
    action.side_effect = event
    self.assertRaises(SystemExit, self.cr._ProcessTasks, conf)
    shutdown.assert_called_with(10, 'Some other reason')
    self.assertTrue(pop.called)

  @mock.patch.object(runner.base.actions, 'SetTimer', autospec=True)
  def testProcessWithActionError(self, set_timer):
    set_timer.side_effect = runner.base.actions.ActionError
    self.assertRaises(runner.ConfigRunnerError, self.cr._ProcessTasks, [{
        'data': {
            'SetTimer': ['Timer1']
        },
        'path': ['/autobuild'],
        'server': 'https://glazier.example.com'
    }])

  def testProcessWithInvalidCommand(self):
    self.assertRaises(runner.ConfigRunnerError, self.cr._ProcessTasks, [{
        'data': {
            'BadSetTimer': ['Timer1']
        },
        'path': ['/autobuild'],
        'server': 'https://glazier.example.com'
    }])

  @mock.patch.object(runner.files, 'Read', autospec=True)
  def testStartWithMissingFile(self, reader):
    reader.side_effect = runner.files.Error
    self.assertRaises(runner.ConfigRunnerError, self.cr.Start,
                      '/tmp/path/missing.yaml')

  @mock.patch.object(runner.base.actions, 'SetTimer', autospec=True)
  @mock.patch.object(runner.files, 'Read', autospec=True)
  @mock.patch.object(runner.files, 'Remove', autospec=True)
  @mock.patch.object(runner.files, 'Dump', autospec=True)
  def testStartWithActions(self, dump, remove, reader, set_timer):
    reader.return_value = [{
        'data': {
            'SetTimer': ['TestTimer']
        },
        'path': ['/autobuild'],
        'server': 'https://glazier.example.com'
    }]
    self.cr.Start('/tmp/path/tasks.yaml')
    reader.assert_called_with('/tmp/path/tasks.yaml')
    set_timer.assert_called_with(build_info=self.buildinfo, args=['TestTimer'])
    self.assertTrue(set_timer.return_value.Run.called)
    self.assertTrue(dump.called)
    self.assertTrue(remove.called)

  @mock.patch.object(runner.download.Download, 'CheckUrl', autospec=True)
  def testVerifyUrls(self, dl):
    dl.return_value = True
    constants.FLAGS.verify_urls = ['http://www.example.com/']
    self.cr._ProcessTasks([])
    dl.assert_called_with(
        mock.ANY, 'http://www.example.com/', [200], max_retries=-1)
    # fail
    dl.return_value = False
    self.assertRaises(runner.ConfigRunnerError, self.cr._ProcessTasks, [])


if __name__ == '__main__':
  absltest.main()