blob: b48253b65bdfded1db6c6fb00bd28e88187c8dc3 [file] [log] [blame]
// -*- mode: C++; indent-tabs-mode: nil; c-basic-offset: 4 -*-
/*
* This brick allows you to build a test runner for shell-based functional
* tests. It comes with fairly elaborate features (although most are only
* available on posix systems), geared toward difficult-to-test software.
*
* It provides a full-featured "main" function (brick::shelltest::run) that you
* can use as a drop-in shell test runner.
*
* Features include:
* - interactive and batch-mode execution
* - collects test results and test logs in a simple text-based format
* - measures resource use of individual tests
* - rugged: suited for running in monitored virtual machines
* - supports test flavouring
*/
/*
* (c) 2014 Petr Rockai <me@mornfall.net>
* (c) 2014 Red Hat, Inc.
*/
/* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE. */
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <errno.h>
#include <vector>
#include <map>
#include <deque>
#include <string>
#include <iostream>
#include <iomanip>
#include <fstream>
#include <sstream>
#include <cassert>
#include <iterator>
#include <algorithm>
#include <stdexcept>
#include <dirent.h>
#ifdef __unix
#include <sys/stat.h>
#include <sys/resource.h> /* rusage */
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/klog.h>
#include <time.h>
#include <unistd.h>
#endif
#include "configure.h"
/* Timeout for the whole test suite in hours */
static const time_t TEST_SUITE_TIMEOUT = 4;
#ifndef BRICK_SHELLTEST_H
#define BRICK_SHELLTEST_H
namespace brick {
namespace shelltest {
/* TODO: remove this section in favour of brick-filesystem.h */
inline std::runtime_error syserr( std::string msg, std::string ctx = "" ) {
return std::runtime_error( std::string( strerror( errno ) ) + " " + msg + " " + ctx );
}
struct dir {
DIR *d;
dir( std::string p ) {
d = opendir( p.c_str() );
if ( !d )
throw syserr( "error opening directory", p );
}
~dir() { closedir( d ); }
};
typedef std::vector< std::string > Listing;
inline void fsync_name( std::string n )
{
int fd = open( n.c_str(), O_WRONLY );
if ( fd >= 0 ) {
fsync( fd );
close( fd );
}
}
inline Listing listdir( std::string p, bool recurse = false, std::string prefix = "" )
{
Listing r;
dir d( p );
#if !defined(__GLIBC__) || (__GLIBC__ < 2) || ((__GLIBC__ == 2) && (__GLIBC_MINOR__ < 23))
/* readdir_r is deprecated with newer GLIBC */
struct dirent entry, *iter = 0;
while ( (errno = readdir_r( d.d, &entry, &iter )) == 0 && iter ) {
std::string ename( entry.d_name );
#else
struct dirent *entry;
errno = 0;
while ( (entry = readdir( d.d )) ) {
std::string ename( entry->d_name );
#endif
if ( ename == "." || ename == ".." )
continue;
if ( recurse ) {
struct stat64 stat;
std::string s = p + "/" + ename;
if ( ::stat64( s.c_str(), &stat ) == -1 ) {
errno = 0;
continue;
}
if ( S_ISDIR(stat.st_mode) ) {
Listing sl = listdir( s, true, prefix + ename + "/" );
for ( Listing::iterator i = sl.begin(); i != sl.end(); ++i )
r.push_back( prefix + *i );
} else
r.push_back( prefix + ename );
} else
r.push_back( ename );
};
if ( errno != 0 )
throw syserr( "error reading directory", p );
return r;
}
/* END remove this section */
struct Journal {
enum R {
STARTED,
RETRIED,
UNKNOWN,
FAILED,
INTERRUPTED,
KNOWNFAIL,
PASSED,
SKIPPED,
TIMEOUT,
WARNED,
};
friend std::ostream &operator<<( std::ostream &o, R r ) {
switch ( r ) {
case STARTED: return o << "started";
case RETRIED: return o << "retried";
case FAILED: return o << "failed";
case INTERRUPTED: return o << "interrupted";
case PASSED: return o << "passed";
case SKIPPED: return o << "skipped";
case TIMEOUT: return o << "timeout";
case WARNED: return o << "warnings";
default: return o << "unknown";
}
}
friend std::istream &operator>>( std::istream &i, R &r ) {
std::string x;
i >> x;
r = UNKNOWN;
if ( x == "started" ) r = STARTED;
if ( x == "retried" ) r = RETRIED;
if ( x == "failed" ) r = FAILED;
if ( x == "interrupted" ) r = INTERRUPTED;
if ( x == "passed" ) r = PASSED;
if ( x == "skipped" ) r = SKIPPED;
if ( x == "timeout" ) r = TIMEOUT;
if ( x == "warnings" ) r = WARNED;
return i;
}
template< typename S, typename T >
friend std::istream &operator>>( std::istream &i, std::pair< S, T > &r ) {
return i >> r.first >> r.second;
}
typedef std::map< std::string, R > Status;
Status status, written;
std::string location, list;
int timeouts;
void append( std::string path ) {
std::ofstream of( path.c_str(), std::fstream::app );
Status::iterator writ;
for ( Status::iterator i = status.begin(); i != status.end(); ++i ) {
writ = written.find( i->first );
if ( writ == written.end() || writ->second != i->second )
of << i->first << " " << i->second << std::endl;
}
written = status;
of.close();
}
void write( std::string path ) {
std::ofstream of( path.c_str() );
for ( Status::iterator i = status.begin(); i != status.end(); ++i )
of << i->first << " " << i->second << std::endl;
of.close();
}
void sync() {
append( location );
fsync_name( location );
write ( list );
fsync_name( list );
}
void started( std::string n ) {
if ( status.count( n ) && status[ n ] == STARTED )
status[ n ] = RETRIED;
else
status[ n ] = STARTED;
sync();
}
void done( std::string n, R r ) {
status[ n ] = r;
if ( r == TIMEOUT )
++ timeouts;
else
timeouts = 0;
sync();
}
bool done( std::string n ) {
if ( !status.count( n ) )
return false;
return status[ n ] != STARTED && status[ n ] != INTERRUPTED;
}
int count( R r ) {
int c = 0;
for ( Status::iterator i = status.begin(); i != status.end(); ++i )
if ( i->second == r )
++ c;
return c;
}
void banner() {
std::cout << std::endl << "### " << status.size() << " tests: "
<< count( PASSED ) << " passed, "
<< count( SKIPPED ) << " skipped, "
<< count( TIMEOUT ) << " timed out, " << count( WARNED ) << " warned, "
<< count( FAILED ) << " failed" << std::endl;
}
void details() {
for ( Status::iterator i = status.begin(); i != status.end(); ++i )
if ( i->second != PASSED )
std::cout << i->second << ": " << i->first << std::endl;
}
void read( std::string n ) {
std::ifstream ifs( n.c_str() );
typedef std::istream_iterator< std::pair< std::string, R > > It;
for ( It i( ifs ); i != It(); ++i )
status[ i->first ] = i->second;
}
void read() { read( location ); }
Journal( std::string dir )
: location( dir + "/journal" ),
list( dir + "/list" ),
timeouts( 0 )
{}
};
struct TimedBuffer {
typedef std::pair< time_t, std::string > Line;
std::deque< Line > data;
Line incomplete;
bool stamp;
Line shift( bool force = false ) {
Line result = std::make_pair( 0, "" );
if ( force && data.empty() )
std::swap( result, incomplete );
else {
result = data.front();
data.pop_front();
}
return result;
}
void push( std::string buf ) {
time_t now = stamp ? time( 0 ) : 0;
std::string::iterator b = buf.begin(), e = buf.begin();
while ( e != buf.end() )
{
e = std::find( b, buf.end(), '\n' );
incomplete.second += std::string( b, e );
if ( !incomplete.first )
incomplete.first = now;
if ( e != buf.end() ) {
incomplete.second += "\n";
data.push_back( incomplete );
if (incomplete.second[0] == '#') {
/* Disable timing between '## 0 STACKTRACE' & '## teardown' keywords */
if (incomplete.second.find("# 0 STACKTRACE", 1) != std::string::npos ||
incomplete.second.find("# timing off", 1) != std::string::npos) {
stamp = false;
now = 0;
} else if (incomplete.second.find("# teardown", 1) != std::string::npos ||
incomplete.second.find("# timing on", 1) != std::string::npos) {
stamp = true;
now = time( 0 );
}
}
incomplete = std::make_pair( now, "" );
}
b = (e == buf.end() ? e : e + 1);
}
}
bool empty( bool force = false ) {
if ( force && !incomplete.second.empty() )
return false;
return data.empty();
}
TimedBuffer() : stamp(true) {}
};
struct Sink {
virtual void outline( bool ) {}
virtual void push( std::string x ) = 0;
virtual void sync( bool ) {}
virtual ~Sink() {}
};
struct Substitute {
typedef std::map< std::string, std::string > Map;
std::string testdir; // replace testdir first
std::string prefix;
std::string map( std::string line ) {
if ( std::string( line, 0, 9 ) == "@TESTDIR=" )
testdir = std::string( line, 9, line.length() - 10 ); // skip \n
else if ( std::string( line, 0, 8 ) == "@PREFIX=" )
prefix = std::string( line, 8, line.length() - 9 ); // skip \n
else {
size_t off;
if (!testdir.empty())
while ( (off = line.find( testdir )) != std::string::npos )
line.replace( off, testdir.length(), "@TESTDIR@" );
if (!prefix.empty())
while ( (off = line.find( prefix )) != std::string::npos )
line.replace( off, prefix.length(), "@PREFIX@" );
}
return line;
}
};
struct Format {
time_t start;
Substitute subst;
std::string format( TimedBuffer::Line l ) {
std::stringstream result;
if ( l.first >= start ) {
time_t rel = l.first - start;
result << "[" << std::setw( 2 ) << std::setfill( ' ' ) << rel / 60
<< ":" << std::setw( 2 ) << std::setfill( '0' ) << rel % 60 << "] ";
}
result << subst.map( l.second );
return result.str();
}
Format() : start( time( 0 ) ) {}
};
struct BufSink : Sink {
TimedBuffer data;
Format fmt;
virtual void push( std::string x ) {
data.push( x );
}
void dump( std::ostream &o ) {
o << std::endl;
while ( !data.empty( true ) )
o << "| " << fmt.format( data.shift( true ) );
}
};
struct FdSink : Sink {
int fd;
TimedBuffer stream;
Format fmt;
bool killed;
virtual void outline( bool force )
{
TimedBuffer::Line line = stream.shift( force );
std::string out = fmt.format( line );
write( fd, out.c_str(), out.length() );
}
virtual void sync( bool force ) {
if ( killed )
return;
while ( !stream.empty( force ) )
outline( force );
}
virtual void push( std::string x ) {
if ( !killed )
stream.push( x );
}
FdSink( int _fd ) : fd( _fd ), killed( false ) {}
};
struct FileSink : FdSink {
std::string file;
FileSink( std::string n ) : FdSink( -1 ), file( n ) {}
void sync( bool force ) {
if ( fd < 0 && !killed ) {
#ifdef O_CLOEXEC
fd = open( file.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644 );
#else
fd = open( file.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644 );
if ( fcntl( fd, F_SETFD, FD_CLOEXEC ) < 0 )
perror("failed to set FD_CLOEXEC on file");
#endif
if ( fd < 0 )
killed = true;
}
FdSink::sync( force );
}
~FileSink() {
if ( fd >= 0 ) {
fsync( fd );
close( fd );
}
}
};
#define BRICK_SYSLOG_ACTION_READ_CLEAR 4
#define BRICK_SYSLOG_ACTION_CLEAR 5
struct Source {
int fd;
virtual void sync( Sink *sink ) {
ssize_t sz;
char buf[ 128 * 1024 ];
if ( (sz = read(fd, buf, sizeof(buf) - 1)) > 0 )
sink->push( std::string( buf, sz ) );
/*
* On RHEL5 box this code busy-loops here, while
* parent process no longer writes anything.
*
* Unclear why 'select()' is anouncing available
* data, while we read 0 bytes with errno == 0.
*
* Temporarily resolved with usleep() instead of loop.
*/
if (!sz && (!errno || errno == EINTR))
usleep(50000);
if ( sz < 0 && errno != EAGAIN )
throw syserr( "reading pipe" );
}
virtual void reset() {}
virtual int fd_set_( fd_set *set ) {
if ( fd >= 0 ) {
FD_SET( fd, set );
return fd;
} else
return -1;
}
Source( int _fd = -1 ) : fd( _fd ) {}
virtual ~Source() {
if ( fd >= 0 )
::close( fd );
}
};
struct FileSource : Source {
std::string file;
FileSource( std::string n ) : Source( -1 ), file( n ) {}
int fd_set_( ::fd_set * ) { return -1; } /* reading a file is always non-blocking */
void sync( Sink *s ) {
if ( fd < 0 ) {
#ifdef O_CLOEXEC
fd = open( file.c_str(), O_RDONLY | O_CLOEXEC | O_NONBLOCK );
#else
fd = open( file.c_str(), O_RDONLY | O_NONBLOCK );
if ( fcntl( fd, F_SETFD, FD_CLOEXEC ) < 0 )
perror("failed to set FD_CLOEXEC on file");
#endif
if ( fd >= 0 )
lseek( fd, 0, SEEK_END );
}
if ( fd >= 0 )
Source::sync( s );
}
};
struct KMsg : Source {
bool can_clear;
KMsg() : can_clear( strcmp(getenv("LVM_TEST_CAN_CLOBBER_DMESG") ? : "0", "0") ) {
#ifdef __unix
if ( (fd = open("/dev/kmsg", O_RDONLY | O_NONBLOCK)) < 0 ) {
if (errno != ENOENT) /* Older kernels (<3.5) do not support /dev/kmsg */
perror("opening /dev/kmsg");
if ( klogctl( BRICK_SYSLOG_ACTION_CLEAR, 0, 0 ) < 0 )
can_clear = false;
} else if (lseek(fd, 0L, SEEK_END) == (off_t) -1)
perror("lseek /dev/kmsg");
#endif
}
bool dev_kmsg() {
return fd >= 0;
}
void sync( Sink *s ) {
#ifdef __unix
ssize_t sz;
char buf[ 128 * 1024 ];
if ( dev_kmsg() ) {
while ( (sz = ::read(fd, buf, sizeof(buf) - 1)) > 0 )
s->push( std::string( buf, sz ) );
} else if ( can_clear ) {
while ( (sz = klogctl( BRICK_SYSLOG_ACTION_READ_CLEAR, buf,
sizeof(buf) - 1 )) > 0 )
s->push( std::string( buf, sz ) );
if ( sz < 0 && errno == EPERM )
can_clear = false;
}
#endif
}
};
struct Observer : Sink {
TimedBuffer stream;
bool warnings;
Observer() : warnings( false ) {}
void push( std::string s ) {
stream.push( s );
}
void sync( bool force ) {
while ( !stream.empty( force ) ) {
TimedBuffer::Line line = stream.shift( force );
if ( line.second.find( "TEST WARNING" ) != std::string::npos )
warnings = true;
}
}
};
struct IO : Sink {
typedef std::vector< Sink* > Sinks;
typedef std::vector< Source* > Sources;
mutable Sinks sinks;
mutable Sources sources;
Observer *_observer;
virtual void push( std::string x ) {
for ( Sinks::iterator i = sinks.begin(); i != sinks.end(); ++i )
(*i)->push( x );
}
void sync( bool force ) {
for ( Sources::iterator i = sources.begin(); i != sources.end(); ++i )
(*i)->sync( this );
for ( Sinks::iterator i = sinks.begin(); i != sinks.end(); ++i )
(*i)->sync( force );
}
void close() {
for ( Sources::iterator i = sources.begin(); i != sources.end(); ++i )
delete *i;
sources.clear();
}
int fd_set_( fd_set *set ) {
int max = -1;
for ( Sources::iterator i = sources.begin(); i != sources.end(); ++i )
max = std::max( (*i)->fd_set_( set ), max );
return max + 1;
}
Observer &observer() { return *_observer; }
IO() {
clear();
}
/* a stealing copy constructor */
IO( const IO &io ) : sinks( io.sinks ), sources( io.sources ), _observer( io._observer )
{
io.sinks.clear();
io.sources.clear();
}
IO &operator= ( const IO &io ) {
this->~IO();
return *new (this) IO( io );
}
void clear( int to_push = 1 ) {
for ( Sinks::iterator i = sinks.begin(); i != sinks.end(); ++i )
delete *i;
sinks.clear();
if ( to_push )
sinks.push_back( _observer = new Observer );
}
~IO() { close(); clear(0); }
};
namespace {
pid_t kill_pid = 0;
bool fatal_signal = false;
bool interrupt = false;
}
struct Options {
bool verbose, batch, interactive, cont, fatal_timeouts, kmsg;
std::string testdir, outdir, workdir, heartbeat;
std::vector< std::string > flavours, filter, skip, watch;
std::string flavour_envvar;
int timeout;
Options() : verbose( false ), batch( false ), interactive( false ),
cont( false ), fatal_timeouts( false ), kmsg( true ),
timeout( 180 ) {}
};
struct TestProcess
{
std::string filename;
bool interactive;
int fd;
void exec() __attribute__ ((noreturn)) {
assert( fd >= 0 );
if ( !interactive ) {
int devnull = ::open( "/dev/null", O_RDONLY );
if ( devnull >= 0 ) { /* gcc really doesn't like to not have stdin */
dup2( devnull, STDIN_FILENO );
close( devnull );
} else
close( STDIN_FILENO );
dup2( fd, STDOUT_FILENO );
dup2( fd, STDERR_FILENO );
close( fd );
}
setpgid( 0, 0 );
execlp( "bash", "bash", "-noprofile", "-norc", filename.c_str(), NULL );
perror( "execlp" );
_exit( 202 );
}
TestProcess( std::string file )
: filename( file ), interactive( false ), fd( -1 )
{}
};
struct TestCase {
TestProcess child;
std::string name, flavour;
IO io;
BufSink *iobuf;
struct rusage usage;
int status;
bool timeout;
pid_t pid;
time_t start, end, silent_start, last_update, last_heartbeat;
Options options;
Journal *journal;
std::string pretty() {
if ( options.batch )
return flavour + ": " + name;
return "[" + flavour + "] " + name;
}
std::string id() {
return flavour + ":" + name;
}
void pipe() {
int fds[2];
if (socketpair( PF_UNIX, SOCK_STREAM, 0, fds )) {
perror("socketpair");
exit(201);
}
#if 0
if (fcntl( fds[0], F_SETFL, O_NONBLOCK ) == -1) {
perror("fcntl on socket");
exit(202);
}
#endif
io.sources.push_back( new Source( fds[0] ) );
child.fd = fds[1];
child.interactive = options.interactive;
}
bool monitor() {
end = time( 0 );
/* heartbeat */
if ( end - last_heartbeat >= 20 && !options.heartbeat.empty() ) {
std::ofstream hb( options.heartbeat.c_str(), std::fstream::app );
hb << ".";
hb.close();
fsync_name( options.heartbeat );
last_heartbeat = end;
}
if ( wait4(pid, &status, WNOHANG, &usage) != 0 ) {
io.sync( true );
return false;
}
/* kill off tests after a timeout silence */
if ( !options.interactive )
if ( end - silent_start > options.timeout ) {
kill( pid, SIGINT );
sleep( 5 ); /* wait a bit for a reaction */
if ( waitpid( pid, &status, WNOHANG ) == 0 ) {
system( "echo t > /proc/sysrq-trigger 2> /dev/null" );
kill( -pid, SIGKILL );
waitpid( pid, &status, 0 );
}
timeout = true;
io.sync( true );
return false;
}
struct timeval wait;
fd_set set;
FD_ZERO( &set );
int nfds = io.fd_set_( &set );
wait.tv_sec = 0;
wait.tv_usec = 500000; /* timeout 0.5s */
if ( !options.verbose && !options.interactive && !options.batch ) {
if ( end - last_update >= 1 ) {
progress( Update ) << tag( "running" ) << pretty() << " "
<< end - start << std::flush;
last_update = end;
}
}
if ( select( nfds, &set, NULL, NULL, &wait ) > 0 ) {
silent_start = end; /* something happened */
io.sync( false );
}
return true;
}
std::string timefmt( time_t t ) {
std::stringstream ss;
ss << t / 60 << ":" << std::setw( 2 ) << std::setfill( '0' ) << t % 60;
return ss.str();
}
std::string rusage()
{
std::stringstream ss;
time_t wall = end - start, user = usage.ru_utime.tv_sec,
system = usage.ru_stime.tv_sec;
size_t rss = usage.ru_maxrss / 1024,
inb = usage.ru_inblock / 100,
outb = usage.ru_oublock / 100;
size_t inb_10 = inb % 10, outb_10 = outb % 10;
inb /= 10; outb /= 10;
ss << timefmt( wall ) << " wall " << timefmt( user ) << " user "
<< timefmt( system ) << " sys " << std::setw( 3 ) << rss << "M RSS | "
<< "IOPS: " << std::setw( 5 ) << inb << "." << inb_10 << "K in "
<< std::setw( 5 ) << outb << "." << outb_10 << "K out";
return ss.str();
}
std::string tag( std::string n ) {
if ( options.batch )
return "## ";
size_t pad = n.length();
pad = (pad < 12) ? 12 - pad : 0;
return "### " + std::string( pad, ' ' ) + n + ": ";
}
std::string tag( Journal::R r ) {
std::stringstream s;
s << r;
return tag( s.str() );
}
enum P { First, Update, Last };
std::ostream &progress( P p = Last )
{
static struct : std::streambuf {} buf;
static std::ostream null(&buf);
if ( options.batch && p == First )
return std::cout;
if ( isatty( STDOUT_FILENO ) && !options.batch ) {
if ( p != First )
return std::cout << "\r";
return std::cout;
}
if ( p == Last )
return std::cout;
return null;
}
void parent()
{
::close( child.fd );
setupIO();
journal->started( id() );
silent_start = start = time( 0 );
progress( First ) << tag( "running" ) << pretty() << std::flush;
if ( options.verbose || options.interactive )
progress() << std::endl;
while ( monitor() )
/* empty */ ;
Journal::R r = Journal::UNKNOWN;
if ( timeout ) {
r = Journal::TIMEOUT;
} else if ( WIFEXITED( status ) ) {
if ( WEXITSTATUS( status ) == 0 )
r = Journal::PASSED;
else if ( WEXITSTATUS( status ) == 200 )
r = Journal::SKIPPED;
else
r = Journal::FAILED;
} else if ( interrupt && WIFSIGNALED( status ) && WTERMSIG( status ) == SIGINT )
r = Journal::INTERRUPTED;
else
r = Journal::FAILED;
if ( r == Journal::PASSED && io.observer().warnings )
r = Journal::WARNED;
io.close();
if ( iobuf && ( r == Journal::FAILED || r == Journal::TIMEOUT ) )
iobuf->dump( std::cout );
journal->done( id(), r );
if ( options.batch ) {
int spaces = std::max( 64 - int(pretty().length()), 0 );
progress( Last ) << " " << std::string( spaces, '.' ) << " "
<< std::left << std::setw( 9 ) << std::setfill( ' ' ) << r;
if ( r != Journal::SKIPPED )
progress( First ) << " " << rusage();
progress( Last ) << std::endl;
} else
progress( Last ) << tag( r ) << pretty() << std::endl;
io.clear();
}
void run() {
pipe();
pid = kill_pid = fork();
if (pid < 0) {
perror("Fork failed.");
exit(201);
} else if (pid == 0) {
io.close();
chdir( options.workdir.c_str() );
if ( !options.flavour_envvar.empty() )
setenv( options.flavour_envvar.c_str(), flavour.c_str(), 1 );
child.exec();
} else {
parent();
}
}
void setupIO() {
iobuf = 0;
if ( options.verbose || options.interactive )
io.sinks.push_back( new FdSink( 1 ) );
else if ( !options.batch )
io.sinks.push_back( iobuf = new BufSink() );
std::string n = id();
std::replace( n.begin(), n.end(), '/', '_' );
std::string fn = options.outdir + "/" + n + ".txt";
io.sinks.push_back( new FileSink( fn ) );
for ( std::vector< std::string >::iterator i = options.watch.begin();
i != options.watch.end(); ++i )
io.sources.push_back( new FileSource( *i ) );
if ( options.kmsg )
io.sources.push_back( new KMsg );
}
TestCase( Journal &j, Options opt, std::string path, std::string _name, std::string _flavour )
: child( path ), name( _name ), flavour( _flavour ), timeout( false ),
last_update( 0 ), last_heartbeat( 0 ), options( opt ), journal( &j )
{
}
};
struct Main {
bool die;
time_t start;
typedef std::vector< TestCase > Cases;
typedef std::vector< std::string > Flavours;
Journal journal;
Options options;
Cases cases;
void setup() {
bool filter;
Listing l = listdir( options.testdir, true );
std::sort( l.begin(), l.end() );
for ( Flavours::iterator flav = options.flavours.begin();
flav != options.flavours.end(); ++flav ) {
for ( Listing::iterator i = l.begin(); i != l.end(); ++i ) {
if ( ( i->length() < 3 ) || ( i->substr( i->length() - 3, i->length() ) != ".sh" ) )
continue;
if ( i->substr( 0, 4 ) == "lib/" )
continue;
if (!options.filter.empty()) {
filter = true;
for ( std::vector< std::string >::iterator filt = options.filter.begin();
filt != options.filter.end(); ++filt ) {
if ( i->find( *filt ) != std::string::npos ) {
filter = false;
break;
}
}
if ( filter )
continue;
}
if (!options.skip.empty()) {
filter = false;
for ( std::vector< std::string >::iterator filt = options.skip.begin();
filt != options.skip.end(); ++filt ) {
if ( i->find( *filt ) != std::string::npos ) {
filter = true;
break;
}
}
if ( filter )
continue;
}
cases.push_back( TestCase( journal, options, options.testdir + *i, *i, *flav ) );
cases.back().options = options;
}
}
if ( options.cont )
journal.read();
else
::unlink( journal.location.c_str() );
}
int run() {
setup();
start = time( 0 );
std::cerr << "running " << cases.size() << " tests" << std::endl;
for ( Cases::iterator i = cases.begin(); i != cases.end(); ++i ) {
if ( options.cont && journal.done( i->id() ) )
continue;
i->run();
if ( options.fatal_timeouts && journal.timeouts >= 2 ) {
journal.started( i->id() ); // retry the test on --continue
std::cerr << "E: Hit 2 timeouts in a row with --fatal-timeouts" << std::endl;
std::cerr << "Suspending (please restart the VM)." << std::endl;
sleep( 3600 );
die = 1;
}
if ( time(0) - start > (TEST_SUITE_TIMEOUT * 3600) ) {
std::cerr << TEST_SUITE_TIMEOUT << " hours passed, giving up..." << std::endl;
die = 1;
}
if ( die || fatal_signal )
break;
}
journal.banner();
if ( die || fatal_signal )
return 1;
return journal.count( Journal::FAILED ) || journal.count( Journal::TIMEOUT ) ? 1 : 0;
}
Main( Options o ) : die( false ), journal( o.outdir ), options( o ) {}
};
namespace {
void handler( int sig ) {
signal( sig, SIG_DFL ); /* die right away next time */
if ( kill_pid > 0 )
kill( -kill_pid, sig );
fatal_signal = true;
if ( sig == SIGINT )
interrupt = true;
}
void setup_handlers() {
/* set up signal handlers */
for ( int i = 0; i <= 32; ++i )
switch (i) {
case SIGCHLD: case SIGWINCH: case SIGURG:
case SIGKILL: case SIGSTOP: break;
default: signal(i, handler);
}
}
}
/* TODO remove in favour of brick-commandline.h */
struct Args {
typedef std::vector< std::string > V;
V args;
Args( int argc, const char **argv ) {
for ( int i = 1; i < argc; ++ i )
args.push_back( argv[ i ] );
}
bool has( std::string fl ) {
return std::find( args.begin(), args.end(), fl ) != args.end();
}
// TODO: This does not handle `--option=VALUE`:
std::string opt( std::string fl ) {
V::iterator i = std::find( args.begin(), args.end(), fl );
if ( i == args.end() || i + 1 == args.end() )
return "";
return *(i + 1);
}
};
namespace {
bool hasenv( const char *name ) {
const char *v = getenv( name );
if ( !v )
return false;
if ( strlen( v ) == 0 || !strcmp( v, "0" ) )
return false;
return true;
}
template< typename C >
void split( std::string s, C &c ) {
std::stringstream ss( s );
std::string item;
while ( std::getline( ss, item, ',' ) )
c.push_back( item );
}
}
const char *DEF_FLAVOURS="ndev-vanilla";
std::string resolve_path(std::string a_path, const char *default_path=".")
{
char temp[PATH_MAX];
const char *p;
p = a_path.empty() ? default_path : a_path.c_str();
if ( !realpath( p, temp ) )
throw syserr( "Failed to resolve path", p );
return temp;
}
static int run( int argc, const char **argv, std::string fl_envvar = "TEST_FLAVOUR" )
{
Args args( argc, argv );
Options opt;
if ( args.has( "--help" ) ) {
std::cout <<
" lvm2-testsuite - Run a lvm2 testsuite.\n\n"
"lvm2-testsuite"
"\n\t"
" [--flavours FLAVOURS]"
" [--only TESTS]"
"\n\t"
" [--outdir OUTDIR]"
" [--testdir TESTDIR]"
" [--workdir WORKDIR]"
"\n\t"
" [--batch|--verbose|--interactive]"
"\n\t"
" [--fatal-timeouts]"
" [--continue]"
" [--heartbeat]"
" [--watch WATCH]"
" [--timeout TIMEOUT]"
" [--nokmsg]\n\n"
/* TODO: list of flavours:
"lvm2-testsuite"
"\n\t"
" --list-flavours [--testdir TESTDIR]"
*/
"\n\n"
"OPTIONS:\n\n"
// TODO: looks like this could be worth a man page...
"Filters:\n"
" --flavours FLAVOURS\n\t\t- comma separated list of flavours to run.\n\t\t For the list of flavours see `$TESTDIR/lib/flavour-*`.\n\t\t Default: \"" << DEF_FLAVOURS << "\".\n"
" --only TESTS\t- comma separated list of tests to run. Default: All tests.\n"
"\n"
"Directories:\n"
" --testdir TESTDIR\n\t\t- directory where tests reside. Default: \"" TESTSUITE_DATA "\".\n"
" --workdir WORKDIR\n\t\t- directory to change to when running tests.\n\t\t This is directory containing testing libs. Default: TESTDIR.\n"
" --outdir OUTDIR\n\t\t- directory where all the output files should go. Default: \".\".\n"
"\n"
"Formatting:\n"
" --batch\t- Brief format for automated runs.\n"
" --verbose\t- More verbose format for automated runs displaying progress on stdout.\n"
" --interactive\t- Verbose format for interactive runs.\n"
"\n"
"Other:\n"
" --fatal-timeouts\n\t\t- exit after encountering 2 timeouts in a row.\n"
" --continue\t- If set append to journal. Otherwise it will be overwritten.\n"
" --heartbeat HEARTBEAT\n\t\t- Name of file to update periodicaly while running.\n"
" --watch WATCH\t- Comma separated list of files to watch and print.\n"
" --timeout TIMEOUT\n\t\t- Period of silence in seconds considered a timeout. Default: 180.\n"
" --nokmsg\t- Do not try to read kernel messages.\n"
"\n\n"
"ENV.VARIABLES:\n\n"
" T\t\t- see --only\n"
" INTERACTIVE\t- see --interactive\n"
" VERBOSE\t- see --verbose\n"
" BATCH\t\t- see --batch\n"
" LVM_TEST_CAN_CLOBBER_DMESG\n\t\t- when set and non-empty tests are allowed to flush\n\t\t kmsg in an attempt to read it."
"\n\n"
"FORMATS:\n\n"
"When multiple formats are specified interactive overrides verbose\n"
"which overrides batch. Command line options override environment\n"
"variables.\n\n"
;
return 0;
}
opt.flavour_envvar = fl_envvar;
if ( args.has( "--continue" ) )
opt.cont = true;
if ( args.has( "--only" ) )
split( args.opt( "--only" ), opt.filter );
else if ( hasenv( "T" ) )
split( getenv( "T" ), opt.filter );
if ( args.has( "--skip" ) )
split( args.opt( "--skip" ), opt.skip );
else if ( hasenv( "S" ) )
split( getenv( "S" ), opt.skip );
if ( args.has( "--fatal-timeouts" ) )
opt.fatal_timeouts = true;
if ( args.has( "--heartbeat" ) )
opt.heartbeat = args.opt( "--heartbeat" );
if ( args.has( "--batch" ) || args.has( "--verbose" ) || args.has( "--interactive" ) ) {
if ( args.has( "--batch" ) ) {
opt.verbose = false;
opt.batch = true;
}
if ( args.has( "--verbose" ) ) {
opt.batch = false;
opt.verbose = true;
}
if ( args.has( "--interactive" ) ) {
opt.verbose = false;
opt.batch = false;
opt.interactive = true;
}
} else {
if ( hasenv( "BATCH" ) ) {
opt.verbose = false;
opt.batch = true;
}
if ( hasenv( "VERBOSE" ) ) {
opt.batch = false;
opt.verbose = true;
}
if ( hasenv( "INTERACTIVE" ) ) {
opt.verbose = false;
opt.batch = false;
opt.interactive = true;
}
}
if ( args.has( "--flavours" ) )
split( args.opt( "--flavours" ), opt.flavours );
else
split( DEF_FLAVOURS, opt.flavours );
if ( args.has( "--watch" ) )
split( args.opt( "--watch" ), opt.watch );
if ( args.has( "--timeout" ) )
opt.timeout = atoi( args.opt( "--timeout" ).c_str() );
if ( args.has( "--nokmsg" ) )
opt.kmsg = false;
opt.testdir = resolve_path( args.opt( "--testdir" ), TESTSUITE_DATA ) + "/";
opt.workdir = resolve_path( args.opt( "--workdir" ), opt.testdir.c_str() );
opt.outdir = resolve_path( args.opt( "--outdir" ), "." );
setup_handlers();
Main main( opt );
return main.run();
}
}
}
#endif
#ifdef BRICK_DEMO
int main( int argc, const char **argv ) {
return brick::shelltest::run( argc, argv );
}
#endif
// vim: syntax=cpp tabstop=4 shiftwidth=4 expandtab