| #!/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() |