# Copyright 2017 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.

from __future__ import print_function
from __future__ import division
from __future__ import absolute_import

import base64
import itertools
import json
import mock
import random
import string
import sys
import webapp2
import webtest
import zlib

from google.appengine.ext import ndb

from dashboard import add_histograms
from dashboard import add_histograms_queue
from dashboard.api import api_auth
from dashboard.api import api_request_handler
from dashboard.common import testing_common
from dashboard.common import utils
from dashboard.models import graph_data
from dashboard.models import histogram
from dashboard.sheriff_config_client import SheriffConfigClient
from tracing.value import histogram as histogram_module
from tracing.value import histogram_set
from tracing.value.diagnostics import breakdown
from tracing.value.diagnostics import date_range
from tracing.value.diagnostics import generic_set
from tracing.value.diagnostics import reserved_infos

# pylint: disable=too-many-lines


def SetGooglerOAuth(mock_oauth):
  mock_oauth.get_current_user.return_value = testing_common.INTERNAL_USER
  mock_oauth.get_client_id.return_value = api_auth.OAUTH_CLIENT_ID_WHITELIST[0]


def _CreateHistogram(
    name='hist', master=None, bot=None, benchmark=None,
    device=None, owner=None, stories=None, story_tags=None,
    benchmark_description=None, commit_position=None, summary_options=None,
    samples=None, max_samples=None, is_ref=False, is_summary=None,
    point_id=None, build_url=None, revision_timestamp=None):
  hists = [histogram_module.Histogram(name, 'count')]
  if max_samples:
    hists[0].max_num_sample_values = max_samples
  if summary_options:
    hists[0].CustomizeSummaryOptions(summary_options)
  if samples:
    for s in samples:
      hists[0].AddSample(s)

  histograms = histogram_set.HistogramSet(hists)
  if master:
    histograms.AddSharedDiagnosticToAllHistograms(
        reserved_infos.MASTERS.name,
        generic_set.GenericSet([master]))
  if bot:
    histograms.AddSharedDiagnosticToAllHistograms(
        reserved_infos.BOTS.name,
        generic_set.GenericSet([bot]))
  if commit_position:
    histograms.AddSharedDiagnosticToAllHistograms(
        reserved_infos.CHROMIUM_COMMIT_POSITIONS.name,
        generic_set.GenericSet([commit_position]))
  if benchmark:
    histograms.AddSharedDiagnosticToAllHistograms(
        reserved_infos.BENCHMARKS.name,
        generic_set.GenericSet([benchmark]))
  if benchmark_description:
    histograms.AddSharedDiagnosticToAllHistograms(
        reserved_infos.BENCHMARK_DESCRIPTIONS.name,
        generic_set.GenericSet([benchmark_description]))
  if owner:
    histograms.AddSharedDiagnosticToAllHistograms(
        reserved_infos.OWNERS.name,
        generic_set.GenericSet([owner]))
  if device:
    histograms.AddSharedDiagnosticToAllHistograms(
        reserved_infos.DEVICE_IDS.name,
        generic_set.GenericSet([device]))
  if stories:
    histograms.AddSharedDiagnosticToAllHistograms(
        reserved_infos.STORIES.name,
        generic_set.GenericSet(stories))
  if story_tags:
    histograms.AddSharedDiagnosticToAllHistograms(
        reserved_infos.STORY_TAGS.name,
        generic_set.GenericSet(story_tags))
  if is_ref:
    histograms.AddSharedDiagnosticToAllHistograms(
        reserved_infos.IS_REFERENCE_BUILD.name,
        generic_set.GenericSet([True]))
  if is_summary is not None:
    histograms.AddSharedDiagnosticToAllHistograms(
        reserved_infos.SUMMARY_KEYS.name,
        generic_set.GenericSet(is_summary))
  if point_id is not None:
    histograms.AddSharedDiagnosticToAllHistograms(
        reserved_infos.POINT_ID.name,
        generic_set.GenericSet([point_id]))
  if build_url is not None:
    histograms.AddSharedDiagnosticToAllHistograms(
        reserved_infos.BUILD_URLS.name,
        generic_set.GenericSet([['build', build_url]]))
  if revision_timestamp is not None:
    histograms.AddSharedDiagnosticToAllHistograms(
        reserved_infos.REVISION_TIMESTAMPS.name,
        date_range.DateRange(revision_timestamp))
  return histograms


