[tools 2/3] winetest/build-patterns: Add a page showing failure patterns

Francois Gouget fgouget at codeweavers.com
Tue Apr 27 05:45:07 CDT 2021


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 at 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"
 
-- 
2.20.1




More information about the wine-devel mailing list