2 # Copyright (c) 2013 The Chromium 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.
14 # Where all the data lives.
15 ROOT_URL
= "http://build.chromium.org/p/chromium.memory.fyi/builders"
17 # TODO(groby) - support multi-line search from the command line. Useful when
18 # scanning for classes of failures, see below.
19 SEARCH_STRING
= """<p class=\"failure result\">
20 Failed memory test: content
23 # Location of the log cache.
24 CACHE_DIR
= "buildlogs.tmp"
26 # If we don't find anything after searching |CUTOFF| logs, we're probably done.
30 """Makes sure |path| does exist, tries to create it if it doesn't."""
33 except OSError as exception
:
34 if exception
.errno
!= errno
.EEXIST
:
39 def __init__(self
, root_dir
):
40 self
._root
_dir
= os
.path
.abspath(root_dir
)
42 def _LocalName(self
, name
):
43 """If name is a relative path, treat it as relative to cache root.
44 If it is absolute and under cache root, pass it through.
45 Otherwise, raise error.
47 if os
.path
.isabs(name
):
48 assert os
.path
.commonprefix([name
, self
._root
_dir
]) == self
._root
_dir
50 name
= os
.path
.join(self
._root
_dir
, name
)
53 def _FetchLocal(self
, local_name
):
54 local_name
= self
._LocalName
(local_name
)
55 EnsurePath(os
.path
.dirname(local_name
))
56 if os
.path
.exists(local_name
):
57 f
= open(local_name
, 'r')
61 def _FetchRemote(self
, remote_name
):
63 response
= urllib2
.urlopen(remote_name
)
65 print "Could not fetch", remote_name
67 return response
.read()
69 def Update(self
, local_name
, remote_name
):
70 local_name
= self
._LocalName
(local_name
)
71 EnsurePath(os
.path
.dirname(local_name
))
72 blob
= self
._FetchRemote
(remote_name
)
73 f
= open(local_name
, "w")
75 return blob
.splitlines()
77 def FetchData(self
, local_name
, remote_name
):
78 result
= self
._FetchLocal
(local_name
)
81 # If we get here, the local cache does not exist yet. Fetch, and store.
82 return self
.Update(local_name
, remote_name
)
85 class Builder(object):
86 def __init__(self
, waterfall
, name
):
88 self
._waterfall
= waterfall
93 def LatestBuild(self
):
94 return self
._waterfall
.GetLatestBuild(self
._name
)
96 def GetBuildPath(self
, build_num
):
97 return "%s/%s/builds/%d" % (
98 self
._waterfall
._root
_url
, urllib
.quote(self
._name
), build_num
)
100 def _FetchBuildLog(self
, build_num
):
101 local_build_path
= "builds/%s" % self
._name
102 local_build_file
= os
.path
.join(local_build_path
, "%d.log" % build_num
)
103 return self
._waterfall
._cache
.FetchData(local_build_file
,
104 self
.GetBuildPath(build_num
))
106 def _CheckLog(self
, build_num
, tester
):
107 log_lines
= self
._FetchBuildLog
(build_num
)
108 return any(tester(line
) for line
in log_lines
)
110 def ScanLogs(self
, tester
):
112 build
= self
.LatestBuild()
114 while build
!= 0 and no_results
< CUTOFF
:
115 if self
._CheckLog
(build
, tester
):
116 occurrences
.append(build
)
118 no_results
= no_results
+ 1
123 class Waterfall(object):
124 def __init__(self
, root_url
, cache_dir
):
125 self
._root
_url
= root_url
127 self
._top
_revision
= {}
128 self
._cache
= Cache(cache_dir
)
131 return self
._builders
.values()
134 self
._cache
.Update("builders", self
._root
_url
)
138 if self
._top
_revision
:
141 html
= self
._cache
.FetchData("builders", self
._root
_url
)
143 """ Search for both builders and latest build number in HTML
144 <td class="box"><a href="builders/<builder-name>"> identifies a builder
145 <a href="builders/<builder-name>/builds/<build-num>"> is the latest build.
147 box_matcher
= re
.compile('.*a href[^>]*>([^<]*)\<')
148 build_matcher
= re
.compile('.*a href=\"builders/(.*)/builds/([0-9]+)\".*')
151 if 'a href="builders/' in line
:
152 if 'td class="box"' in line
:
153 last_builder
= box_matcher
.match(line
).group(1)
154 self
._builders
[last_builder
] = Builder(self
, last_builder
)
156 result
= build_matcher
.match(line
)
157 builder
= result
.group(1)
158 assert builder
== urllib
.quote(last_builder
)
159 self
._top
_revision
[last_builder
] = int(result
.group(2))
161 def GetLatestBuild(self
, name
):
163 assert self
._top
_revision
164 return self
._top
_revision
[name
]
167 class MultiLineChange(object):
168 def __init__(self
, lines
):
169 self
._tracked
_lines
= lines
172 def __call__(self
, line
):
173 """ Test a single line against multi-line change.
175 If it matches the currently active line, advance one line.
176 If the current line is the last line, report a match.
178 if self
._tracked
_lines
[self
._current
] in line
:
179 self
._current
= self
._current
+ 1
180 if self
._current
== len(self
._tracked
_lines
):
189 # Create argument parser.
190 parser
= argparse
.ArgumentParser()
191 commands
= parser
.add_mutually_exclusive_group(required
=True)
192 commands
.add_argument("--update", action
='store_true')
193 commands
.add_argument("--find", metavar
='search term')
194 args
= parser
.parse_args()
196 path
= os
.path
.abspath(os
.path
.dirname(argv
[0]))
197 cache_path
= os
.path
.join(path
, CACHE_DIR
)
199 fyi
= Waterfall(ROOT_URL
, cache_path
)
203 for builder
in fyi
.Builders():
204 print "Updating", builder
.Name()
205 builder
.ScanLogs(lambda x
:False)
208 tester
= MultiLineChange(args
.find
.splitlines())
211 print "SCANNING FOR ", args
.find
212 for builder
in fyi
.Builders():
213 print "Scanning", builder
.Name()
214 occurrences
= builder
.ScanLogs(tester
)
216 min_build
= min(occurrences
)
217 path
= builder
.GetBuildPath(min_build
)
218 print "Earliest occurrence in build %d" % min_build
219 print "Latest occurrence in build %d" % max(occurrences
)
220 print "Latest build: %d" % builder
.LatestBuild()
222 print "%d total" % len(occurrences
)
225 if __name__
== "__main__":
226 sys
.exit(main(sys
.argv
))