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
6 use FindBin qw($RealBin $Script);
7 use lib "$RealBin/web";
9 use Digest::SHA1 qw(sha1_hex);
21 my $result = GetOptions('help|h|?' => \$opt_help,
22 'dry-run|n' => sub { $dry_run++; },
23 'verbose|v' => sub { $opt_verbose++; });
25 exit(1) unless ($result);
28 print "$Script [OPTIONS]\n";
30 print " --help This help message\n";
31 print " --verbose Be verbose\n";
32 print " --dry-run Dry run\n";
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
42 -n Will cause the script to send output to stdout instead of
48 my $unpacked_dir = "/home/ftp/pub/unpacked";
50 # we open readonly here as only apache(www-run) has write access
51 my $db = new data($RealBin, 1);
53 my $hostdb = new hostdb("$RealBin/hostdb.sqlite");
55 my $dbh = $hostdb->{dbh};
57 my @compilers = @{$db->{compilers}};
58 my @hosts = @{$db->{hosts_list}};
59 my %trees = %{$db->{trees}};
61 sub get_log_svn($$$$$)
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";
68 $log->{change_log} = `$cmd` || confess "$cmd: failed";
69 #print($log->{change_log});
71 # get the list of possible culprits
72 my $log2 = $log->{change_log};
74 while ($log2 =~ /\nr\d+ \| (\w+) \|.*?line(s?)\n(.*)$/s) {
75 $log->{committers}->{"$1\@samba.org"} = 1;
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;
82 $log->{recipients} = $log->{committers};
87 sub get_log_git($$$$$)
89 my ($host, $tree, $compiler, $cur, $old) = @_;
90 my $cmd = "cd $unpacked_dir/$tree && git log --pretty=full $old->{rev}..$cur->{rev} ./";
93 $log->{change_log} = `$cmd` || confess "$cmd: failed";
94 #print($log->{change_log});
96 # get the list of possible culprits
97 my $log2 = $log->{change_log};
99 while ($log2 =~ /[\n]*Author: [^<]*<([^>]+)>\nCommit: [^<]*<([^>]+)>\n(.*)$/s) {
104 # handle cherry-picks from svnmirror repo
105 $author =~ s/0c0555d6-39d7-0310-84fc-f1cc0bd64818/samba\.org/;
107 # for now only send reports to samba.org addresses.
108 $author = undef unless $author =~ /\@samba\.org/;
109 # $committer = undef unless $committer =~ /\@samba\.org/;
111 $log->{authors}->{$author} = 1 if defined($author);
112 $log->{committers}->{$committer} = 1 if defined($committer);
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;
119 push(@all, keys %{$log->{authors}}) if defined($log->{authors});
120 push(@all, keys %{$log->{committers}}) if defined($log->{committers});
122 foreach my $k (@all) {
125 $log->{recipients} = $all;
132 my ($host, $tree, $compiler, $cur, $old) = @_;
133 my $treedir = "$unpacked_dir/$tree";
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);
144 sub check_and_send_mails($$$$$)
146 my ($tree, $host, $compiler, $cur, $old) = @_;
147 my $t = $trees{$tree};
149 printf("rev=$cur->{rev} status=$cur->{string}\n") if $dry_run;
151 printf("old rev=$old->{rev} status=$old->{string}\n") if $dry_run;
153 my $cmp = $db->status_info_cmp($old, $cur);
154 #printf("cmp: $cmp\n");
157 printf("the build didn't get worse ($cmp)\n") if $dry_run;
158 return unless $dry_run;
161 my $log = get_log($host, $tree, $compiler, $cur, $old);
162 if (not defined($log)) {
163 printf("no log\n") if $dry_run;
167 my $recipients = undef;
168 $recipients = join(",", keys %{$log->{recipients}}) if defined($log->{recipients});
170 my $subject = "BUILD of $tree:$t->{branch} BROKEN on $host with $compiler AT REVISION $cur->{rev}";
174 print "To: $recipients\n" if defined($recipients);
175 print "Subject: $subject\n";
178 if (defined($recipients)) {
179 open(MAIL,"|Mail -a \"Content-Type: text/plain;charset=utf-8\" -a \"Precedence: bulk\" -s \"$subject\" $recipients");
181 open(MAIL,"|cat >/dev/null");
185 my $body = << "__EOF__";
186 Broken build for tree $tree on host $host with compiler $compiler
188 Tree $tree is $t->{scm} branch $t->{branch}.
190 Build status for new revision $cur->{rev} is $cur->{string}
191 Build status for old revision $old->{rev} was $old->{string}
193 See http://build.samba.org/?function=View+Build;host=$host;tree=$tree;compiler=$compiler
195 The build may have been broken by one of the following commits:
205 foreach my $host (@hosts) {
206 foreach my $tree (keys %trees) {
207 foreach my $compiler (@compilers) {
210 if ($opt_verbose >= 2) {
211 print "Looking for a log file for $host $compiler $tree...\n";
214 # By building the log file name this way, using only the list of
215 # hosts, trees and compilers as input, we ensure we
217 my $logfn = $db->build_fname($tree, $host, $compiler);
218 my $stat = stat($logfn . ".log");
221 if ($opt_verbose >= 2) {
222 print "Processing $logfn...\n";
226 my $expression = "SELECT checksum FROM build WHERE age >= ? AND tree = ? AND host = ? AND compiler = ?";
227 my $st = $dbh->prepare($expression);
229 $st->execute($stat->ctime, $tree, $host, $compiler);
231 # Don't bother if we've already processed this file
232 my $relevant_rows = $st->fetchall_arrayref();
236 if ($#$relevant_rows > -1) {
237 if ($opt_verbose > 1) {
238 print "retry relevant_rows=$#$relevant_rows\n";
240 die "next please"; #Moves to the next record in the exception handler
243 # By reading the log file this way, using only the list of
244 # hosts, trees and compilers as input, we ensure we
246 my $data = util::FileLoad($logfn.".log");
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";
253 die "next please"; #Moves to the next record in the exception handler
256 my $err = util::FileLoad($logfn.".err");
257 $err = "" unless defined($err);
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";
266 die "next please"; #Moves to the next record in the exception handler
268 if ($opt_verbose) { print "$logfn\n"; }
270 ($rev) = ($data =~ /BUILD REVISION: ([^\n]+)/);
272 if ($data =~ /BUILD COMMIT REVISION: (.*)/) {
282 my $status_html = $db->build_status_from_logs($data, $err);
284 if ($opt_verbose > 1) {
285 print "Found rev=$rev commit=$commit status=$status_html\n";
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);
295 while ( my @row = $st->fetchrow_array ) {
296 $old_status_html = @row[0];
298 $old_commit = @row[2];
301 if ($opt_verbose > 1) {
302 print "Old rev=$old_rev old_commit=$commit status=$old_status_html\n";
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);
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');
314 # $st = $dbh->prepare("INSERT INTO test_run (build, test, result, output) VALUES ($build, ?, ?, ?)");
316 # while ($data =~ /--==--==--==--==--==--==--==--==--==--==--.*?
317 # Running\ test\ ([\w\-=,_:\ \/.&;]+).*?
318 # --==--==--==--==--==--==--==--==--==--==--
320 # ==========================================.*?
321 # TEST\ (FAILED|PASSED|SKIPPED):.*?
322 # ==========================================\s+
324 # # Note: output is discarded ($2)
325 # $st->execute($1, $3, undef);
330 my $cur_status = $db->build_status_info_from_html($rev, $commit, $status_html);
333 if ($opt_verbose > 1) {
334 print "cur_status=$cur_status\n";
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";
343 check_and_send_mails($tree, $host, $compiler, $cur_status, $old_status);
348 die "next please"; #Moves to the next record in the exception handler
351 if ($opt_verbose > 1) {
352 print "Committing\n";
359 local $dbh->{RaiseError} = 0;
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";
368 } elsif ($@ =~ /database is locked/ and $retry < 3) {
374 print "Failed to process record for reason: $@";
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
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";
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";