# -*- coding: utf-8 -*-
# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Unittests for generic stages."""

from __future__ import print_function

import contextlib
import copy
import os
import sys
import unittest

import mock

from chromite.cbuildbot import cbuildbot_run
from chromite.cbuildbot import commands
from chromite.cbuildbot.stages import generic_stages
from chromite.lib import config_lib
from chromite.lib import constants
from chromite.lib import cidb
from chromite.lib import cros_build_lib
from chromite.lib import cros_test_lib
from chromite.lib import fake_cidb
from chromite.lib import failures_lib
from chromite.lib import failure_message_lib
from chromite.lib import osutils
from chromite.lib import parallel
from chromite.lib import partial_mock
from chromite.lib import portage_util
from chromite.lib import results_lib
from chromite.lib.buildstore import FakeBuildStore, BuildIdentifier
from chromite.scripts import cbuildbot


DEFAULT_BUILD_NUMBER = 1234321
DEFAULT_BUILD_ID = 31337
DEFAULT_BUILD_STAGE_ID = 313377

# pylint: disable=protected-access


# The inheritance order ensures the patchers are stopped before
# cleaning up the temporary directories.
class StageTestCase(cros_test_lib.MockOutputTestCase,
                    cros_test_lib.TempDirTestCase):
  """Test running a single stage in isolation."""

  TARGET_MANIFEST_BRANCH = 'ooga_booga'
  BUILDROOT = 'buildroot'

  # Subclass should override this to default to a different build config
  # for its tests.
  BOT_ID = 'amd64-generic-full'

  # Subclasses can override this.  If non-None, value is inserted into
  # self._run.attrs.release_tag.
  RELEASE_TAG = None

  def setUp(self):
    # Prepare a fake build root in self.tempdir, save at self.build_root.
    self.build_root = os.path.join(self.tempdir, self.BUILDROOT)
    osutils.SafeMakedirs(os.path.join(self.build_root, '.repo'))

    self._manager = parallel.Manager()
    # Pylint-1.9 has a false positive on this for some reason.
    self._manager.__enter__()  # pylint: disable=no-value-for-parameter

    # These are here to make pylint happy.  Values filled in by _Prepare.
    self._bot_id = None
    self._current_board = None
    self._boards = None
    self._run = None
    self._model = None
    self.buildstore = FakeBuildStore()

  def CreateMockOverlay(self, overlay, build_root=None):
    """Helper for creating an overlay in the fake buildroot.

    Args:
      overlay: The overlay name to create. Usually the board name.
      build_root: The buildroot in which to create the mock overlay.
    """
    if not build_root:
      build_root = self.build_root

    layout_path = os.path.join(build_root, 'src', 'overlays',
                               'overlay-%s' % overlay,
                               'metadata', 'layout.conf')

    layout_content = 'repo-name = %s\n' % overlay

    # Don't use osutils.WriteFile, some tests mock it out.
    osutils.SafeMakedirs(os.path.dirname(layout_path))
    with open(layout_path, 'w') as f:
      f.writelines(cros_build_lib.iflatten_instance(layout_content))

  def _Prepare(self, bot_id=None, extra_config=None, cmd_args=None,
               extra_cmd_args=None, build_id=DEFAULT_BUILD_ID,
               master_build_id=None, buildbucket_id=None,
               site_config=None):
    """Prepare a BuilderRun at self._run for this test.

    This method must allow being called more than once.  Subclasses can
    override this method, but those subclass methods should also call this one.

    The idea is that all test preparation that falls out from the choice of
    build config and cbuildbot options should go in _Prepare.

    This will populate the following attributes on self:
      run: A BuilderRun object.
      bot_id: The bot id (name) that was used from the site_config.
      self._boards: Same as self._run.config.boards.  TODO(mtennant): remove.
      self._current_board: First board in list, if there is one.

    Args:
      bot_id: Name of build config to use, defaults to self.BOT_ID.
      extra_config: Dict used to add to the build config for the given
        bot_id.  Example: {'push_image': True}.
      cmd_args: List to override the default cbuildbot command args, including
        the bot_id.
      extra_cmd_args: List to add to default cbuildbot command args.  This
        is a good way to adjust an options value for your test.
        Example: ['branch-name', 'some-branch-name'] will effectively cause
        self._run.options.branch_name to be set to 'some-branch-name'.
      build_id: mock build id
      buildbucket_id: mock buildbucket_id
      master_build_id: mock build id of master build.
      site_config: SiteConfig to use (or MockSiteConfig)
    """
    assert not bot_id or not cmd_args

    # Use cbuildbot parser to create options object and populate default values.
    if not cmd_args:
      # Fill in default command args.
      cmd_args = [
          '-r', self.build_root, '--buildbot', '--noprebuilts',
          '--buildnumber', str(DEFAULT_BUILD_NUMBER),
          '--branch', self.TARGET_MANIFEST_BRANCH,
          bot_id or self.BOT_ID
      ]
    if extra_cmd_args:
      cmd_args += extra_cmd_args

    parser = cbuildbot._CreateParser()
    options = cbuildbot.ParseCommandLine(parser, cmd_args)
    self._bot_id = options.build_config_name

    if site_config is None:
      site_config = config_lib.GetConfig()

    # Populate build_config corresponding to self._bot_id.
    build_config = copy.deepcopy(site_config[self._bot_id])
    build_config['manifest_repo_url'] = 'fake_url'
    if extra_config:
      build_config.update(extra_config)
    options.managed_chrome = build_config['sync_chrome']

    self._boards = build_config['boards']
    self._current_board = self._boards[0] if self._boards else None
    self._model = self._current_board

    # Some preliminary sanity checks.
    self.assertEqual(options.buildroot, self.build_root)

    # Construct a real BuilderRun using options and build_config.
    self._run = cbuildbot_run.BuilderRun(
        options, site_config, build_config, self._manager)

    if build_id is not None:
      self._run.attrs.metadata.UpdateWithDict({'build_id': build_id})

    if buildbucket_id is not None:
      self._run.options.buildbucket_id = buildbucket_id

    if master_build_id is not None:
      self._run.options.master_build_id = master_build_id

    if self.RELEASE_TAG is not None:
      self._run.attrs.release_tag = self.RELEASE_TAG

    portage_util._OVERLAY_LIST_CMD = '/bin/true'

  def tearDown(self):
    # Mimic exiting with statement for self._manager.
    self._manager.__exit__(None, None, None)

  def AutoPatch(self, to_patch):
    """Patch a list of objects with autospec=True.

    Args:
      to_patch: A list of tuples in the form (target, attr) to patch.  Will be
      directly passed to mock.patch.object.
    """
    for item in to_patch:
      self.PatchObject(*item, autospec=True)

  def GetHWTestSuite(self):
    """Get the HW test suite for the current bot."""
    hw_tests = self._run.config['hw_tests']
    if not hw_tests:
      # TODO(milleral): Add HWTests back to lumpy-chrome-perf.
      raise unittest.SkipTest('Missing HWTest for %s' % (self._bot_id,))

    return hw_tests[0]

  def assertRaisesStringifyable(self, exception, functor, *args, **kwargs):
    """assertRaises replacement that also verifies exception is Stringifyable.

    This helper is intended to be used anywhere assertRaises can be used, but
    will also verify the exception raised can pass through
    BuilderStage._StringifyException.

    Args:
      exception: See unittest.TestCase.assertRaises.
      functor: See unittest.TestCase.assertRaises.
      args: See unittest.TestCase.assertRaises.
      kwargs: See unittest.TestCase.assertRaises.

    Raises:
      Unittest failures if the expected exception is not raised, or
      _StringifyException exceptions if that process fails.
    """
    try:
      functor(*args, **kwargs)

      # We didn't get the exception, fail the test.
      self.fail('%s was not raised.' % exception)

    except exception:
      # Ensure that this exception can be converted properly.
      # Verifies fix for crbug.com/418358 and related.
      generic_stages.BuilderStage._StringifyException(sys.exc_info())

    except Exception as e:
      # We didn't get the exception, fail the test.
      self.fail('%s raised instead of %s' % (e, exception))


