#!/usr/bin/env python
# Copyright 2018 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Tests for code coverage tools."""

import os
import re
import shutil
import subprocess
import sys
import unittest
import coverage_utils


def _RecursiveDirectoryListing(dirpath):
  """Returns a list of relative paths to all files in a given directory."""
  result = []
  for root, _, files in os.walk(dirpath):
    for f in files:
      result.append(os.path.relpath(os.path.join(root, f), dirpath))
  return result


def _ReadFile(filepath):
  """Returns contents of a given file."""
  with open(filepath) as f:
    return f.read()


class CoverageTest(unittest.TestCase):
  def setUp(self):
    self.maxDiff = 1000
    self.COVERAGE_TOOLS_DIR = os.path.abspath(os.path.dirname(__file__))
    self.COVERAGE_SCRIPT = os.path.join(self.COVERAGE_TOOLS_DIR, 'coverage.py')
    self.COVERAGE_UTILS = os.path.join(
      self.COVERAGE_TOOLS_DIR, 'coverage_utils.py'
    )

    self.CHROMIUM_SRC_DIR = os.path.dirname(
      os.path.dirname(self.COVERAGE_TOOLS_DIR)
    )
    self.BUILD_DIR = os.path.join(
      self.CHROMIUM_SRC_DIR, 'out', 'code_coverage_tools_test'
    )

    self.REPORT_DIR_1 = os.path.join(self.BUILD_DIR, 'report1')
    self.REPORT_DIR_1_NO_COMPONENTS = os.path.join(
      self.BUILD_DIR, 'report1_no_components'
    )
    self.REPORT_DIR_2 = os.path.join(self.BUILD_DIR, 'report2')
    self.REPORT_DIR_3 = os.path.join(self.BUILD_DIR, 'report3')
    self.REPORT_DIR_4 = os.path.join(self.BUILD_DIR, 'report4')

    self.LLVM_COV = os.path.join(
      self.CHROMIUM_SRC_DIR,
      'third_party',
      'llvm-build',
      'Release+Asserts',
      'bin',
      'llvm-cov',
    )

    self.PYTHON = 'python3'
    self.PLATFORM = coverage_utils.GetHostPlatform()
    if self.PLATFORM == 'win32':
      self.LLVM_COV += '.exe'
      self.PYTHON += '.exe'

    # Even though 'is_component_build=false' is recommended, we intentionally
    # use 'is_component_build=true' to test handling of shared libraries.
    self.GN_ARGS = (
      'use_clang_coverage=true '
      'dcheck_always_on=true '
      'ffmpeg_branding="ChromeOS" '
      'is_component_build=true '
      'is_debug=false '
      'proprietary_codecs=true '
      'use_libfuzzer=true'
    )

    shutil.rmtree(self.BUILD_DIR, ignore_errors=True)

    gn_gen_cmd = ['gn', 'gen', self.BUILD_DIR, '--args=%s' % self.GN_ARGS]
    self.run_cmd(gn_gen_cmd)

    build_cmd = [
      'autoninja',
      '-C',
      self.BUILD_DIR,
      'crypto_unittests',
      'libpng_read_fuzzer',
    ]
    self.run_cmd(build_cmd)

  def tearDown(self):
    shutil.rmtree(self.BUILD_DIR, ignore_errors=True)

  def run_cmd(self, cmd):
    return subprocess.check_output(cmd, cwd=self.CHROMIUM_SRC_DIR)

  def verify_component_view(self, filepath):
    """Asserts that a given component view looks correct."""
    # There must be several Blink and Internals components.
    with open(filepath) as f:
      data = f.read()

    counts = data.count('Blink') + data.count('Internals')
    self.assertGreater(counts, 5)

  def verify_directory_view(self, filepath):
    """Asserts that a given directory view looks correct."""
    # Directory view page does a redirect to another page, extract its URL.
    with open(filepath) as f:
      data = f.read()

    url = re.search(r'.*refresh.*url=([a-zA-Z0-9_\-\/.]+).*', data).group(1)
    directory_view_path = os.path.join(os.path.dirname(filepath), url)

    # There must be at least 'crypto' and 'third_party' directories.
    with open(directory_view_path) as f:
      data = f.read()

    self.assertTrue('crypto' in data and 'third_party' in data)

  def verify_file_view(self, filepath):
    """Asserts that a given file view looks correct."""
    # There must be hundreds of '.*crypto.*' files and 10+ of '.*libpng.*'.
    with open(filepath) as f:
      data = f.read()

    self.assertGreater(data.count('crypto'), 100)
    self.assertGreater(data.count('libpng'), 10)

  def verify_lcov_file(self, filepath):
    """Asserts that a given lcov file looks correct."""
    with open(filepath) as f:
      data = f.read()

    self.assertGreater(data.count('SF:'), 100)
    self.assertGreater(data.count('crypto'), 100)
    self.assertGreater(data.count('libpng'), 10)

  def test_different_workflows_and_cross_check_the_results(self):
    """Test a few different workflows and assert that the results are the same

    and look legit.
    """
    # Testcase 1. End-to-end report generation using coverage.py script. This is
    # the workflow of a regular user.
    cmd = [
      self.COVERAGE_SCRIPT,
      'crypto_unittests',
      'libpng_read_fuzzer',
      '-v',
      '-b',
      self.BUILD_DIR,
      '-o',
      self.REPORT_DIR_1,
      '-c%s/crypto_unittests' % self.BUILD_DIR,
      '-c',
      '%s/libpng_read_fuzzer -runs=0 third_party/libpng/' % self.BUILD_DIR,
    ]
    self.run_cmd(cmd)

    output_dir = os.path.join(self.REPORT_DIR_1, self.PLATFORM)
    self.verify_component_view(
      os.path.join(output_dir, 'component_view_index.html')
    )
    self.verify_directory_view(
      os.path.join(output_dir, 'directory_view_index.html')
    )
    self.verify_file_view(os.path.join(output_dir, 'file_view_index.html'))

    # Also try generating a report without components view. Useful for cross
    # checking with the report produced in the testcase #3.
    cmd = [
      self.COVERAGE_SCRIPT,
      'crypto_unittests',
      'libpng_read_fuzzer',
      '-v',
      '-b',
      self.BUILD_DIR,
      '-o',
      self.REPORT_DIR_1_NO_COMPONENTS,
      '-c%s/crypto_unittests' % self.BUILD_DIR,
      '-c',
      '%s/libpng_read_fuzzer -runs=0 third_party/libpng/' % self.BUILD_DIR,
      '--no-component-view',
    ]
    self.run_cmd(cmd)

    output_dir = os.path.join(self.REPORT_DIR_1_NO_COMPONENTS, self.PLATFORM)
    self.verify_directory_view(
      os.path.join(output_dir, 'directory_view_index.html')
    )
    self.verify_file_view(os.path.join(output_dir, 'file_view_index.html'))
    self.assertFalse(
      os.path.exists(os.path.join(output_dir, 'component_view_index.html'))
    )

    # Testcase #2. Run the script for post processing in Chromium tree. This is
    # the workflow of the code coverage bots.
    instr_profile_path = os.path.join(
      self.REPORT_DIR_1, self.PLATFORM, 'coverage.profdata'
    )

    cmd = [
      self.COVERAGE_SCRIPT,
      'crypto_unittests',
      'libpng_read_fuzzer',
      '-v',
      '-b',
      self.BUILD_DIR,
      '-p',
      instr_profile_path,
      '-o',
      self.REPORT_DIR_2,
    ]
    self.run_cmd(cmd)

    # Verify that the output dirs are the same except of the expected diff.
    report_1_listing = set(_RecursiveDirectoryListing(self.REPORT_DIR_1))
    report_2_listing = set(_RecursiveDirectoryListing(self.REPORT_DIR_2))
    logs_subdir = os.path.join(self.PLATFORM, 'logs')
    self.assertEqual(
      set(
        [
          os.path.join(self.PLATFORM, 'coverage.profdata'),
          os.path.join(logs_subdir, 'crypto_unittests_output.log'),
          os.path.join(logs_subdir, 'libpng_read_fuzzer_output.log'),
        ]
      ),
      report_1_listing - report_2_listing,
    )

    output_dir = os.path.join(self.REPORT_DIR_2, self.PLATFORM)
    self.verify_component_view(
      os.path.join(output_dir, 'component_view_index.html')
    )
    self.verify_directory_view(
      os.path.join(output_dir, 'directory_view_index.html')
    )
    self.verify_file_view(os.path.join(output_dir, 'file_view_index.html'))

    # Verify that the file view pages are binary equal.
    report_1_file_view_data = _ReadFile(
      os.path.join(self.REPORT_DIR_1, self.PLATFORM, 'file_view_index.html')
    )
    report_2_file_view_data = _ReadFile(
      os.path.join(self.REPORT_DIR_2, self.PLATFORM, 'file_view_index.html')
    )
    self.assertEqual(report_1_file_view_data, report_2_file_view_data)

    # Testcase #3, run coverage_utils.py on manually produced report and summary
    # file. This is the workflow of OSS-Fuzz code coverage job.
    objects = [
      '-object=%s' % os.path.join(self.BUILD_DIR, 'crypto_unittests'),
      '-object=%s' % os.path.join(self.BUILD_DIR, 'libpng_read_fuzzer'),
    ]

    cmd = [
      self.PYTHON,
      self.COVERAGE_UTILS,
      '-v',
      'shared_libs',
      '-build-dir=%s' % self.BUILD_DIR,
    ] + objects

    shared_libraries = self.run_cmd(cmd)
    objects.extend(shared_libraries.split())

    instr_profile_path = os.path.join(
      self.REPORT_DIR_1_NO_COMPONENTS, self.PLATFORM, 'coverage.profdata'
    )
    cmd = [
      self.LLVM_COV,
      'show',
      '-format=html',
      '-output-dir=%s' % self.REPORT_DIR_3,
      '-instr-profile=%s' % instr_profile_path,
    ] + objects
    if self.PLATFORM in ['linux', 'mac']:
      cmd.extend(['-Xdemangler', 'c++filt', '-Xdemangler', '-n'])
    self.run_cmd(cmd)

    cmd = [
      self.LLVM_COV,
      'export',
      '-summary-only',
      '-instr-profile=%s' % instr_profile_path,
    ] + objects
    summary_output = self.run_cmd(cmd)

    summary_path = os.path.join(self.REPORT_DIR_3, 'summary.json')
    with open(summary_path, 'wb') as f:
      f.write(summary_output)

    cmd = [
      self.PYTHON,
      self.COVERAGE_UTILS,
      '-v',
      'post_process',
      '-src-root-dir=%s' % self.CHROMIUM_SRC_DIR,
      '-summary-file=%s' % summary_path,
      '-output-dir=%s' % self.REPORT_DIR_3,
    ]
    self.run_cmd(cmd)

    output_dir = os.path.join(self.REPORT_DIR_3, self.PLATFORM)
    self.verify_directory_view(
      os.path.join(output_dir, 'directory_view_index.html')
    )
    self.verify_file_view(os.path.join(output_dir, 'file_view_index.html'))
    self.assertFalse(
      os.path.exists(os.path.join(output_dir, 'component_view_index.html'))
    )

    # Verify that the file view pages are binary equal.
    report_1_file_view_data_no_component = _ReadFile(
      os.path.join(
        self.REPORT_DIR_1_NO_COMPONENTS, self.PLATFORM, 'file_view_index.html'
      )
    )
    report_3_file_view_data = _ReadFile(
      os.path.join(self.REPORT_DIR_3, self.PLATFORM, 'file_view_index.html')
    )
    self.assertEqual(
      report_1_file_view_data_no_component, report_3_file_view_data
    )

    # Testcase 4. Export coverage data in lcov format using coverage.py script.
    cmd = [
      self.COVERAGE_SCRIPT,
      'crypto_unittests',
      'libpng_read_fuzzer',
      '--format',
      'lcov',
      '-v',
      '-b',
      self.BUILD_DIR,
      '-o',
      self.REPORT_DIR_4,
      '-c%s/crypto_unittests' % self.BUILD_DIR,
      '-c',
      '%s/libpng_read_fuzzer -runs=0 third_party/libpng/' % self.BUILD_DIR,
    ]
    self.run_cmd(cmd)

    output_dir = os.path.join(self.REPORT_DIR_4, self.PLATFORM)
    self.verify_lcov_file(os.path.join(output_dir, 'coverage.lcov'))


if __name__ == '__main__':
  unittest.main()
