# Copyright 2014 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import json
import os
import shutil
import sys
import tempfile
import unittest
import logging

import mock

from py_utils import cloud_storage
from py_utils.constants import exit_codes

from telemetry import benchmark
from telemetry.core import exceptions
from telemetry.core import util
from telemetry.internal.actions import page_action
from telemetry.internal.results import page_test_results
from telemetry.internal.results import results_options
from telemetry.internal import story_runner
from telemetry.page import legacy_page_test
from telemetry import story as story_module
from telemetry.story import story_filter
from telemetry.testing import fakes
from telemetry.testing import options_for_unittests
from telemetry.testing import test_stories
from telemetry.web_perf import story_test
from telemetry.wpr import archive_info


class RunStorySetTest(unittest.TestCase):
  """Tests that run dummy story sets with a mock StoryTest.

  The main entry point for these tests is story_runner.RunStorySet.
  """
  def setUp(self):
    self.options = options_for_unittests.GetRunOptions(
        output_dir=tempfile.mkdtemp())
    # We use a mock platform and story set, so tests can inspect which methods
    # were called and easily override their behavior.
    self.mock_platform = test_stories.TestSharedState.mock_platform
    self.mock_story_test = mock.Mock(spec=story_test.StoryTest)

  def tearDown(self):
    shutil.rmtree(self.options.output_dir)

  def RunStories(self, stories, **kwargs):
    story_set = test_stories.DummyStorySet(stories)
    with results_options.CreateResults(
        self.options, benchmark_name='benchmark') as results:
      story_runner.RunStorySet(
          self.mock_story_test, story_set, self.options, results, **kwargs)

  def ReadTestResults(self):
    return results_options.ReadTestResults(self.options.intermediate_dir)

  def testRunStorySet(self):
    self.RunStories(['story1', 'story2', 'story3'])
    test_results = self.ReadTestResults()
    self.assertTrue(['PASS', 'PASS', 'PASS'],
                    [test['status'] for test in test_results])

  def testRunStoryWithLongName(self):
    with self.assertRaises(ValueError):
      self.RunStories(['l' * 182])

  def testCallOrderInStoryTest(self):
    """Check the call order of StoryTest methods is as expected."""
    self.RunStories(['foo', 'bar', 'baz'])
    self.assertEqual([call[0] for call in self.mock_story_test.mock_calls],
                     ['WillRunStory', 'Measure', 'DidRunStory'] * 3)

  @mock.patch.object(test_stories.TestSharedState, 'DidRunStory')
  @mock.patch.object(test_stories.TestSharedState, 'RunStory')
  @mock.patch.object(test_stories.TestSharedState, 'WillRunStory')
  def testCallOrderBetweenStoryTestAndSharedState(
      self, will_run_story, run_story, did_run_story):
    """Check the call order between StoryTest and SharedState is correct."""
    root_mock = mock.MagicMock()
    root_mock.attach_mock(self.mock_story_test, 'test')
    root_mock.attach_mock(will_run_story, 'state.WillRunStory')
    root_mock.attach_mock(run_story, 'state.RunStory')
    root_mock.attach_mock(did_run_story, 'state.DidRunStory')

    self.RunStories(['story1'])
    self.assertEqual([call[0] for call in root_mock.mock_calls], [
        'test.WillRunStory',
        'state.WillRunStory',
        'state.RunStory',
        'test.Measure',
        'test.DidRunStory',
        'state.DidRunStory'
    ])

  def testAppCrashExceptionCausesFailure(self):
    self.RunStories([test_stories.DummyStory(
        'story',
        run_side_effect=exceptions.AppCrashException(msg='App Foo crashes'))])
    test_results = self.ReadTestResults()
    self.assertEqual(['FAIL'],
                     [test['status'] for test in test_results])
    self.assertIn('App Foo crashes', sys.stderr.getvalue())

  @mock.patch.object(test_stories.TestSharedState, 'TearDownState')
  def testExceptionRaisedInSharedStateTearDown(self, tear_down_state):
    class TestOnlyException(Exception):
      pass

    tear_down_state.side_effect = TestOnlyException()
    with self.assertRaises(TestOnlyException):
      self.RunStories(['story'])

  def testUnknownExceptionIsNotFatal(self):
    class UnknownException(Exception):
      pass

    self.RunStories([
        test_stories.DummyStory(
            'foo', run_side_effect=UnknownException('FooException')),
        test_stories.DummyStory('bar')])
    test_results = self.ReadTestResults()
    self.assertEqual(['FAIL', 'PASS'],
                     [test['status'] for test in test_results])
    self.assertIn('FooException', sys.stderr.getvalue())

  def testRaiseBrowserGoneExceptionFromRunPage(self):
    self.RunStories([
        test_stories.DummyStory(
            'foo', run_side_effect=exceptions.BrowserGoneException(
                None, 'i am a browser crash message')),
        test_stories.DummyStory('bar')])
    test_results = self.ReadTestResults()
    self.assertEqual(['FAIL', 'PASS'],
                     [test['status'] for test in test_results])
    self.assertIn('i am a browser crash message', sys.stderr.getvalue())

  @mock.patch.object(test_stories.TestSharedState,
                     'DumpStateUponStoryRunFailure')
  @mock.patch.object(test_stories.TestSharedState, 'TearDownState')
  def testAppCrashThenRaiseInTearDown_Interrupted(
      self, tear_down_state, dump_state_upon_story_run_failure):
    class TearDownStateException(Exception):
      pass

    tear_down_state.side_effect = TearDownStateException()
    root_mock = mock.Mock()
    root_mock.attach_mock(tear_down_state, 'state.TearDownState')
    root_mock.attach_mock(dump_state_upon_story_run_failure,
                          'state.DumpStateUponStoryRunFailure')
    self.RunStories([
        test_stories.DummyStory(
            'foo', run_side_effect=exceptions.AppCrashException(msg='crash!')),
        test_stories.DummyStory('bar')])

    self.assertEqual([call[0] for call in root_mock.mock_calls], [
        'state.DumpStateUponStoryRunFailure',
        # This tear down happens because of the app crash.
        'state.TearDownState',
        # This one happens since state must be re-created to check whether
        # later stories should be skipped or unexpectedly skipped. Then
        # state is torn down normally at the end of the runs.
        'state.TearDownState'
    ])

    test_results = self.ReadTestResults()
    self.assertEqual(len(test_results), 2)
    # First story unexpectedly failed with AppCrashException.
    self.assertEqual(test_results[0]['status'], 'FAIL')
    self.assertFalse(test_results[0]['expected'])
    # Second story unexpectedly skipped due to exception during tear down.
    self.assertEqual(test_results[1]['status'], 'SKIP')
    self.assertFalse(test_results[1]['expected'])

  def testPagesetRepeat(self):
    self.options.pageset_repeat = 2
    self.RunStories(['story1', 'story2'])
    test_results = self.ReadTestResults()
    self.assertEqual(['benchmark/story1', 'benchmark/story2'] * 2,
                     [test['testPath'] for test in test_results])
    self.assertEqual(['PASS', 'PASS', 'PASS', 'PASS'],
                     [test['status'] for test in test_results])

  def _testMaxFailuresOptionIsRespectedAndOverridable(
      self, num_failing_stories, runner_max_failures, options_max_failures,
      expected_num_failures, expected_num_skips):
    if options_max_failures:
      self.options.max_failures = options_max_failures
    self.RunStories([
        test_stories.DummyStory(
            'failing_%d' % i, run_side_effect=Exception('boom!'))
        for i in range(num_failing_stories)
    ], max_failures=runner_max_failures)
    test_results = self.ReadTestResults()
    self.assertEqual(len(test_results),
                     expected_num_failures + expected_num_skips)
    for i, test in enumerate(test_results):
      expected_status = 'FAIL' if i < expected_num_failures else 'SKIP'
      self.assertEqual(test['status'], expected_status)

  def testMaxFailuresNotSpecified(self):
    self._testMaxFailuresOptionIsRespectedAndOverridable(
        num_failing_stories=5, runner_max_failures=None,
        options_max_failures=None, expected_num_failures=5,
        expected_num_skips=0)

  def testMaxFailuresSpecifiedToRun(self):
    # Runs up to max_failures+1 failing tests before stopping, since
    # every tests after max_failures failures have been encountered
    # may all be passing.
    self._testMaxFailuresOptionIsRespectedAndOverridable(
        num_failing_stories=5, runner_max_failures=3,
        options_max_failures=None, expected_num_failures=4,
        expected_num_skips=1)

  def testMaxFailuresOption(self):
    # Runs up to max_failures+1 failing tests before stopping, since
    # every tests after max_failures failures have been encountered
    # may all be passing.
    self._testMaxFailuresOptionIsRespectedAndOverridable(
        num_failing_stories=5, runner_max_failures=3,
        options_max_failures=1, expected_num_failures=2,
        expected_num_skips=3)