class AbstractStageTestCase(StageTestCase):
  """Base class for tests that test a particular build stage.

  Abstract base class that sets up the build config and options with some
  default values for testing BuilderStage and its derivatives.
  """
  def setUp(self):
    # Value which will be populated as the stage runs, so that tests can examine
    # it's state afterwards.
    self.stage = None

  def ConstructStage(self):
    """Returns an instance of the stage to be tested.

    Note: Must be implemented in subclasses.
    """
    raise NotImplementedError(self, 'ConstructStage: Implement in your test')

  def RunStage(self, **kwargs):
    """Creates and runs an instance of the stage to be tested.

    Note: Requires ConstructStage() to be implemented.

    Raises:
      NotImplementedError: ConstructStage() was not implemented.
    """

    # Stage construction is usually done as late as possible because the tests
    # set up the build configuration and options used in constructing the stage.
    results_lib.Results.Clear()
    self.stage = self.ConstructStage(**kwargs)
    self.stage.Run()
    self.assertTrue(results_lib.Results.BuildSucceededSoFar())


def patch(*args, **kwargs):
  """Convenience wrapper for mock.patch.object.

  Sets autospec=True by default.
  """
  kwargs.setdefault('autospec', True)
  return mock.patch.object(*args, **kwargs)


@contextlib.contextmanager
def patches(*args):
  """Context manager for a list of patch objects."""
  with cros_build_lib.ContextManagerStack() as stack:
    for arg in args:
      stack.Add(lambda ret=arg: ret)
    yield


class BuilderStageTest(AbstractStageTestCase):
  """Tests for BuilderStage class."""

  def setUp(self):
    self._Prepare()
    self.mock_cidb = mock.MagicMock()
    self.buildstore = FakeBuildStore(self.mock_cidb)
    cidb.CIDBConnectionFactory.SetupMockCidb(self.mock_cidb)
    # Many tests modify the global results_lib.Results instance.
    results_lib.Results.Clear()

  def tearDown(self):
    cidb.CIDBConnectionFactory.ClearMock()

  def _ConstructStageWithExpectations(self, stage_class):
    """Construct an instance of the stage, verifying expectations from init.

    Args:
      stage_class: The class to instantitate.

    Returns:
      The instantiated class instance.
    """
    if stage_class is None:
      stage_class = generic_stages.BuilderStage

    self.PatchObject(
        FakeBuildStore, 'InsertBuildStage',
        return_value=constants.MOCK_STAGE_ID)
    self.PatchObject(
        FakeBuildStore, 'StartBuildStage',
        return_value=constants.MOCK_STAGE_ID)
    self.PatchObject(
        FakeBuildStore, 'WaitBuildStage',
        return_value=constants.MOCK_STAGE_ID)
    self.PatchObject(
        FakeBuildStore, 'FinishBuildStage',
        return_value=constants.MOCK_STAGE_ID)
    stage = stage_class(self._run, self.buildstore)
    self.buildstore.InsertBuildStage.assert_called_once_with(
        constants.MOCK_BUILD_ID, stage.name, None,
        constants.BUILDER_STATUS_PLANNED)
    return stage

  def ConstructStage(self):
    return self._ConstructStageWithExpectations(generic_stages.BuilderStage)

  def testGetPortageEnvVar(self):
    """Basic test case for _GetPortageEnvVar function."""
    stage = self.ConstructStage()
    board = self._current_board

    envvar = 'EXAMPLE'
    envvar_val = 'RESULT'
    self.PatchObject(portage_util, 'PortageqEnvvars',
                     return_value={envvar: envvar_val})

    result = stage._GetPortageEnvVar(envvar, board)
    self.assertEqual(result, envvar_val)

  def testStageNamePrefixSmoke(self):
    """Basic test for the StageNamePrefix() function."""
    stage = self.ConstructStage()
    self.assertEqual(stage.StageNamePrefix(), 'Builder')

  def testGetStageNamesSmoke(self):
    """Basic test for the GetStageNames() function."""
    stage = self.ConstructStage()
    self.assertEqual(stage.GetStageNames(), ['Builder'])

  def testConstructDashboardURLSmoke(self):
    """Basic test for the ConstructDashboardURL() function."""
    stage = self.ConstructStage()

    exp_url = 'https://ci.chromium.org/b/fake_bb_id'
    self.assertEqual(stage.ConstructDashboardURL(), exp_url)

    stage_name = 'Archive'
    exp_url = ('https://luci-logdog.appspot.com/v/?s=chromeos/buildbucket/'
               'cr-buildbucket.appspot.com/1234321/%2B/steps/Archive/0/stdout')

    self.assertEqual(stage.ConstructDashboardURL(stage=stage_name), exp_url)

  def test_PrintSmoke(self):
    """Basic test for the _Print() function."""
    stage = self.ConstructStage()
    with self.OutputCapturer():
      stage._Print('hi there')
    self.AssertOutputContainsLine('hi there', check_stderr=True)

  def test_PrintLoudlySmoke(self):
    """Basic test for the _PrintLoudly() function."""
    stage = self.ConstructStage()
    with self.OutputCapturer():
      stage._PrintLoudly('hi there')
    self.AssertOutputContainsLine(r'\*{10}', check_stderr=True)
    self.AssertOutputContainsLine('hi there', check_stderr=True)

  def testRunSmoke(self):
    """Basic passing test for the Run() function."""
    stage = self.ConstructStage()
    with self.OutputCapturer():
      stage.Run()

  def _RunCapture(self, stage):
    """Helper method to run Run() with captured output."""
    output = self.OutputCapturer()
    output.StartCapturing()
    try:
      stage.Run()
    finally:
      output.StopCapturing()
    return output

  def testRunException(self):
    """Verify stage exceptions are handled."""
    class TestError(Exception):
      """Unique test exception"""

    perform_mock = self.PatchObject(generic_stages.BuilderStage, 'PerformStage')
    perform_mock.side_effect = TestError('fail!')

    stage = self.ConstructStage()
    results_lib.Results.Clear()
    self.assertRaises(failures_lib.StepFailure, self._RunCapture, stage)

    results = results_lib.Results.Get()[0]
    self.assertTrue(isinstance(results.result, TestError))
    self.assertEqual(str(results.result), 'fail!')
    self.buildstore.StartBuildStage.assert_called_once_with(
        DEFAULT_BUILD_STAGE_ID)
    self.buildstore.FinishBuildStage.assert_called_once_with(
        DEFAULT_BUILD_STAGE_ID,
        constants.BUILDER_STATUS_FAILED)

  def testRunExitEarlyException(self):
    """Verify stage exit early exceptions are handled."""
    class TestError(Exception):
      """Unique test exception"""

    perform_mock = self.PatchObject(generic_stages.BuilderStage, 'PerformStage')
    perform_mock.side_effect = TestError('fail!')

    stage = self.ConstructStage()
    results_lib.Results.Clear()
    self.assertRaises(failures_lib.StepFailure, self._RunCapture, stage)

    results = results_lib.Results.Get()[0]
    self.assertTrue(isinstance(results.result, TestError))
    self.assertEqual(str(results.result), 'fail!')
    self.buildstore.StartBuildStage.assert_called_once_with(
        DEFAULT_BUILD_STAGE_ID)
    self.buildstore.FinishBuildStage.assert_called_once_with(
        DEFAULT_BUILD_STAGE_ID,
        constants.BUILDER_STATUS_FAILED)

  def testRunWithWaitFailure(self):
    """Test Run when WaitUntilReady returns False"""
    stage = self.ConstructStage()
    self.PatchObject(generic_stages.BuilderStage,
                     'WaitUntilReady',
                     return_value=False)
    stage.Run()
    self.buildstore.WaitBuildStage.assert_called_once_with(
        DEFAULT_BUILD_STAGE_ID)
    self.buildstore.FinishBuildStage.assert_called_once_with(
        DEFAULT_BUILD_STAGE_ID,
        constants.BUILDER_STATUS_SKIPPED)
    self.assertFalse(self.mock_cidb.StartBuildStage.called)

  @osutils.TempFileDecorator
  def testRunSkipsPreviouslyCompletedStage(self):
    """Test that a stage that has run before is skipped, and marked as such."""
    handle_skip_mock = self.PatchObject(generic_stages.BuilderStage,
                                        'HandleSkip')
    stage = self.ConstructStage()

    # Record a result as if the stage succeeded in a _previous_ run.
    results_lib.Results.Record(stage.name, results_lib.Results.SUCCESS,
                               description='Injected success')
    with open(self.tempfile, 'w') as out:
      results_lib.Results.SaveCompletedStages(out)
    results_lib.Results.Clear()
    with open(self.tempfile, 'r') as out:
      results_lib.Results.RestoreCompletedStages(out)

    output = self._RunCapture(stage)
    all_out = output.GetStdout()
    all_out += output.GetStderr()
    self.assertTrue('[PREVIOUSLY PROCESSED]' in all_out)
    self.assertTrue(handle_skip_mock.called)

  def testHandleExceptionException(self):
    """Verify exceptions in HandleException handlers are themselves handled."""
    class TestError(Exception):
      """Unique test exception"""

    class BadStage(generic_stages.BuilderStage):
      """Stage that throws an exception when PerformStage is called."""

      handled_exceptions = []

      def PerformStage(self):
        raise TestError('first fail')

      def _HandleStageException(self, exc_info):
        self.handled_exceptions.append(str(exc_info[1]))
        raise TestError('nested')

    stage = self._ConstructStageWithExpectations(BadStage)
    results_lib.Results.Clear()
    self.assertRaises(failures_lib.StepFailure, self._RunCapture, stage)

    # Verify the results tracked the original exception.
    results = results_lib.Results.Get()[0]
    self.assertTrue(isinstance(results.result, TestError))
    self.assertEqual(str(results.result), 'first fail')

    self.assertEqual(stage.handled_exceptions, ['first fail'])

    # Verify the stage is still marked as failed in cidb.
    self.buildstore.StartBuildStage.assert_called_once_with(
        DEFAULT_BUILD_STAGE_ID)
    self.buildstore.FinishBuildStage.assert_called_once_with(
        DEFAULT_BUILD_STAGE_ID,
        constants.BUILDER_STATUS_FAILED)


