# Copyright 2016 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 logging
import os
import shutil
import sys
import tempfile

_SRC_DIR = os.path.abspath(os.path.join(
    os.path.dirname(__file__), '..', '..', '..'))

sys.path.append(os.path.join(_SRC_DIR, 'third_party', 'catapult', 'devil'))
from devil.android import device_utils

sys.path.append(os.path.join(_SRC_DIR, 'third_party', 'catapult', 'telemetry',
    'third_party', 'websocket-client'))
import websocket

import chrome_cache
import common_util
import controller
import devtools_monitor
import device_setup
import loading_trace


# Standard filenames in the sandwich runner's output directory.
ERROR_FILENAME = 'error'
TRACE_FILENAME = 'trace.json'
VIDEO_FILENAME = 'video.mp4'
WPR_LOG_FILENAME = 'wpr.log'

# Memory dump category used to get memory metrics.
MEMORY_DUMP_CATEGORY = 'disabled-by-default-memory-infra'

# Devtools timeout of 1 minute to avoid websocket timeout on slow
# network condition.
_DEVTOOLS_TIMEOUT = 60

# Categories to enable or disable for all traces collected. Disabled categories
# are prefixed with '-'.
_TRACING_CATEGORIES = [
  'blink',
  'blink.net',
  'blink.user_timing',
  'devtools.timeline',
  'java',
  'navigation',
  'toplevel',
  'v8',
  '-cc',  # A lot of unnecessary events are enabled by default in "cc".
]

TTFMP_ADDITIONAL_CATEGORIES = [
  'loading',
  'disabled-by-default-blink.debug.layout',
]

def _CleanArtefactsFromPastRuns(output_directories_path):
  """Cleans artifacts generated from past run in the output directory.

  Args:
    output_directories_path: The output directory path where to clean the
        previous traces.
  """
  for dirname in os.listdir(output_directories_path):
    directory_path = os.path.join(output_directories_path, dirname)
    if not os.path.isdir(directory_path):
      continue
    try:
      int(dirname)
    except ValueError:
      continue
    shutil.rmtree(directory_path)


class CacheOperation(object):
  CLEAR, SAVE, PUSH = range(3)


class SandwichRunnerError(Exception):
  pass


