]> rtime.felk.cvut.cz Git - wvtest.git/commitdiff
Copy several helper scripts from NUL project repository
authorMichal Sojka <sojkam1@fel.cvut.cz>
Wed, 15 Aug 2012 13:56:23 +0000 (15:56 +0200)
committerMichal Sojka <sojkam1@fel.cvut.cz>
Wed, 15 Aug 2012 13:56:23 +0000 (15:56 +0200)
tools/wvformat [new file with mode: 0755]
tools/wvnulrun [new file with mode: 0755]
tools/wvperf2html.py [new file with mode: 0755]
tools/wvtest2html.py [new file with mode: 0755]
tools/wvwrap [new file with mode: 0755]

diff --git a/tools/wvformat b/tools/wvformat
new file mode 100755 (executable)
index 0000000..7b3d5d9
--- /dev/null
@@ -0,0 +1,174 @@
+#!/usr/bin/perl -w
+#
+# WvTest:
+#   Copyright (C)2007-2009 Versabanq Innovations Inc. and contributors.
+#       Licensed under the GNU Library General Public License, version 2.
+#       See the included file named LICENSE for license information.
+#
+use strict;
+use Getopt::Long;
+
+sub usage() {
+    print STDERR "Usage: $0 [-vs] <file>\n";
+    exit 127;
+}
+
+my ($verbose, $summary, $limit_lines);
+
+GetOptions (
+    "verbose|v"             => \$verbose,
+    "summary|s"                     => \$summary,
+    "before-failure|b=i"     => \$limit_lines,
+    ) or usage();
+
+my $istty = -t STDOUT && $ENV{'TERM'} ne "dumb";
+my $columns;
+if ($istty) {
+    $columns = `tput cols`;
+} else {
+    $columns = $ENV{'COLUMNS'} || 80;
+}
+
+my @log = ();
+my ($file, $sect);
+my ($gpasses, $gfails) = (0,0);
+my ($lpasses, $lfails) = (0,0);
+my ($tpasses, $tfails) = (0,0);
+
+sub colourize($$)
+{
+    my ($column, $result) = @_;
+    my $pass = ($result eq "ok");
+
+    my $begcolour = $istty ? ($pass ? "\e[32;1m" : "\e[31;1m") : "";
+    my $endcolour = $istty ? "\e[0m" : "";
+
+    my $dots = $columns - 15 - $column%$columns;
+    $dots += $columns if ($dots < 0);
+    my $leader = "."x$dots;
+    return "$begcolour$leader $result$endcolour";
+}
+
+sub highlight($)
+{
+    my $msg = shift;
+    my $begcolour = $istty ? "\e[1m\e[4m" : "";
+    my $endcolour = $istty ? "\e[0m" : "";
+    return "$begcolour$msg$endcolour";
+}
+
+sub resultline($$$)
+{
+    my ($prefix, $name, $result) = @_;
+    if (!defined $prefix) { $prefix = ''; }
+    return sprintf("$prefix! %s %s", $name, colourize(length($prefix)+2+length($name)+1, $result));
+}
+
+sub print_and_clear_log()
+{
+    print "v"x($columns-1) . "\n"; # Top marker
+    my @log2print = ();
+    my $skipped = 0;
+    foreach (@log) {
+       if (/^(\([0-9]+\) (#   )?)?!\s*(.*?)\s+(\S+)\s*$/) {
+           my ($prefix, $name, $result) = ($1, $3, $4);
+           push @log2print, resultline($prefix, $name, $result);
+           if ($result ne "ok") {
+               print "wvformat: skipped $skipped lines\n" if $skipped;
+               print join("\n", @log2print) . "\n";
+               @log2print = ();
+               $skipped = 0;
+           }
+       } else {
+           push @log2print, $_;
+       }
+       while (defined $limit_lines && scalar(@log2print) > $limit_lines) {
+           shift @log2print;
+           $skipped++;
+       }
+    }
+    print "wvformat: skipped $skipped lines\n" if $skipped;
+    print join("\n", @log2print) . "\n";
+    print "^"x($columns-1) . "\n"; # Bottom marker
+    @log = ();
+}
+
+sub print_test_summary()
+{
+    if (!$verbose) {
+       print(resultline("", sprintf("%s %s [%d/%d]", $file, $sect, $lpasses, $lpasses+$lfails),
+                        $lfails ? "FAILED" : "ok") . "\n");
+       print_and_clear_log() if ($lfails && !$summary);
+    }
+    if ($lfails) { $tfails++; }
+    else        { $tpasses++; }
+    ($lpasses, $lfails) = (0,0);
+}
+
+my $startup = 1;
+
+while (<>)
+{
+    chomp;
+    s/\r//g;
+
+    if (/^(\([0-9]+\) )?\s*Testing "(.*)" in (.*):\s*$/)
+    {
+       print_test_summary() unless $startup;
+       $startup = 0;
+       (undef, $sect, $file) = ($1, $2, $3);
+       @log = ();
+       if ($verbose) {
+           print highlight($_), "\n";
+       }
+    }
+    elsif (/^(\([0-9]+\) (#   )?)?!\s*(.*?)\s+(\S+)\s*$/)
+    {
+       my ($prefix, $name, $result) = ($1, $3, $4);
+
+       if ($startup) {
+           $file = "";
+           $sect = "Startup";
+           $startup = 0;
+       }
+
+       if ($verbose) {
+           print resultline($prefix, $name, $result) . "\n";
+       } else {
+           push @log, $_;
+       }
+
+       if ($result eq "ok") {
+           $gpasses++;
+           $lpasses++;
+       } else {
+           $gfails++;
+           $lfails++;
+       }
+    }
+    else
+    {
+       if ($verbose) {
+           print "$_\n";
+       } else {
+           push @log, $_;
+       }
+    }
+}
+if (!$verbose) {
+    if (!$startup) {
+       print_test_summary();
+    } else {
+       print_and_clear_log();
+    }
+}
+
+my $gtotal = $gpasses+$gfails;
+my $ttotal = $tpasses+$tfails;
+printf("\nSummary: %d test%s, %d failure%s".
+       "\n         %d check%s, %d failure%s\n",
+    $ttotal, $ttotal==1 ? "" : "s",
+    $tfails, $tfails==1 ? "" : "s",
+    $gtotal, $gtotal==1 ? "" : "s",
+    $gfails, $gfails==1 ? "" : "s");
+exit(0);
diff --git a/tools/wvnulrun b/tools/wvnulrun
new file mode 100755 (executable)
index 0000000..6ecc4e5
--- /dev/null
@@ -0,0 +1,215 @@
+#!/usr/bin/perl -w
+#
+# @file
+# Script to supervise the execution of wvtest-based tests.
+#
+# It takes care of killing test (qemu or serial reader) when the test
+# finishes or hangs.
+#
+# Copyright (C) 2011, 2012, Michal Sojka <sojka@os.inf.tu-dresden.de>
+# Economic rights: Technische Universitaet Dresden (Germany)
+#
+# This file is part of NUL (NOVA user land).
+#
+# NUL is free software: you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version
+# 2 as published by the Free Software Foundation.
+#
+# NUL 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
+# General Public License version 2 for more details.
+#/
+
+use strict;
+use IO::Pty;
+
+# always flush
+$| = 1;
+
+my $istty = -t STDOUT && $ENV{'TERM'} ne "dumb";
+my $pty = new IO::Pty;
+
+my $pid = fork();
+die "Cannot fork" if not defined $pid;
+if (!$pid) {
+    # child
+    $pty->make_slave_controlling_terminal();
+#    setpgrp(); # Terminal won't send signals to the child
+    my $slave = $pty->slave();
+    close $pty;
+    $slave->clone_winsize_from(\*STDIN) if $istty;
+    $slave->set_raw();
+
+    open(STDIN,"<&". $slave->fileno())
+      or die "Couldn't reopen STDIN for reading, $!\n";
+    open(STDOUT,">&". $slave->fileno())
+      or die "Couldn't reopen STDOUT for writing, $!\n";
+    open(STDERR,">&". $slave->fileno())
+      or die "Couldn't reopen STDERR for writing, $!\n";
+
+    close $slave;
+
+    exec(@ARGV);
+    die "Cannot exec(@ARGV): $!";
+}
+
+$pty->close_slave();
+#$pty->set_raw(); # from IO::Pty "try" script. Do we need this?
+
+sub winch {
+  $pty->slave->clone_winsize_from(\*STDIN);
+  kill WINCH => $pid if $pid;
+  $SIG{WINCH} = \&winch;
+}
+
+$SIG{WINCH} = \&winch if $istty;
+
+sub bigkill($)
+{
+    my $pid = shift;
+    ($pid > 0) || die("pid is '$pid'?!\n");
+    my $count;
+    local $SIG{CHLD} = sub { }; # this will wake us from sleep() faster
+    $count = kill -15, $pid;
+    sleep(2);
+
+    kill -9, $pid if ($pid > 1);
+
+    exit(125);
+}
+
+my $timeout = 100;
+
+# parent
+local $SIG{INT} = sub { bigkill($pid); };
+local $SIG{TERM} = sub { bigkill($pid); };
+local $SIG{ALRM} = sub {
+    print STDERR "! $0: Alarm timed out!  No test output for $timeout seconds.  FAILED\n";
+    bigkill($pid);
+};
+
+my $allstart = time();
+my ($start, $stop);
+my $tests_executed = 0;
+my $tests_failed = 0;
+my $waits_for_child = 0;
+my $kill_ok = 0;
+my $ignore_exit_patterns = 0;
+my $expected_test_count = 0;
+my $expected_test_base = 0;
+
+sub matches_exit_pattern($)
+{
+    return 0 if $ignore_exit_patterns;
+    if ($ENV{WVTEST_EXIT_PATTERN}) {
+       return /$ENV{WVTEST_EXIT_PATTERN}/
+    } else {
+       return
+           (/sc: done.$/ && $waits_for_child) ||
+           /resetting machine via method/ ||
+           /wvtest: done\s*$/ ||
+           / # .*System halted. *$/
+           ;
+    }
+}
+
+sub check_number_of_tests()
+{
+    if ($expected_test_count) {
+       my $executed = $tests_executed - $expected_test_base;
+       my $result;
+       if ($executed == $expected_test_count) {
+           $result = "ok";
+       } else {
+           $result = "FAILED";
+           $tests_failed++;
+           print "Expected $expected_test_count tests, executed $executed tests.\n"
+       }
+       print "! $0: tests_expected == tests_executed  $result\n";
+    }
+}
+
+my $wvtest_output;
+if ($ENV{WVTEST_OUTPUT}) {
+    open $wvtest_output, ">", $ENV{WVTEST_OUTPUT};
+}
+
+alarm($timeout);
+while (<$pty>)
+{
+    alarm($timeout);
+    print;
+    print $wvtest_output $_ if $wvtest_output;
+    chomp;
+    s/\r//g;
+
+    if (/^(\([0-9]+\) (#   )?)?!\s*(.*?)\s+(\S+)\s*$/) {
+       $tests_executed++;
+       $tests_failed++ if ($4 ne "ok");
+    }
+    elsif (/wvtest: timeout (\d+)\s*$/) {
+       $timeout=$1;
+       alarm($timeout);
+    }
+    elsif (/sc: wait for child/) { $waits_for_child = 1; }
+    elsif (/wvtest: ignore exit patterns/) { $ignore_exit_patterns = 1; }
+    elsif (/wvtest: expect ([0-9]+) tests/) {
+       check_number_of_tests();
+       $expected_test_count = $1;
+       $expected_test_base = $tests_executed;
+    }
+    elsif (matches_exit_pattern($_))
+    {
+       if (!$ENV{WVTEST_NOKILL}) {
+           kill 15, $pid;      # Kill novaboot or qemu
+           $kill_ok = 1;
+       } else {
+           use POSIX;
+           my $pid = fork();
+           if ($pid) {
+               print "Keeping PID $pid alive\n";
+               print $wvtest_output "Keeping PID $pid alive\n" if $wvtest_output;
+               close($wvtest_output) if $wvtest_output;
+               POSIX::_exit(0);
+           } else {
+               # Continue printing on background
+               close($wvtest_output) if $wvtest_output;
+               while (<$pty>) { print; }
+               POSIX::_exit(0);
+           }
+       }
+    }
+}
+my $newpid = waitpid($pid, 0);
+if ($newpid != $pid) {
+    die("waitpid returned '$newpid', expected '$pid'\n");
+}
+
+my $code = $?;
+my $ret = ($code >> 8);
+
+if ($code && !$ret) {
+    if ($kill_ok && $code == 15) {
+       # We have killed the child - it is OK
+       $code = 0;
+    } else {
+       # return death-from-signal exits as >128.  This is what bash does if you ran
+       # the program directly.
+       $ret = $code | 128;
+    }
+}
+
+if ($ret != 0) {
+    print "! $0: Program '", join(" ", @ARGV), "' returned non-zero exit code ($ret)  FAILED\n";
+}
+
+if (!$ENV{WVTEST_EXIT_PATTERN}) {
+    printf "! $0: \$tests_executed > 0  %s\n", ($tests_executed > 0) ? "ok" : "FAILED";
+}
+check_number_of_tests();
+
+if ($tests_failed > 0) { $ret = 1; }
+if ($ret == 0 && $tests_executed == 0) { $ret = 1; }
+
+exit $ret;
diff --git a/tools/wvperf2html.py b/tools/wvperf2html.py
new file mode 100755 (executable)
index 0000000..26fb433
--- /dev/null
@@ -0,0 +1,332 @@
+#!/usr/bin/env python
+
+import sys
+import re
+import os
+import os.path
+import string
+import time
+import numpy as np
+
+class Axis:
+    def __init__(self, name=None, units=None):
+        self.name = name
+        self.units = units
+        self.num = None
+    def getLabel(self):
+        if self.units and self.name:
+            return "%s [%s]" % (self.name, self.units)
+        elif self.units:
+            return self.units
+        else:
+            return self.name
+    def __repr__(self): return "Axis(name=%s units=%s, id=%s)" % (self.name, self.units, hex(id(self)))
+
+class Column:
+    def __init__(self, name, units, axis):
+        self.name = name
+        self.units = units
+        self.axis = axis
+    def __repr__(self): return "Column(name=%s units=%s axis=%s)" % (self.name, self.units, repr(self.axis))
+
+class Row(dict):
+    def __init__(self, graph, date):
+        self.graph = graph
+        self.date = date
+    def __getitem__(self, column):
+        try:
+            return dict.__getitem__(self, column)
+        except KeyError:
+            return None
+    def getDate(self):
+        d = time.gmtime(time.mktime(self.date))
+        return "Date.UTC(%s, %s, %s, %s, %s, %s)" % \
+            (d.tm_year, d.tm_mon-1, d.tm_mday, d.tm_hour, d.tm_min, d.tm_sec)
+
+class Graph:
+    def __init__(self, id, title):
+        self.columns = {}
+        self.columns_ordered = []
+        self.id = id
+        self.title = title
+        self.rows = []
+        self.date2row = {}
+        self.axes = {}
+        self.axes_ordered = []
+
+    def __getitem__(self, date):
+        try:
+            rownum = self.date2row[date]
+        except KeyError:
+            rownum = len(self.rows)
+            self.date2row[date] = rownum
+        try:
+            return self.rows[rownum]
+        except IndexError:
+            self.rows[rownum:rownum] = [Row(self, date)]
+            return self.rows[rownum]
+
+    def addValue(self, date, col, val, units):
+        row = self[date]
+        row[col] = val
+        if not self.columns.has_key(col):
+            axis=self.getAxis(units) or self.addAxis(units, Axis(units=units))
+            column = Column(col, units, axis)
+            self.columns[col] = column
+            self.columns_ordered.append(column)
+        else:
+            column = self.columns[col]
+            self.columns_ordered.remove(column)
+            self.columns_ordered.append(column)
+        self.columns[col].units=units
+        self.columns[col].axis.units=units
+
+    def addAxis(self, key, axis):
+        self.axes[key] = axis
+        return axis
+
+    def setAxis(self, col, key):
+        self.columns[col].axis = self.getAxis(key) or self.addAxis(key, Axis(name=key))
+
+    def getAxis(self, key):
+        if not self.axes.has_key(key): return None
+        return self.axes[key]
+
+    def findRanges(self):
+        for axis in self.axes.values():
+            cols = [col for col in self.columns.values() if col.axis == axis]
+            low = None
+            high = None
+            all_in_range = True
+            for col in cols:
+                values = np.array([row[col.name] for row in self.rows if row[col.name] != None], np.float64)
+                if low == None and high == None:
+                    lastmonth = values[-30:]
+                    median = np.median(lastmonth)
+                    low  = median * 0.95
+                    high = median * 1.05
+
+                if (values > high).any() or (values < low).any():
+                    all_in_range = False
+            if all_in_range:
+                axis.yrange_max = high
+                axis.yrange_min = low
+            else:
+                axis.yrange_max = None
+                axis.yrange_min = None
+
+    def fixupAxisNumbers(self):
+        # Sort axes according to the columns and number them
+        num = 0
+        for column in self.columns_ordered:
+            axis = column.axis
+            if axis not in self.axes_ordered:
+                self.axes_ordered.insert(0, axis)
+                axis.num = num
+                num += 1
+        num = 0
+        for axis in self.axes_ordered:
+            axis.num = num
+            num += 1
+
+    def jschart(self):
+        print """
+                       window.chart = new Highcharts.StockChart({
+                           chart: {
+                               renderTo: '"""+self.id+"""'
+                           },
+
+                           rangeSelector: {
+                               selected: 1
+                           },
+
+                           title: {
+                               text: '"""+self.title+"""'
+                           },
+                            legend: {
+                                enabled: true,
+                                floating: false,
+                                verticalAlign: "top",
+                                x: 100,
+                                y: 60,
+                            },
+                            tooltip: {
+                                formatter: function() {
+                                    var s = '<b>'+ Highcharts.dateFormat('%a, %d %b %Y %H:%M:%S', this.x) +'</b><br/>';
+                                    s += commitMap[this.x].msg;
+                                    $.each(this.points, function(i, point) {
+                                        s += '<br/><span style="color:'+ point.series.color+';">'+ point.series.name +'</span>: '+point.y;
+                                    });
+                                    return s;
+                                },
+                                style: {
+                                    whiteSpace: 'normal',
+                                    width: '400px',
+                                },
+                            },
+                           plotOptions: {
+                               series: {
+                                   events: {
+                                       click: function(event) {
+                                           var lastpoint = null;
+                                           for (var i in this.data) {
+                                             if (event.point == this.data[i]) {
+                                               if (i > 0) lastpoint = this.data[i-1];
+                                               break;
+                                             }
+                                           }
+                                           if (lastpoint)
+                                             window.location = "http://os.inf.tu-dresden.de/~jsteckli/cgi-bin/cgit.cgi/nul/log/?qt=range&q="+commitMap[lastpoint.x].hash+'..'+commitMap[event.point.x].hash;
+                                           else
+                                             window.location = "http://os.inf.tu-dresden.de/~jsteckli/cgi-bin/cgit.cgi/nul/log/?id="+commitMap[event.point.x].hash;
+                                       }
+                                   }
+                               }
+                           },
+                           yAxis: ["""
+       for axis in self.axes_ordered:
+            print "\t\t\t\t{"
+            print "\t\t\t\t\tlineWidth: 1,"
+            print "\t\t\t\t\tlabels: { align: 'right', x: -3 },"
+            print "\t\t\t\t\ttitle: { text: '%s' }," % axis.getLabel()
+            #print "\t\t\t\t\tplotBands: { from: %s, to: %s, color: '#eee' }," % (col.low, col.high)
+            if axis.yrange_min: print "\t\t\t\t\tmin: %s," % axis.yrange_min
+            if axis.yrange_max: print "\t\t\t\t\tmax: %s," % axis.yrange_max
+            print "\t\t\t\t},"
+        print """\t\t\t    ],
+
+                           series: ["""
+        num = 0
+       for col in self.columns_ordered:
+            print "\t\t\t\t{ name: '%s [%s]', yAxis: %d, data: [" % (col.name, col.units, col.axis.num)
+            num += 1
+            for row in self.rows:
+                val = row[col.name]
+                if val == None: val = "null"
+                print "\t\t\t\t\t[%s, %s], " % (row.getDate(), val)
+            print "\t\t\t\t]},"
+        print """\t\t\t    ],
+                       });"""
+
+class Graphs(dict):
+    pass
+
+graphs = Graphs()
+commits = {}
+
+re_date = re.compile('^Date: (.*)')
+re_testing = re.compile('^(\([0-9]+\) (#   )?)?\s*Testing "(.*)" in (.*):\s*$')
+re_commit = re.compile('.*(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}).*, commit: (.*)')
+re_commithash = re.compile('([0-9a-f]{7})(-dirty)? \(')
+re_check = re.compile('^(\([0-9]+\) (#   )?)?!\s*(.*?)\s+(\S+)\s*$')
+re_perf =  re.compile('^(\([0-9]+\) (#   )?)?!\s*(.*?)\s+PERF:\s*(.*?)\s+(\S+)\s*$')
+re_perfaxis = re.compile('axis="([^"]+)"')
+
+date = time.localtime(time.time())
+
+for line in sys.stdin.readlines():
+    line = line.rstrip()
+
+    match = re_date.match(line)
+    if (match):
+        date = time.strptime(match.group(1), "%a, %d %b %Y %H:%M:%S +0200")
+        continue
+
+    match = re_testing.match(line)
+    if match:
+        what = match.group(3)
+        where = match.group(4)
+
+        match = re_commit.match(what)
+        if match:
+            date = time.strptime(match.group(1), "%Y-%m-%d %H:%M:%S")
+            commit = match.group(2)
+            match = re_commithash.search(commit);
+            if match:
+                commithash = match.group(1)
+            else:
+                commithash = None
+            commits[date] = (commit, commithash)
+
+        (basename, ext) = os.path.splitext(os.path.basename(where))
+
+        if what != "all": title = what
+        else: title = basename
+        try:
+            graph = graphs[basename]
+        except KeyError:
+            graph = Graph(basename, title)
+            graphs[basename] = graph
+        continue
+
+    match = re_perf.match(line)
+    if match:
+        perfstr = match.group(4)
+        perf = perfstr.split()
+        col = perf[0]
+        try:
+            val = float(perf[1])
+        except ValueError:
+            val = None
+        try:
+            units = perf[2]
+            if '=' in units: units = None
+        except:
+            units = None
+        if match.group(5) != "ok":
+            val=None
+
+        graph.addValue(date, col, val, units)
+
+        match = re_perfaxis.search(perfstr)
+        if match:
+            graph.setAxis(col, match.group(1));
+
+graphs = [g for g in graphs.values() if len(g.columns)]
+graphs = sorted(graphs, key=lambda g: g.title.lower())
+
+for g in graphs:
+    g.findRanges()
+    g.fixupAxisNumbers()
+
+print """
+<!DOCTYPE HTML>
+<html>
+    <head>
+       <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+       <title>NUL Performance Plots</title>
+
+       <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js"></script>
+       <script type="text/javascript">
+               var commitMap = {"""
+for d in sorted(commits.iterkeys()):
+    v = commits[d];
+    print '\t\t\t%d: { msg: "%s", hash: "%s" },' % (1000*time.mktime(d), v[0].replace('"', '\\"'), str(v[1]).replace('"', '\\"'))
+print """\t\t};
+               $(function() {"""
+for graph in graphs:
+    graph.jschart()
+print """
+               });
+       </script>
+    </head>
+
+    <body>
+       <h1>NUL Performance Plots</h1>
+       <script type="text/javascript" src="js/highstock.js"></script>
+        <ul>
+"""
+for graph in graphs:
+    print "    <li><a href='#%s'>%s</a></li>" % (graph.title, graph.title)
+print "    </ul>"
+for graph in graphs:
+    print "    <h2><a name='%s'>%s</a></h2>" % (graph.title, graph.title)
+    print '    <div id="%s" style="height: 400px"></div>' % graph.id
+print """
+    </body>
+</html>
+"""
+
+# Local Variables:
+# compile-command: "cat nul-nightly/nul_*.log|./wvperfpreprocess.py|./wvperf2html.py > graphs.html"
+# End:
diff --git a/tools/wvtest2html.py b/tools/wvtest2html.py
new file mode 100755 (executable)
index 0000000..ff0deba
--- /dev/null
@@ -0,0 +1,186 @@
+#!/usr/bin/env python
+
+import sys
+import re
+import os
+import os.path
+import string
+import time
+import numpy as np
+import cgi
+
+re_date = re.compile('^Date: (.*)')
+re_testing = re.compile('^(\([0-9]+\) (#   )?)?\s*Testing "(.*)" in (.*):\s*$')
+re_commit = re.compile('.*(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}).*, commit: (.*)')
+re_commithash = re.compile('([0-9a-f]{7})(-dirty)? \(')
+re_check = re.compile('^(\([0-9]+\) (#   )?)?!\s*(.*?)\s+(\S+)\s*$')
+re_perf =  re.compile('^(\([0-9]+\) (#   )?)?!\s*(.*?)\s+PERF:\s*(.*?)\s+(\S+)\s*$')
+re_perfaxis = re.compile('axis="([^"]+)"')
+
+date = time.localtime(time.time())
+
+class Test:
+    def __init__(self, what, where):
+        self.what = what
+        self.where = where
+        self.output = []
+        self.status = 'ok'
+        self.check_count = 0
+        self.failures = 0
+        self.num = None
+
+    def add_line(self, line):
+        self.output.append(line)
+        match = re_check.match(line)
+        if match:
+            self.check_count += 1
+            result = match.group(4)
+            if result != "ok":
+                self.status = result
+                self.failures += 1
+    def title(self):
+        if self.what == "all":
+            title = self.where
+        else:
+            title = '%s (%s)' % (self.what, self.where)
+       return title
+
+    def printSummaryHtml(self, file):
+        if self.status == "ok": status_class="ok"
+        else: status_class = "failed"
+        file.write("<tr class='testheader status-%s'><td class='testnum'>%d.</td><td class='testname'><a href='test%d.html'>%s</a></td>"
+                  % (status_class, self.num, self.num, cgi.escape(self.title())))
+        file.write("<td>%s</td></tr>\n" % (cgi.escape(self.status)))
+
+    def printDetailHtml(self, file):
+       file.write("""\
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+<title>NUL Test Report</title>
+<link rel="stylesheet" href="wvtest.css" type="text/css" />
+</head>
+
+<body>
+<h1>NUL Test Report</h1>
+%s
+<h2>%d. %s</h2>
+<table class='output'>
+""" % (date_and_commit, self.num, cgi.escape(self.title())))
+        for line in self.output:
+            match = re_check.match(line)
+            if match:
+                result = match.group(4)
+                if result == "ok":
+                    status_class = "ok"
+                else:
+                    status_class = "failed"
+                linestatus = " status-%s" % status_class
+                resultstatus = " class='status-%s'" % status_class
+            else:
+                linestatus = ''
+                resultstatus = ''
+                result = ''
+
+            file.write("<tr><td class='outputline%s'>%s</td><td%s>%s</td></tr>\n" % \
+                (linestatus, cgi.escape(line), resultstatus, cgi.escape(result)))
+       file.write("</table></body></html>")
+
+tests = []
+test = None
+
+for line in sys.stdin.readlines():
+    line = line.rstrip()
+
+    match = re_date.match(line)
+    if (match):
+        date = time.strptime(match.group(1), "%a, %d %b %Y %H:%M:%S +0200")
+        continue
+
+    match = re_testing.match(line)
+    if match:
+        what = match.group(3)
+        where = match.group(4)
+
+        test = Test(what, where)
+        tests.append(test)
+
+        match = re_commit.match(what)
+        if match:
+            date = time.strptime(match.group(1), "%Y-%m-%d %H:%M:%S")
+            commit = match.group(2)
+            match = re_commithash.search(commit);
+            if match:
+                commithash = match.group(1)
+            else:
+                commithash = None
+        continue
+
+    if test: test.add_line(line)
+
+tests_nonempty = [t for t in tests if t.check_count > 0]
+num = 1
+for t in tests_nonempty:
+    t.num = num
+    num += 1
+
+try:
+    date_and_commit = (time.strftime("%a, %d %b %Y %H:%M:%S +0000", date) + " " + commit)
+except:
+    date_and_commit = time.strftime("%a, %d %b %Y %H:%M:%S %Z")
+    pass
+
+targetDir = sys.argv[1]
+if not os.path.isdir(targetDir):
+    os.mkdir(targetDir)
+
+wvtest_css = open(os.path.join(targetDir, "wvtest.css"), 'w')
+wvtest_css.write("""\
+table {
+  border: solid 1px black;
+  max-width: 100%%;
+}
+.status-ok { background: lightgreen; }
+.status-failed { background: red; }
+.testnum { text-align: right; }
+.outputrow { display: none; }
+.output { width: 100%%; }
+.outputline { white-space: pre-wrap; font-family: monospace; }
+.testheader { font-weight: bold; }
+""")
+wvtest_css.close()
+
+index_html = open(os.path.join(targetDir, "index.html"), 'w')
+
+index_html.write("""\
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+<title>NUL Test Report</title>
+<link rel="stylesheet" href="wvtest.css" type="text/css" />
+</head>
+
+<body>
+<h1>NUL Test Report</h1>
+%s
+<table>
+""" % date_and_commit)
+for test in tests_nonempty:
+    test.printSummaryHtml(index_html)
+index_html.write("""\
+</table>
+</body>
+</html>
+""")
+
+for test in tests_nonempty:
+    f = open(os.path.join(targetDir, "test%d.html" % test.num), 'w')
+    test.printDetailHtml(f)
+    f.close()
+
+
+# Local Variables:
+# compile-command: "cat $(ls nul-nightly/nul_*.log|tail -n 1)|./wvtest2html.py html"
+# End:
diff --git a/tools/wvwrap b/tools/wvwrap
new file mode 100755 (executable)
index 0000000..189481d
--- /dev/null
@@ -0,0 +1,43 @@
+#!/usr/bin/perl -w
+#
+# WvTest:
+#   Copyright (C) 2007-2009 Versabanq Innovations Inc. and contributors.
+#   Copyright (C) 2012 Michal Sojka <sojka@os.inf.tu-dresden.de>
+#       Licensed under the GNU Library General Public License, version 2.
+#       See the included file named LICENSE for license information.
+#
+use strict;
+use Getopt::Long;
+
+sub usage() {
+    print STDERR "Usage: $0 < wvtest.log\n";
+    exit 127;
+}
+
+usage() if (@ARGV > 0);
+
+my $istty = -t STDOUT && $ENV{'TERM'} ne "dumb";
+my $columns;
+if ($istty) {
+    $columns = `tput cols`;
+} else {
+    $columns = $ENV{'COLUMNS'} || 80;
+}
+
+$| = 1;
+
+while (<>)
+{
+    chomp;
+    s/\r//g;
+
+    if (/^(\([0-9]+\) (#   )?)?!\s*(.*?)\s+(\S+)\s*$/) {
+       my $line = $_;
+       do {
+           print substr($line, 0, $columns) . "\n";
+           $line = length($line) > $columns ? substr($line, $columns) : '';
+       } while (length($line));
+    } else {
+       print "$_\n";
+    }
+}