class BuilderStageGetBuildFailureMessage(AbstractStageTestCase):
  """Test GetBuildFailureMessage in BuilderStage."""

  def setUp(self):
    self._Prepare()
    self.buildstore = FakeBuildStore()
    # Many tests modify the global results_lib.Results instance.
    results_lib.Results.Clear()

  def tearDown(self):
    cidb.CIDBConnectionFactory.ClearMock()

  def ConstructStage(self):
    return generic_stages.BuilderStage(self._run, self.buildstore)

  def testGetBuildFailureMessageFromResults(self):
    """Test GetBuildFailureMessageFromResults."""
    ex = failures_lib.StepFailure()
    results_lib.Results.Record('CommitQueueSync', ex)
    stage = self.ConstructStage()
    build_failure_msg = stage.GetBuildFailureMessageFromResults()
    self.assertEqual(build_failure_msg.builder, self.BOT_ID)
    self.assertTrue(isinstance(build_failure_msg.failure_messages[0],
                               failure_message_lib.StageFailureMessage))

  def testGetBuildFailureMessageWithoutBuildStore(self):
    """Test GetBuildFailureMessage without working BuildStore instance."""
    stage = self.ConstructStage()
    message = 'foo'
    get_msg_from_cidb = self.PatchObject(
        stage, 'GetBuildFailureMessageFromBuildStore', return_value=message)
    self.PatchObject(FakeBuildStore, 'AreClientsReady', return_value=False)
    get_msg_from_results = self.PatchObject(
        stage, 'GetBuildFailureMessageFromResults', return_value=message)

    stage.GetBuildFailureMessage()
    get_msg_from_cidb.assert_not_called()
    get_msg_from_results.assert_called_once_with()

  def testGetBuildFailureMessageWithBuildStore(self):
    """Test GetBuildFailureMessage with working BuildStore."""
    db = fake_cidb.FakeCIDBConnection()
    cidb.CIDBConnectionFactory.SetupMockCidb(db)
    stage = self.ConstructStage()
    message = 'foo'
    get_msg_from_cidb = self.PatchObject(
        stage, 'GetBuildFailureMessageFromBuildStore', return_value=message)
    self.PatchObject(FakeBuildStore, 'AreClientsReady', return_value=True)
    get_msg_from_results = self.PatchObject(
        stage, 'GetBuildFailureMessageFromResults', return_value=message)

    build_identifier = BuildIdentifier(cidb_id=constants.MOCK_BUILD_ID,
                                       buildbucket_id=None)
    self.PatchObject(stage._run, 'GetCIDBHandle',
                     return_value=[build_identifier, None])
    stage.GetBuildFailureMessage()
    get_msg_from_cidb.assert_called_once_with(
        self.buildstore, build_identifier)
    get_msg_from_results.assert_not_called()

  def testMeaningfulMessage(self):
    """Tests that all essential components are in the message."""
    stage = self.ConstructStage()

    exception = Exception('failed!')
    stage_failure_message = failures_lib.GetStageFailureMessageFromException(
        'TacoStage', 1, exception, stage_prefix_name='TacoStage')
    self.PatchObject(FakeBuildStore, 'AreClientsReady', return_value=False)
    self.PatchObject(
        results_lib.Results, 'GetStageFailureMessage',
        return_value=[stage_failure_message])

    msg = stage.GetBuildFailureMessage()
    self.assertTrue(stage._run.config.name in msg.message_summary)
    self.assertTrue(stage._run.ConstructDashboardURL() in msg.message_summary)
    self.assertTrue('the builder failed' in msg.message_summary)