class SandwichRunner(object):
  """Sandwich runner.

  This object is meant to be configured first and then run using the Run()
  method.
  """
  _ATTEMPT_COUNT = 3
  _STOP_DELAY_MULTIPLIER = 2
  _ABORT_RUN_TIMEOUT_SECONDS = 30 * 60

  def __init__(self):
    """Configures a sandwich runner out of the box.

    Public members are meant to be configured before calling Run().
    """
    # Cache operation to do before doing the chrome navigation.
    self.cache_operation = CacheOperation.CLEAR

    # The cache archive's path to save to or push from. Is str or None.
    self.cache_archive_path = None

    # List of additional chrome command line flags.
    self.chrome_args = []

    # Controls whether the WPR server should do script injection.
    self.disable_wpr_script_injection = False

    # Number of times to repeat the url.
    self.repeat = 1

    # Network conditions to emulate. None if no emulation.
    self.network_condition = None

    # Network condition emulator. Can be: browser,wpr
    self.network_emulator = 'browser'

    # Output directory where to save the traces, videos, etc. Is str or None.
    self.output_dir = None

    # URL to navigate to.
    self.url = None

    # Configures whether to record speed-index video.
    self.record_video = False

    # Configures whether to record memory dumps.
    self.record_memory_dumps = False

    # Configures whether to record tracing categories needed for TTFMP.
    self.record_first_meaningful_paint = False

    # Path to the WPR archive to load or save. Is str or None.
    self.wpr_archive_path = None

    # Configures whether the WPR archive should be read or generated.
    self.wpr_record = False

    # The android DeviceUtils to run sandwich on or None to run it locally.
    self.android_device = None

    self._chrome_ctl = None
    self._local_cache_directory_path = None

  def _CleanTraceOutputDirectory(self):
    assert self.output_dir
    if not os.path.isdir(self.output_dir):
      try:
        os.makedirs(self.output_dir)
      except OSError:
        logging.error('Cannot create directory for results: %s',
            self.output_dir)
        raise
    else:
      _CleanArtefactsFromPastRuns(self.output_dir)

  def _GetEmulatorNetworkCondition(self, emulator):
    if self.network_emulator == emulator:
      return self.network_condition
    return None

  def _RunNavigation(self, clear_cache, repeat_id=None):
    """Run a page navigation to the given URL.

    Args:
      clear_cache: Whether if the cache should be cleared before navigation.
      repeat_id: Id of the run in the output directory. If it is None, then no
        trace or video will be saved.
    """
    run_path = None
    if repeat_id is not None:
      run_path = os.path.join(self.output_dir, str(repeat_id))
      if not os.path.isdir(run_path):
        os.makedirs(run_path)
    self._chrome_ctl.SetNetworkEmulation(
        self._GetEmulatorNetworkCondition('browser'))
    categories = _TRACING_CATEGORIES
    if self.record_memory_dumps:
      categories += [MEMORY_DUMP_CATEGORY]
    if self.record_first_meaningful_paint:
      categories += TTFMP_ADDITIONAL_CATEGORIES
    stop_delay_multiplier = 0
    if self.wpr_record or self.cache_operation == CacheOperation.SAVE:
      stop_delay_multiplier = self._STOP_DELAY_MULTIPLIER
    # TODO(gabadie): add a way to avoid recording a trace.
    with common_util.TimeoutScope(
        self._ABORT_RUN_TIMEOUT_SECONDS, 'Sandwich run overdue.'):
      with self._chrome_ctl.Open() as connection:
        if clear_cache:
          connection.ClearCache()

        # Binds all parameters of RecordUrlNavigation() to avoid repetition.
        def RecordTrace():
          return loading_trace.LoadingTrace.RecordUrlNavigation(
              url=self.url,
              connection=connection,
              chrome_metadata=self._chrome_ctl.ChromeMetadata(),
              categories=categories,
              timeout_seconds=_DEVTOOLS_TIMEOUT,
              stop_delay_multiplier=stop_delay_multiplier)

        if run_path is not None and self.record_video:
          device = self._chrome_ctl.GetDevice()
          if device is None:
            raise RuntimeError('Can only record video on a remote device.')
          video_recording_path = os.path.join(run_path, VIDEO_FILENAME)
          with device_setup.RemoteSpeedIndexRecorder(device, connection,
                                                     video_recording_path):
            trace = RecordTrace()
        else:
          trace = RecordTrace()
        for event in trace.request_track.GetEvents():
          if event.failed:
            logging.warning(
                'request to %s failed: %s', event.url, event.error_text)
        if not trace.tracing_track.HasLoadingSucceeded():
          raise SandwichRunnerError('Page load has failed.')
    if run_path is not None:
      trace_path = os.path.join(run_path, TRACE_FILENAME)
      trace.ToJsonFile(trace_path)

  def _RunInRetryLoop(self, repeat_id, perform_dry_run_before):
    """Attempts to run monitoring navigation.

    Args:
      repeat_id: Id of the run in the output directory.
      perform_dry_run_before: Whether it should do a dry run attempt before the
        actual monitoring run.

    Returns:
      Whether the device should be rebooted to continue attempting for that
      given |repeat_id|.
    """
    resume_attempt_id = 0
    if perform_dry_run_before:
      resume_attempt_id = 1
    for attempt_id in xrange(resume_attempt_id, self._ATTEMPT_COUNT):
      try:
        if perform_dry_run_before:
          logging.info('Do sandwich dry run attempt %d', attempt_id)
        else:
          logging.info('Do sandwich run attempt %d', attempt_id)
        self._chrome_ctl.ResetBrowserState()
        clear_cache = False
        if self.cache_operation == CacheOperation.CLEAR:
          clear_cache = True
        elif self.cache_operation == CacheOperation.PUSH:
          self._chrome_ctl.PushBrowserCache(self._local_cache_directory_path)
        elif self.cache_operation == CacheOperation.SAVE:
          clear_cache = repeat_id == 0
        self._RunNavigation(clear_cache=clear_cache, repeat_id=repeat_id)
        if not perform_dry_run_before or attempt_id > resume_attempt_id:
          break
      except controller.ChromeControllerError as error:
        request_reboot = False
        is_intermittent = error.IsIntermittent()
        if (self.android_device and
            attempt_id == 0 and
            error.error_type is websocket.WebSocketConnectionClosedException):
          assert not perform_dry_run_before
          # On Android, the first socket connection closure is likely caused by
          # memory pressure on the device and therefore considered intermittent,
          # and therefore request a reboot of the device to the caller.
          request_reboot = True
          is_intermittent = True
        if is_intermittent and attempt_id + 1 != self._ATTEMPT_COUNT:
          dump_filename = '{}_intermittent_failure'.format(attempt_id)
          dump_path = os.path.join(
              self.output_dir, str(repeat_id), dump_filename)
        else:
          dump_path = os.path.join(self.output_dir, ERROR_FILENAME)
        with open(dump_path, 'w') as dump_output:
          error.Dump(dump_output)
        if not is_intermittent:
          error.RaiseOriginal()
        if request_reboot:
          assert resume_attempt_id is 0
          return True
    else:
      logging.error('Failed to navigate to %s after %d attemps' % \
                    (self.url, self._ATTEMPT_COUNT))
      error.RaiseOriginal()
    return False

  def _RunWithWpr(self, resume_repeat_id, perform_dry_run_before):
    """Opens WPR and attempts to run repeated monitoring navigation.

    Args:
      resume_repeat_id: Id of the run to resume.
      perform_dry_run_before: Whether the repeated run to resume should first do
        a dry run navigation attempt.

    Returns:
      Number of repeat performed. If < self.repeat, then it means that the
        device should be rebooted.
    """
    with self._chrome_ctl.OpenWprHost(self.wpr_archive_path,
        record=self.wpr_record,
        network_condition_name=self._GetEmulatorNetworkCondition('wpr'),
        disable_script_injection=self.disable_wpr_script_injection,
        out_log_path=os.path.join(self.output_dir, WPR_LOG_FILENAME)):
      for repeat_id in xrange(resume_repeat_id, self.repeat):
        reboot_requested = self._RunInRetryLoop(
            repeat_id, perform_dry_run_before)
        if reboot_requested:
          return repeat_id
    return self.repeat

  def _PullCacheFromDevice(self):
    assert self.cache_operation == CacheOperation.SAVE
    assert self.cache_archive_path, 'Need to specify where to save the cache'

    cache_directory_path = self._chrome_ctl.PullBrowserCache()
    chrome_cache.ZipDirectoryContent(
        cache_directory_path, self.cache_archive_path)
    shutil.rmtree(cache_directory_path)

  def Run(self):
    """SandwichRunner main entry point meant to be called once configured."""
    assert self.output_dir is not None
    assert self._chrome_ctl == None
    assert self._local_cache_directory_path == None
    self._CleanTraceOutputDirectory()

    if self.android_device:
      self._chrome_ctl = controller.RemoteChromeController(self.android_device)
    else:
      self._chrome_ctl = controller.LocalChromeController()
    self._chrome_ctl.AddChromeArguments(['--disable-infobars'])
    self._chrome_ctl.AddChromeArguments(self.chrome_args)
    if self.cache_operation == CacheOperation.SAVE:
      self._chrome_ctl.SetSlowDeath()
    try:
      if self.cache_operation == CacheOperation.PUSH:
        assert os.path.isfile(self.cache_archive_path)
        self._local_cache_directory_path = tempfile.mkdtemp(suffix='.cache')
        chrome_cache.UnzipDirectoryContent(
            self.cache_archive_path, self._local_cache_directory_path)
      times_repeated = self._RunWithWpr(0, False)
      if times_repeated < self.repeat:
        self._chrome_ctl.RebootDevice()
        self._RunWithWpr(times_repeated, True)
    finally:
      if self._local_cache_directory_path:
        shutil.rmtree(self._local_cache_directory_path)
        self._local_cache_directory_path = None
    if self.cache_operation == CacheOperation.SAVE:
      self._PullCacheFromDevice()

    self._chrome_ctl = None


