The new page shows the failure patterns across time and test configurations to help detect Wine changes that cause new failures, and identify when failures happen in specific configurations.
Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=48164 Signed-off-by: Francois Gouget fgouget@codeweavers.com --- This patch is independent from the 1/3 dissect patch. The 3/3 dissect patch adds a link to this patch's patterns on the report pages. On their own 1/3 and 3/3 are not worth regenerating the report pages.
However I have not tested build-patterns against the old set of status values issued by dissect. Also build-patterns also needs the testresults.txt files generated by the new gather code.
So if the patches look ok I recommend the following procedure to update everything in one go:
* Apply the 3 patches.
* Update the summary.txt files generated by dissect to use the new status codes. As a side-effect this refreshes the report pages with the new links:
cd winetest/data find `pwd`/ -name report -print | \ nice xargs -P8 -n 1 dissect --update
* Generate each build's testresults.txt file with gather:
find `pwd`/ -name total.txt -print | \ while read p; do dirname $p; done | \ nice xargs -P8 -n 1 gather --update
* Generate the new pattern pages with build-patterns:
build-patterns
The paths to the scripts should be adjusted as appropriate. Also adjust -P8 for the desired level of parallelism. Here refreshing my test.winehq.org mirror with -P8 takes under 5 minutes. Since I have the same set of reports the time should be similar on winehq.org.
This is the complex patchset: I don't forsee any need for such global updates after this one. --- winetest/build-index | 3 +- winetest/build-patterns | 636 ++++++++++++++++++++++++++++++++++++++++ winetest/gather | 65 ++++ winetest/report.css | 46 +++ winetest/winetest.cron | 1 + 5 files changed, 750 insertions(+), 1 deletion(-) create mode 100755 winetest/build-patterns
diff --git a/winetest/build-index b/winetest/build-index index a469dfb6d..7eacb87f9 100755 --- a/winetest/build-index +++ b/winetest/build-index @@ -220,7 +220,7 @@ foreach my $build (readdir(DIR)) { if ($build !~ /^[0-9a-f]{40}$/) { - if ($build !~ /^(..?|errors.html|index.html|tests)$/) + if ($build !~ /^(?:..?|(?:errors|index|patterns).html|tests)$/) { error("'data/$build' is not a valid build directory\n"); } @@ -423,6 +423,7 @@ print OUT <<"EOF"; <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> </head> <body> +<div class="navbar"><a href="patterns.html">failure patterns</a></div> <div class="main"> <h2>Wine test runs</h2> EOF diff --git a/winetest/build-patterns b/winetest/build-patterns new file mode 100755 index 000000000..f470c266f --- /dev/null +++ b/winetest/build-patterns @@ -0,0 +1,636 @@ +#!/usr/bin/perl +# +# Copyright 2021 Francois Gouget +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +use strict; +use warnings; +use open ':utf8'; +use CGI qw(:standard); + +sub BEGIN +{ + if ($0 !~ m=^/=) + { + # Turn $0 into an absolute path so it can safely be used in @INC + require Cwd; + $0 = Cwd::cwd() . "/$0"; + } + unshift @INC, $1 if ($0 =~ m=^(/.*)/[^/]+$=); +} +use vars qw/$workdir $gitdir/; +require "winetest.conf"; + +my $name0=$0; +$name0 =~ s+^.*/++; + + +# +# Common helpers +# + +sub error(@) +{ + print STDERR "$name0:error: ", @_; +} + +$ENV{GIT_DIR} = $gitdir; + +sub get_build_info($) +{ + my ($build) = @_; + my ($date, $subject); + + my $commit = `git log --max-count=1 --pretty="format:%ct %s" "$build^0" 2>/dev/null` if ($build =~ /^[0-9a-f]{40}$/); + if ($commit && $commit =~ /^(\d+) (.*)$/) + { + ($date, $subject) = ($1, $2); + # Make sure the directory's mtime matches the commit time + utime $date, $date, "data/$build"; + } + else + { + $date = (stat "data/$build")[9]; + $subject = ""; + } + return ($date, $subject); +} + +use POSIX qw(locale_h strftime); +setlocale(LC_ALL, "C"); + +sub short_date($) +{ + my ($date) = @_; + return strftime("%b %d", gmtime($date)); +} + +sub date_range($$) +{ + my ($start, $end) = @_; + return short_date($start) if (!$end); + return short_date($start) ." - ". short_date($end); +} + + +# +# Command line processing +# + +my ($opt_workdir, $usage); + +sub check_opt_val($$) +{ + my ($option, $val) = @_; + + if (defined $val) + { + error("$option can only be specified once\n"); + $usage = 2; # but continue processing this option + } + if (!@ARGV) + { + error("missing value for $option\n"); + $usage = 2; + return undef; + } + return shift @ARGV; +} + +while (@ARGV) +{ + my $arg = shift @ARGV; + if ($arg eq "--workdir") + { + $workdir = $opt_workdir = check_opt_val($arg, $opt_workdir); + } + elsif ($arg eq "--help") + { + $usage = 0; + } + else + { + error("unknown argument '$arg'\n"); + $usage = 2; + } +} +if (!defined $usage) +{ + if (!defined $workdir) + { + require Cwd; + $workdir = Cwd::cwd(); + } + elsif ($workdir !~ m%^/%) + { + require Cwd; + $workdir = Cwd::cwd() . "/$workdir"; + } + if (!-f "$workdir/report.css") + { + error("'$workdir' is not a valid work directory\n"); + $usage = 2; + } +} +if (defined $usage) +{ + if ($usage) + { + error("try '$name0 --help' for more information\n"); + exit $usage; + } + print <<EOF; +Usage: $name0 [--workdir DIR] [--help] + +Collect test unit failures from all builds and produce a report highlighting +patterns in the failures of each test unit. + +Where: + --workdir DIR Specifies the directory containing the winetest website + files. Can be omitted if set in winetest.conf. + --help Shows this usage message. + +Actions: + $name0 should be called after the gather script has updated all the builds. + +Generated files: + $workdir/data/patterns.html + +Exit: + 0 - success + 2 - usage error +EOF + exit 0; +} + +chdir($workdir) or die "could not chdir to the work directory: $!"; + + +# +# Get the list of builds +# + +# A hashtable of build objects indexed by their name. +# Each object has the following fields: +# +# - name +# The build's unique identifier (its Git commit id). +# +# - date +# The commit date. +# +# - hasreport +# A hashtable indexed by the report directory which returns true if the +# report is available for this build. +# +# - hastest +# A hashtable indexed by the test name which returns true if the test +# existed in this build. +my %builds; + +opendir(DIR, "data") or die "could not open the 'data' directory: $!"; +foreach my $build (readdir(DIR)) +{ + if ($build !~ /^[0-9a-f]{40}$/) + { + if ($build !~ /^(?:..?|(?:errors|index|patterns).html|tests)$/) + { + error("'data/$build' is not a valid build directory\n"); + } + next; + } + next unless -f "data/$build/index.html"; + + my ($date, $subject) = get_build_info($build); + $builds{$build} = { name => $build, date => $date}; +} + +closedir(DIR); + +# The builds, sorted by commit date +my @sortedbuilds = sort { $a->{date} <=> $b->{date} } values %builds; + + +# +# Read each build's testresults.txt file +# + +# A hashtable of report objects indexed by their directory. +# Each object has the following fields: +# +# - dir +# The report's uniquely identifying directory name. +# +# - platform, tag, num +# The components of the report directory. num may be undefined. +# +# - is_rerun +# True if this is not the first result for this report (i.e num is set). +my %reports; + +# A hashtable of test objects indexed by their name. +# Each object has the following fields: +# +# - name +# The uniquely identifying test name in the form 'module:unit'. +# +# - testreports +# A hashtable mapping report directory names to objects storing the results +# for that test and report combination. Each testreport object has the +# following fields: +# +# - failed +# True if one of the builds had errors. In other words, if true the report +# should be included in that test's failure pattern. +# +# - failtype +# The type of the most recent failure (see fail_type()). +# In particular this distinguishes random failures from non-random ones. +# +# - status +# A hashtable of test results indexed by the build name. +my %tests; + +foreach my $build (@sortedbuilds) +{ + my $filename = "data/$build->{name}/testresults.txt"; + open(my $fh, "<", $filename) or next; + my $reportlist = <$fh>; + if ($reportlist !~ s/^* //) + { + error("'$filename' does not contain a valid reports list\n"); + next; + } + chomp $reportlist; + foreach my $reportdir (split / /, $reportlist) + { + my $report = $reports{$reportdir}; + if (!$report) + { + my ($platform, $tag, $num) = split /_/, $reportdir; + $report = { + dir => $reportdir, + platform => $platform, + tag => $tag, + num => $num, + is_rerun => ($num ? 1 : 0), + }; + $reports{$reportdir} = $report; + } + $build->{hasreport}->{$reportdir} = 1; + } + + while (my $line = <$fh>) + { + chomp $line; + my ($testname, $_source, @items) = split / /, $line; + if ($testname !~ /:/) + { + error("found an invalid test unit name ($testname) in '$filename'\n"); + next; + } + $build->{hastest}->{$testname} = 1; + my $test = $tests{$testname}; + $tests{$testname} = $test = { name => $testname } if (!$test); + + foreach my $statreps (@items) + { + my ($status, @reportdirs) = split /:/, $statreps; + foreach my $reportdir (@reportdirs) + { + if (!$build->{hasreport}->{$reportdir}) + { + error("the $testname line contains an undeclared report ($reportdir) in '$filename'\n"); + next; + } + $test->{testreports}->{$reportdir}->{status}->{$build->{name}} = $status; + } + } + } + close($fh); +} + + +# +# Build a sorted report list +# + +my %platform_order = ( + 95 => 1, 98 => 2, me => 3, + nt3 => 4, nt4 => 5, 2000 => 6, + xp => 7, 2003 => 8, + vista => 9, 2008 => 10, + win7 => 11, + win8 => 21, win81 => 22, + win1507 => 1507, win1511 => 1511, + win1607 => 1607, win1703 => 1703, + win1709 => 1709, win1803 => 1803, + win1809 => 1809, win1903 => 1903, + win1909 => 1909, win2004 => 2004, + win2009 => 2009, + win10 => 2100, # for backward compatibility + wine => 3000, linux => 3001, mac => 3001, + bsd => 3002, solaris => 3003 +); + +sub cmpreports +{ + my $ra = $reports{$a}; + my $rb = $reports{$b}; + return $ra->{is_rerun} <=> $rb->{is_rerun} || + ($platform_order{$ra->{platform}} || 0) <=> ($platform_order{$rb->{platform}} || 0) || + $ra->{tag} cmp $rb->{tag} || + ($ra->{num} || 0) <=> ($rb->{num} || 0); +} + +my @sortedreports = sort cmpreports keys %reports; + + +# +# Analyze single-report patterns +# + +sub fail_type($) +{ + my ($status) = @_; + + return !$status ? "" : + # 'missing' is random but not 'missingdll', ... + ($status =~ /^(?:missing.|native|skipped|stub)/) ? $status : + # 'loaderror' and other failure types are random too + "random"; +} + +foreach my $testname (keys %tests) +{ + my $test = $tests{$testname}; + foreach my $reportdir (@sortedreports) + { + my $testreport = $test->{testreports}->{$reportdir}; + next if (!$testreport); + + # Record information about the failures for this report: + # - Type of failure: random or not (missing dll, etc.) + $testreport->{failtype} = ""; + + foreach my $build (@sortedbuilds) + { + my $status = $testreport->{status}->{$build->{name}}; + if (!defined $status) + { + if ($build->{hasreport}->{$reportdir} and + $build->{hastest}->{$testname}) + { + $testreport->{failtype} ||= 0; # success + } + # else WineTest was not run for this build + next; + } + next if ($status eq "skipped"); # same as if WineTest was not run + + # It is not an error if the dll is missing (or other similar + # error) _and_ it has always been so. + my $failtype = fail_type($status); + next if ($testreport->{failtype} eq "" and $failtype ne "random"); + + if ($testreport->{failtype} ne $failtype) + { + # Either there was no failure before; or the failure type + # changed in a way that cannot be explained by randomness; e.g. + # going from not being able to run the test because of a + # missing dll to the test running and crashing. + # Either way the relevant round of failures starts now. + $testreport->{failed} = 1; + last; + } + } + } +} + + +# +# Write the failure patterns page for the given set of reports +# + +my %status2html = ( + # Dll information status values + # <status> => [ <symbol>, <class-char>, <title>, <link> ] + "missing" => ["n", "n", "not run for an unknown reason", "report"], + "missingdll" => ["m", "m", "missing dll", "version"], + "missingentrypoint" => ["e", "e", "missing entry point", "version"], + "missingordinal" => ["o", "o", "missing ordinal", "version"], + "missingsxs" => ["v", "v", "missing side-by-side dll version", "version"], + "stub" => ["u", "u", "stub Windows dll", "version"], + "native" => ["N", "N", "native Windows dll", "version"], + "loaderror258" => ["I", "I", "timed out while getting the test list", "version"], + # Other status values + "skipped" => ["-", "s", "skipped by user request", ""], + "crash" => ["C", "C", "crash", "t"], + "258" => ["T", "T", "timeout", "t"], +); + +# Returns a tuple containing the symbol, CSS class, title and link type +# for the specified status. +sub get_status_html($) +{ + my ($status) = @_; + + return @{$status2html{$status}} if ($status2html{$status}); + + if ($status =~ /^[0-9]+$/) + { + return ("F", "F", "$status failures", "t"); + } + if ($status =~ /^loaderror(.*)$/) + { + return ("L", "L", "got error $1 while getting the test list", "version"); + } + return ("?", "", "unknown status $status", "report"); +} + +sub write_patterns_list($$) +{ + my ($html, $testnames) = @_; + + for my $i (0..@$testnames-1) + { + my $testname = $testnames->[$i]; + my $test = $tests{$testname}; + + print $html "<div class='testfile' id='$testname'>\n"; + print $html "<div class='updownbar'><a href='tests/$testname.html'>$testname</a>"; + print $html "<div class='ralign'>"; + + my $href = $i ? $testnames->[$i-1] : ""; + print $html "<a href='#$href'>↑</a>"; + $href = $i+1 < @$testnames ? $testnames->[$i+1] : undef; + print $html "<a href='#$href'>↓</a>" if (defined $href); + + print $html "</div></div>\n"; + + print $html "<div class='test'>\n"; + foreach my $reportdir (@sortedreports) + { + my $testreport = $test->{testreports}->{$reportdir}; + next if (!$testreport->{failed}); + print $html "<div class='pattern'>"; + + my ($range_symbol, $range_count) = ("", 0); + my ($range_start, $range_end, $range_title); + foreach my $build (@sortedbuilds) + { + my ($symbol, $class, $title); + my ($tag, $attrs) = ("span", ""); + my $status = $testreport->{status}->{$build->{name}}; + + if (!defined $status) + { + if (!$build->{hasreport}->{$reportdir}) + { + $symbol = "_"; + $class = "W"; + $title = "WineTest was not run"; + } + elsif ($build->{hastest}->{$testname}) + { + $symbol = "."; + $class = "S"; + $title = "success"; + } + else + { + $symbol = " "; + $class = "A"; + $title = "no such test in this build"; + } + } + else + { + ($symbol, $class, $title, my $link) = get_status_html($status); + if ($link eq "t") + { + $tag = "a"; + $attrs .= sprintf " href='%s/%s/%s.html'", + $build->{name}, $reportdir, $testname; + } + elsif ($link) + { + $tag = "a"; + my $dll = $testname; + $dll =~ s/:.*//; + $attrs .= sprintf " href='%s/%s/%s.html#%s'", + $build->{name}, $reportdir, $link, $dll; + } + } + + if ($range_symbol eq $symbol) + { + $range_end = $build->{date}; + $range_count++; + } + else + { + # Close the previous range of patterns + if ($range_count) + { + printf $html " title='%s : %s'>%s</span>", + date_range($range_start, $range_end), + $range_title, $range_symbol x $range_count; + $range_symbol = $range_end = ""; + $range_count = 0; + } + + # Start a new pattern range + $class = " class='pat$class'" if ($class); + print $html "<$tag$class$attrs"; + if ($tag eq "a") + { + printf $html " title='%s : %s'>%s</a>", + short_date($build->{date}), $title, $symbol; + } + else + { + $range_symbol = $symbol; + $range_start = $build->{date}; + $range_title = $title; + $range_count = 1; + } + } + } + if ($range_count) + { + printf $html " title='%s : %s'>%s</span>", + date_range($range_start, $range_end), + $range_title, $range_symbol x $range_count; + } + print $html "</div> $reportdir\n"; + } + print $html "</div></div>\n"; + } +} + +sub write_patterns_page($) +{ + my ($title) = (@_); + + my $filename = "data/patterns.html"; + open(my $html, ">", "$filename.new") or die "could not open '$filename.new' for writing: $!"; + + print $html <<"EOF"; +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" + "http://www.w3.org/TR/html4/strict.dtd"> +<html> +<head> + <title>$title</title> + <link rel="stylesheet" href="/report.css" type="text/css"> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> +</head> +<body> +<div class="navbar"><a href="..">index</a></div> +<div class="main"> +EOF + + # Build a list of test units that will appear on this page so we can + # link them to each other. + my $testnames = []; + unit: foreach my $testname (sort keys %tests) + { + my $test = $tests{$testname}; + foreach my $testreport (values %{$test->{testreports}}) + { + if ($testreport->{failed}) + { + push @$testnames, $testname; + next unit; + } + } + } + + print $html "<h2>$title</h2>\n"; + write_patterns_list($html, $testnames); + print $html "</div></body></html>\n"; + close($html); + + if (!rename "$filename.new", "$filename") + { + error("could not move '$filename.new' into place: $!\n"); + unlink "$filename.new"; + } +} + +write_patterns_page("Test failure patterns"); + +exit 0; diff --git a/winetest/gather b/winetest/gather index fa5525b88..4698ac3b3 100755 --- a/winetest/gather +++ b/winetest/gather @@ -291,6 +291,7 @@ Actions:
Generated files: $workdir/data/BUILD/summary.txt + $workdir/data/BUILD/testresults.txt $workdir/data/BUILD/total.txt $workdir/data/BUILD/index.html $workdir/data/BUILD/index_GROUP.html @@ -962,6 +963,68 @@ sub write_totals($) }
+# +# Write the data/BUILD/testresults.txt file +# This provides the statistics for the failure patterns pages. +# + +sub write_testresults($) +{ + my ($groups)=@_; + + my $fh; + my $filename = "$builddir/testresults.txt"; + if (!open($fh, ">", "$filename.new")) + { + error("could not open '$filename.new' for writing: $!\n"); + return; + } + print $fh "*"; + foreach my $group (@$groups) + { + my $reports = exists $group->{reports} ? $group->{reports} : + exists $group->{name} ? [] : # empty group + [$group]; # $group is in fact a lone report + map { print $fh " $_->{dir}" } @$reports; + } + print $fh "\n"; + + foreach my $testname (sort keys %alltests) { + print $fh "$testname $alltests{$testname}"; + my %statreps; + foreach my $group (@$groups) + { + my $reports = exists $group->{reports} ? $group->{reports} : + exists $group->{name} ? [] : # empty group + [$group]; # $group is in fact a lone report + foreach my $report (@$reports) + { + my $status = $report->{$testname}->{status} eq "run" ? + $report->{$testname}->{errors}->[1] : + $report->{$testname}->{status}; + + # Record all the non successful test unit results, including + # if it did not run ($status eq "missing"). So any report + # present on the first line and missing for this test unit + # ran the test successfully. + push @{$statreps{$status}}, $report->{dir} if ($status ne "0"); + } + } + foreach my $status (sort keys %statreps) + { + print $fh " ", join(":", $status, @{$statreps{$status}}); + } + print $fh "\n"; + } + close($fh); + if (!rename "$filename.new", "$filename") + { + error("could not move '$filename.new' into place: $!\n"); + unlink "$filename.new"; + } +} + + # # Actually generate the build's files # @@ -990,6 +1053,8 @@ if (!rename "$filename.new", "$filename")
write_totals(@groups);
+write_testresults(@groups); + DONE: if (!unlink "$builddir/outdated" and !$!{ENOENT}) { diff --git a/winetest/report.css b/winetest/report.css index 41b0b049a..cca1ba986 100644 --- a/winetest/report.css +++ b/winetest/report.css @@ -67,3 +67,49 @@ td.arrow :hover { color: #ffffff; text-decoration: underline; } display: inline-block; float: right; } + +.pattern { + display: inline-block; + font-family: monospace; +} +div.pattern :link { color: black; text-decoration: none; } +div.pattern :visited { color: black; text-decoration: none; } +div.pattern :hover { color: black; text-decoration: underline; } + +.patA { /* no such test in this build */ + background-color: lightgrey; +} +.patW { /* WineTest was not run */ + background-color: lightgrey; +} +/* .patS success */ +.patT { /* timeout */ + background-color: #ff55ff; +} +.patC { /* crash */ + background-color: #ff5555; +} +.patF { /* failure(s) */ + background-color: #ff0000; +} +/* .patn not run for an unknown reason */ +/* .patm missing dll */ +.pate { /* missing entrypoint */ + background-color: #ffe6ff; +} +.pato { /* missing ordinal */ + background-color: #ffe6e6; +} +.patv { /* missing side-by-side dll version */ + background-color: #e6ffe6; +} +/* .patu stub Windows dll */ +.patN { /* native Windows dll */ + background-color: #cc80ff; +} +.patI { /* timed out while getting the test list */ + background-color: #3399ff; +} +.patL { /* error while getting the test list */ + background-color: #80aaff; +} diff --git a/winetest/winetest.cron b/winetest/winetest.cron index 6806f6ef1..8019fa40c 100755 --- a/winetest/winetest.cron +++ b/winetest/winetest.cron @@ -70,6 +70,7 @@ then refresh_index=1 refresh_errors=1 fi + [ -n "$refresh_index" ] && "$tools/build-patterns" [ -n "$refresh_index" ] && "$tools/build-index" [ -n "$refresh_errors" ] && "$tools/build-errors"