class BoardSpecificBuilderStageTest(AbstractStageTestCase):
  """Tests option/config settings on board-specific stages."""

  DEFAULT_BOARD_NAME = 'my_shiny_test_board'

  def setUp(self):
    self.buildstore = FakeBuildStore()
    self._Prepare()

  def ConstructStage(self):
    return generic_stages.BoardSpecificBuilderStage(self._run,
                                                    self.buildstore,
                                                    self.DEFAULT_BOARD_NAME)

  def testBuilderNameContainsBoardName(self):
    self._run.config.grouped = True
    stage = self.ConstructStage()
    self.assertTrue(self.DEFAULT_BOARD_NAME in stage.name)

  # TODO (yjhong): Fix this test.
  # def testCheckOptions(self):
  #   """Makes sure options/config settings are setup correctly."""
  #   parser = cbuildbot._CreateParser()
  #   (options, _) = parser.parse_args([])

  #   for attr in dir(stages):
  #     obj = eval('stages.' + attr)
  #     if not hasattr(obj, '__base__'):
  #       continue
  #     if not obj.__base__ is stages.BoardSpecificBuilderStage:
  #       continue
  #     if obj.option_name:
  #       self.assertTrue(getattr(options, obj.option_name))
  #     if obj.config_name:
  #       if not obj.config_name in config._settings:
  #         self.fail(('cbuildbot_stages.%s.config_name "%s" is missing from '
  #                    'cbuildbot_config._settings') % (attr, obj.config_name))

  def testListOfPackagesToBuild(self):
    """Test the default list of packages to build."""
    stage = self.ConstructStage()
    packages = stage.GetListOfPackagesToBuild()
    expected = ['virtual/target-os', 'virtual/target-os-dev',
                'virtual/target-os-test', 'virtual/target-os-factory',
                'virtual/target-os-factory-shim', 'chromeos-base/autotest-all']
    self.assertEqual(expected, packages)

    # Test if an explicit list of packages is configured, only that list is set.
    self._run.config.packages = ['pkgA', 'pkgB']
    packages = stage.GetListOfPackagesToBuild()
    self.assertEqual(['pkgA', 'pkgB'], packages)

    # Test if a list of packages is set for ChromeOS Findit builds, they are
    # added to an explicit list.
    self._run.options.cbb_build_packages = ['pkgC', 'pkgD']
    packages = stage.GetListOfPackagesToBuild()
    self.assertEqual(['pkgC', 'pkgD', 'pkgA', 'pkgB'], packages)

    # Only check packages setup for ChromeOS Findit.
    self._run.config.packages = []
    packages = stage.GetListOfPackagesToBuild()
    self.assertEqual(['pkgC', 'pkgD'], packages)


class RunCommandAbstractStageTestCase(
    AbstractStageTestCase, cros_test_lib.RunCommandTestCase):
  """Base test class for testing a stage and mocking run."""

  # pylint: disable=abstract-method

  FULL_BOT_ID = 'amd64-generic-full'
  BIN_BOT_ID = 'amd64-generic-incremental'

  def _PrepareFull(self, **kwargs):
    self._Prepare(self.FULL_BOT_ID, **kwargs)

  def _PrepareBin(self, **kwargs):
    self._Prepare(self.BIN_BOT_ID, **kwargs)


class ArchivingStageMixinMock(partial_mock.PartialMock):
  """Partial mock for ArchivingStageMixin."""

  TARGET = 'chromite.cbuildbot.stages.generic_stages.ArchivingStageMixin'
  ATTRS = ('UploadArtifact',)

  def UploadArtifact(self, *args, **kwargs):
    with patch(commands, 'ArchiveFile', return_value='foo.txt'):
      with patch(commands, 'UploadArchivedFile'):
        self.backup['UploadArtifact'](*args, **kwargs)

# -*- coding: utf-8 -*-
# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Unittests for generic stages."""

from __future__ import print_function

import contextlib
import copy
import os
import sys
import unittest

import mock

from chromite.cbuildbot import cbuildbot_run
from chromite.cbuildbot import commands
from chromite.cbuildbot.stages import generic_stages
from chromite.lib import config_lib
from chromite.lib import constants
from chromite.lib import cidb
from chromite.lib import cros_build_lib
from chromite.lib import cros_test_lib
from chromite.lib import fake_cidb
from chromite.lib import failures_lib
from chromite.lib import failure_message_lib
from chromite.lib import osutils
from chromite.lib import parallel
from chromite.lib import partial_mock
from chromite.lib import portage_util
from chromite.lib import results_lib
from chromite.lib.buildstore import FakeBuildStore, BuildIdentifier
from chromite.scripts import cbuildbot


DEFAULT_BUILD_NUMBER = 1234321
DEFAULT_BUILD_ID = 31337
DEFAULT_BUILD_STAGE_ID = 313377

# pylint: disable=protected-access