class UpdateAndCheckArchivesTest(unittest.TestCase):
  """Tests for the private _UpdateAndCheckArchives."""
  def setUp(self):
    mock.patch.object(archive_info.WprArchiveInfo,
                      'DownloadArchivesIfNeeded').start()

  def tearDown(self):
    mock.patch.stopall()

  def testMissingArchiveDataFile(self):
    story_set = test_stories.DummyStorySet(['story'])
    with self.assertRaises(story_runner.ArchiveError):
      story_runner._UpdateAndCheckArchives(
          story_set.archive_data_file, story_set.wpr_archive_info,
          story_set.stories)

  def testArchiveDataFileDoesNotExist(self):
    story_set = test_stories.DummyStorySet(
        ['story'], archive_data_file='does_not_exist.json')
    with self.assertRaises(story_runner.ArchiveError):
      story_runner._UpdateAndCheckArchives(
          story_set.archive_data_file, story_set.wpr_archive_info,
          story_set.stories)

  def testUpdateAndCheckArchivesSuccess(self):
    # This test file has a recording for a 'http://www.testurl.com' story only.
    archive_data_file = os.path.join(
        util.GetUnittestDataDir(), 'archive_files', 'test.json')
    story_set = test_stories.DummyStorySet(
        ['http://www.testurl.com'], archive_data_file=archive_data_file)
    success = story_runner._UpdateAndCheckArchives(
        story_set.archive_data_file, story_set.wpr_archive_info,
        story_set.stories)
    self.assertTrue(success)

  def testArchiveWithMissingStory(self):
    # This test file has a recording for a 'http://www.testurl.com' story only.
    archive_data_file = os.path.join(
        util.GetUnittestDataDir(), 'archive_files', 'test.json')
    story_set = test_stories.DummyStorySet(
        ['http://www.testurl.com', 'http://www.google.com'],
        archive_data_file=archive_data_file)
    with self.assertRaises(story_runner.ArchiveError):
      story_runner._UpdateAndCheckArchives(
          story_set.archive_data_file, story_set.wpr_archive_info,
          story_set.stories)

  def testArchiveWithMissingWprFile(self):
    # This test file claims to have recordings for both
    # 'http://www.testurl.com' and 'http://www.google.com'; but the file with
    # the wpr recording for the later story is actually missing.
    archive_data_file = os.path.join(
        util.GetUnittestDataDir(), 'archive_files',
        'test_missing_wpr_file.json')
    story_set = test_stories.DummyStorySet(
        ['http://www.testurl.com', 'http://www.google.com'],
        archive_data_file=archive_data_file)
    with self.assertRaises(story_runner.ArchiveError):
      story_runner._UpdateAndCheckArchives(
          story_set.archive_data_file, story_set.wpr_archive_info,
          story_set.stories)


