1# Copyright 2021 Google LLC
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#      http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14"""Tests for coverage.py"""
15import os
16import json
17import unittest
18from unittest import mock
19
20import coverage
21
22# pylint: disable=protected-access
23
24TEST_DATA_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)),
25                              'test_data')
26
27PROJECT_NAME = 'curl'
28REPO_PATH = '/src/curl'
29FUZZ_TARGET = 'curl_fuzzer'
30PROJECT_COV_JSON_FILENAME = 'example_curl_cov.json'
31FUZZ_TARGET_COV_JSON_FILENAME = 'example_curl_fuzzer_cov.json'
32INVALID_TARGET = 'not-a-fuzz-target'
33
34with open(os.path.join(TEST_DATA_PATH,
35                       PROJECT_COV_JSON_FILENAME),) as cov_file_handle:
36  PROJECT_COV_INFO = json.loads(cov_file_handle.read())
37
38
39class GetFuzzerStatsDirUrlTest(unittest.TestCase):
40  """Tests _get_fuzzer_stats_dir_url."""
41
42  @mock.patch('coverage.get_json_from_url',
43              return_value={
44                  'fuzzer_stats_dir':
45                      'gs://oss-fuzz-coverage/systemd/fuzzer_stats/20210303'
46              })
47  def test_get_valid_project(self, mocked_get_json_from_url):
48    """Tests that a project's coverage report can be downloaded and parsed.
49
50    NOTE: This test relies on the PROJECT_NAME repo's coverage report.
51    The "example" project was not used because it has no coverage reports.
52    """
53    result = coverage._get_fuzzer_stats_dir_url(PROJECT_NAME)
54    (url,), _ = mocked_get_json_from_url.call_args
55    self.assertEqual(
56        'https://storage.googleapis.com/oss-fuzz-coverage/'
57        'latest_report_info/curl.json', url)
58
59    expected_result = (
60        'https://storage.googleapis.com/oss-fuzz-coverage/systemd/fuzzer_stats/'
61        '20210303')
62    self.assertEqual(result, expected_result)
63
64  def test_get_invalid_project(self):
65    """Tests that passing a bad project returns None."""
66    self.assertIsNone(coverage._get_fuzzer_stats_dir_url('not-a-proj'))
67
68
69class GetTargetCoverageReportTest(unittest.TestCase):
70  """Tests get_target_coverage_report."""
71
72  def setUp(self):
73    with mock.patch('coverage._get_latest_cov_report_info',
74                    return_value=PROJECT_COV_INFO):
75      self.coverage_getter = coverage.OssFuzzCoverageGetter(
76          PROJECT_NAME, REPO_PATH)
77
78  @mock.patch('coverage.get_json_from_url', return_value={})
79  def test_valid_target(self, mocked_get_json_from_url):
80    """Tests that a target's coverage report can be downloaded and parsed."""
81    self.coverage_getter.get_target_coverage_report(FUZZ_TARGET)
82    (url,), _ = mocked_get_json_from_url.call_args
83    self.assertEqual(
84        'https://storage.googleapis.com/oss-fuzz-coverage/'
85        'curl/fuzzer_stats/20200226/curl_fuzzer.json', url)
86
87  def test_invalid_target(self):
88    """Tests that passing an invalid target coverage report returns None."""
89    self.assertIsNone(
90        self.coverage_getter.get_target_coverage_report(INVALID_TARGET))
91
92  @mock.patch('coverage._get_latest_cov_report_info', return_value=None)
93  def test_invalid_project_json(self, _):
94    """Tests an invalid project JSON results in None being returned."""
95    coverage_getter = coverage.OssFuzzCoverageGetter(PROJECT_NAME, REPO_PATH)
96    self.assertIsNone(coverage_getter.get_target_coverage_report(FUZZ_TARGET))
97
98
99class GetFilesCoveredByTargetTest(unittest.TestCase):
100  """Tests get_files_covered_by_target."""
101
102  def setUp(self):
103    with mock.patch('coverage._get_latest_cov_report_info',
104                    return_value=PROJECT_COV_INFO):
105      self.coverage_getter = coverage.OssFuzzCoverageGetter(
106          PROJECT_NAME, REPO_PATH)
107
108  def test_valid_target(self):
109    """Tests that covered files can be retrieved from a coverage report."""
110    with open(os.path.join(TEST_DATA_PATH,
111                           FUZZ_TARGET_COV_JSON_FILENAME),) as file_handle:
112      fuzzer_cov_info = json.loads(file_handle.read())
113
114    with mock.patch('coverage.OssFuzzCoverageGetter.get_target_coverage_report',
115                    return_value=fuzzer_cov_info):
116      file_list = self.coverage_getter.get_files_covered_by_target(FUZZ_TARGET)
117
118    curl_files_list_path = os.path.join(TEST_DATA_PATH,
119                                        'example_curl_file_list.json')
120    with open(curl_files_list_path) as file_handle:
121      expected_file_list = json.loads(file_handle.read())
122    self.assertCountEqual(file_list, expected_file_list)
123
124  def test_invalid_target(self):
125    """Tests passing invalid fuzz target returns None."""
126    self.assertIsNone(
127        self.coverage_getter.get_files_covered_by_target(INVALID_TARGET))
128
129
130class IsFileCoveredTest(unittest.TestCase):
131  """Tests for is_file_covered."""
132
133  def test_is_file_covered_covered(self):
134    """Tests that is_file_covered returns True for a covered file."""
135    file_coverage = {
136        'filename': '/src/systemd/src/basic/locale-util.c',
137        'summary': {
138            'regions': {
139                'count': 204,
140                'covered': 200,
141                'notcovered': 200,
142                'percent': 98.03
143            }
144        }
145    }
146    self.assertTrue(coverage.is_file_covered(file_coverage))
147
148  def test_is_file_covered_not_covered(self):
149    """Tests that is_file_covered returns False for a not covered file."""
150    file_coverage = {
151        'filename': '/src/systemd/src/basic/locale-util.c',
152        'summary': {
153            'regions': {
154                'count': 204,
155                'covered': 0,
156                'notcovered': 0,
157                'percent': 0
158            }
159        }
160    }
161    self.assertFalse(coverage.is_file_covered(file_coverage))
162
163
164class GetLatestCovReportInfo(unittest.TestCase):
165  """Tests that _get_latest_cov_report_info works as intended."""
166
167  PROJECT = 'project'
168  LATEST_REPORT_INFO_URL = ('https://storage.googleapis.com/oss-fuzz-coverage/'
169                            'latest_report_info/project.json')
170
171  @mock.patch('logging.error')
172  @mock.patch('coverage.get_json_from_url', return_value={'coverage': 1})
173  def test_get_latest_cov_report_info(self, mocked_get_json_from_url,
174                                      mocked_error):
175    """Tests that _get_latest_cov_report_info works as intended."""
176    result = coverage._get_latest_cov_report_info(self.PROJECT)
177    self.assertEqual(result, {'coverage': 1})
178    mocked_error.assert_not_called()
179    mocked_get_json_from_url.assert_called_with(self.LATEST_REPORT_INFO_URL)
180
181  @mock.patch('logging.error')
182  @mock.patch('coverage.get_json_from_url', return_value=None)
183  def test_get_latest_cov_report_info_fail(self, _, mocked_error):
184    """Tests that _get_latest_cov_report_info works as intended when we can't
185    get latest report info."""
186    result = coverage._get_latest_cov_report_info('project')
187    self.assertIsNone(result)
188    mocked_error.assert_called_with(
189        'Could not get the coverage report json from url: %s.',
190        self.LATEST_REPORT_INFO_URL)
191
192
193if __name__ == '__main__':
194  unittest.main()
195