# The inheritance order ensures the patchers are stopped before
# cleaning up the temporary directories.
class StageTestCase(cros_test_lib.MockOutputTestCase,
                    cros_test_lib.TempDirTestCase):
  """Test running a single stage in isolation."""

  TARGET_MANIFEST_BRANCH = 'ooga_booga'
  BUILDROOT = 'buildroot'

  # Subclass should override this to default to a different build config
  # for its tests.
  BOT_ID = 'amd64-generic-full'

  # Subclasses can override this.  If non-None, value is inserted into
  # self._run.attrs.release_tag.
  RELEASE_TAG = None

  def setUp(self):
    # Prepare a fake build root in self.tempdir, save at self.build_root.
    self.build_root = os.path.join(self.tempdir, self.BUILDROOT)
    osutils.SafeMakedirs(os.path.join(self.build_root, '.repo'))

    self._manager = parallel.Manager()
    # Pylint-1.9 has a false positive on this for some reason.
    self._manager.__enter__()  # pylint: disable=no-value-for-parameter

    # These are here to make pylint happy.  Values filled in by _Prepare.
    self._bot_id = None
    self._current_board = None
    self._boards = None
    self._run = None
    self._model = None
    self.buildstore = FakeBuildStore()

  def CreateMockOverlay(self, overlay, build_root=None):
    """Helper for creating an overlay in the fake buildroot.

    Args:
      overlay: The overlay name to create. Usually the board name.
      build_root: The buildroot in which to create the mock overlay.
    """
    if not build_root:
      build_root = self.build_root

    layout_path = os.path.join(build_root, 'src', 'overlays',
                               'overlay-%s' % overlay,
                               'metadata', 'layout.conf')

    layout_content = 'repo-name = %s\n' % overlay

    # Don't use osutils.WriteFile, some tests mock it out.
    osutils.SafeMakedirs(os.path.dirname(layout_path))
    with open(layout_path, 'w') as f:
      f.writelines(cros_build_lib.iflatten_instance(layout_content))

  def _Prepare(self, bot_id=None, extra_config=None, cmd_args=None,
               extra_cmd_args=None, build_id=DEFAULT_BUILD_ID,
               master_build_id=None, buildbucket_id=None,
               site_config=None):
    """Prepare a BuilderRun at self._run for this test.

    This method must allow being called more than once.  Subclasses can
    override this method, but those subclass methods should also call this one.

    The idea is that all test preparation that falls out from the choice of
    build config and cbuildbot options should go in _Prepare.

    This will populate the following attributes on self:
      run: A BuilderRun object.
      bot_id: The bot id (name) that was used from the site_config.
      self._boards: Same as self._run.config.boards.  TODO(mtennant): remove.
      self._current_board: First board in list, if there is one.

    Args:
      bot_id: Name of build config to use, defaults to self.BOT_ID.
      extra_config: Dict used to add to the build config for the given
        bot_id.  Example: {'push_image': True}.
      cmd_args: List to override the default cbuildbot command args, including
        the bot_id.
      extra_cmd_args: List to add to default cbuildbot command args.  This
        is a good way to adjust an options value for your test.
        Example: ['branch-name', 'some-branch-name'] will effectively cause
        self._run.options.branch_name to be set to 'some-branch-name'.
      build_id: mock build id
      buildbucket_id: mock buildbucket_id
      master_build_id: mock build id of master build.
      site_config: SiteConfig to use (or MockSiteConfig)
    """
    assert not bot_id or not cmd_args

    # Use cbuildbot parser to create options object and populate default values.
    if not cmd_args:
      # Fill in default command args.
      cmd_args = [
          '-r', self.build_root, '--buildbot', '--noprebuilts',
          '--buildnumber', str(DEFAULT_BUILD_NUMBER),
          '--branch', self.TARGET_MANIFEST_BRANCH,
          bot_id or self.BOT_ID
      ]
    if extra_cmd_args:
      cmd_args += extra_cmd_args

    parser = cbuildbot._CreateParser()
    options = cbuildbot.ParseCommandLine(parser, cmd_args)
    self._bot_id = options.build_config_name

    if site_config is None:
      site_config = config_lib.GetConfig()

    # Populate build_config corresponding to self._bot_id.
    build_config = copy.deepcopy(site_config[self._bot_id])
    build_config['manifest_repo_url'] = 'fake_url'
    if extra_config:
      build_config.update(extra_config)
    options.managed_chrome = build_config['sync_chrome']

    self._boards = build_config['boards']
    self._current_board = self._boards[0] if self._boards else None
    self._model = self._current_board

    # Some preliminary sanity checks.
    self.assertEqual(options.buildroot, self.build_root)

    # Construct a real BuilderRun using options and build_config.
    self._run = cbuildbot_run.BuilderRun(
        options, site_config, build_config, self._manager)

    if build_id is not None:
      self._run.attrs.metadata.UpdateWithDict({'build_id': build_id})

    if buildbucket_id is not None:
      self._run.options.buildbucket_id = buildbucket_id

    if master_build_id is not None:
      self._run.options.master_build_id = master_build_id

    if self.RELEASE_TAG is not None:
      self._run.attrs.release_tag = self.RELEASE_TAG

    portage_util._OVERLAY_LIST_CMD = '/bin/true'

  def tearDown(self):
    # Mimic exiting with statement for self._manager.
    self._manager.__exit__(None, None, None)

  def AutoPatch(self, to_patch):
    """Patch a list of objects with autospec=True.

    Args:
      to_patch: A list of tuples in the form (target, attr) to patch.  Will be
      directly passed to mock.patch.object.
    """
    for item in to_patch:
      self.PatchObject(*item, autospec=True)

  def GetHWTestSuite(self):
    """Get the HW test suite for the current bot."""
    hw_tests = self._run.config['hw_tests']
    if not hw_tests:
      # TODO(milleral): Add HWTests back to lumpy-chrome-perf.
      raise unittest.SkipTest('Missing HWTest for %s' % (self._bot_id,))

    return hw_tests[0]

  def assertRaisesStringifyable(self, exception, functor, *args, **kwargs):
    """assertRaises replacement that also verifies exception is Stringifyable.

    This helper is intended to be used anywhere assertRaises can be used, but
    will also verify the exception raised can pass through
    BuilderStage._StringifyException.

    Args:
      exception: See unittest.TestCase.assertRaises.
      functor: See unittest.TestCase.assertRaises.
      args: See unittest.TestCase.assertRaises.
      kwargs: See unittest.TestCase.assertRaises.

    Raises:
      Unittest failures if the expected exception is not raised, or
      _StringifyException exceptions if that process fails.
    """
    try:
      functor(*args, **kwargs)

      # We didn't get the exception, fail the test.
      self.fail('%s was not raised.' % exception)

    except exception:
      # Ensure that this exception can be converted properly.
      # Verifies fix for crbug.com/418358 and related.
      generic_stages.BuilderStage._StringifyException(sys.exc_info())

    except Exception as e:
      # We didn't get the exception, fail the test.
      self.fail('%s raised instead of %s' % (e, exception))


class AbstractStageTestCase(StageTestCase):
  """Base class for tests that test a particular build stage.

  Abstract base class that sets up the build config and options with some
  default values for testing BuilderStage and its derivatives.
  """
  def setUp(self):
    # Value which will be populated as the stage runs, so that tests can examine
    # it's state afterwards.
    self.stage = None

  def ConstructStage(self):
    """Returns an instance of the stage to be tested.

    Note: Must be implemented in subclasses.
    """
    raise NotImplementedError(self, 'ConstructStage: Implement in your test')

  def RunStage(self, **kwargs):
    """Creates and runs an instance of the stage to be tested.

    Note: Requires ConstructStage() to be implemented.

    Raises:
      NotImplementedError: ConstructStage() was not implemented.
    """

    # Stage construction is usually done as late as possible because the tests
    # set up the build configuration and options used in constructing the stage.
    results_lib.Results.Clear()
    self.stage = self.ConstructStage(**kwargs)
    self.stage.Run()
    self.assertTrue(results_lib.Results.BuildSucceededSoFar())


def patch(*args, **kwargs):
  """Convenience wrapper for mock.patch.object.

  Sets autospec=True by default.
  """
  kwargs.setdefault('autospec', True)
  return mock.patch.object(*args, **kwargs)


@contextlib.contextmanager
def patches(*args):
  """Context manager for a list of patch objects."""
  with cros_build_lib.ContextManagerStack() as stack:
    for arg in args:
      stack.Add(lambda ret=arg: ret)
    yield