class RunStoryAndProcessErrorIfNeededTest(unittest.TestCase):
  """Tests for the private _RunStoryAndProcessErrorIfNeeded.

  All these tests:
  - Use mocks for all objects, including stories. No real browser is involved.
  - Call story_runner._RunStoryAndProcessErrorIfNeeded as entry point.
  """
  def _CreateErrorProcessingMock(self, method_exceptions=None,
                                 legacy_test=False):
    if legacy_test:
      test_class = legacy_page_test.LegacyPageTest
    else:
      test_class = story_test.StoryTest

    root_mock = mock.NonCallableMock(
        story=mock.NonCallableMagicMock(story_module.Story),
        results=mock.NonCallableMagicMock(page_test_results.PageTestResults),
        test=mock.NonCallableMagicMock(test_class),
        state=mock.NonCallableMagicMock(
            story_module.SharedState,
            CanRunStory=mock.Mock(return_value=True)))

    if method_exceptions:
      root_mock.configure_mock(**{
          path + '.side_effect': exception
          for path, exception in method_exceptions.iteritems()})

    return root_mock

  def testRunStoryAndProcessErrorIfNeeded_success(self):
    root_mock = self._CreateErrorProcessingMock()

    story_runner._RunStoryAndProcessErrorIfNeeded(
        root_mock.story, root_mock.results, root_mock.state, root_mock.test)

    self.assertEquals(root_mock.method_calls, [
        mock.call.results.CreateArtifact('logs.txt'),
        mock.call.test.WillRunStory(root_mock.state.platform),
        mock.call.state.WillRunStory(root_mock.story),
        mock.call.state.CanRunStory(root_mock.story),
        mock.call.state.RunStory(root_mock.results),
        mock.call.test.Measure(root_mock.state.platform, root_mock.results),
        mock.call.test.DidRunStory(root_mock.state.platform, root_mock.results),
        mock.call.state.DidRunStory(root_mock.results),
    ])

  def testRunStoryAndProcessErrorIfNeeded_successLegacy(self):
    root_mock = self._CreateErrorProcessingMock(legacy_test=True)

    story_runner._RunStoryAndProcessErrorIfNeeded(
        root_mock.story, root_mock.results, root_mock.state, root_mock.test)

    self.assertEquals(root_mock.method_calls, [
        mock.call.results.CreateArtifact('logs.txt'),
        mock.call.state.WillRunStory(root_mock.story),
        mock.call.state.CanRunStory(root_mock.story),
        mock.call.state.RunStory(root_mock.results),
        mock.call.test.DidRunPage(root_mock.state.platform),
        mock.call.state.DidRunStory(root_mock.results),
    ])

  def testRunStoryAndProcessErrorIfNeeded_tryTimeout(self):
    root_mock = self._CreateErrorProcessingMock(method_exceptions={
        'state.WillRunStory': exceptions.TimeoutException('foo')
    })

    story_runner._RunStoryAndProcessErrorIfNeeded(
        root_mock.story, root_mock.results, root_mock.state, root_mock.test)

    self.assertEquals(root_mock.method_calls, [
        mock.call.results.CreateArtifact('logs.txt'),
        mock.call.test.WillRunStory(root_mock.state.platform),
        mock.call.state.WillRunStory(root_mock.story),
        mock.call.state.DumpStateUponStoryRunFailure(root_mock.results),
        mock.call.results.Fail(
            'Exception raised running %s' % root_mock.story.name),
        mock.call.test.DidRunStory(root_mock.state.platform, root_mock.results),
        mock.call.state.DidRunStory(root_mock.results),
    ])

  def testRunStoryAndProcessErrorIfNeeded_tryAppCrash(self):
    tmp = tempfile.NamedTemporaryFile(delete=False)
    tmp.close()
    temp_file_path = tmp.name
    fake_app = fakes.FakeApp()
    fake_app.recent_minidump_path = temp_file_path
    try:
      app_crash_exception = exceptions.AppCrashException(fake_app, msg='foo')
      root_mock = self._CreateErrorProcessingMock(method_exceptions={
          'state.WillRunStory': app_crash_exception
      })

      with self.assertRaises(exceptions.AppCrashException):
        story_runner._RunStoryAndProcessErrorIfNeeded(
            root_mock.story, root_mock.results, root_mock.state, root_mock.test)

      self.assertListEqual(root_mock.method_calls, [
          mock.call.results.CreateArtifact('logs.txt'),
          mock.call.test.WillRunStory(root_mock.state.platform),
          mock.call.state.WillRunStory(root_mock.story),
          mock.call.state.DumpStateUponStoryRunFailure(root_mock.results),
          mock.call.results.Fail(
              'Exception raised running %s' % root_mock.story.name),
          mock.call.test.DidRunStory(
              root_mock.state.platform, root_mock.results),
          mock.call.state.DidRunStory(root_mock.results),
      ])
    finally:
      os.remove(temp_file_path)

  def testRunStoryAndProcessErrorIfNeeded_tryError(self):
    root_mock = self._CreateErrorProcessingMock(method_exceptions={
        'state.CanRunStory': exceptions.Error('foo')
    })

    with self.assertRaisesRegexp(exceptions.Error, 'foo'):
      story_runner._RunStoryAndProcessErrorIfNeeded(
          root_mock.story, root_mock.results, root_mock.state, root_mock.test)

    self.assertEquals(root_mock.method_calls, [
        mock.call.results.CreateArtifact('logs.txt'),
        mock.call.test.WillRunStory(root_mock.state.platform),
        mock.call.state.WillRunStory(root_mock.story),
        mock.call.state.CanRunStory(root_mock.story),
        mock.call.state.DumpStateUponStoryRunFailure(root_mock.results),
        mock.call.results.Fail(
            'Exception raised running %s' % root_mock.story.name),
        mock.call.test.DidRunStory(root_mock.state.platform, root_mock.results),
        mock.call.state.DidRunStory(root_mock.results),
    ])

  def testRunStoryAndProcessErrorIfNeeded_tryUnsupportedAction(self):
    root_mock = self._CreateErrorProcessingMock(method_exceptions={
        'state.RunStory': page_action.PageActionNotSupported('foo')
    })

    story_runner._RunStoryAndProcessErrorIfNeeded(
        root_mock.story, root_mock.results, root_mock.state, root_mock.test)
    self.assertEquals(root_mock.method_calls, [
        mock.call.results.CreateArtifact('logs.txt'),
        mock.call.test.WillRunStory(root_mock.state.platform),
        mock.call.state.WillRunStory(root_mock.story),
        mock.call.state.CanRunStory(root_mock.story),
        mock.call.state.RunStory(root_mock.results),
        mock.call.results.Skip('Unsupported page action: foo'),
        mock.call.test.DidRunStory(root_mock.state.platform, root_mock.results),
        mock.call.state.DidRunStory(root_mock.results),
    ])

  def testRunStoryAndProcessErrorIfNeeded_tryUnhandlable(self):
    root_mock = self._CreateErrorProcessingMock(method_exceptions={
        'test.WillRunStory': Exception('foo')
    })

    with self.assertRaisesRegexp(Exception, 'foo'):
      story_runner._RunStoryAndProcessErrorIfNeeded(
          root_mock.story, root_mock.results, root_mock.state, root_mock.test)

    self.assertEquals(root_mock.method_calls, [
        mock.call.results.CreateArtifact('logs.txt'),
        mock.call.test.WillRunStory(root_mock.state.platform),
        mock.call.state.DumpStateUponStoryRunFailure(root_mock.results),
        mock.call.results.Fail(
            'Exception raised running %s' % root_mock.story.name),
        mock.call.test.DidRunStory(root_mock.state.platform, root_mock.results),
        mock.call.state.DidRunStory(root_mock.results),
    ])

  def testRunStoryAndProcessErrorIfNeeded_finallyException(self):
    exc = Exception('bar')
    root_mock = self._CreateErrorProcessingMock(method_exceptions={
        'state.DidRunStory': exc,
    })

    with self.assertRaisesRegexp(Exception, 'bar'):
      story_runner._RunStoryAndProcessErrorIfNeeded(
          root_mock.story, root_mock.results, root_mock.state, root_mock.test)

    self.assertEquals(root_mock.method_calls, [
        mock.call.results.CreateArtifact('logs.txt'),
        mock.call.test.WillRunStory(root_mock.state.platform),
        mock.call.state.WillRunStory(root_mock.story),
        mock.call.state.CanRunStory(root_mock.story),
        mock.call.state.RunStory(root_mock.results),
        mock.call.test.Measure(root_mock.state.platform, root_mock.results),
        mock.call.test.DidRunStory(root_mock.state.platform, root_mock.results),
        mock.call.state.DidRunStory(root_mock.results),
        mock.call.state.DumpStateUponStoryRunFailure(root_mock.results),
    ])

  def testRunStoryAndProcessErrorIfNeeded_tryTimeout_finallyException(self):
    root_mock = self._CreateErrorProcessingMock(method_exceptions={
        'state.RunStory': exceptions.TimeoutException('foo'),
        'state.DidRunStory': Exception('bar')
    })

    story_runner._RunStoryAndProcessErrorIfNeeded(
        root_mock.story, root_mock.results, root_mock.state, root_mock.test)

    self.assertEquals(root_mock.method_calls, [
        mock.call.results.CreateArtifact('logs.txt'),
        mock.call.test.WillRunStory(root_mock.state.platform),
        mock.call.state.WillRunStory(root_mock.story),
        mock.call.state.CanRunStory(root_mock.story),
        mock.call.state.RunStory(root_mock.results),
        mock.call.state.DumpStateUponStoryRunFailure(root_mock.results),
        mock.call.results.Fail(
            'Exception raised running %s' % root_mock.story.name),
        mock.call.test.DidRunStory(root_mock.state.platform, root_mock.results),
        mock.call.state.DidRunStory(root_mock.results),
    ])

  def testRunStoryAndProcessErrorIfNeeded_tryError_finallyException(self):
    root_mock = self._CreateErrorProcessingMock(method_exceptions={
        'state.WillRunStory': exceptions.Error('foo'),
        'test.DidRunStory': Exception('bar')
    })

    with self.assertRaisesRegexp(exceptions.Error, 'foo'):
      story_runner._RunStoryAndProcessErrorIfNeeded(
          root_mock.story, root_mock.results, root_mock.state, root_mock.test)

    self.assertEquals(root_mock.method_calls, [
        mock.call.results.CreateArtifact('logs.txt'),
        mock.call.test.WillRunStory(root_mock.state.platform),
        mock.call.state.WillRunStory(root_mock.story),
        mock.call.state.DumpStateUponStoryRunFailure(root_mock.results),
        mock.call.results.Fail(
            'Exception raised running %s' % root_mock.story.name),
        mock.call.test.DidRunStory(root_mock.state.platform, root_mock.results),
    ])

  def testRunStoryAndProcessErrorIfNeeded_tryUnsupportedAction_finallyException(
      self):
    root_mock = self._CreateErrorProcessingMock(method_exceptions={
        'test.WillRunStory': page_action.PageActionNotSupported('foo'),
        'state.DidRunStory': Exception('bar')
    })

    story_runner._RunStoryAndProcessErrorIfNeeded(
        root_mock.story, root_mock.results, root_mock.state, root_mock.test)

    self.assertEquals(root_mock.method_calls, [
        mock.call.results.CreateArtifact('logs.txt'),
        mock.call.test.WillRunStory(root_mock.state.platform),
        mock.call.results.Skip('Unsupported page action: foo'),
        mock.call.test.DidRunStory(
            root_mock.state.platform, root_mock.results),
        mock.call.state.DidRunStory(root_mock.results),
    ])

  def testRunStoryAndProcessErrorIfNeeded_tryUnhandlable_finallyException(self):
    root_mock = self._CreateErrorProcessingMock(method_exceptions={
        'test.Measure': Exception('foo'),
        'test.DidRunStory': Exception('bar')
    })

    with self.assertRaisesRegexp(Exception, 'foo'):
      story_runner._RunStoryAndProcessErrorIfNeeded(
          root_mock.story, root_mock.results, root_mock.state, root_mock.test)

    self.assertEquals(root_mock.method_calls, [
        mock.call.results.CreateArtifact('logs.txt'),
        mock.call.test.WillRunStory(root_mock.state.platform),
        mock.call.state.WillRunStory(root_mock.story),
        mock.call.state.CanRunStory(root_mock.story),
        mock.call.state.RunStory(root_mock.results),
        mock.call.test.Measure(root_mock.state.platform, root_mock.results),
        mock.call.state.DumpStateUponStoryRunFailure(root_mock.results),
        mock.call.results.Fail(
            'Exception raised running %s' % root_mock.story.name),
        mock.call.test.DidRunStory(root_mock.state.platform, root_mock.results),
    ])


