mirror of
git://sourceware.org/git/lvm2.git
synced 2024-12-21 13:34:40 +03:00
11dd362454
Keep the code compilatible without warnings on newer glibc.
1328 lines
38 KiB
C++
1328 lines
38 KiB
C++
// -*- 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
|