# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""A reproducing entity.

Part of the Chrome build flags optimization.

The Task class is used by different modules. Each module fills in the
corresponding information into a Task instance. Class Task contains the bit set
representing the flags selection. The builder module is responsible for filling
the image and the checksum field of a Task. The executor module will put the
execution output to the execution field.
"""

__author__ = 'yuhenglong@google.com (Yuheng Long)'

import os
import subprocess
import sys
from uuid import uuid4

BUILD_STAGE = 1
TEST_STAGE = 2

# Message indicating that the build or test failed.
ERROR_STRING = 'error'

# The maximum number of tries a build can have. Some compilations may fail due
# to unexpected environment circumstance. This variable defines how many tries
# the build should attempt before giving up.
BUILD_TRIES = 3

# The maximum number of tries a test can have. Some tests may fail due to
# unexpected environment circumstance. This variable defines how many tries the
# test should attempt before giving up.
TEST_TRIES = 3


# Create the file/directory if it does not already exist.
def _CreateDirectory(file_name):
  directory = os.path.dirname(file_name)
  if not os.path.exists(directory):
    os.makedirs(directory)


class Task(object):
  """A single reproducing entity.

  A single test of performance with a particular set of flags. It records the
  flag set, the image, the check sum of the image and the cost.
  """

  # The command that will be used in the build stage to compile the tasks.
  BUILD_COMMAND = None
  # The command that will be used in the test stage to test the tasks.
  TEST_COMMAND = None
  # The directory to log the compilation and test results.
  LOG_DIRECTORY = None

  @staticmethod
  def InitLogCommand(build_command, test_command, log_directory):
    """Set up the build and test command for the task and the log directory.

    This framework is generic. It lets the client specify application specific
    compile and test methods by passing different build_command and
    test_command.

    Args:
      build_command: The command that will be used in the build stage to compile
        this task.
      test_command: The command that will be used in the test stage to test this
        task.
      log_directory: The directory to log the compilation and test results.
    """

    Task.BUILD_COMMAND = build_command
    Task.TEST_COMMAND = test_command
    Task.LOG_DIRECTORY = log_directory

  def __init__(self, flag_set):
    """Set up the optimization flag selection for this task.

    Args:
      flag_set: The optimization flag set that is encapsulated by this task.
    """

    self._flag_set = flag_set

    # A unique identifier that distinguishes this task from other tasks.
    self._task_identifier = uuid4()

    self._log_path = (Task.LOG_DIRECTORY, self._task_identifier)

    # Initiate the hash value. The hash value is used so as not to recompute it
    # every time the hash method is called.
    self._hash_value = None

    # Indicate that the task has not been compiled/tested.
    self._build_cost = None
    self._exe_cost = None
    self._checksum = None
    self._image = None
    self._file_length = None
    self._text_length = None

  def __eq__(self, other):
    """Test whether two tasks are equal.

    Two tasks are equal if their flag_set are equal.

    Args:
      other: The other task with which this task is tested equality.
    Returns:
      True if the encapsulated flag sets are equal.
    """
    if isinstance(other, Task):
      return self.GetFlags() == other.GetFlags()
    return False

  def __hash__(self):
    if self._hash_value is None:
      # Cache the hash value of the flags, so as not to recompute them.
      self._hash_value = hash(self._flag_set)
    return self._hash_value

  def GetIdentifier(self, stage):
    """Get the identifier of the task in the stage.

    The flag set uniquely identifies a task in the build stage. The checksum of
    the image of the task uniquely identifies the task in the test stage.

    Args:
      stage: The stage (build/test) in which this method is called.
    Returns:
      Return the flag set in build stage and return the checksum in test stage.
    """

    # Define the dictionary for different stage function lookup.
    get_identifier_functions = {BUILD_STAGE: self.FormattedFlags,
                                TEST_STAGE: self.__GetCheckSum}

    assert stage in get_identifier_functions
    return get_identifier_functions[stage]()

  def GetResult(self, stage):
    """Get the performance results of the task in the stage.

    Args:
      stage: The stage (build/test) in which this method is called.
    Returns:
      Performance results.
    """

    # Define the dictionary for different stage function lookup.
    get_result_functions = {BUILD_STAGE: self.__GetBuildResult,
                            TEST_STAGE: self.GetTestResult}

    assert stage in get_result_functions

    return get_result_functions[stage]()

  def SetResult(self, stage, result):
    """Set the performance results of the task in the stage.

    This method is called by the pipeling_worker to set the results for
    duplicated tasks.

    Args:
      stage: The stage (build/test) in which this method is called.
      result: The performance results of the stage.
    """

    # Define the dictionary for different stage function lookup.
    set_result_functions = {BUILD_STAGE: self.__SetBuildResult,
                            TEST_STAGE: self.__SetTestResult}

    assert stage in set_result_functions

    set_result_functions[stage](result)

  def Done(self, stage):
    """Check whether the stage is done.

    Args:
      stage: The stage to be checked, build or test.
    Returns:
      True if the stage is done.
    """

    # Define the dictionary for different result string lookup.
    done_string = {BUILD_STAGE: self._build_cost, TEST_STAGE: self._exe_cost}

    assert stage in done_string

    return done_string[stage] is not None

  def Work(self, stage):
    """Perform the task.

    Args:
      stage: The stage in which the task is performed, compile or test.
    """

    # Define the dictionary for different stage function lookup.
    work_functions = {BUILD_STAGE: self.__Compile, TEST_STAGE: self.__Test}

    assert stage in work_functions

    work_functions[stage]()

  def FormattedFlags(self):
    """Format the optimization flag set of this task.

    Returns:
      The formatted optimization flag set that is encapsulated by this task.
    """
    return str(self._flag_set.FormattedForUse())

  def GetFlags(self):
    """Get the optimization flag set of this task.

    Returns:
      The optimization flag set that is encapsulated by this task.
    """

    return self._flag_set

  def __GetCheckSum(self):
    """Get the compilation image checksum of this task.

    Returns:
      The compilation image checksum of this task.
    """

    # The checksum should be computed before this method is called.
    assert self._checksum is not None
    return self._checksum

  def __Compile(self):
    """Run a compile.

    This method compile an image using the present flags, get the image,
    test the existent of the image and gathers monitoring information, and sets
    the internal cost (fitness) for this set of flags.
    """

    # Format the flags as a string as input to compile command. The unique
    # identifier is passed to the compile command. If concurrent processes are
    # used to compile different tasks, these processes can use the identifier to
    # write to different file.
    flags = self._flag_set.FormattedForUse()
    command = '%s %s %s' % (Task.BUILD_COMMAND, ' '.join(flags),
                            self._task_identifier)

    # Try BUILD_TRIES number of times before confirming that the build fails.
    for _ in range(BUILD_TRIES):
      try:
        # Execute the command and get the execution status/results.
        p = subprocess.Popen(command.split(),
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE)
        (out, err) = p.communicate()

        if out:
          out = out.strip()
          if out != ERROR_STRING:
            # Each build results contains the checksum of the result image, the
            # performance cost of the build, the compilation image, the length
            # of the build, and the length of the text section of the build.
            (checksum, cost, image, file_length, text_length) = out.split()
            # Build successfully.
            break

        # Build failed.
        cost = ERROR_STRING
      except _:
        # If there is exception getting the cost information of the build, the
        # build failed.
        cost = ERROR_STRING

    # Convert the build cost from String to integer. The build cost is used to
    # compare a task with another task. Set the build cost of the failing task
    # to the max integer. The for loop will keep trying until either there is a
    # success or BUILD_TRIES number of tries have been conducted.
    self._build_cost = sys.maxint if cost == ERROR_STRING else float(cost)

    self._checksum = checksum
    self._file_length = file_length
    self._text_length = text_length
    self._image = image

    self.__LogBuildCost(err)

  def __Test(self):
    """__Test the task against benchmark(s) using the input test command."""

    # Ensure that the task is compiled before being tested.
    assert self._image is not None

    # If the task does not compile, no need to test.
    if self._image == ERROR_STRING:
      self._exe_cost = ERROR_STRING
      return

    # The unique identifier is passed to the test command. If concurrent
    # processes are used to compile different tasks, these processes can use the
    # identifier to write to different file.
    command = '%s %s %s' % (Task.TEST_COMMAND, self._image,
                            self._task_identifier)

    # Try TEST_TRIES number of times before confirming that the build fails.
    for _ in range(TEST_TRIES):
      try:
        p = subprocess.Popen(command.split(),
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE)
        (out, err) = p.communicate()

        if out:
          out = out.strip()
          if out != ERROR_STRING:
            # The test results contains the performance cost of the test.
            cost = out
            # Test successfully.
            break

        # Test failed.
        cost = ERROR_STRING
      except _:
        # If there is exception getting the cost information of the test, the
        # test failed. The for loop will keep trying until either there is a
        # success or TEST_TRIES number of tries have been conducted.
        cost = ERROR_STRING

    self._exe_cost = sys.maxint if (cost == ERROR_STRING) else float(cost)

    self.__LogTestCost(err)

  def __SetBuildResult(self, (checksum, build_cost, image, file_length,
                              text_length)):
    self._checksum = checksum
    self._build_cost = build_cost
    self._image = image
    self._file_length = file_length
    self._text_length = text_length

  def __GetBuildResult(self):
    return (self._checksum, self._build_cost, self._image, self._file_length,
            self._text_length)

  def GetTestResult(self):
    return self._exe_cost

  def __SetTestResult(self, exe_cost):
    self._exe_cost = exe_cost

  def LogSteeringCost(self):
    """Log the performance results for the task.

    This method is called by the steering stage and this method writes the
    results out to a file. The results include the build and the test results.
    """

    steering_log = '%s/%s/steering.txt' % self._log_path

    _CreateDirectory(steering_log)

    with open(steering_log, 'w') as out_file:
      # Include the build and the test results.
      steering_result = (self._flag_set, self._checksum, self._build_cost,
                         self._image, self._file_length, self._text_length,
                         self._exe_cost)

      # Write out the result in the comma-separated format (CSV).
      out_file.write('%s,%s,%s,%s,%s,%s,%s\n' % steering_result)

  def __LogBuildCost(self, log):
    """Log the build results for the task.

    The build results include the compilation time of the build, the result
    image, the checksum, the file length and the text length of the image.
    The file length of the image includes the length of the file of the image.
    The text length only includes the length of the text section of the image.

    Args:
      log: The build log of this task.
    """

    build_result_log = '%s/%s/build.txt' % self._log_path

    _CreateDirectory(build_result_log)

    with open(build_result_log, 'w') as out_file:
      build_result = (self._flag_set, self._build_cost, self._image,
                      self._checksum, self._file_length, self._text_length)

      # Write out the result in the comma-separated format (CSV).
      out_file.write('%s,%s,%s,%s,%s,%s\n' % build_result)

    # The build information about running the build.
    build_run_log = '%s/%s/build_log.txt' % self._log_path
    _CreateDirectory(build_run_log)

    with open(build_run_log, 'w') as out_log_file:
      # Write out the execution information.
      out_log_file.write('%s' % log)

  def __LogTestCost(self, log):
    """Log the test results for the task.

    The test results include the runtime execution time of the test.

    Args:
      log: The test log of this task.
    """

    test_log = '%s/%s/test.txt' % self._log_path

    _CreateDirectory(test_log)

    with open(test_log, 'w') as out_file:
      test_result = (self._flag_set, self._checksum, self._exe_cost)

      # Write out the result in the comma-separated format (CSV).
      out_file.write('%s,%s,%s\n' % test_result)

    # The execution information about running the test.
    test_run_log = '%s/%s/test_log.txt' % self._log_path

    _CreateDirectory(test_run_log)

    with open(test_run_log, 'w') as out_log_file:
      # Append the test log information.
      out_log_file.write('%s' % log)

  def IsImproved(self, other):
    """Compare the current task with another task.

    Args:
      other: The other task against which the current task is compared.

    Returns:
      True if this task has improvement upon the other task.
    """

    # The execution costs must have been initiated.
    assert self._exe_cost is not None
    assert other.GetTestResult() is not None

    return self._exe_cost < other.GetTestResult()