class BufferedFakeFile(object):
  def __init__(self, data=str()):
    self.data = data
    self.position = 0

  def read(self, size=None): # pylint: disable=invalid-name
    if self.position == len(self.data):
      return ''
    if size is None or size < 0:
      result = self.data[self.position:]
      self.position = len(self.data)
      return result
    if size > len(self.data) + self.position:
      result = self.data[self.position:]
      self.position = len(self.data)
      return result

    current_position = self.position
    self.position += size
    result = self.data[current_position:self.position]
    return result

  def write(self, data): # pylint: disable=invalid-name
    self.data += data
    return len(data)

  def close(self): # pylint: disable=invalid-name
    pass

  def __exit__(self, *args):
    self.close()
    return False

  def __enter__(self):
    return self


class AddHistogramsBaseTest(testing_common.TestCase):

  def setUp(self):
    super(AddHistogramsBaseTest, self).setUp()
    app = webapp2.WSGIApplication([
        ('/add_histograms', add_histograms.AddHistogramsHandler),
        ('/add_histograms/process', add_histograms.AddHistogramsProcessHandler),
        ('/add_histograms_queue',
         add_histograms_queue.AddHistogramsQueueHandler)
    ])
    self.testapp = webtest.TestApp(app)
    testing_common.SetIsInternalUser('foo@bar.com', True)
    self.SetCurrentUser('foo@bar.com', is_admin=True)
    oauth_patcher = mock.patch.object(api_auth, 'oauth')
    self.addCleanup(oauth_patcher.stop)
    SetGooglerOAuth(oauth_patcher.start())

    identity_patcher = mock.patch.object(utils, 'app_identity')
    self.addCleanup(identity_patcher.stop)
    self.mock_utils = identity_patcher.start()
    self.mock_utils.get_application_id.return_value = 'chromeperf'

    patcher = mock.patch.object(add_histograms, 'cloudstorage')
    self.mock_cloudstorage = patcher.start()
    self.addCleanup(patcher.stop)

    patcher = mock.patch('logging.error')
    self.mock_error = patcher.start()
    self.addCleanup(patcher.stop)

  def PostAddHistogram(self, data, status=200):
    mock_obj = mock.MagicMock(wraps=BufferedFakeFile())
    self.mock_cloudstorage.open.return_value = mock_obj

    self.testapp.post('/add_histograms', data, status=status)
    self.ExecuteTaskQueueTasks('/add_histograms/process', 'default')

  def PostAddHistogramProcess(self, data):
    mock_read = mock.MagicMock(wraps=BufferedFakeFile(zlib.compress(data)))
    self.mock_cloudstorage.open.return_value = mock_read

    # TODO(simonhatch): Should we surface the error somewhere that can be
    # retrieved by the uploader?

    r = self.testapp.post(
        '/add_histograms/process', json.dumps({'gcs_file_path': ''}))
    self.assertTrue(self.mock_error.called)
    return r


#TODO(fancl): mocking Match to return some actuall result
@mock.patch.object(SheriffConfigClient, '__init__',
                   mock.MagicMock(return_value=None))
@mock.patch.object(SheriffConfigClient, 'Match',
                   mock.MagicMock(return_value=([], None)))