def WalkRepeatedRuns(runner_output_dir):
  """Yields unordered (repeat id, path of the repeat directory).

  Args:
    runner_output_dir: Same as for SandwichRunner.output_dir.
  """
  repeated_run_count = 0
  for node_name in os.listdir(runner_output_dir):
    repeat_dir = os.path.join(runner_output_dir, node_name)
    if not os.path.isdir(repeat_dir):
      continue
    try:
      repeat_id = int(node_name)
    except ValueError:
      continue
    yield repeat_id, repeat_dir
    repeated_run_count += 1
  assert repeated_run_count > 0, ('Error: not a sandwich runner output '
                                  'directory: {}').format(runner_output_dir)

# Copyright 2016 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 logging
import os
import shutil
import sys
import tempfile

_SRC_DIR = os.path.abspath(os.path.join(
    os.path.dirname(__file__), '..', '..', '..'))

sys.path.append(os.path.join(_SRC_DIR, 'third_party', 'catapult', 'devil'))
from devil.android import device_utils

sys.path.append(os.path.join(_SRC_DIR, 'third_party', 'catapult', 'telemetry',
    'third_party', 'websocket-client'))
import websocket

import chrome_cache
import common_util
import controller
import devtools_monitor
import device_setup
import loading_trace


# Standard filenames in the sandwich runner's output directory.
ERROR_FILENAME = 'error'
TRACE_FILENAME = 'trace.json'
VIDEO_FILENAME = 'video.mp4'
WPR_LOG_FILENAME = 'wpr.log'