class FakeBenchmark(benchmark.Benchmark):
  test = test_stories.DummyStoryTest
  NAME = 'fake_benchmark'

  def __init__(self, stories=None, **kwargs):
    """A customizable fake_benchmark.

    Args:
      stories: Optional sequence of either story names or objects. Instances
        of DummyStory are useful here. If omitted the benchmark will contain
        a single DummyStory.
      other kwargs are passed to the test_stories.DummyStorySet constructor.
    """
    super(FakeBenchmark, self).__init__()
    self._story_set = test_stories.DummyStorySet(
        stories if stories is not None else ['story'], **kwargs)

  @classmethod
  def Name(cls):
    return cls.NAME

  def CreateStorySet(self, _):
    return self._story_set


class FakeStoryFilter(object):
  def __init__(self, stories_to_filter_out=None, stories_to_skip=None):
    self._stories_to_filter = stories_to_filter_out or []
    self._stories_to_skip = stories_to_skip or []
    assert isinstance(self._stories_to_filter, list)
    assert isinstance(self._stories_to_skip, list)

  def FilterStories(self, story_set):
    return [story for story in story_set
            if story.name not in self._stories_to_filter]

  def ShouldSkip(self, story):
    return 'fake_reason' if story.name in self._stories_to_skip else ''


def ReadDiagnostics(test_result):
  artifact = test_result['outputArtifacts'][page_test_results.DIAGNOSTICS_NAME]
  with open(artifact['filePath']) as f:
    return json.load(f)['diagnostics']


