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