#!/usr/bin/python # Copyright 2018 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. """Tests for code coverage tools.""" import os import re import shutil import subprocess import sys import unittest # Appends third_party/ so that coverage_utils can import jinja2 from # third_party/, note that this is not done inside coverage_utils because # coverage_utils is also used outside of Chromium source tree. sys.path.append( os.path.join( os.path.dirname(__file__), os.path.pardir, os.path.pardir, 'third_party')) 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.LLVM_COV = os.path.join(self.CHROMIUM_SRC_DIR, 'third_party', 'llvm-build', 'Release+Asserts', 'bin', 'llvm-cov') self.PYTHON = 'python' 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 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, 'w') 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) if __name__ == '__main__': unittest.main()