class BuilderStageTest(AbstractStageTestCase):
  """Tests for BuilderStage class."""

  def setUp(self):
    self._Prepare()
    self.mock_cidb = mock.MagicMock()
    self.buildstore = FakeBuildStore(self.mock_cidb)
    cidb.CIDBConnectionFactory.SetupMockCidb(self.mock_cidb)
    # Many tests modify the global results_lib.Results instance.
    results_lib.Results.Clear()

  def tearDown(self):
    cidb.CIDBConnectionFactory.ClearMock()

  def _ConstructStageWithExpectations(self, stage_class):
    """Construct an instance of the stage, verifying expectations from init.

    Args:
      stage_class: The class to instantitate.

    Returns:
      The instantiated class instance.
    """
    if stage_class is None:
      stage_class = generic_stages.BuilderStage

    self.PatchObject(
        FakeBuildStore, 'InsertBuildStage',
        return_value=constants.MOCK_STAGE_ID)
    self.PatchObject(
        FakeBuildStore, 'StartBuildStage',
        return_value=constants.MOCK_STAGE_ID)
    self.PatchObject(
        FakeBuildStore, 'WaitBuildStage',
        return_value=constants.MOCK_STAGE_ID)
    self.PatchObject(
        FakeBuildStore, 'FinishBuildStage',
        return_value=constants.MOCK_STAGE_ID)
    stage = stage_class(self._run, self.buildstore)
    self.buildstore.InsertBuildStage.assert_called_once_with(
        constants.MOCK_BUILD_ID, stage.name, None,
        constants.BUILDER_STATUS_PLANNED)
    return stage

  def ConstructStage(self):
    return self._ConstructStageWithExpectations(generic_stages.BuilderStage)

  def testGetPortageEnvVar(self):
    """Basic test case for _GetPortageEnvVar function."""
    stage = self.ConstructStage()
    board = self._current_board

    envvar = 'EXAMPLE'
    envvar_val = 'RESULT'
    self.PatchObject(portage_util, 'PortageqEnvvars',
                     return_value={envvar: envvar_val})

    result = stage._GetPortageEnvVar(envvar, board)
    self.assertEqual(result, envvar_val)

  def testStageNamePrefixSmoke(self):
    """Basic test for the StageNamePrefix() function."""
    stage = self.ConstructStage()
    self.assertEqual(stage.StageNamePrefix(), 'Builder')

  def testGetStageNamesSmoke(self):
    """Basic test for the GetStageNames() function."""
    stage = self.ConstructStage()
    self.assertEqual(stage.GetStageNames(), ['Builder'])

  def testConstructDashboardURLSmoke(self):
    """Basic test for the ConstructDashboardURL() function."""
    stage = self.ConstructStage()

    exp_url = 'https://ci.chromium.org/b/fake_bb_id'
    self.assertEqual(stage.ConstructDashboardURL(), exp_url)

    stage_name = 'Archive'
    exp_url = ('https://luci-logdog.appspot.com/v/?s=chromeos/buildbucket/'
               'cr-buildbucket.appspot.com/1234321/%2B/steps/Archive/0/stdout')

    self.assertEqual(stage.ConstructDashboardURL(stage=stage_name), exp_url)

  def test_PrintSmoke(self):
    """Basic test for the _Print() function."""
    stage = self.ConstructStage()
    with self.OutputCapturer():
      stage._Print('hi there')
    self.AssertOutputContainsLine('hi there', check_stderr=True)

  def test_PrintLoudlySmoke(self):
    """Basic test for the _PrintLoudly() function."""
    stage = self.ConstructStage()
    with self.OutputCapturer():
      stage._PrintLoudly('hi there')
    self.AssertOutputContainsLine(r'\*{10}', check_stderr=True)
    self.AssertOutputContainsLine('hi there', check_stderr=True)

  def testRunSmoke(self):
    """Basic passing test for the Run() function."""
    stage = self.ConstructStage()
    with self.OutputCapturer():
      stage.Run()

  def _RunCapture(self, stage):
    """Helper method to run Run() with captured output."""
    output = self.OutputCapturer()
    output.StartCapturing()
    try:
      stage.Run()
    finally:
      output.StopCapturing()
    return output

  def testRunException(self):
    """Verify stage exceptions are handled."""
    class TestError(Exception):
      """Unique test exception"""

    perform_mock = self.PatchObject(generic_stages.BuilderStage, 'PerformStage')
    perform_mock.side_effect = TestError('fail!')

    stage = self.ConstructStage()
    results_lib.Results.Clear()
    self.assertRaises(failures_lib.StepFailure, self._RunCapture, stage)

    results = results_lib.Results.Get()[0]
    self.assertTrue(isinstance(results.result, TestError))
    self.assertEqual(str(results.result), 'fail!')
    self.buildstore.StartBuildStage.assert_called_once_with(
        DEFAULT_BUILD_STAGE_ID)
    self.buildstore.FinishBuildStage.assert_called_once_with(
        DEFAULT_BUILD_STAGE_ID,
        constants.BUILDER_STATUS_FAILED)

  def testRunExitEarlyException(self):
    """Verify stage exit early exceptions are handled."""
    class TestError(Exception):
      """Unique test exception"""

    perform_mock = self.PatchObject(generic_stages.BuilderStage, 'PerformStage')
    perform_mock.side_effect = TestError('fail!')

    stage = self.ConstructStage()
    results_lib.Results.Clear()
    self.assertRaises(failures_lib.StepFailure, self._RunCapture, stage)

    results = results_lib.Results.Get()[0]
    self.assertTrue(isinstance(results.result, TestError))
    self.assertEqual(str(results.result), 'fail!')
    self.buildstore.StartBuildStage.assert_called_once_with(
        DEFAULT_BUILD_STAGE_ID)
    self.buildstore.FinishBuildStage.assert_called_once_with(
        DEFAULT_BUILD_STAGE_ID,
        constants.BUILDER_STATUS_FAILED)

  def testRunWithWaitFailure(self):
    """Test Run when WaitUntilReady returns False"""
    stage = self.ConstructStage()
    self.PatchObject(generic_stages.BuilderStage,
                     'WaitUntilReady',
                     return_value=False)
    stage.Run()
    self.buildstore.WaitBuildStage.assert_called_once_with(
        DEFAULT_BUILD_STAGE_ID)
    self.buildstore.FinishBuildStage.assert_called_once_with(
        DEFAULT_BUILD_STAGE_ID,
        constants.BUILDER_STATUS_SKIPPED)
    self.assertFalse(self.mock_cidb.StartBuildStage.called)

  @osutils.TempFileDecorator
  def testRunSkipsPreviouslyCompletedStage(self):
    """Test that a stage that has run before is skipped, and marked as such."""
    handle_skip_mock = self.PatchObject(generic_stages.BuilderStage,
                                        'HandleSkip')
    stage = self.ConstructStage()

    # Record a result as if the stage succeeded in a _previous_ run.
    results_lib.Results.Record(stage.name, results_lib.Results.SUCCESS,
                               description='Injected success')
    with open(self.tempfile, 'w') as out:
      results_lib.Results.SaveCompletedStages(out)
    results_lib.Results.Clear()
    with open(self.tempfile, 'r') as out:
      results_lib.Results.RestoreCompletedStages(out)

    output = self._RunCapture(stage)
    all_out = output.GetStdout()
    all_out += output.GetStderr()
    self.assertTrue('[PREVIOUSLY PROCESSED]' in all_out)
    self.assertTrue(handle_skip_mock.called)

  def testHandleExceptionException(self):
    """Verify exceptions in HandleException handlers are themselves handled."""
    class TestError(Exception):
      """Unique test exception"""

    class BadStage(generic_stages.BuilderStage):
      """Stage that throws an exception when PerformStage is called."""

      handled_exceptions = []

      def PerformStage(self):
        raise TestError('first fail')

      def _HandleStageException(self, exc_info):
        self.handled_exceptions.append(str(exc_info[1]))
        raise TestError('nested')

    stage = self._ConstructStageWithExpectations(BadStage)
    results_lib.Results.Clear()
    self.assertRaises(failures_lib.StepFailure, self._RunCapture, stage)

    # Verify the results tracked the original exception.
    results = results_lib.Results.Get()[0]
    self.assertTrue(isinstance(results.result, TestError))
    self.assertEqual(str(results.result), 'first fail')

    self.assertEqual(stage.handled_exceptions, ['first fail'])

    # Verify the stage is still marked as failed in cidb.
    self.buildstore.StartBuildStage.assert_called_once_with(
        DEFAULT_BUILD_STAGE_ID)
    self.buildstore.FinishBuildStage.assert_called_once_with(
        DEFAULT_BUILD_STAGE_ID,
        constants.BUILDER_STATUS_FAILED)