# Memory dump category used to get memory metrics.
MEMORY_DUMP_CATEGORY = 'disabled-by-default-memory-infra'

# Devtools timeout of 1 minute to avoid websocket timeout on slow
# network condition.
_DEVTOOLS_TIMEOUT = 60

# Categories to enable or disable for all traces collected. Disabled categories
# are prefixed with '-'.
_TRACING_CATEGORIES = [
  'blink',
  'blink.net',
  'blink.user_timing',
  'devtools.timeline',
  'java',
  'navigation',
  'toplevel',
  'v8',
  '-cc',  # A lot of unnecessary events are enabled by default in "cc".
]

TTFMP_ADDITIONAL_CATEGORIES = [
  'loading',
  'disabled-by-default-blink.debug.layout',
]

def _CleanArtefactsFromPastRuns(output_directories_path):
  """Cleans artifacts generated from past run in the output directory.

  Args:
    output_directories_path: The output directory path where to clean the
        previous traces.
  """
  for dirname in os.listdir(output_directories_path):
    directory_path = os.path.join(output_directories_path, dirname)
    if not os.path.isdir(directory_path):
      continue
    try:
      int(dirname)
    except ValueError:
      continue
    shutil.rmtree(directory_path)


class CacheOperation(object):
  CLEAR, SAVE, PUSH = range(3)


class SandwichRunnerError(Exception):
  pass


