#include "TimeoutAqlTest.hpp" #include <chrono> #include <cstring> #include <exception> #include <fstream> #include <signal.h> #include <string.h> #include <sys/wait.h> #include <unistd.h> #include <ext/exception> #include <ext/random> #include "AqlTest.hpp" #include "common/ResultInterpret.h" #include "global/GlobalData.h" #include "parser/Parser.h" #include "readline/IstreamLineInterface.h" #include "readline/StringLineInterface.h" #define PIPE_RD 0 #define PIPE_WR 1 #define FD_STDOUT 1 #define FD_STDERR 2 /* Communication between signal handler and the rest of the program */ std::array < int, 2 > g_Wakeup; // pipe for wakeup bool g_RecvSignal; // signalled flag bool waitSignalTimeout ( const std::chrono::microseconds& duration ) { struct timeval tv; fd_set rd; auto duration_secs = std::chrono::duration_cast < std::chrono::seconds > ( duration ); auto duration_usecs = std::chrono::duration_cast < std::chrono::microseconds > ( duration - duration_secs ); tv.tv_sec = duration_secs.count ( ); tv.tv_usec = duration_usecs.count ( ); FD_ZERO ( &rd ); FD_SET ( g_Wakeup [ PIPE_RD ], &rd ); select ( g_Wakeup [ PIPE_RD ] + 1, &rd, nullptr, nullptr, &tv ); return g_RecvSignal; } std::string readFromFD ( int fd ) { static const size_t BUFSIZE = 64; std::string res; int rd; std::array < char, BUFSIZE > buf; while ( ( rd = read ( fd, buf.data ( ), buf.size ( ) - 1 ) ) > 0 ) { res.append ( buf.data ( ), rd ); } return res; } void writeToFD ( int fd, const std::string & message, const std::string & errorDesc ) { if ( write ( fd, message.c_str ( ), message.length ( ) ) != static_cast < ssize_t > ( message.length ( ) ) ) throw std::runtime_error ( "TimeoutAqlTest: write() to pipe failed (" + errorDesc + ")" ); } void newSigChild ( int ) { g_RecvSignal = true; // write into the pipe so select can read something, this effectively means that SIGCHILD was raised writeToFD ( g_Wakeup [ PIPE_WR ], " ", "wakeup signalling" ); } struct ChildStatus { int status; unsigned seed; std::string outAql, outStdout, outStderr; }; ChildStatus TimeoutAqlTestImpl ( const std::chrono::microseconds & timeout, std::istream& is ) { /* Register SIGCHLD handler */ struct sigaction act; memset ( &act, 0, sizeof ( act ) ); act . sa_handler = newSigChild; sigaction ( SIGCHLD, &act, nullptr ); std::array < int, 2 > pipeAqlOutput; /* parent-child communication ( aql output ) */ std::array < int, 2 > pipeStdout; /* parent-child communication ( child stdout ) */ std::array < int, 2 > pipeStderr; /* parent-child communication ( child stderr ) */ if ( pipe ( pipeAqlOutput.data ( ) ) != 0 ) throw std::runtime_error ( "TimeoutAqlTest: Failed to initialize pipe (aql output)" ); if ( pipe ( pipeStdout. data ( ) ) != 0 ) throw std::runtime_error ( "TimeoutAqlTest: Failed to initialize pipe (child stdout)" ); if ( pipe ( pipeStderr. data ( ) ) != 0 ) throw std::runtime_error ( "TimeoutAqlTest: Failed to initialize pipe (child stderr)" ); /* SIGCHLD was not yet raised, initialize communication pipe */ g_RecvSignal = false; if ( pipe ( g_Wakeup.data ( ) ) ) throw std::runtime_error ( "TimeoutAqlTest: Failed to initialize pipe (wakeup signalling)" ); /* random seed for aql */ unsigned seed = ext::random_devices::random ( ); /* do the forking */ pid_t pid = fork (); REQUIRE ( pid >= 0 ); if ( pid == 0 ) { /* child, run the test here */ act . sa_handler = SIG_DFL; sigaction ( SIGCHLD, &act, nullptr ); /* close unused ends of pipes in child */ close ( g_Wakeup [ PIPE_RD ] ); close ( g_Wakeup [ PIPE_WR ] ); close ( pipeAqlOutput [ PIPE_RD ] ); close ( pipeStdout [ PIPE_RD ] ); close ( pipeStderr [ PIPE_RD ] ); /* redirect stderr and stdout to pipe */ dup2 ( pipeStdout [ PIPE_WR ], FD_STDOUT ); dup2 ( pipeStderr [ PIPE_WR ], FD_STDERR ); /* run test */ AqlTestResult res = AqlTest ( is, seed ); writeToFD ( pipeAqlOutput [ PIPE_WR ], res.output, "writing cli output" ); exit ( res.retcode ); } /* close unused ends of pipes in parent */ close ( pipeAqlOutput [ PIPE_WR ] ); close ( pipeStdout [ PIPE_WR ] ); close ( pipeStderr [ PIPE_WR ] ); /* lets wait the specified time of microseconds, maybe the child will terminate on its own */ if ( ! waitSignalTimeout ( timeout ) ) { /* ... and in case it did not ... */ kill ( pid, SIGTERM ); while ( ! waitSignalTimeout ( 250ms ) ) /* 1/4 second */ kill ( pid, SIGKILL ); } /* child termination confirmed */ ChildStatus status; status.seed = seed; waitpid ( pid, &status.status, 0 ); close ( g_Wakeup [ PIPE_RD ] ); close ( g_Wakeup [ PIPE_WR ] ); /* read child outputs */ status.outAql = readFromFD ( pipeAqlOutput [ PIPE_RD ] ); status.outStdout = readFromFD ( pipeStdout [ PIPE_RD ] ); status.outStderr = readFromFD ( pipeStderr [ PIPE_RD ] ); /* communication is done */ close ( pipeAqlOutput [ PIPE_RD ] ); close ( pipeStdout [ PIPE_RD ] ); close ( pipeStderr [ PIPE_RD ] ); return status; } void processTestChildStatus ( const ChildStatus & status, const std::chrono::microseconds & timeout, bool timeoutError, const std::string & test ) { enum class ChildTerminationStatus { Signal, Timeout, OK, Fail, } terminationState = ChildTerminationStatus::OK; /* make compiler happy */ /* determine test result */ if ( WIFSIGNALED ( status.status ) && ( WTERMSIG ( status.status ) == SIGTERM || WTERMSIG ( status.status ) == SIGKILL ) ) terminationState = ChildTerminationStatus::Timeout; else if ( WIFSIGNALED ( status.status ) && WTERMSIG ( status.status ) != SIGTERM && WTERMSIG ( status.status ) != SIGKILL ) terminationState = ChildTerminationStatus::Signal; else if ( WIFEXITED ( status.status ) && WEXITSTATUS ( status.status ) == 0 ) terminationState = ChildTerminationStatus::OK; else if ( WIFEXITED ( status.status ) && WEXITSTATUS ( status.status ) != 0 ) terminationState = ChildTerminationStatus::Fail; else FAIL ( ); /* format message */ std::ostringstream oss; oss << "AqlTest: "; if ( terminationState == ChildTerminationStatus::Timeout ) { oss << "timeout (" << timeout.count ( ) << " us) reached"; } else if ( terminationState == ChildTerminationStatus::Signal ) { oss << "killed by signal " << WTERMSIG ( status.status ) << " (" << strsignal ( WTERMSIG ( status.status ) ) << ")"; } else if ( terminationState == ChildTerminationStatus::OK) { oss << "completed succesfully"; } else if ( terminationState == ChildTerminationStatus::Fail ) { oss << "completed unsuccessfully"; } else { FAIL ( ); } oss << "\n"; oss << test << "\n"; oss << "Seed was: " << status.seed << "\n"; oss << "Child aqlout was: >" << status.outAql << "<\n"; oss << "Child stdout was: >" << status.outStdout << "<\n"; oss << "Child stderr was: >" << status.outStderr << "<\n"; /* process result */ if ( terminationState == ChildTerminationStatus::Timeout ) { WARN ( oss.str ( ) ); if ( timeoutError ) { FAIL ( ); } else { CHECK_NOFAIL ( "Timeout reached" ); } } else if ( terminationState == ChildTerminationStatus::Signal ) { WARN ( oss.str ( ) ); FAIL ( ); } else if ( terminationState == ChildTerminationStatus::OK ) { // pass } else if ( terminationState == ChildTerminationStatus::Fail ) { WARN ( oss.str ( ) ); REQUIRE ( WEXITSTATUS ( status.status ) == 0 ); } else { FAIL ( ); } } std::string printTest ( const std::filesystem::path & file ) { return "Trying to execute testfile " + std::string ( file ) + "\n"; } std::string printTest ( const std::vector < std::string > & queries ) { std::ostringstream oss; oss << "Trying to execute queries:\n"; for ( const std::string & query : queries ) oss << " " << query << "\n"; return oss.str ( ); } void TimeoutAqlTestInt ( const std::chrono::microseconds & timeout, const std::filesystem::path & file, bool timeoutError ) { std::ifstream ifs ( file ); REQUIRE ( ifs.is_open ( ) ); auto testChildStatus = TimeoutAqlTestImpl ( timeout, ifs ); processTestChildStatus ( testChildStatus, timeout, timeoutError, printTest ( file ) ); } void TimeoutAqlTestInt ( const std::chrono::microseconds & timeout, const std::vector < std::string > & queries, bool timeoutError ) { std::stringstream ifs; for ( const auto & q : queries ) ifs << q << "\n"; auto testChildStatus = TimeoutAqlTestImpl ( timeout, ifs ); processTestChildStatus ( testChildStatus, timeout, timeoutError, printTest ( queries ) ); }