class BuilderStageGetBuildFailureMessage(AbstractStageTestCase):
  """Test GetBuildFailureMessage in BuilderStage."""

  def setUp(self):
    self._Prepare()
    self.buildstore = FakeBuildStore()
    # Many tests modify the global results_lib.Results instance.
    results_lib.Results.Clear()

  def tearDown(self):
    cidb.CIDBConnectionFactory.ClearMock()

  def ConstructStage(self):
    return generic_stages.BuilderStage(self._run, self.buildstore)

  def testGetBuildFailureMessageFromResults(self):
    """Test GetBuildFailureMessageFromResults."""
    ex = failures_lib.StepFailure()
    results_lib.Results.Record('CommitQueueSync', ex)
    stage = self.ConstructStage()
    build_failure_msg = stage.GetBuildFailureMessageFromResults()
    self.assertEqual(build_failure_msg.builder, self.BOT_ID)
    self.assertTrue(isinstance(build_failure_msg.failure_messages[0],
                               failure_message_lib.StageFailureMessage))

  def testGetBuildFailureMessageWithoutBuildStore(self):
    """Test GetBuildFailureMessage without working BuildStore instance."""
    stage = self.ConstructStage()
    message = 'foo'
    get_msg_from_cidb = self.PatchObject(
        stage, 'GetBuildFailureMessageFromBuildStore', return_value=message)
    self.PatchObject(FakeBuildStore, 'AreClientsReady', return_value=False)
    get_msg_from_results = self.PatchObject(
        stage, 'GetBuildFailureMessageFromResults', return_value=message)

    stage.GetBuildFailureMessage()
    get_msg_from_cidb.assert_not_called()
    get_msg_from_results.assert_called_once_with()

  def testGetBuildFailureMessageWithBuildStore(self):
    """Test GetBuildFailureMessage with working BuildStore."""
    db = fake_cidb.FakeCIDBConnection()
    cidb.CIDBConnectionFactory.SetupMockCidb(db)
    stage = self.ConstructStage()
    message = 'foo'
    get_msg_from_cidb = self.PatchObject(
        stage, 'GetBuildFailureMessageFromBuildStore', return_value=message)
    self.PatchObject(FakeBuildStore, 'AreClientsReady', return_value=True)
    get_msg_from_results = self.PatchObject(
        stage, 'GetBuildFailureMessageFromResults', return_value=message)

    build_identifier = BuildIdentifier(cidb_id=constants.MOCK_BUILD_ID,
                                       buildbucket_id=None)
    self.PatchObject(stage._run, 'GetCIDBHandle',
                     return_value=[build_identifier, None])
    stage.GetBuildFailureMessage()
    get_msg_from_cidb.assert_called_once_with(
        self.buildstore, build_identifier)
    get_msg_from_results.assert_not_called()

  def testMeaningfulMessage(self):
    """Tests that all essential components are in the message."""
    stage = self.ConstructStage()

    exception = Exception('failed!')
    stage_failure_message = failures_lib.GetStageFailureMessageFromException(
        'TacoStage', 1, exception, stage_prefix_name='TacoStage')
    self.PatchObject(FakeBuildStore, 'AreClientsReady', return_value=False)
    self.PatchObject(
        results_lib.Results, 'GetStageFailureMessage',
        return_value=[stage_failure_message])

    msg = stage.GetBuildFailureMessage()
    self.assertTrue(stage._run.config.name in msg.message_summary)
    self.assertTrue(stage._run.ConstructDashboardURL() in msg.message_summary)
    self.assertTrue('the builder failed' in msg.message_summary)


class BoardSpecificBuilderStageTest(AbstractStageTestCase):
  """Tests option/config settings on board-specific stages."""

  DEFAULT_BOARD_NAME = 'my_shiny_test_board'

  def setUp(self):
    self.buildstore = FakeBuildStore()
    self._Prepare()

  def ConstructStage(self):
    return generic_stages.BoardSpecificBuilderStage(self._run,
                                                    self.buildstore,
                                                    self.DEFAULT_BOARD_NAME)

  def testBuilderNameContainsBoardName(self):
    self._run.config.grouped = True
    stage = self.ConstructStage()
    self.assertTrue(self.DEFAULT_BOARD_NAME in stage.name)

  # TODO (yjhong): Fix this test.
  # def testCheckOptions(self):
  #   """Makes sure options/config settings are setup correctly."""
  #   parser = cbuildbot._CreateParser()
  #   (options, _) = parser.parse_args([])

  #   for attr in dir(stages):
  #     obj = eval('stages.' + attr)
  #     if not hasattr(obj, '__base__'):
  #       continue
  #     if not obj.__base__ is stages.BoardSpecificBuilderStage:
  #       continue
  #     if obj.option_name:
  #       self.assertTrue(getattr(options, obj.option_name))
  #     if obj.config_name:
  #       if not obj.config_name in config._settings:
  #         self.fail(('cbuildbot_stages.%s.config_name "%s" is missing from '
  #                    'cbuildbot_config._settings') % (attr, obj.config_name))

  def testListOfPackagesToBuild(self):
    """Test the default list of packages to build."""
    stage = self.ConstructStage()
    packages = stage.GetListOfPackagesToBuild()
    expected = ['virtual/target-os', 'virtual/target-os-dev',
                'virtual/target-os-test', 'virtual/target-os-factory',
                'virtual/target-os-factory-shim', 'chromeos-base/autotest-all']
    self.assertEqual(expected, packages)

    # Test if an explicit list of packages is configured, only that list is set.
    self._run.config.packages = ['pkgA', 'pkgB']
    packages = stage.GetListOfPackagesToBuild()
    self.assertEqual(['pkgA', 'pkgB'], packages)

    # Test if a list of packages is set for ChromeOS Findit builds, they are
    # added to an explicit list.
    self._run.options.cbb_build_packages = ['pkgC', 'pkgD']
    packages = stage.GetListOfPackagesToBuild()
    self.assertEqual(['pkgC', 'pkgD', 'pkgA', 'pkgB'], packages)

    # Only check packages setup for ChromeOS Findit.
    self._run.config.packages = []
    packages = stage.GetListOfPackagesToBuild()
    self.assertEqual(['pkgC', 'pkgD'], packages)


