Skip to content

Commit

Permalink
Allow running experiments from PRs using GCB (#1637)
Browse files Browse the repository at this point in the history
Fixes: #1599
  • Loading branch information
jonathanmetzman authored Jan 25, 2023
1 parent ce154ed commit 33a2631
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 10 deletions.
5 changes: 2 additions & 3 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
.pytest_cache
.pytype
.venv
__pycache__
**__pycache__*
docs
report*
third_party/oss-fuzz/build
report*
24 changes: 17 additions & 7 deletions experiment/run_experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,11 +282,16 @@ def check_no_uncommitted_changes():
raise ValidationError('Local uncommitted changes found, exiting.')


def get_git_hash():
def get_git_hash(allow_uncommitted_changes):
"""Return the git hash for the last commit in the local repo."""
output = subprocess.check_output(['git', 'rev-parse', 'HEAD'],
cwd=utils.ROOT_DIR)
return output.strip().decode('utf-8')
try:
output = subprocess.check_output(['git', 'rev-parse', 'HEAD'],
cwd=utils.ROOT_DIR)
return output.strip().decode('utf-8')
except subprocess.CalledProcessError as error:
if not allow_uncommitted_changes:
raise error
return ''


def start_experiment( # pylint: disable=too-many-arguments
Expand Down Expand Up @@ -315,7 +320,7 @@ def start_experiment( # pylint: disable=too-many-arguments
config['fuzzers'] = fuzzers
config['benchmarks'] = benchmarks
config['experiment'] = experiment_name
config['git_hash'] = get_git_hash()
config['git_hash'] = get_git_hash(allow_uncommitted_changes)
config['no_seeds'] = no_seeds
config['no_dictionaries'] = no_dictionaries
config['oss_fuzz_corpus'] = oss_fuzz_corpus
Expand Down Expand Up @@ -597,7 +602,12 @@ def get_dispatcher(config: Dict) -> BaseDispatcher:


def main():
"""Run an experiment in the cloud."""
"""Run an experiment."""
return run_experiment_main()


def run_experiment_main(args=None):
"""Run an experiment."""
logs.initialize()

parser = argparse.ArgumentParser(
Expand Down Expand Up @@ -687,7 +697,7 @@ def main():
required=False,
default=False,
action='store_true')
args = parser.parse_args()
args = parser.parse_args(args)
fuzzers = args.fuzzers or all_fuzzers

concurrent_builds = args.concurrent_builds
Expand Down
32 changes: 32 additions & 0 deletions service/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Copyright 2023 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.
#
################################################################################

FROM gcr.io/cloud-builders/gcloud

RUN apt-get update && apt-get install python3-pip -y

# Do this expensive step before the cache is destroyed.
COPY ./requirements.txt /tmp/requirements.txt
RUN pip install -r /tmp/requirements.txt
RUN pip install PyGithub==1.51

ENV FUZZBENCH_DIR /opt/fuzzbench
COPY . $FUZZBENCH_DIR

WORKDIR $FUZZBENCH_DIR
ENV PYTHONPATH=$FUZZBENCH_DIR
ENV FORCE_LOCAL=1
ENTRYPOINT ["python3", "/opt/fuzzbench/service/gcbrun_experiment.py"]
89 changes: 89 additions & 0 deletions service/gcbrun_experiment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Copyright 2023 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.
#
################################################################################
"""Entrypoint for gcbrun into run_experiment. This script will get the command
from the last PR comment containing "/gcbrun" and pass it to run_experiment.py
which will run an experiment."""

import logging
import os
import sys

# pytype: disable=import-error
import github # pylint: disable=import-error

from experiment import run_experiment

TRIGGER_COMMAND = '/gcbrun'
RUN_EXPERIMENT_COMMAND_STR = f'{TRIGGER_COMMAND} run_experiment.py '
SKIP_COMMAND_STR = f'{TRIGGER_COMMAND} skip'


def get_comments(pull_request_number):
"""Returns comments on the GitHub Pull request referenced by
|pull_request_number|."""
github_obj = github.Github()
repo = github_obj.get_repo('google/fuzzbench')
pull = repo.get_pull(pull_request_number)
pull_comments = list(pull.get_comments())
issue = repo.get_issue(pull_request_number)
issue_comments = list(issue.get_comments())
# Github only returns comments if from the pull object when a pull request
# is open. If it is a draft, it will only return comments from the issue
# object.
return pull_comments + issue_comments


def get_latest_gcbrun_command(comments):
"""Gets the last /gcbrun comment from comments."""
for comment in reversed(comments):
# This seems to get comments on code too.
body = comment.body
if body.startswith(SKIP_COMMAND_STR):
return None
if not body.startswith(RUN_EXPERIMENT_COMMAND_STR):
continue
if len(body) == len(RUN_EXPERIMENT_COMMAND_STR):
return None
return body[len(RUN_EXPERIMENT_COMMAND_STR):].strip().split(' ')
return None


def exec_command_from_github(pull_request_number):
"""Executes the gcbrun command for run_experiment.py in the most recent
command on |pull_request_number|."""
comments = get_comments(pull_request_number)
print(comments)
command = get_latest_gcbrun_command(comments)
if command is None:
logging.info('Experiment not requested.')
return None
print(command)
logging.info('Command: %s.', command)
return run_experiment.run_experiment_main(command)


def main():
"""Entrypoint for GitHub CI into run_experiment.py"""
logging.basicConfig(level=logging.INFO)
pull_request_number = int(os.environ['PULL_REQUEST_NUMBER'])
result = exec_command_from_github(pull_request_number)
if result or result is None:
return 0
return 1


if __name__ == '__main__':
sys.exit(main())
24 changes: 24 additions & 0 deletions service/run_experiment_cloudbuild.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
steps:
- name: 'gcr.io/cloud-builders/docker'
args:
- build
- -t
- gcr.io/fuzzbench/experiment-runner
- --build-arg
- BUILDKIT_INLINE_CACHE=1
- --cache-from
- gcr.io/fuzzbench/experiment-runner
- -f
- service/Dockerfile
- .
env:
- 'DOCKER_BUILDKIT=1'
- name: 'gcr.io/fuzzbench/experiment-runner'
args: []
env:
- 'PULL_REQUEST_NUMBER=${_PR_NUMBER}'
- 'POSTGRES_PASSWORD=${_POSTGRES_PASSWORD}'
timeout: 1800s # 30 minutes
timeout: 1800s
options:
logging: CLOUD_LOGGING_ONLY

0 comments on commit 33a2631

Please sign in to comment.