scope of $commit needs to be wider
[build-farm.git] / import-and-analyse.pl
1 #!/usr/bin/perl
2 # Write sqlite entries for test reports in the build farm
3 # Copyright (C) 2007 Jelmer Vernooij <jelmer@samba.org>
4 # Published under the GNU GPL
5
6 use FindBin qw($RealBin $Script);
7 use lib "$RealBin/web";
8 use DBI;
9 use Digest::SHA1 qw(sha1_hex);
10 use strict;
11 use util;
12 use File::stat;
13 use Getopt::Long;
14 use hostdb;
15 use data;
16 use Carp;
17
18 my $opt_help = 0;
19 my $opt_verbose = 0;
20 my $dry_run = 0;
21 my $result = GetOptions('help|h|?' => \$opt_help,
22                         'dry-run|n' => sub { $dry_run++; },
23                         'verbose|v' => sub { $opt_verbose++; });
24
25 exit(1) unless ($result);
26
27 if ($opt_help) {
28         print "$Script [OPTIONS]\n";
29         print "Options:\n";
30         print " --help         This help message\n";
31         print " --verbose      Be verbose\n";
32         print " --dry-run      Dry run\n";
33         exit;
34
35         print <<EOU;
36
37 Script to parse build farm log files from the data directory, import
38 them into the database, add links to the oldrevs/ directory and send
39 some mail chastising the possible culprits when the build fails, based
40 on recent commits.
41
42 -n  Will cause the script to send output to stdout instead of
43     to sendmail.
44 EOU
45         exit(1);
46 }
47
48 my $unpacked_dir = "/home/ftp/pub/unpacked";
49
50 # we open readonly here as only apache(www-run) has write access
51 my $db = new data($RealBin, 1);
52
53 my $hostdb = new hostdb("$RealBin/hostdb.sqlite");
54
55 my $dbh = $hostdb->{dbh};
56
57 my @compilers = @{$db->{compilers}};
58 my @hosts = @{$db->{hosts_list}};
59 my %trees = %{$db->{trees}};
60
61 sub get_log_svn($$$$$)
62 {
63         my ($host, $tree, $compiler, $cur, $old) = @_;
64         my $firstrev = $old->{rev} + 1;
65         my $cmd = "svn log --non-interactive -r $firstrev:$cur->{rev} $unpacked_dir/$tree";
66         my $log = undef;
67
68         $log->{change_log} = `$cmd` || confess "$cmd: failed";
69         #print($log->{change_log});
70
71         # get the list of possible culprits
72         my $log2 = $log->{change_log};
73
74         while ($log2 =~ /\nr\d+ \| (\w+) \|.*?line(s?)\n(.*)$/s) {
75                 $log->{committers}->{"$1\@samba.org"} = 1;
76                 $log2 = $3;
77         }
78
79         # Add a URL to the diffs for each change
80         $log->{change_log} =~ s/\n(r(\d+).*)/\n$1\nhttp:\/\/build.samba.org\/?function=diff;tree=${tree};revision=$2/g;
81
82         $log->{recipients} = $log->{committers};
83
84         return $log;
85 }
86
87 sub get_log_git($$$$$)
88 {
89         my ($host, $tree, $compiler, $cur, $old) = @_;
90         my $cmd = "cd $unpacked_dir/$tree && git log --pretty=full $old->{rev}..$cur->{rev} ./";
91         my $log = undef;
92
93         $log->{change_log} = `$cmd` || confess "$cmd: failed";
94         #print($log->{change_log});
95
96         # get the list of possible culprits
97         my $log2 = $log->{change_log};
98
99         while ($log2 =~ /[\n]*Author: [^<]*<([^>]+)>\nCommit: [^<]*<([^>]+)>\n(.*)$/s) {
100                 my $author = $1;
101                 my $committer = $2;
102                 $log2 = $3;
103                 
104                 # handle cherry-picks from svnmirror repo
105                 $author =~ s/0c0555d6-39d7-0310-84fc-f1cc0bd64818/samba\.org/;
106                 
107                 # for now only send reports to samba.org addresses.
108                 $author = undef unless $author =~ /\@samba\.org/;
109                 # $committer = undef unless $committer =~ /\@samba\.org/;
110
111                 $log->{authors}->{$author} = 1 if defined($author);
112                 $log->{committers}->{$committer} = 1 if defined($committer);
113         }
114
115         # Add a URL to the diffs for each change
116         $log->{change_log} =~ s/([\n]*commit ([0-9a-f]+))/$1\nhttp:\/\/build.samba.org\/?function=diff;tree=${tree};revision=$2/g;
117
118         my @all = ();
119         push(@all, keys %{$log->{authors}}) if defined($log->{authors});
120         push(@all, keys %{$log->{committers}}) if defined($log->{committers});
121         my $all = undef;
122         foreach my $k (@all) {
123                 $all->{$k} = 1;
124         }
125         $log->{recipients} = $all;
126
127         return $log;
128 }
129
130 sub get_log($$$$$)
131 {
132         my ($host, $tree, $compiler, $cur, $old) = @_;
133         my $treedir = "$unpacked_dir/$tree";
134
135         if (-d "$treedir/.svn") {
136                 return get_log_svn($host, $tree, $compiler, $cur, $old);
137         } elsif (-d "$treedir/.git") {
138                 return get_log_git($host, $tree, $compiler, $cur, $old);
139         }
140
141         return undef;
142 }
143
144 sub check_and_send_mails($$$$$) 
145 {
146     my ($tree, $host, $compiler, $cur, $old) = @_;
147     my $t = $trees{$tree};
148     
149     printf("rev=$cur->{rev} status=$cur->{string}\n") if $dry_run;
150     
151     printf("old rev=$old->{rev} status=$old->{string}\n") if $dry_run;
152     
153     my $cmp = $db->status_info_cmp($old, $cur);
154 #printf("cmp: $cmp\n");
155     
156     if ($cmp <= 0) {
157         printf("the build didn't get worse ($cmp)\n") if $dry_run;
158         return unless $dry_run;
159     }
160     
161     my $log = get_log($host, $tree, $compiler, $cur, $old);
162     if (not defined($log)) {
163         printf("no log\n") if $dry_run;
164         return;
165     }
166     
167     my $recipients = undef;
168     $recipients = join(",", keys %{$log->{recipients}}) if defined($log->{recipients});
169     
170     my $subject = "BUILD of $tree:$t->{branch} BROKEN on $host with $compiler AT REVISION $cur->{rev}";
171     
172 # send the nastygram
173     if ($dry_run) {
174         print "To: $recipients\n" if defined($recipients);
175         print "Subject: $subject\n";
176         open(MAIL,"|cat");
177     } else {
178         if (defined($recipients)) {
179             open(MAIL,"|Mail -a \"Content-Type: text/plain;charset=utf-8\" -a \"Precedence: bulk\" -s \"$subject\" $recipients");
180         } else {
181             open(MAIL,"|cat >/dev/null");
182         }
183     }
184     
185     my $body = << "__EOF__";
186 Broken build for tree $tree on host $host with compiler $compiler
187
188 Tree $tree is $t->{scm} branch $t->{branch}.
189
190 Build status for new revision $cur->{rev} is $cur->{string}
191 Build status for old revision $old->{rev} was $old->{string}
192
193 See http://build.samba.org/?function=View+Build;host=$host;tree=$tree;compiler=$compiler
194
195 The build may have been broken by one of the following commits:
196
197 $log->{change_log}
198 __EOF__
199     print MAIL $body;
200
201     close(MAIL);
202 }
203
204
205 foreach my $host (@hosts) {
206     foreach my $tree (keys %trees) {
207         foreach my $compiler (@compilers) {
208             my $rev, $commit;
209             my $retry = 0;
210             if ($opt_verbose >= 2) {
211                 print "Looking for a log file for $host $compiler $tree...\n";
212             }
213
214             # By building the log file name this way, using only the list of
215             # hosts, trees and compilers as input, we ensure we
216             # control the inputs
217             my $logfn = $db->build_fname($tree, $host, $compiler);
218             my $stat = stat($logfn . ".log");
219             next if (!$stat);
220     
221             if ($opt_verbose >= 2) {
222                 print "Processing $logfn...\n";
223             }
224             
225             eval {
226                 my $expression = "SELECT checksum FROM build WHERE age >= ? AND tree = ? AND host = ? AND compiler = ?";
227                 my $st = $dbh->prepare($expression);
228             
229                 $st->execute($stat->ctime, $tree, $host, $compiler);
230             
231                 # Don't bother if we've already processed this file
232                 my $relevant_rows = $st->fetchall_arrayref();
233                 
234                 $st->finish();
235
236                 if ($#$relevant_rows > -1) {
237                     if ($opt_verbose > 1) {
238                             print "retry relevant_rows=$#$relevant_rows\n";
239                     }
240                     die "next please"; #Moves to the next record in the exception handler
241                 }
242             
243                 # By reading the log file this way, using only the list of
244                 # hosts, trees and compilers as input, we ensure we
245                 # control the inputs
246                 my $data = util::FileLoad($logfn.".log");
247                 
248                 # Don't bother with empty logs, they have no meaning (and would all share the same checksum)
249                 if (not $data or $data eq "") {
250                     if ($opt_verbose > 1) {
251                             print "retry empty data\n";
252                     }
253                     die "next please"; #Moves to the next record in the exception handler
254                 }
255                 
256                 my $err = util::FileLoad($logfn.".err");
257                 $err = "" unless defined($err);
258                 
259                 my $checksum = sha1_hex($data);
260                 if ($dbh->selectrow_array("SELECT checksum FROM build WHERE checksum = '$checksum'")) {
261                     $dbh->do("UPDATE BUILD SET age = ? WHERE checksum = ?", undef, 
262                              ($stat->ctime, $checksum));
263                     if ($opt_verbose > 1) {
264                             print "retry checksum match\n";
265                     }
266                     die "next please"; #Moves to the next record in the exception handler
267                 }
268                 if ($opt_verbose) { print "$logfn\n"; }
269                 
270                 ($rev) = ($data =~ /BUILD REVISION: ([^\n]+)/);
271
272                 if ($data =~ /BUILD COMMIT REVISION: (.*)/) {
273                     $commit = $1;
274                 } else {
275                     $commit = $rev;
276                 }
277
278                 if ($rev == "") {
279                         $rev = $commit;
280                 }
281
282                 my $status_html = $db->build_status_from_logs($data, $err);
283
284                 if ($opt_verbose > 1) {
285                         print "Found rev=$rev commit=$commit status=$status_html\n";
286                 }
287                 
288                 # Look up the database to find the previous status
289                 $st = $dbh->prepare("SELECT status, revision, commit_revision FROM build WHERE tree = ? AND host = ? AND compiler = ? AND revision != ? AND commit_revision != ? ORDER BY id DESC LIMIT 1");
290                 $st->execute( $tree, $host, $compiler, $rev, $commit);
291                 
292                 my $old_status_html;
293                 my $old_rev;
294                 my $old_commit;
295                 while ( my @row = $st->fetchrow_array ) {
296                     $old_status_html = @row[0];
297                     $old_rev = @row[1];
298                     $old_commit = @row[2];
299                 }
300
301                 if ($opt_verbose > 1) {
302                         print "Old rev=$old_rev old_commit=$commit status=$old_status_html\n";
303                 }
304                 
305                 $st->finish();
306                 
307                 $st = $dbh->prepare("INSERT INTO build (tree, revision, commit_revision, host, compiler, checksum, age, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
308                 $st->execute($tree, $rev, $commit, $host, $compiler, $checksum, $stat->ctime, $status_html);
309
310
311 #   SKIP This code, as it creates massive databases, until we get code to use the information, and a way to expire the results
312 #           my $build = $dbh->func('last_insert_rowid');
313 #           
314 #           $st = $dbh->prepare("INSERT INTO test_run (build, test, result, output) VALUES ($build, ?, ?, ?)");
315 #           
316 #           while ($data =~ /--==--==--==--==--==--==--==--==--==--==--.*?
317 #       Running\ test\ ([\w\-=,_:\ \/.&;]+).*?
318 #       --==--==--==--==--==--==--==--==--==--==--
319 #       (.*?)
320 #       ==========================================.*?
321 #       TEST\ (FAILED|PASSED|SKIPPED):.*?
322 #       ==========================================\s+
323 #       /sxg) {
324 #               # Note: output is discarded ($2)
325 #               $st->execute($1, $3, undef);
326 #           }
327
328                 $st->finish();
329
330                 my $cur_status = $db->build_status_info_from_html($rev, $commit, $status_html);
331                 my $old_status;
332                 
333                 if ($opt_verbose > 1) {
334                         print "cur_status=$cur_status\n";
335                 }
336
337                 # Can't send a nastygram until there are 2 builds..
338                 if (defined($old_status_html)) {
339                     $old_status = $db->build_status_info_from_html($old_rev, $old_commit, $old_status_html);
340                     if ($opt_verbose > 1) {
341                             print "old_status=$old_status\n";
342                     }
343                     check_and_send_mails($tree, $host, $compiler, $cur_status, $old_status);
344                 }
345                 
346                 if ($dry_run) {
347                     $dbh->rollback;
348                     die "next please"; #Moves to the next record in the exception handler
349                 }
350
351                 if ($opt_verbose > 1) {
352                         print "Committing\n";
353                 }
354
355                 $dbh->commit;
356             };
357
358             if ($@) {
359                 local $dbh->{RaiseError} = 0;
360                 $dbh->rollback;
361                 
362                 if ($@ =~ /^next please/) {
363                     # Ignore errors and hope for better luck next time the script is run
364                     if ($opt_verbose > 1) {
365                         print "next please retry\n";
366                     }               
367                     next;
368                 } elsif ($@ =~ /database is locked/ and $retry < 3) {
369                     $retry++;
370                     sleep(1);
371                     redo;
372                 }
373                 
374                 print "Failed to process record for reason: $@";
375                 next;
376             }
377
378             if ($commit) {
379                 # If we were able to put this into the DB (ie, a
380                 # one-off event, so we won't repeat this), then also
381                 # hard-link the log files to the revision, if we know
382                 # it.
383
384
385                 # This ensures that the names under 'oldrev' are well known and well formed 
386                 my $log_rev = $db->build_fname($tree, $host, $compiler, $commit) . ".log";
387                 my $err_rev = $db->build_fname($tree, $host, $compiler, $commit) . ".err";
388                 if ($opt_verbose >= 2) {
389                         print "Linking $logfn to $log_rev\n";
390                         print "Linking $logfn to $err_rev\n";
391                 }
392                 unlink $log_rev;
393                 unlink $err_rev;
394                 link($logfn . ".log", $log_rev) || die "Failed to link $logfn to $log_rev";
395                 link($logfn . ".err", $err_rev) || die "Failed to link $logfn to $err_rev";
396             }
397         }
398     }
399 }
400
401 $dbh->disconnect();