Add log_checksum function.
[amitay/build-farm.git] / buildfarm / data.py
1 #!/usr/bin/python
2 # Simple database query script for the buildfarm
3 #
4 # Copyright (C) Andrew Tridgell <tridge@samba.org>     2001-2005
5 # Copyright (C) Andrew Bartlett <abartlet@samba.org>   2001
6 # Copyright (C) Vance Lankhaar  <vance@samba.org>      2002-2005
7 # Copyright (C) Martin Pool <mbp@samba.org>            2001
8 # Copyright (C) Jelmer Vernooij <jelmer@samba.org>         2007-2010
9 #
10 #   This program is free software; you can redistribute it and/or modify
11 #   it under the terms of the GNU General Public License as published by
12 #   the Free Software Foundation; either version 2 of the License, or
13 #   (at your option) any later version.
14 #
15 #   This program is distributed in the hope that it will be useful,
16 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
17 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 #   GNU General Public License for more details.
19 #
20 #   You should have received a copy of the GNU General Public License
21 #   along with this program; if not, write to the Free Software
22 #   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
23
24
25 import ConfigParser
26 import hashlib
27 import os
28 import re
29 import time
30 import util
31
32
33 class BuildStatus(object):
34
35     def __init__(self, stages, other_failures):
36         self.stages = stages
37         self.other_failures = other_failures
38
39     def __str__(self):
40         return repr((self.stages, self.other_failures))
41
42     def setcheckerstage(self, val):
43         self.stages[4] = val
44
45     def getcheckerstage(self):
46         return self.stages[4]
47
48 def check_dir_exists(kind, path):
49     if not os.path.isdir(path):
50         raise Exception("%s directory %s does not exist" % (kind, path))
51
52
53 def build_status_from_logs(log, err):
54     """get status of build"""
55     m = re.search("TEST STATUS:(\s*\d+)", log)
56     if m:
57         tstatus = int(m.group(1).strip())
58     else:
59         m = re.search("ACTION (PASSED|FAILED): test", log)
60         if m:
61             test_failures = len(re.findall("testsuite-(failure|error): ", log))
62             test_successes = len(re.findall("testsuite-success: ", log))
63             if test_successes > 0:
64                 tstatus = test_failures
65             else:
66                 tstatus = 255
67             if m.group(1) == "FAILED" and tstatus == 0:
68                 tstatus = -1
69         else:
70             tstatus = None
71
72     m = re.search("INSTALL STATUS:(\s*\d+)", log)
73     if m:
74         istatus = int(m.group(1).strip())
75     else:
76         istatus = None
77
78     m = re.search("BUILD STATUS:(\s*\d+)", log)
79     if m:
80         bstatus = int(m.group(1).strip())
81     else:
82         bstatus = None
83
84     m = re.search("CONFIGURE STATUS:(\s*\d+)", log)
85     if m:
86         cstatus = int(m.group(1).strip())
87     else:
88         cstatus = None
89
90     other_failures = set()
91     m = re.search("(PANIC|INTERNAL ERROR):.*", log)
92     if m:
93         other_failures.add("panic")
94
95     if "No space left on device" in err or "No space left on device" in log:
96         other_failures.add("disk full")
97
98     if "maximum runtime exceeded" in log:
99         other_failures.add("timeout")
100
101     m = re.search("CC_CHECKER STATUS:(\s*\d+)", log)
102     if m:
103         sstatus = int(m.group(1).strip())
104     else:
105         sstatus = None
106
107     return BuildStatus([cstatus, bstatus, istatus, tstatus, sstatus], other_failures)
108
109
110 def lcov_extract_percentage(text):
111     m = re.search('\<td class="headerItem".*?\>Code\&nbsp\;covered\:\<\/td\>.*?\n.*?\<td class="headerValue".*?\>([0-9.]+) \%', text)
112     if m:
113         return m.group(1)
114     else:
115         return None
116
117
118 class NoSuchBuildError(Exception):
119     """The build with the specified name does not exist."""
120
121     def __init__(self, tree, host, compiler, rev=None):
122         self.tree = tree
123         self.host = host
124         self.compiler = compiler
125         self.rev = rev
126
127
128 class Tree(object):
129     """A tree to build."""
130
131     def __init__(self, name, scm, repo, branch, subdir="", srcdir=""):
132         self.name = name
133         self.repo = repo
134         self.scm = scm
135         self.branch = branch
136         self.subdir = subdir
137         self.srcdir = srcdir
138         self.scm = scm
139
140     def __repr__(self):
141         return "<%s %r>" % (self.__class__.__name__, self.name)
142
143
144 class Build(object):
145     """A single build of a tree on a particular host using a particular compiler.
146     """
147
148     def __init__(self, store, tree, host, compiler, rev=None):
149         self._store = store
150         self.tree = tree
151         self.host = host
152         self.compiler = compiler
153         self.rev = rev
154
155     ###################
156     # the mtime age is used to determine if builds are still happening
157     # on a host.
158     # the ctime age is used to determine when the last real build happened
159
160     def age_mtime(self):
161         """get the age of build from mtime"""
162         file = self._store.build_fname(self.tree, self.host, self.compiler, self.rev)
163
164         st = os.stat("%s.log" % file)
165         return time.time() - st.st_mtime
166
167     def age_ctime(self):
168         """get the age of build from ctime"""
169         file = self._store.build_fname(self.tree, self.host, self.compiler, self.rev)
170
171         st = os.stat("%s.log" % file)
172         return time.time() - st.st_ctime
173
174     def read_log(self):
175         """read full log file"""
176         f = open(self._store.build_fname(self.tree, self.host, self.compiler, self.rev)+".log", "r")
177         try:
178             return f.read()
179         finally:
180             f.close()
181
182     def read_err(self):
183         """read full err file"""
184         return util.FileLoad(self._store.build_fname(self.tree, self.host, self.compiler, self.rev)+".err")
185
186     def log_checksum(self):
187         return hashlib.sha1(self.read_log()).hexdigest()
188
189     def revision_details(self):
190         """get the revision of build
191
192         :return: Tuple with revision id and timestamp (if available)
193         """
194         file = self._store.build_fname(self.tree, self.host, self.compiler, self.rev)
195
196         revid = None
197         timestamp = None
198         f = open("%s.log" % file, 'r')
199         try:
200             for l in f.readlines():
201                 if l.startswith("BUILD COMMIT REVISION: "):
202                     revid = l.split(":", 1)[1].strip()
203                 elif l.startswith("BUILD REVISION: ") and not revid:
204                     revid = l.split(":", 1)[1].strip()
205                 elif l.startswith("BUILD COMMIT TIME"):
206                     timestamp = l.split(":", 1)[1].strip()
207         finally:
208             f.close()
209
210         return (revid, timestamp)
211
212     def status(self):
213         """get status of build
214
215         :return: tuple with build status
216         """
217
218         log = self.read_log()
219         err = self.read_err()
220
221         return build_status_from_logs(log, err)
222
223     def err_count(self):
224         """get status of build"""
225         file = self._store.build_fname(self.tree, self.host, self.compiler, self.rev)
226
227         try:
228             err = util.FileLoad("%s.err" % file)
229         except OSError:
230             # File does not exist
231             return 0
232
233         return util.count_lines(err)
234
235
236 class CachingBuild(Build):
237     """Build subclass that caches some of the results that are expensive
238     to calculate."""
239
240     def revision_details(self):
241         file = self._store.build_fname(self.tree, self.host, self.compiler, self.rev)
242         cachef = self._store.cache_fname(self.tree, self.host, self.compiler, self.rev)
243         st1 = os.stat("%s.log" % file)
244
245         try:
246             st2 = os.stat("%s.revision" % cachef)
247         except OSError:
248             # File does not exist
249             st2 = None
250
251         # the ctime/mtime asymmetry is needed so we don't get fooled by
252         # the mtime update from rsync
253         if st2 and st1.st_ctime <= st2.st_mtime:
254             (revid, timestamp) = util.FileLoad("%s.revision" % cachef).split(":", 1)
255             if timestamp == "":
256                 return (revid, None)
257             else:
258                 return (revid, timestamp)
259         (revid, timestamp) = super(CachingBuild, self).revision_details()
260         if not self._store.readonly:
261             util.FileSave("%s.revision" % cachef, "%s:%s" % (revid, timestamp or ""))
262         return (revid, timestamp)
263
264     def err_count(self):
265         file = self._store.build_fname(self.tree, self.host, self.compiler, self.rev)
266         cachef = self._store.cache_fname(self.tree, self.host, self.compiler, self.rev)
267         st1 = os.stat("%s.err" % file)
268
269         try:
270             st2 = os.stat("%s.errcount" % cachef)
271         except OSError:
272             # File does not exist
273             st2 = None
274
275         if st2 and st1.st_ctime <= st2.st_mtime:
276             return util.FileLoad("%s.errcount" % cachef)
277
278         ret = super(CachingBuild, self).err_count()
279
280         if not self._store.readonly:
281             util.FileSave("%s.errcount" % cachef, str(ret))
282
283         return ret
284
285     def status(self):
286         file = self._store.build_fname(self.tree, self.host, self.compiler, self.rev)
287         cachefile = self._store.cache_fname(self.tree, self.host, self.compiler, self.rev)+".status"
288
289         st1 = os.stat("%s.log" % file)
290
291         try:
292             st2 = os.stat(cachefile)
293         except OSError:
294             # No such file
295             st2 = None
296
297         if st2 and st1.st_ctime <= st2.st_mtime:
298             return BuildStatus(*eval(util.FileLoad(cachefile)))
299
300         ret = super(CachingBuild, self).status()
301
302         if not self._store.readonly:
303             util.FileSave(cachefile, str(ret))
304
305         return ret
306
307
308
309 def read_trees_from_conf(path):
310     """Read trees from a configuration file."""
311     ret = {}
312     cfp = ConfigParser.ConfigParser()
313     cfp.readfp(open(path))
314     for s in cfp.sections():
315         ret[s] = Tree(name=s, **dict(cfp.items(s)))
316     return ret
317
318
319 class BuildResultStore(object):
320     """The build farm build result database."""
321
322     OLDAGE = 60*60*4,
323     DEADAGE = 60*60*24*4
324     LCOVHOST = "magni"
325
326     def __init__(self, basedir, readonly=False):
327         """Open the database.
328
329         :param basedir: Build result base directory
330         :param readonly: Whether to avoid saving cache files
331         """
332         self.basedir = basedir
333         check_dir_exists("base", self.basedir)
334         self.readonly = readonly
335
336         self.webdir = os.path.join(basedir, "web")
337         check_dir_exists("web", self.webdir)
338
339         self.datadir = os.path.join(basedir, "data")
340         check_dir_exists("data", self.datadir)
341
342         self.cachedir = os.path.join(basedir, "cache")
343         check_dir_exists("cache", self.cachedir)
344
345         self.lcovdir = os.path.join(basedir, "lcov/data")
346         check_dir_exists("lcov", self.lcovdir)
347
348         self.compilers = util.load_list(os.path.join(self.webdir, "compilers.list"))
349
350         self.trees = read_trees_from_conf(os.path.join(self.webdir, "trees.conf"))
351
352     def get_build(self, tree, host, compiler, rev=None):
353         logf = self.build_fname(tree, host, compiler, rev) + ".log"
354         if not os.path.exists(logf):
355             raise NoSuchBuildError(tree, host, compiler, rev)
356         return CachingBuild(self, tree, host, compiler, rev)
357
358     def cache_fname(self, tree, host, compiler, rev=None):
359         if rev is not None:
360             return os.path.join(self.cachedir, "build.%s.%s.%s-%s" % (tree, host, compiler, rev))
361         else:
362             return os.path.join(self.cachedir, "build.%s.%s.%s" % (tree, host, compiler))
363
364     def build_fname(self, tree, host, compiler, rev=None):
365         """get the name of the build file"""
366         if rev is not None:
367             return os.path.join(self.datadir, "oldrevs/build.%s.%s.%s-%s" % (tree, host, compiler, rev))
368         return os.path.join(self.datadir, "upload/build.%s.%s.%s" % (tree, host, compiler))
369
370     def lcov_status(self, tree):
371         """get status of build"""
372         cachefile = os.path.join(self.cachedir, "lcov.%s.%s.status" % (
373             self.LCOVHOST, tree))
374         file = os.path.join(self.lcovdir, self.LCOVHOST, tree, "index.html")
375         try:
376             st1 = os.stat(file)
377         except OSError:
378             # File does not exist
379             raise NoSuchBuildError(tree, self.LCOVHOST, "lcov")
380         try:
381             st2 = os.stat(cachefile)
382         except OSError:
383             # file does not exist
384             st2 = None
385
386         if st2 and st1.st_ctime <= st2.st_mtime:
387             ret = util.FileLoad(cachefile)
388             if ret == "":
389                 return None
390             return ret
391
392         lcov_html = util.FileLoad(file)
393         perc = lcov_extract_percentage(lcov_html)
394         if perc is None:
395             ret = ""
396         else:
397             ret = perc
398         if self.readonly:
399             util.FileSave(cachefile, ret)
400         return perc
401
402     def get_old_revs(self, tree, host, compiler):
403         """get a list of old builds and their status."""
404         ret = []
405         directory = os.path.join(self.datadir, "oldrevs")
406         logfiles = [d for d in os.listdir(directory) if d.startswith("build.%s.%s.%s-" % (tree, host, compiler)) and d.endswith(".log")]
407         for l in logfiles:
408             m = re.match(".*-([0-9A-Fa-f]+).log$", l)
409             if m:
410                 rev = m.group(1)
411                 stat = os.stat(os.path.join(directory, l))
412                 # skip the current build
413                 if stat.st_nlink == 2:
414                     continue
415                 build = self.get_build(tree, host, compiler, rev)
416                 r = {
417                     "STATUS": build.status(),
418                     "REVISION": rev,
419                     "TIMESTAMP": build.age_ctime(),
420                     }
421                 ret.append(r)
422
423         ret.sort(lambda a, b: cmp(a["TIMESTAMP"], b["TIMESTAMP"]))
424
425         return ret
426
427     def has_host(self, host):
428         for name in os.listdir(os.path.join(self.datadir, "upload")):
429             try:
430                 if name.split(".")[2] == host:
431                     return True
432             except IndexError:
433                 pass
434         return False
435
436     def host_age(self, host):
437         """get the overall age of a host"""
438         ret = None
439         for compiler in self.compilers:
440             for tree in self.trees:
441                 try:
442                     build = self.get_build(tree, host, compiler)
443                 except NoSuchBuildError:
444                     pass
445                 else:
446                     ret = min(ret, build.age_mtime())
447         return ret