aboutsummaryrefslogtreecommitdiff
path: root/infra/cifuzz/affected_fuzz_targets.py
blob: f9f2242a3bc51cd0da95538f7464ac3b58b1882f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# Copyright 2021 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Module for dealing with fuzz targets affected by the change-under-test
(CUT)."""
import logging
import os
import sys

import coverage

# pylint: disable=wrong-import-position,import-error
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import utils


def remove_unaffected_fuzz_targets(project_name, out_dir, files_changed,
                                   repo_path):
  """Removes all non affected fuzz targets in the out directory.

  Args:
    project_name: The name of the relevant OSS-Fuzz project.
    out_dir: The location of the fuzz target binaries.
    files_changed: A list of files changed compared to HEAD.
    repo_path: The location of the OSS-Fuzz repo in the docker image.

  This function will not delete fuzz targets unless it knows that the fuzz
  targets are unaffected. For example, this means that fuzz targets which don't
  have coverage data on will not be deleted.
  """
  # TODO(metzman): Make this use clusterfuzz deployment.
  if not files_changed:
    # Don't remove any fuzz targets if there is no difference from HEAD.
    logging.info('No files changed compared to HEAD.')
    return

  logging.info('Files changed in PR: %s', files_changed)

  fuzz_target_paths = utils.get_fuzz_targets(out_dir)
  if not fuzz_target_paths:
    # Nothing to remove.
    logging.error('No fuzz targets found in out dir.')
    return

  coverage_getter = coverage.OssFuzzCoverageGetter(project_name, repo_path)
  if not coverage_getter.fuzzer_stats_url:
    # Don't remove any fuzz targets unless we have data.
    logging.error('Could not find latest coverage report.')
    return

  affected_fuzz_targets = get_affected_fuzz_targets(coverage_getter,
                                                    fuzz_target_paths,
                                                    files_changed)

  if not affected_fuzz_targets:
    logging.info('No affected fuzz targets detected, keeping all as fallback.')
    return

  logging.info('Using affected fuzz targets: %s.', affected_fuzz_targets)
  unaffected_fuzz_targets = set(fuzz_target_paths) - affected_fuzz_targets
  logging.info('Removing unaffected fuzz targets: %s.', unaffected_fuzz_targets)

  # Remove all the targets that are not affected.
  for fuzz_target_path in unaffected_fuzz_targets:
    try:
      os.remove(fuzz_target_path)
    except OSError as error:
      logging.error('%s occurred while removing file %s', error,
                    fuzz_target_path)


def is_fuzz_target_affected(coverage_getter, fuzz_target_path, files_changed):
  """Returns True if a fuzz target (|fuzz_target_path|) is affected by
  |files_changed|."""
  fuzz_target = os.path.basename(fuzz_target_path)
  covered_files = coverage_getter.get_files_covered_by_target(fuzz_target)
  if not covered_files:
    # Assume a fuzz target is affected if we can't get its coverage from
    # OSS-Fuzz.
    # TODO(metzman): Figure out what we should do if covered_files is [].
    # Should we act as if we couldn't get the coverage?
    logging.info('Could not get coverage for %s. Treating as affected.',
                 fuzz_target)
    return True

  logging.info('Fuzz target %s is affected by: %s', fuzz_target, covered_files)
  for filename in files_changed:
    if filename in covered_files:
      logging.info('Fuzz target %s is affected by changed file: %s',
                   fuzz_target, filename)
      return True

  logging.info('Fuzz target %s is not affected.', fuzz_target)
  return False


def get_affected_fuzz_targets(coverage_getter, fuzz_target_paths,
                              files_changed):
  """Returns a list of paths of affected targets."""
  affected_fuzz_targets = set()
  for fuzz_target_path in fuzz_target_paths:
    if is_fuzz_target_affected(coverage_getter, fuzz_target_path,
                               files_changed):
      affected_fuzz_targets.add(fuzz_target_path)

  return affected_fuzz_targets