class SandwichRunner(object):
  """Sandwich runner.

  This object is meant to be configured first and then run using the Run()
  method.
  """
  _ATTEMPT_COUNT = 3
  _STOP_DELAY_MULTIPLIER = 2
  _ABORT_RUN_TIMEOUT_SECONDS = 30 * 60

  def __init__(self):
    """Configures a sandwich runner out of the box.

    Public members are meant to be configured before calling Run().
    """
    # Cache operation to do before doing the chrome navigation.
    self.cache_operation = CacheOperation.CLEAR

    # The cache archive's path to save to or push from. Is str or None.
    self.cache_archive_path = None

    # List of additional chrome command line flags.
    self.chrome_args = []

    # Controls whether the WPR server should do script injection.
    self.disable_wpr_script_injection = False

    # Number of times to repeat the url.
    self.repeat = 1

    # Network conditions to emulate. None if no emulation.
    self.network_condition = None

    # Network condition emulator. Can be: browser,wpr
    self.network_emulator = 'browser'

    # Output directory where to save the traces, videos, etc. Is str or None.
    self.output_dir = None

    # URL to navigate to.
    self.url = None

    # Configures whether to record speed-index video.
    self.record_video = False

    # Configures whether to record memory dumps.
    self.record_memory_dumps = False

    # Configures whether to record tracing categories needed for TTFMP.
    self.record_first_meaningful_paint = False

    # Path to the WPR archive to load or save. Is str or None.
    self.wpr_archive_path = None

    # Configures whether the WPR archive should be read or generated.
    self.wpr_record = False

    # The android DeviceUtils to run sandwich on or None to run it locally.
    self.android_device = None

    self._chrome_ctl = None
    self._local_cache_directory_path = None

  def _CleanTraceOutputDirectory(self):
    assert self.output_dir
    if not os.path.isdir(self.output_dir):
      try:
        os.makedirs(self.output_dir)
      except OSError:
        logging.error('Cannot create directory for results: %s',
            self.output_dir)
        raise
    else:
      _CleanArtefactsFromPastRuns(self.output_dir)

  def _GetEmulatorNetworkCondition(self, emulator):
    if self.network_emulator == emulator:
      return self.network_condition
    return None

  def _RunNavigation(self, clear_cache, repeat_id=None):
    """Run a page navigation to the given URL.

    Args:
      clear_cache: Whether if the cache should be cleared before navigation.
      repeat_id: Id of the run in the output directory. If it is None, then no
        trace or video will be saved.
    """
    run_path = None
    if repeat_id is not None:
      run_path = os.path.join(self.output_dir, str(repeat_id))
      if not os.path.isdir(run_path):
        os.makedirs(run_path)
    self._chrome_ctl.SetNetworkEmulation(
        self._GetEmulatorNetworkCondition('browser'))
    categories = _TRACING_CATEGORIES
    if self.record_memory_dumps:
      categories += [MEMORY_DUMP_CATEGORY]
    if self.record_first_meaningful_paint:
      categories += TTFMP_ADDITIONAL_CATEGORIES
    stop_delay_multiplier = 0
    if self.wpr_record or self.cache_operation == CacheOperation.SAVE:
      stop_delay_multiplier = self._STOP_DELAY_MULTIPLIER
    # TODO(gabadie): add a way to avoid recording a trace.
    with common_util.TimeoutScope(
        self._ABORT_RUN_TIMEOUT_SECONDS, 'Sandwich run overdue.'):
      with self._chrome_ctl.Open() as connection:
        if clear_cache:
          connection.ClearCache()

        # Binds all parameters of RecordUrlNavigation() to avoid repetition.
        def RecordTrace():
          return loading_trace.LoadingTrace.RecordUrlNavigation(
              url=self.url,
              connection=connection,
              chrome_metadata=self._chrome_ctl.ChromeMetadata(),
              categories=categories,
              timeout_seconds=_DEVTOOLS_TIMEOUT,
              stop_delay_multiplier=stop_delay_multiplier)

        if run_path is not None and self.record_video:
          device = self._chrome_ctl.GetDevice()
          if device is None:
            raise RuntimeError('Can only record video on a remote device.')
          video_recording_path = os.path.join(run_path, VIDEO_FILENAME)
          with device_setup.RemoteSpeedIndexRecorder(device, connection,
                                                     video_recording_path):
            trace = RecordTrace()
        else:
          trace = RecordTrace()
        for event in trace.request_track.GetEvents():
          if event.failed:
            logging.warning(
                'request to %s failed: %s', event.url, event.error_text)
        if not trace.tracing_track.HasLoadingSucceeded():
          raise SandwichRunnerError('Page load has failed.')
    if run_path is not None:
      trace_path = os.path.join(run_path, TRACE_FILENAME)
      trace.ToJsonFile(trace_path)

  def _RunInRetryLoop(self, repeat_id, perform_dry_run_before):
    """Attempts to run monitoring navigation.

    Args:
      repeat_id: Id of the run in the output directory.
      perform_dry_run_before: Whether it should do a dry run attempt before the
        actual monitoring run.

    Returns:
      Whether the device should be rebooted to continue attempting for that
      given |repeat_id|.
    """
    resume_attempt_id = 0
    if perform_dry_run_before:
      resume_attempt_id = 1
    for attempt_id in xrange(resume_attempt_id, self._ATTEMPT_COUNT):
      try:
        if perform_dry_run_before:
          logging.info('Do sandwich dry run attempt %d', attempt_id)
        else:
          logging.info('Do sandwich run attempt %d', attempt_id)
        self._chrome_ctl.ResetBrowserState()
        clear_cache = False
        if self.cache_operation == CacheOperation.CLEAR:
          clear_cache = True
        elif self.cache_operation == CacheOperation.PUSH:
          self._chrome_ctl.PushBrowserCache(self._local_cache_directory_path)
        elif self.cache_operation == CacheOperation.SAVE:
          clear_cache = repeat_id == 0
        self._RunNavigation(clear_cache=clear_cache, repeat_id=repeat_id)
        if not perform_dry_run_before or attempt_id > resume_attempt_id:
          break
      except controller.ChromeControllerError as error:
        request_reboot = False
        is_intermittent = error.IsIntermittent()
        if (self.android_device and
            attempt_id == 0 and
            error.error_type is websocket.WebSocketConnectionClosedException):
          assert not perform_dry_run_before
          # On Android, the first socket connection closure is likely caused by
          # memory pressure on the device and therefore considered intermittent,
          # and therefore request a reboot of the device to the caller.
          request_reboot = True
          is_intermittent = True
        if is_intermittent and attempt_id + 1 != self._ATTEMPT_COUNT:
          dump_filename = '{}_intermittent_failure'.format(attempt_id)
          dump_path = os.path.join(
              self.output_dir, str(repeat_id), dump_filename)
        else:
          dump_path = os.path.join(self.output_dir, ERROR_FILENAME)
        with open(dump_path, 'w') as dump_output:
          error.Dump(dump_output)
        if not is_intermittent:
          error.RaiseOriginal()
        if request_reboot:
          assert resume_attempt_id is 0
          return True
    else:
      logging.error('Failed to navigate to %s after %d attemps' % \
                    (self.url, self._ATTEMPT_COUNT))
      error.RaiseOriginal()
    return False

  def _RunWithWpr(self, resume_repeat_id, perform_dry_run_before):
    """Opens WPR and attempts to run repeated monitoring navigation.

    Args:
      resume_repeat_id: Id of the run to resume.
      perform_dry_run_before: Whether the repeated run to resume should first do
        a dry run navigation attempt.

    Returns:
      Number of repeat performed. If < self.repeat, then it means that the
        device should be rebooted.
    """
    with self._chrome_ctl.OpenWprHost(self.wpr_archive_path,
        record=self.wpr_record,
        network_condition_name=self._GetEmulatorNetworkCondition('wpr'),
        disable_script_injection=self.disable_wpr_script_injection,
        out_log_path=os.path.join(self.output_dir, WPR_LOG_FILENAME)):
      for repeat_id in xrange(resume_repeat_id, self.repeat):
        reboot_requested = self._RunInRetryLoop(
            repeat_id, perform_dry_run_before)
        if reboot_requested:
          return repeat_id
    return self.repeat

  def _PullCacheFromDevice(self):
    assert self.cache_operation == CacheOperation.SAVE
    assert self.cache_archive_path, 'Need to specify where to save the cache'

    cache_directory_path = self._chrome_ctl.PullBrowserCache()
    chrome_cache.ZipDirectoryContent(
        cache_directory_path, self.cache_archive_path)
    shutil.rmtree(cache_directory_path)

  def Run(self):
    """SandwichRunner main entry point meant to be called once configured."""
    assert self.output_dir is not None
    assert self._chrome_ctl == None
    assert self._local_cache_directory_path == None
    self._CleanTraceOutputDirectory()

    if self.android_device:
      self._chrome_ctl = controller.RemoteChromeController(self.android_device)
    else:
      self._chrome_ctl = controller.LocalChromeController()
    self._chrome_ctl.AddChromeArguments(['--disable-infobars'])
    self._chrome_ctl.AddChromeArguments(self.chrome_args)
    if self.cache_operation == CacheOperation.SAVE:
      self._chrome_ctl.SetSlowDeath()
    try:
      if self.cache_operation == CacheOperation.PUSH:
        assert os.path.isfile(self.cache_archive_path)
        self._local_cache_directory_path = tempfile.mkdtemp(suffix='.cache')
        chrome_cache.UnzipDirectoryContent(
            self.cache_archive_path, self._local_cache_directory_path)
      times_repeated = self._RunWithWpr(0, False)
      if times_repeated < self.repeat:
        self._chrome_ctl.RebootDevice()
        self._RunWithWpr(times_repeated, True)
    finally:
      if self._local_cache_directory_path:
        shutil.rmtree(self._local_cache_directory_path)
        self._local_cache_directory_path = None
    if self.cache_operation == CacheOperation.SAVE:
      self._PullCacheFromDevice()

    self._chrome_ctl = None


