1#!/usr/bin/env python
2# Copyright 2014 the V8 project authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""
7Script to check for new clusterfuzz issues since the last rolled v8 revision.
8
9Returns a json list with test case IDs if any.
10
11Security considerations: The security key and request data must never be
12written to public logs. Public automated callers of this script should
13suppress stdout and stderr and only process contents of the results_file.
14"""
15
16
17import argparse
18import httplib
19import json
20import os
21import re
22import sys
23import urllib
24import urllib2
25
26
27# Constants to git repos.
28BASE_URL = "https://chromium.googlesource.com"
29DEPS_LOG = BASE_URL + "/chromium/src/+log/master/DEPS?format=JSON"
30
31# Constants for retrieving v8 rolls.
32CRREV = "https://cr-rev.appspot.com/_ah/api/crrev/v1/commit/%s"
33V8_COMMIT_RE = re.compile(
34    r"^Update V8 to version \d+\.\d+\.\d+ \(based on ([a-fA-F0-9]+)\)\..*")
35
36# Constants for the clusterfuzz backend.
37HOSTNAME = "backend-dot-cluster-fuzz.appspot.com"
38
39# Crash patterns.
40V8_INTERNAL_RE = re.compile(r"^v8::internal.*")
41ANY_RE = re.compile(r".*")
42
43# List of all api requests.
44BUG_SPECS = [
45  {
46    "args": {
47      "job_type": "linux_asan_chrome_v8",
48      "reproducible": "True",
49      "open": "True",
50      "bug_information": "",
51    },
52    "crash_state": V8_INTERNAL_RE,
53  },
54  {
55    "args": {
56      "job_type": "linux_asan_d8",
57      "reproducible": "True",
58      "open": "True",
59      "bug_information": "",
60    },
61    "crash_state": ANY_RE,
62  },
63  {
64    "args": {
65      "job_type": "linux_asan_d8_dbg",
66      "reproducible": "True",
67      "open": "True",
68      "bug_information": "",
69    },
70    "crash_state": ANY_RE,
71  },
72  {
73    "args": {
74      "job_type": "linux_asan_d8_v8_arm_dbg",
75      "reproducible": "True",
76      "open": "True",
77      "bug_information": "",
78    },
79    "crash_state": ANY_RE,
80  },
81  {
82    "args": {
83      "job_type": "linux_asan_d8_v8_arm64_dbg",
84      "reproducible": "True",
85      "open": "True",
86      "bug_information": "",
87    },
88    "crash_state": ANY_RE,
89  },
90  {
91    "args": {
92      "job_type": "linux_asan_d8_v8_mipsel_dbg",
93      "reproducible": "True",
94      "open": "True",
95      "bug_information": "",
96    },
97    "crash_state": ANY_RE,
98  },
99]
100
101
102def GetRequest(url):
103  url_fh = urllib2.urlopen(url, None, 60)
104  try:
105    return url_fh.read()
106  finally:
107    url_fh.close()
108
109
110def GetLatestV8InChromium():
111  """Returns the commit position number of the latest v8 roll in chromium."""
112
113  # Check currently rolled v8 revision.
114  result = GetRequest(DEPS_LOG)
115  if not result:
116    return None
117
118  # Strip security header and load json.
119  commits = json.loads(result[5:])
120
121  git_revision = None
122  for commit in commits["log"]:
123    # Get latest commit that matches the v8 roll pattern. Ignore cherry-picks.
124    match = re.match(V8_COMMIT_RE, commit["message"])
125    if match:
126      git_revision = match.group(1)
127      break
128  else:
129    return None
130
131  # Get commit position number for v8 revision.
132  result = GetRequest(CRREV % git_revision)
133  if not result:
134    return None
135
136  commit = json.loads(result)
137  assert commit["repo"] == "v8/v8"
138  return commit["number"]
139
140
141def APIRequest(key, **params):
142  """Send a request to the clusterfuzz api.
143
144  Returns a json dict of the response.
145  """
146
147  params["api_key"] = key
148  params = urllib.urlencode(params)
149
150  headers = {"Content-type": "application/x-www-form-urlencoded"}
151
152  try:
153    conn = httplib.HTTPSConnection(HOSTNAME)
154    conn.request("POST", "/_api/", params, headers)
155
156    response = conn.getresponse()
157
158    # Never leak "data" into public logs.
159    data = response.read()
160  except:
161    raise Exception("ERROR: Connection problem.")
162
163  try:
164    return json.loads(data)
165  except:
166    raise Exception("ERROR: Could not read response. Is your key valid?")
167
168  return None
169
170
171def Main():
172  parser = argparse.ArgumentParser()
173  parser.add_argument("-k", "--key-file", required=True,
174                      help="A file with the clusterfuzz api key.")
175  parser.add_argument("-r", "--results-file",
176                      help="A file to write the results to.")
177  options = parser.parse_args()
178
179  # Get api key. The key's content must never be logged.
180  assert options.key_file
181  with open(options.key_file) as f:
182    key = f.read().strip()
183  assert key
184
185  revision_number = GetLatestV8InChromium()
186
187  results = []
188  for spec in BUG_SPECS:
189    args = dict(spec["args"])
190    # Use incremented revision as we're interested in all revision greater than
191    # what's currently rolled into chromium.
192    if revision_number:
193      args["revision_greater_or_equal"] = str(int(revision_number) + 1)
194
195    # Never print issue details in public logs.
196    issues = APIRequest(key, **args)
197    assert issues is not None
198    for issue in issues:
199      if re.match(spec["crash_state"], issue["crash_state"]):
200        results.append(issue["id"])
201
202  if options.results_file:
203    with open(options.results_file, "w") as f:
204      f.write(json.dumps(results))
205  else:
206    print results
207
208
209if __name__ == "__main__":
210  sys.exit(Main())
211