class RunBenchmarkTest(unittest.TestCase):
  """Tests that run fake benchmarks, no real browser is involved.

  All these tests:
  - Use a FakeBenchmark instance.
  - Call GetFakeBrowserOptions to get options for a fake browser.
  - Call story_runner.RunBenchmark as entry point.
  """
  def setUp(self):
    self.output_dir = tempfile.mkdtemp()

  def tearDown(self):
    shutil.rmtree(self.output_dir)

  def GetFakeBrowserOptions(self, overrides=None):
    return options_for_unittests.GetRunOptions(
        output_dir=self.output_dir,
        fake_browser=True, overrides=overrides)

  def ReadTestResults(self):
    return results_options.ReadTestResults(
        os.path.join(self.output_dir, 'artifacts'))

  def testDisabledBenchmarkViaCanRunOnPlatform(self):
    fake_benchmark = FakeBenchmark()
    fake_benchmark.SUPPORTED_PLATFORMS = []
    options = self.GetFakeBrowserOptions()
    story_runner.RunBenchmark(fake_benchmark, options)
    test_results = self.ReadTestResults()
    self.assertFalse(test_results)  # No tests ran at all.

  def testSkippedWithStoryFilter(self):
    fake_benchmark = FakeBenchmark(stories=['fake_story'])
    options = self.GetFakeBrowserOptions()
    fake_story_filter = FakeStoryFilter(stories_to_skip=['fake_story'])
    with mock.patch(
        'telemetry.story.story_filter.StoryFilterFactory.BuildStoryFilter',
        return_value=fake_story_filter):
      story_runner.RunBenchmark(fake_benchmark, options)
    test_results = self.ReadTestResults()
    self.assertTrue(test_results)  # Some tests ran, but all skipped.
    self.assertTrue(all(t['status'] == 'SKIP' for t in test_results))

  def testOneStorySkippedOneNot(self):
    fake_story_filter = FakeStoryFilter(stories_to_skip=['story1'])
    fake_benchmark = FakeBenchmark(stories=['story1', 'story2'])
    options = self.GetFakeBrowserOptions()
    with mock.patch(
        'telemetry.story.story_filter.StoryFilterFactory.BuildStoryFilter',
        return_value=fake_story_filter):
      story_runner.RunBenchmark(fake_benchmark, options)
    test_results = self.ReadTestResults()
    status = [t['status'] for t in test_results]
    self.assertEqual(len(status), 2)
    self.assertIn('SKIP', status)
    self.assertIn('PASS', status)

  def testOneStoryFilteredOneNot(self):
    fake_story_filter = FakeStoryFilter(stories_to_filter_out=['story1'])
    fake_benchmark = FakeBenchmark(stories=['story1', 'story2'])
    options = self.GetFakeBrowserOptions()
    with mock.patch(
        'telemetry.story.story_filter.StoryFilterFactory.BuildStoryFilter',
        return_value=fake_story_filter):
      story_runner.RunBenchmark(fake_benchmark, options)
    test_results = self.ReadTestResults()
    self.assertEqual(len(test_results), 1)
    self.assertEqual(test_results[0]['status'], 'PASS')
    self.assertTrue(test_results[0]['testPath'].endswith('/story2'))

  def testValidateBenchmarkName(self):
    class FakeBenchmarkWithBadName(FakeBenchmark):
      NAME = 'bad/benchmark (name)'

    fake_benchmark = FakeBenchmarkWithBadName()
    options = self.GetFakeBrowserOptions()
    return_code = story_runner.RunBenchmark(fake_benchmark, options)
    self.assertEqual(return_code, 2)
    self.assertIn('Invalid benchmark name', sys.stderr.getvalue())

  def testWithOwnerInfo(self):

    @benchmark.Owner(emails=['alice@chromium.org', 'bob@chromium.org'],
                     component='fooBar',
                     documentation_url='https://example.com/')
    class FakeBenchmarkWithOwner(FakeBenchmark):
      pass

    fake_benchmark = FakeBenchmarkWithOwner()
    options = self.GetFakeBrowserOptions()
    story_runner.RunBenchmark(fake_benchmark, options)
    test_results = self.ReadTestResults()
    diagnostics = ReadDiagnostics(test_results[0])
    self.assertEqual(diagnostics['owners'],
                     ['alice@chromium.org', 'bob@chromium.org'])
    self.assertEqual(diagnostics['bugComponents'], ['fooBar'])
    self.assertEqual(diagnostics['documentationLinks'],
                     [['Benchmark documentation link', 'https://example.com/']])

  def testWithOwnerInfoButNoUrl(self):

    @benchmark.Owner(emails=['alice@chromium.org'])
    class FakeBenchmarkWithOwner(FakeBenchmark):
      pass

    fake_benchmark = FakeBenchmarkWithOwner()
    options = self.GetFakeBrowserOptions()
    story_runner.RunBenchmark(fake_benchmark, options)
    test_results = self.ReadTestResults()
    diagnostics = ReadDiagnostics(test_results[0])
    self.assertEqual(diagnostics['owners'], ['alice@chromium.org'])
    self.assertNotIn('documentationLinks', diagnostics)

  def testDeviceInfo(self):
    fake_benchmark = FakeBenchmark(stories=['fake_story'])
    options = self.GetFakeBrowserOptions()
    options.fake_possible_browser = fakes.FakePossibleBrowser(
        arch_name='abc', os_name='win', os_version_name='win10')
    story_runner.RunBenchmark(fake_benchmark, options)
    test_results = self.ReadTestResults()
    diagnostics = ReadDiagnostics(test_results[0])
    self.assertEqual(diagnostics['architectures'], ['abc'])
    self.assertEqual(diagnostics['osNames'], ['win'])
    self.assertEqual(diagnostics['osVersions'], ['win10'])

  def testReturnCodeDisabledStory(self):
    fake_benchmark = FakeBenchmark(stories=['fake_story'])
    fake_story_filter = FakeStoryFilter(stories_to_skip=['fake_story'])
    options = self.GetFakeBrowserOptions()
    with mock.patch(
        'telemetry.story.story_filter.StoryFilterFactory.BuildStoryFilter',
        return_value=fake_story_filter):
      return_code = story_runner.RunBenchmark(fake_benchmark, options)
    self.assertEqual(return_code, exit_codes.ALL_TESTS_SKIPPED)

  def testReturnCodeSuccessfulRun(self):
    fake_benchmark = FakeBenchmark()
    options = self.GetFakeBrowserOptions()
    return_code = story_runner.RunBenchmark(fake_benchmark, options)
    self.assertEqual(return_code, exit_codes.SUCCESS)

  def testReturnCodeCaughtException(self):
    fake_benchmark = FakeBenchmark(stories=[
        test_stories.DummyStory(
            'story', run_side_effect=exceptions.AppCrashException())])
    options = self.GetFakeBrowserOptions()
    return_code = story_runner.RunBenchmark(fake_benchmark, options)
    self.assertEqual(return_code, exit_codes.TEST_FAILURE)

  def testReturnCodeUnhandleableError(self):
    fake_benchmark = FakeBenchmark(stories=[
        test_stories.DummyStory(
            'story', run_side_effect=MemoryError('Unhandleable'))])
    options = self.GetFakeBrowserOptions()
    return_code = story_runner.RunBenchmark(fake_benchmark, options)
    self.assertEqual(return_code, exit_codes.FATAL_ERROR)

  def testRunStoryWithMissingArchiveFile(self):
    fake_benchmark = FakeBenchmark(archive_data_file='data/does-not-exist.json')
    options = self.GetFakeBrowserOptions()
    return_code = story_runner.RunBenchmark(fake_benchmark, options)
    self.assertEqual(return_code, 2)  # Benchmark was interrupted.
    self.assertIn('ArchiveError', sys.stderr.getvalue())

  def testDownloadMinimalServingDirs(self):
    fake_benchmark = FakeBenchmark(stories=[
        test_stories.DummyStory(
            'story_foo', serving_dir='/files/foo', tags=['foo']),
        test_stories.DummyStory(
            'story_bar', serving_dir='/files/bar', tags=['bar']),
    ], cloud_bucket=cloud_storage.PUBLIC_BUCKET)
    options = self.GetFakeBrowserOptions(overrides={'story_tag_filter': 'foo'})
    with mock.patch(
        'py_utils.cloud_storage.GetFilesInDirectoryIfChanged') as get_files:
      story_runner.RunBenchmark(fake_benchmark, options)

    # Foo is the only included story serving dir.
    self.assertEqual(get_files.call_count, 1)
    get_files.assert_called_once_with('/files/foo', cloud_storage.PUBLIC_BUCKET)

  def testAbridged(self):
    options = self.GetFakeBrowserOptions()
    options.run_abridged_story_set = True
    story_filter.StoryFilterFactory.ProcessCommandLineArgs(
        parser=None, args=options)
    fake_benchmark = FakeBenchmark(stories=[
        test_stories.DummyStory('story1', tags=['important']),
        test_stories.DummyStory('story2', tags=['other']),
    ], abridging_tag='important')
    story_runner.RunBenchmark(fake_benchmark, options)
    test_results = self.ReadTestResults()
    self.assertEqual(len(test_results), 1)
    self.assertTrue(test_results[0]['testPath'].endswith('/story1'))

  def testFullRun(self):
    options = self.GetFakeBrowserOptions()
    story_filter.StoryFilterFactory.ProcessCommandLineArgs(
        parser=None, args=options)
    fake_benchmark = FakeBenchmark(stories=[
        test_stories.DummyStory('story1', tags=['important']),
        test_stories.DummyStory('story2', tags=['other']),
    ], abridging_tag='important')
    story_runner.RunBenchmark(fake_benchmark, options)
    test_results = self.ReadTestResults()
    self.assertEqual(len(test_results), 2)

  def testStoryFlag(self):
    options = self.GetFakeBrowserOptions()
    args = fakes.FakeParsedArgsForStoryFilter(stories=['story1', 'story3'])
    story_filter.StoryFilterFactory.ProcessCommandLineArgs(
        parser=None, args=args)
    fake_benchmark = FakeBenchmark(stories=['story1', 'story2', 'story3'])
    story_runner.RunBenchmark(fake_benchmark, options)
    test_results = self.ReadTestResults()
    self.assertEqual(len(test_results), 2)
    self.assertTrue(test_results[0]['testPath'].endswith('/story1'))
    self.assertTrue(test_results[1]['testPath'].endswith('/story3'))

  def testArtifactLogsContainHandleableException(self):
    def failed_run():
      logging.warning('This will fail gracefully')
      raise exceptions.TimeoutException('karma!')

    fake_benchmark = FakeBenchmark(stories=[
        test_stories.DummyStory('story1', run_side_effect=failed_run),
        test_stories.DummyStory('story2')
    ])

    options = self.GetFakeBrowserOptions()
    return_code = story_runner.RunBenchmark(fake_benchmark, options)
    self.assertEqual(return_code, exit_codes.TEST_FAILURE)
    test_results = self.ReadTestResults()
    self.assertEqual(len(test_results), 2)

    # First story failed.
    self.assertEqual(test_results[0]['testPath'], 'fake_benchmark/story1')
    self.assertEqual(test_results[0]['status'], 'FAIL')
    self.assertIn('logs.txt', test_results[0]['outputArtifacts'])

    with open(test_results[0]['outputArtifacts']['logs.txt']['filePath']) as f:
      test_log = f.read()

    # Ensure that the log contains warning messages and python stack.
    self.assertIn('Handleable error', test_log)
    self.assertIn('This will fail gracefully', test_log)
    self.assertIn("raise exceptions.TimeoutException('karma!')", test_log)

    # Second story ran fine.
    self.assertEqual(test_results[1]['testPath'], 'fake_benchmark/story2')
    self.assertEqual(test_results[1]['status'], 'PASS')

  def testArtifactLogsContainUnhandleableException(self):
    def failed_run():
      logging.warning('This will fail badly')
      raise MemoryError('this is a fatal exception')

    fake_benchmark = FakeBenchmark(stories=[
        test_stories.DummyStory('story1', run_side_effect=failed_run),
        test_stories.DummyStory('story2')
    ])

    options = self.GetFakeBrowserOptions()
    return_code = story_runner.RunBenchmark(fake_benchmark, options)
    self.assertEqual(return_code, exit_codes.FATAL_ERROR)
    test_results = self.ReadTestResults()
    self.assertEqual(len(test_results), 2)

    # First story failed.
    self.assertEqual(test_results[0]['testPath'], 'fake_benchmark/story1')
    self.assertEqual(test_results[0]['status'], 'FAIL')
    self.assertIn('logs.txt', test_results[0]['outputArtifacts'])

    with open(test_results[0]['outputArtifacts']['logs.txt']['filePath']) as f:
      test_log = f.read()

    # Ensure that the log contains warning messages and python stack.
    self.assertIn('Unhandleable error', test_log)
    self.assertIn('This will fail badly', test_log)
    self.assertIn("raise MemoryError('this is a fatal exception')", test_log)

    # Second story was skipped.
    self.assertEqual(test_results[1]['testPath'], 'fake_benchmark/story2')
    self.assertEqual(test_results[1]['status'], 'SKIP')

  def testUnexpectedSkipsWithFiltering(self):
    # We prepare side effects for 50 stories, the first 30 run fine, the
    # remaining 20 fail with a fatal error.
    fatal_error = MemoryError('this is an unexpected exception')
    side_effects = [None] * 30 + [fatal_error] * 20

    fake_benchmark = FakeBenchmark(stories=(
        test_stories.DummyStory('story_%i' % i, run_side_effect=effect)
        for i, effect in enumerate(side_effects)))

    # Set the filtering to only run from story_10 --> story_40
    options = self.GetFakeBrowserOptions({
        'story_shard_begin_index': 10,
        'story_shard_end_index': 41})
    return_code = story_runner.RunBenchmark(fake_benchmark, options)
    self.assertEquals(exit_codes.FATAL_ERROR, return_code)

    # The results should contain entries of story 10 --> story 40. Of those
    # entries, story 31's actual result is 'FAIL' and
    # stories from 31 to 40 will shows 'SKIP'.
    test_results = self.ReadTestResults()
    self.assertEqual(len(test_results), 31)

    expected = []
    expected.extend(('story_%i' % i, 'PASS') for i in xrange(10, 30))
    expected.append(('story_30', 'FAIL'))
    expected.extend(('story_%i' % i, 'SKIP') for i in xrange(31, 41))

    for (story, status), result in zip(expected, test_results):
      self.assertEqual(result['testPath'], 'fake_benchmark/%s' % story)
      self.assertEqual(result['status'], status)