class AddHistogramsEndToEndTest(AddHistogramsBaseTest):

  def setUp(self):
    super(AddHistogramsEndToEndTest, self).setUp()

  @mock.patch.object(
      add_histograms_queue.graph_revisions, 'AddRowsToCacheAsync')
  @mock.patch.object(add_histograms_queue.find_anomalies, 'ProcessTestsAsync')
  def testPost_Succeeds(self, mock_process_test, mock_graph_revisions):
    hs = _CreateHistogram(
        master='master', bot='bot', benchmark='benchmark', commit_position=123,
        benchmark_description='Benchmark description.', samples=[1, 2, 3])
    data = json.dumps(hs.AsDicts())

    self.PostAddHistogram({'data': data})
    self.ExecuteTaskQueueTasks('/add_histograms_queue',
                               add_histograms.TASK_QUEUE_NAME)
    diagnostics = histogram.SparseDiagnostic.query().fetch()
    self.assertEqual(4, len(diagnostics))
    histograms = histogram.Histogram.query().fetch()
    self.assertEqual(1, len(histograms))

    tests = graph_data.TestMetadata.query().fetch()

    self.assertEqual('Benchmark description.', tests[0].description)

    # Verify that an anomaly processing was called.
    self.assertEqual(mock_process_test.call_count, 1)
    rows = graph_data.Row.query().fetch()
    # We want to verify that the method was called with all rows that have
    # been added, but the ordering will be different because we produce
    # the rows by iterating over a dict.
    mock_graph_revisions.assert_called_once_with(mock.ANY)
    self.assertEqual(len(mock_graph_revisions.mock_calls[0][1][0]), len(rows))

  @mock.patch.object(
      add_histograms_queue.graph_revisions, 'AddRowsToCacheAsync')
  @mock.patch.object(add_histograms_queue.find_anomalies, 'ProcessTestsAsync')
  def testPost_ZlibSucceeds(self, mock_process_test, mock_graph_revisions):
    hs = _CreateHistogram(
        master='master', bot='bot', benchmark='benchmark', commit_position=123,
        benchmark_description='Benchmark description.', samples=[1, 2, 3])
    data = zlib.compress(json.dumps(hs.AsDicts()))

    self.PostAddHistogram(data)
    self.ExecuteTaskQueueTasks('/add_histograms_queue',
                               add_histograms.TASK_QUEUE_NAME)
    diagnostics = histogram.SparseDiagnostic.query().fetch()
    self.assertEqual(4, len(diagnostics))
    histograms = histogram.Histogram.query().fetch()
    self.assertEqual(1, len(histograms))

    tests = graph_data.TestMetadata.query().fetch()

    self.assertEqual('Benchmark description.', tests[0].description)

    # Verify that an anomaly processing was called.
    self.assertEqual(mock_process_test.call_count, 1)

    rows = graph_data.Row.query().fetch()
    # We want to verify that the method was called with all rows that have
    # been added, but the ordering will be different because we produce
    # the rows by iterating over a dict.
    mock_graph_revisions.assert_called_once_with(mock.ANY)
    self.assertEqual(len(mock_graph_revisions.mock_calls[0][1][0]), len(rows))

  @mock.patch.object(
      add_histograms_queue.graph_revisions, 'AddRowsToCacheAsync',
      mock.MagicMock())
  @mock.patch.object(
      add_histograms_queue.find_anomalies, 'ProcessTestsAsync',
      mock.MagicMock())
  def testPost_BuildUrls_Added(self):
    hs = _CreateHistogram(
        master='master', bot='bot', benchmark='benchmark', commit_position=123,
        benchmark_description='Benchmark description.', samples=[1, 2, 3],
        build_url='http://foo')
    data = zlib.compress(json.dumps(hs.AsDicts()))

    self.PostAddHistogram(data)
    self.ExecuteTaskQueueTasks('/add_histograms_queue',
                               add_histograms.TASK_QUEUE_NAME)

    rows = graph_data.Row.query().fetch()
    self.assertEqual(rows[0].a_build_uri, '[build](http://foo)')

  def testPost_NotZlib_Fails(self):
    hs = _CreateHistogram(
        master='master', bot='bot', benchmark='benchmark', commit_position=123,
        benchmark_description='Benchmark description.', samples=[1, 2, 3])
    data = json.dumps(hs.AsDicts())

    self.PostAddHistogram(data, status=400)

  @mock.patch.object(
      add_histograms_queue.graph_revisions, 'AddRowsToCacheAsync',
      mock.MagicMock())
  @mock.patch.object(
      add_histograms_queue.find_anomalies, 'ProcessTestsAsync',
      mock.MagicMock())
  def testPost_BenchmarkTotalDuration_Filtered(self):
    hs = _CreateHistogram(
        name='benchmark_total_duration', master='master', bot='bot',
        benchmark='v8.browsing', commit_position=123, samples=[1], is_ref=True)
    data = json.dumps(hs.AsDicts())

    self.testapp.post('/add_histograms', {'data': data})
    self.ExecuteTaskQueueTasks('/add_histograms_queue',
                               add_histograms.TASK_QUEUE_NAME)
    tests = graph_data.TestMetadata.query().fetch()
    for t in tests:
      parts = t.key.id().split('/')
      if len(parts) <= 3:
        continue
      self.assertEqual('benchmark_total_duration', parts[3])

  def testPost_PurgesBinData(self):
    hs = _CreateHistogram(
        master='m', bot='b', benchmark='s', commit_position=1)
    b = breakdown.Breakdown()
    dm = histogram_module.DiagnosticMap()
    dm['breakdown'] = b
    hs.GetFirstHistogram().AddSample(0, dm)
    data = json.dumps(hs.AsDicts())

    self.PostAddHistogram({'data': data})
    self.ExecuteTaskQueueTasks('/add_histograms_queue',
                               add_histograms.TASK_QUEUE_NAME)

    histograms = histogram.Histogram.query().fetch()
    hist = histogram_module.Histogram.FromDict(histograms[0].data)
    for b in hist.bins:
      for dm in b.diagnostic_maps:
        self.assertEqual(0, len(dm))

  def testPost_IllegalMasterName_Fails(self):
    hs = _CreateHistogram(
        master='m/m', bot='b', benchmark='s', commit_position=1, samples=[1])
    data = json.dumps(hs.AsDicts())

    response = self.PostAddHistogramProcess(data)
    self.assertIn('Illegal slash', response.body)

  def testPost_IllegalBotName_Fails(self):
    hs = _CreateHistogram(
        master='m', bot='b/b', benchmark='s', commit_position=1, samples=[1])
    data = json.dumps(hs.AsDicts())

    response = self.PostAddHistogramProcess(data)
    self.assertIn('Illegal slash', response.body)

  def testPost_IllegalSuiteName_Fails(self):
    hs = _CreateHistogram(
        master='m', bot='b', benchmark='s/s', commit_position=1, samples=[1])
    data = json.dumps(hs.AsDicts())

    response = self.PostAddHistogramProcess(data)
    self.assertIn('Illegal slash', response.body)

  def testPost_DuplicateHistogram_Fails(self):
    hs1 = _CreateHistogram(
        master='m', bot='b', benchmark='s', commit_position=1, samples=[1])
    hs = _CreateHistogram(
        master='m', bot='b', benchmark='s', commit_position=1, samples=[1])
    hs.ImportDicts(hs1.AsDicts())
    data = json.dumps(hs.AsDicts())

    response = self.PostAddHistogramProcess(data)
    self.assertIn('Duplicate histogram detected', response.body)

  @mock.patch.object(
      add_histograms_queue.graph_revisions, 'AddRowsToCacheAsync')
  @mock.patch.object(add_histograms_queue.find_anomalies, 'ProcessTestsAsync')
  @mock.patch.object(
      add_histograms_queue, 'CreateRowEntities',
      mock.MagicMock(return_value=None))
  def testPost_EmptyHistogram_NotAdded(
      self, mock_process_test, mock_graph_revisions):
    hs = _CreateHistogram(
        master='m', bot='b', benchmark='s', name='foo', commit_position=1,
        samples=[])
    data = json.dumps(hs.AsDicts())

    self.testapp.post('/add_histograms', {'data': data})
    self.ExecuteTaskQueueTasks('/add_histograms_queue',
                               add_histograms.TASK_QUEUE_NAME)

    rows = graph_data.Row.query().fetch()
    self.assertEqual(0, len(rows))

    histograms = histogram.Histogram.query().fetch()
    self.assertEqual(0, len(histograms))

    self.assertFalse(mock_process_test.called)
    self.assertFalse(mock_graph_revisions.called)

  @mock.patch.object(add_histograms_queue.find_anomalies, 'ProcessTestsAsync')
  def testPost_TestNameEndsWithUnderscoreRef_ProcessTestIsNotCalled(
      self, mock_process_test):
    """Tests that Tests ending with "_ref" aren't analyzed for Anomalies."""
    hs = _CreateHistogram(
        master='master', bot='bot', benchmark='benchmark',
        commit_position=424242, stories=['abcd'], samples=[1, 2, 3],
        is_ref=True)
    data = json.dumps(hs.AsDicts())
    self.PostAddHistogram({'data': data})
    self.ExecuteTaskQueueTasks('/add_histograms_queue',
                               add_histograms.TASK_QUEUE_NAME)
    mock_process_test.assert_called_once_with([])

  @mock.patch.object(add_histograms_queue.find_anomalies, 'ProcessTestsAsync')
  def testPost_TestNameEndsWithSlashRef_ProcessTestIsNotCalled(
      self, mock_process_test):
    """Tests that leaf tests named ref aren't added to the task queue."""
    hs = _CreateHistogram(
        master='master', bot='bot', benchmark='benchmark',
        commit_position=424242, stories=['ref'], samples=[1, 2, 3])
    data = json.dumps(hs.AsDicts())
    self.PostAddHistogram({'data': data})
    self.ExecuteTaskQueueTasks('/add_histograms_queue',
                               add_histograms.TASK_QUEUE_NAME)
    mock_process_test.assert_called_once_with([])

  @mock.patch.object(add_histograms_queue.find_anomalies, 'ProcessTestsAsync')
  def testPost_TestNameEndsContainsButDoesntEndWithRef_ProcessTestIsCalled(
      self, mock_process_test):
    hs = _CreateHistogram(
        master='master', bot='bot', benchmark='benchmark',
        commit_position=424242, stories=['_ref_abcd'], samples=[1, 2, 3])
    data = json.dumps(hs.AsDicts())
    self.PostAddHistogram({'data': data})
    self.ExecuteTaskQueueTasks('/add_histograms_queue',
                               add_histograms.TASK_QUEUE_NAME)
    self.assertTrue(mock_process_test.called)

  @mock.patch.object(
      add_histograms_queue.graph_revisions, 'AddRowsToCacheAsync',
      mock.MagicMock())
  @mock.patch.object(
      add_histograms_queue.find_anomalies, 'ProcessTestsAsync',
      mock.MagicMock())
  def testPost_DeduplicateByName(self):
    hs = _CreateHistogram(
        master='m', bot='b', benchmark='s', stories=['s1', 's2'],
        commit_position=1111, device='device1', owner='owner1', samples=[42])
    self.PostAddHistogram({'data': json.dumps(hs.AsDicts())})
    self.ExecuteTaskQueueTasks('/add_histograms_queue',
                               add_histograms.TASK_QUEUE_NAME)

    # There should be the 4 suite level and 2 histogram level diagnostics
    diagnostics = histogram.SparseDiagnostic.query().fetch()
    self.assertEqual(6, len(diagnostics))

    hs = _CreateHistogram(
        master='m', bot='b', benchmark='s', stories=['s1', 's2'],
        commit_position=1112, device='device1', owner='owner1', samples=[42])
    self.PostAddHistogram({'data': json.dumps(hs.AsDicts())})
    self.ExecuteTaskQueueTasks('/add_histograms_queue',
                               add_histograms.TASK_QUEUE_NAME)

    # There should STILL be the 4 suite level and 2 histogram level diagnostics
    diagnostics = histogram.SparseDiagnostic.query().fetch()
    self.assertEqual(6, len(diagnostics))

    hs = _CreateHistogram(
        master='m', bot='b', benchmark='s', stories=['s1', 's2'],
        commit_position=1113, device='device2', owner='owner1', samples=[42])
    self.PostAddHistogram({'data': json.dumps(hs.AsDicts())})
    self.ExecuteTaskQueueTasks('/add_histograms_queue',
                               add_histograms.TASK_QUEUE_NAME)

    # There should one additional device diagnostic since that changed
    diagnostics = histogram.SparseDiagnostic.query().fetch()
    self.assertEqual(7, len(diagnostics))

    hs = _CreateHistogram(
        master='m', bot='b', benchmark='s', stories=['s1', 's2'],
        commit_position=1114, device='device2', owner='owner2')
    self.PostAddHistogram({'data': json.dumps(hs.AsDicts())})
    self.ExecuteTaskQueueTasks('/add_histograms_queue',
                               add_histograms.TASK_QUEUE_NAME)

    # Now there's an additional owner diagnostic
    diagnostics = histogram.SparseDiagnostic.query().fetch()
    self.assertEqual(8, len(diagnostics))

    hs = _CreateHistogram(
        master='m', bot='b', benchmark='s', stories=['s1', 's2'],
        commit_position=1115, device='device2', owner='owner2', samples=[42])
    self.PostAddHistogram({'data': json.dumps(hs.AsDicts())})
    self.ExecuteTaskQueueTasks('/add_histograms_queue',
                               add_histograms.TASK_QUEUE_NAME)

    # No more new diagnostics
    diagnostics = histogram.SparseDiagnostic.query().fetch()
    self.assertEqual(8, len(diagnostics))

  @mock.patch.object(
      add_histograms_queue.graph_revisions, 'AddRowsToCacheAsync',
      mock.MagicMock())
  @mock.patch.object(
      add_histograms_queue.find_anomalies, 'ProcessTestsAsync',
      mock.MagicMock())
  def testPost_NamesAreSet(self):
    hs = _CreateHistogram(
        master='master', bot='bot', benchmark='benchmark',
        commit_position=12345, device='foo', samples=[42])

    self.PostAddHistogram({'data': json.dumps(hs.AsDicts())})
    self.ExecuteTaskQueueTasks('/add_histograms_queue',
                               add_histograms.TASK_QUEUE_NAME)

    diagnostics = histogram.SparseDiagnostic.query().fetch()
    self.assertEqual(4, len(diagnostics))

    # The first 3 are all suite level diagnostics, the last is a histogram
    # level diagnostic.
    names = [
        reserved_infos.MASTERS.name,
        reserved_infos.BOTS.name,
        reserved_infos.BENCHMARKS.name,
        reserved_infos.DEVICE_IDS.name]
    for d in diagnostics:
      self.assertIn(d.name, names)
      names.remove(d.name)

  def _TestDiagnosticsInternalOnly(self):
    hs = _CreateHistogram(
        master='master', bot='bot', benchmark='benchmark',
        commit_position=12345)

    self.PostAddHistogram({'data': json.dumps(hs.AsDicts())})
    self.ExecuteTaskQueueTasks('/add_histograms_queue',
                               add_histograms.TASK_QUEUE_NAME)

  @mock.patch.object(
      add_histograms_queue.graph_revisions, 'AddRowsToCacheAsync',
      mock.MagicMock())
  @mock.patch.object(
      add_histograms_queue.find_anomalies, 'ProcessTestsAsync',
      mock.MagicMock())
  def testPost_DiagnosticsInternalOnly_False(self):
    graph_data.Bot(key=ndb.Key('Master', 'master', 'Bot', 'bot'),
                   internal_only=False).put()
    self._TestDiagnosticsInternalOnly()

    diagnostics = histogram.SparseDiagnostic.query().fetch()
    for d in diagnostics:
      self.assertFalse(d.internal_only)

  @mock.patch.object(
      add_histograms_queue.graph_revisions, 'AddRowsToCacheAsync',
      mock.MagicMock())
  @mock.patch.object(
      add_histograms_queue.find_anomalies, 'ProcessTestsAsync',
      mock.MagicMock())
  def testPost_DiagnosticsInternalOnly_True(self):
    self._TestDiagnosticsInternalOnly()

    diagnostics = histogram.SparseDiagnostic.query().fetch()
    for d in diagnostics:
      self.assertTrue(d.internal_only)

  def testPost_SetsCorrectTestPathForSummary(self):
    histograms = _CreateHistogram(
        master='master', bot='bot', benchmark='benchmark',
        commit_position=12345, device='device_foo', stories=['story'],
        story_tags=['group:media', 'case:browse'], is_summary=['name'],
        samples=[42])

    self.PostAddHistogram({'data': json.dumps(histograms.AsDicts())})
    self.ExecuteTaskQueueTasks('/add_histograms_queue',
                               add_histograms.TASK_QUEUE_NAME)

    tests = graph_data.TestMetadata.query().fetch()

    expected = [
        'master/bot/benchmark',
        'master/bot/benchmark/hist',
        'master/bot/benchmark/hist_avg',
        'master/bot/benchmark/hist_count',
        'master/bot/benchmark/hist_max',
        'master/bot/benchmark/hist_min',
        'master/bot/benchmark/hist_std',
        'master/bot/benchmark/hist_sum',
    ]

    for test in tests:
      self.assertIn(test.key.id(), expected)
      expected.remove(test.key.id())
    self.assertEqual(0, len(expected))

  def testPost_SetsCorrectTestPathForTIRLabelSummary(self):
    histograms = _CreateHistogram(
        master='master', bot='bot', benchmark='benchmark',
        commit_position=12345, device='device_foo', stories=['story'],
        story_tags=['group:media', 'case:browse'],
        is_summary=['name', 'storyTags'], samples=[42])

    self.PostAddHistogram({'data': json.dumps(histograms.AsDicts())})
    self.ExecuteTaskQueueTasks('/add_histograms_queue',
                               add_histograms.TASK_QUEUE_NAME)

    tests = graph_data.TestMetadata.query().fetch()

    expected = [
        'master/bot/benchmark',
        'master/bot/benchmark/hist',
        'master/bot/benchmark/hist/browse_media',
        'master/bot/benchmark/hist_avg',
        'master/bot/benchmark/hist_avg/browse_media',
        'master/bot/benchmark/hist_count',
        'master/bot/benchmark/hist_count/browse_media',
        'master/bot/benchmark/hist_max',
        'master/bot/benchmark/hist_max/browse_media',
        'master/bot/benchmark/hist_min',
        'master/bot/benchmark/hist_min/browse_media',
        'master/bot/benchmark/hist_std',
        'master/bot/benchmark/hist_std/browse_media',
        'master/bot/benchmark/hist_sum',
        'master/bot/benchmark/hist_sum/browse_media',
    ]

    for test in tests:
      self.assertIn(test.key.id(), expected)
      expected.remove(test.key.id())
    self.assertEqual(0, len(expected))

  def testPost_SetsCorrectTestPathForNonSummary(self):
    histograms = _CreateHistogram(
        master='master', bot='bot', benchmark='benchmark',
        commit_position=12345, device='device_foo', stories=['story'],
        is_summary=None, samples=[42])

    self.PostAddHistogram({'data': json.dumps(histograms.AsDicts())})
    self.ExecuteTaskQueueTasks('/add_histograms_queue',
                               add_histograms.TASK_QUEUE_NAME)

    tests = [t.key.id() for t in graph_data.TestMetadata.query().fetch()]
    self.assertEqual(15, len(tests))  # suite + hist + stats + story per stat
    self.assertIn('master/bot/benchmark/hist/story', tests)

  def testPost_SetsCorrectTestPathForSummaryAbsent(self):
    histograms = _CreateHistogram(
        master='master', bot='bot', benchmark='benchmark',
        commit_position=12345, device='device_foo', stories=['story'],