class RunCommandAbstractStageTestCase(
    AbstractStageTestCase, cros_test_lib.RunCommandTestCase):
  """Base test class for testing a stage and mocking run."""

  # pylint: disable=abstract-method

  FULL_BOT_ID = 'amd64-generic-full'
  BIN_BOT_ID = 'amd64-generic-incremental'

  def _PrepareFull(self, **kwargs):
    self._Prepare(self.FULL_BOT_ID, **kwargs)

  def _PrepareBin(self, **kwargs):
    self._Prepare(self.BIN_BOT_ID, **kwargs)


class ArchivingStageMixinMock(partial_mock.PartialMock):
  """Partial mock for ArchivingStageMixin."""

  TARGET = 'chromite.cbuildbot.stages.generic_stages.ArchivingStageMixin'
  ATTRS = ('UploadArtifact',)

  def UploadArtifact(self, *args, **kwargs):
    with patch(commands, 'ArchiveFile', return_value='foo.txt'):
      with patch(commands, 'UploadArchivedFile'):
        self.backup['UploadArtifact'](*args, **kwargs)

# -*- coding: utf-8 -*- # Copyright (c) 2012 The Chromium OS Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. Unittests for generic stages. # pylint: disable=protected-access # The inheritance order ensures the patchers are stopped before # cleaning up the temporary directories. Test running a single stage in isolation. # Subclass should override this to default to a different build config # for its tests. # Subclasses can override this.  If non-None, value is inserted into # self._run.attrs.release_tag. # Prepare a fake build root in self.tempdir, save at self.build_root. # Pylint-1.9 has a false positive on this for some reason. # pylint: disable=no-value-for-parameter # These are here to make pylint happy.  Values filled in by _Prepare. Helper for creating an overlay in the fake buildroot.      Args:       overlay: The overlay name to create. Usually the board name.       build_root: The buildroot in which to create the mock overlay. # Don't use osutils.WriteFile, some tests mock it out. Prepare a BuilderRun at self._run for this test.      This method must allow being called more than once.  Subclasses can     override this method, but those subclass methods should also call this one.      The idea is that all test preparation that falls out from the choice of     build config and cbuildbot options should go in _Prepare.      This will populate the following attributes on self:       run: A BuilderRun object.       bot_id: The bot id (name) that was used from the site_config.       self._boards: Same as self._run.config.boards.  TODO(mtennant): remove.       self._current_board: First board in list, if there is one.      Args:       bot_id: Name of build config to use, defaults to self.BOT_ID.       extra_config: Dict used to add to the build config for the given         bot_id.  Example: {'push_image': True}.       cmd_args: List to override the default cbuildbot command args, including         the bot_id.       extra_cmd_args: List to add to default cbuildbot command args.  This         is a good way to adjust an options value for your test.         Example: ['branch-name', 'some-branch-name'] will effectively cause         self._run.options.branch_name to be set to 'some-branch-name'.       build_id: mock build id       buildbucket_id: mock buildbucket_id       master_build_id: mock build id of master build.       site_config: SiteConfig to use (or MockSiteConfig) # Use cbuildbot parser to create options object and populate default values. # Fill in default command args. # Populate build_config corresponding to self._bot_id. # Some preliminary sanity checks. # Construct a real BuilderRun using options and build_config. # Mimic exiting with statement for self._manager. Patch a list of objects with autospec=True.      Args:       to_patch: A list of tuples in the form (target, attr) to patch.  Will be       directly passed to mock.patch.object. Get the HW test suite for the current bot. # TODO(milleral): Add HWTests back to lumpy-chrome-perf. assertRaises replacement that also verifies exception is Stringifyable.      This helper is intended to be used anywhere assertRaises can be used, but     will also verify the exception raised can pass through     BuilderStage._StringifyException.      Args:       exception: See unittest.TestCase.assertRaises.       functor: See unittest.TestCase.assertRaises.       args: See unittest.TestCase.assertRaises.       kwargs: See unittest.TestCase.assertRaises.      Raises:       Unittest failures if the expected exception is not raised, or       _StringifyException exceptions if that process fails. # We didn't get the exception, fail the test. # Ensure that this exception can be converted properly. # Verifies fix for crbug.com/418358 and related. # We didn't get the exception, fail the test. Base class for tests that test a particular build stage.    Abstract base class that sets up the build config and options with some   default values for testing BuilderStage and its derivatives. # Value which will be populated as the stage runs, so that tests can examine # it's state afterwards. Returns an instance of the stage to be tested.      Note: Must be implemented in subclasses. Creates and runs an instance of the stage to be tested.      Note: Requires ConstructStage() to be implemented.      Raises:       NotImplementedError: ConstructStage() was not implemented. # Stage construction is usually done as late as possible because the tests # set up the build configuration and options used in constructing the stage. Convenience wrapper for mock.patch.object.    Sets autospec=True by default. Context manager for a list of patch objects. Tests for BuilderStage class. # Many tests modify the global results_lib.Results instance. Construct an instance of the stage, verifying expectations from init.      Args:       stage_class: The class to instantitate.      Returns:       The instantiated class instance. Basic test case for _GetPortageEnvVar function. Basic test for the StageNamePrefix() function. Basic test for the GetStageNames() function. Basic test for the ConstructDashboardURL() function. Basic test for the _Print() function. Basic test for the _PrintLoudly() function. Basic passing test for the Run() function. Helper method to run Run() with captured output. Verify stage exceptions are handled. Unique test exception Verify stage exit early exceptions are handled. Unique test exception Test Run when WaitUntilReady returns False Test that a stage that has run before is skipped, and marked as such. # Record a result as if the stage succeeded in a _previous_ run. Verify exceptions in HandleException handlers are themselves handled. Unique test exception Stage that throws an exception when PerformStage is called. # Verify the results tracked the original exception. # Verify the stage is still marked as failed in cidb. Test GetBuildFailureMessage in BuilderStage. # Many tests modify the global results_lib.Results instance. Test GetBuildFailureMessageFromResults. Test GetBuildFailureMessage without working BuildStore instance. Test GetBuildFailureMessage with working BuildStore. Tests that all essential components are in the message. Tests option/config settings on board-specific stages. # TODO (yjhong): Fix this test. # def testCheckOptions(self): #   """Makes sure options/config settings are setup correctly.""" #   parser = cbuildbot._CreateParser() #   (options, _) = parser.parse_args([]) #   for attr in dir(stages): #     obj = eval('stages.' + attr) #     if not hasattr(obj, '__base__'): #       continue #     if not obj.__base__ is stages.BoardSpecificBuilderStage: #       continue #     if obj.option_name: #       self.assertTrue(getattr(options, obj.option_name)) #     if obj.config_name: #       if not obj.config_name in config._settings: #         self.fail(('cbuildbot_stages.%s.config_name "%s" is missing from ' #                    'cbuildbot_config._settings') % (attr, obj.config_name)) Test the default list of packages to build. # Test if an explicit list of packages is configured, only that list is set. # Test if a list of packages is set for ChromeOS Findit builds, they are # added to an explicit list. # Only check packages setup for ChromeOS Findit. Base test class for testing a stage and mocking run. # pylint: disable=abstract-method Partial mock for ArchivingStageMixin.