# Copyright 2023 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Utilities for code coverage related processings."""

import logging
import os
import posixpath
import shutil
import subprocess

from devil import base_error
from pylib import constants

# These are use for code coverage.
LLVM_PROFDATA_PATH = os.path.join(
    constants.DIR_SOURCE_ROOT,
    'third_party',
    'llvm-build',
    'Release+Asserts',
    'bin',
    'llvm-profdata',
)
# Name of the file extension for profraw data files.
_PROFRAW_FILE_EXTENSION = 'profraw'
# Name of the file where profraw data files are merged.
_MERGE_PROFDATA_FILE_NAME = 'coverage_merged.' + _PROFRAW_FILE_EXTENSION


def GetDeviceClangCoverageDir(device):
    """Gets the directory to generate clang coverage data on device.

    Args:
      device: The working device.

    Returns:
      The directory path on the device.
    """
    return posixpath.join(
        device.GetExternalStoragePath(), 'chrome', 'test', 'coverage', 'profraw'
    )


def PullAndMaybeMergeClangCoverageFiles(
    device, device_coverage_dir, output_dir, output_subfolder_name
):
    """Pulls and possibly merges clang coverage file to a single file.

    Only merges when llvm-profdata tool exists. If so, Merged file is at
    `output_dir/coverage_merged.profraw`and raw profraw files before merging
    are deleted.

    Args:
      device: The working device.
      device_coverage_dir: The directory storing coverage data on device.
      output_dir: The output directory on host to store the
          coverage_merged.profraw file.
      output_subfolder_name: The subfolder in |output_dir| to pull
          |device_coverage_dir| into. It will be deleted after merging if
          merging happens.
    """
    if not device.PathExists(device_coverage_dir, retries=0):
        logging.warning(
            'Clang coverage data folder does not exist on device: %s',
            device_coverage_dir,
        )
        return
    # Host side dir to pull device coverage profraw folder into.
    profraw_parent_dir = os.path.join(output_dir, output_subfolder_name)
    # Note: The function pulls |device_coverage_dir| folder,
    # instead of profraw files, into |profraw_parent_dir|. the
    # function also removes |device_coverage_dir| from device.
    PullClangCoverageFiles(device, device_coverage_dir, profraw_parent_dir)
    # Merge data into one merged file if llvm-profdata tool exists.
    if os.path.isfile(LLVM_PROFDATA_PATH):
        profraw_folder_name = os.path.basename(
            os.path.normpath(device_coverage_dir)
        )
        profraw_dir = os.path.join(profraw_parent_dir, profraw_folder_name)
        MergeClangCoverageFiles(output_dir, profraw_dir)
        shutil.rmtree(profraw_parent_dir)


def PullClangCoverageFiles(device, device_coverage_dir, output_dir):
    """Pulls clang coverage files on device to host directory.

    Args:
      device: The working device.
      device_coverage_dir: The directory to store coverage data on device.
      output_dir: The output directory on host.
    """
    try:
        if not os.path.exists(output_dir):
            os.makedirs(output_dir)
        device.PullFile(device_coverage_dir, output_dir)
        if not os.listdir(os.path.join(output_dir, 'profraw')):
            logging.warning('No clang coverage data was generated for this run')
    except (OSError, base_error.BaseError) as e:
        logging.warning('Failed to pull clang coverage data, error: %s', e)
    finally:
        device.RemovePath(device_coverage_dir, force=True, recursive=True)


def MergeClangCoverageFiles(coverage_dir, profdata_dir):
    """Merge coverage data files.

    Each instrumentation activity generates a separate profraw data file. This
    merges all profraw files in profdata_dir into a single file in
    coverage_dir. This happens after each test, rather than waiting until after
    all tests are ran to reduce the memory footprint used by all the profraw
    files.

    Args:
      coverage_dir: The path to the coverage directory.
      profdata_dir: The directory where the profraw data file(s) are located.

    Return:
      None
    """
    # profdata_dir may not exist if pulling coverage files failed.
    if not os.path.exists(profdata_dir):
        logging.debug('Profraw directory does not exist: %s', profdata_dir)
        return

    merge_file = os.path.join(coverage_dir, _MERGE_PROFDATA_FILE_NAME)
    profraw_files = [
        os.path.join(profdata_dir, f)
        for f in os.listdir(profdata_dir)
        if f.endswith(_PROFRAW_FILE_EXTENSION)
    ]

    try:
        logging.debug('Merging target profraw files into merged profraw file.')
        subprocess_cmd = [
            LLVM_PROFDATA_PATH,
            'merge',
            '-o',
            merge_file,
            '-sparse=true',
        ]
        # Grow the merge file by merging it with itself and the new files.
        if os.path.exists(merge_file):
            subprocess_cmd.append(merge_file)
        subprocess_cmd.extend(profraw_files)
        output = subprocess.check_output(subprocess_cmd).decode('utf8')
        logging.debug('Merge output: %s', output)

    except subprocess.CalledProcessError:
        # Don't raise error as that will kill the test run. When code coverage
        # generates a report, that will raise the error in the report generation.
        logging.error(
            'Failed to merge target profdata files to create '
            'merged profraw file for files: %s',
            profraw_files,
        )

    # Free up memory space on bot as all data is in the merge file.
    for f in profraw_files:
        os.remove(f)