def WalkRepeatedRuns(runner_output_dir):
  """Yields unordered (repeat id, path of the repeat directory).

  Args:
    runner_output_dir: Same as for SandwichRunner.output_dir.
  """
  repeated_run_count = 0
  for node_name in os.listdir(runner_output_dir):
    repeat_dir = os.path.join(runner_output_dir, node_name)
    if not os.path.isdir(repeat_dir):
      continue
    try:
      repeat_id = int(node_name)
    except ValueError:
      continue
    yield repeat_id, repeat_dir
    repeated_run_count += 1
  assert repeated_run_count > 0, ('Error: not a sandwich runner output '
                                  'directory: {}').format(runner_output_dir)

# Copyright 2016 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. # Standard filenames in the sandwich runner's output directory. # Memory dump category used to get memory metrics. # Devtools timeout of 1 minute to avoid websocket timeout on slow # network condition. # Categories to enable or disable for all traces collected. Disabled categories # are prefixed with '-'. # A lot of unnecessary events are enabled by default in "cc". Cleans artifacts generated from past run in the output directory.    Args:     output_directories_path: The output directory path where to clean the         previous traces. Sandwich runner.    This object is meant to be configured first and then run using the Run()   method. Configures a sandwich runner out of the box.      Public members are meant to be configured before calling Run(). # Cache operation to do before doing the chrome navigation. # The cache archive's path to save to or push from. Is str or None. # List of additional chrome command line flags. # Controls whether the WPR server should do script injection. # Number of times to repeat the url. # Network conditions to emulate. None if no emulation. # Network condition emulator. Can be: browser,wpr # Output directory where to save the traces, videos, etc. Is str or None. # URL to navigate to. # Configures whether to record speed-index video. # Configures whether to record memory dumps. # Configures whether to record tracing categories needed for TTFMP. # Path to the WPR archive to load or save. Is str or None. # Configures whether the WPR archive should be read or generated. # The android DeviceUtils to run sandwich on or None to run it locally. Run a page navigation to the given URL.      Args:       clear_cache: Whether if the cache should be cleared before navigation.       repeat_id: Id of the run in the output directory. If it is None, then no         trace or video will be saved. # TODO(gabadie): add a way to avoid recording a trace. # Binds all parameters of RecordUrlNavigation() to avoid repetition. Attempts to run monitoring navigation.      Args:       repeat_id: Id of the run in the output directory.       perform_dry_run_before: Whether it should do a dry run attempt before the         actual monitoring run.      Returns:       Whether the device should be rebooted to continue attempting for that       given |repeat_id|. # On Android, the first socket connection closure is likely caused by # memory pressure on the device and therefore considered intermittent, # and therefore request a reboot of the device to the caller. Opens WPR and attempts to run repeated monitoring navigation.      Args:       resume_repeat_id: Id of the run to resume.       perform_dry_run_before: Whether the repeated run to resume should first do         a dry run navigation attempt.      Returns:       Number of repeat performed. If < self.repeat, then it means that the         device should be rebooted. SandwichRunner main entry point meant to be called once configured. Yields unordered (repeat id, path of the repeat directory).    Args:     runner_output_dir: Same as for SandwichRunner.output_dir.