2021-06-27 20:22:25 +00:00
/*
* Copyright ( c ) 2021 , Andrew Kaster < akaster @ serenityos . org >
*
* SPDX - License - Identifier : BSD - 2 - Clause
*/
# include <AK/LexicalPath.h>
# include <LibCore/ArgsParser.h>
# include <LibCore/ConfigFile.h>
# include <LibCore/File.h>
2022-03-28 19:55:25 +00:00
# include <LibCore/System.h>
2022-01-26 19:46:36 +00:00
# include <LibCoredump/Backtrace.h>
2022-03-28 19:55:25 +00:00
# include <LibMain/Main.h>
2021-06-27 20:22:25 +00:00
# include <LibRegex/Regex.h>
# include <LibTest/TestRunner.h>
# include <signal.h>
# include <spawn.h>
2022-01-05 10:17:45 +00:00
# include <stdlib.h>
2021-06-27 20:22:25 +00:00
# include <sys/wait.h>
# include <unistd.h>
namespace Test {
TestRunner * TestRunner : : s_the = nullptr ;
}
using Test : : get_time_in_ms ;
using Test : : print_modifiers ;
struct FileResult {
LexicalPath file_path ;
double time_taken { 0 } ;
Test : : Result result { Test : : Result : : Pass } ;
int stdout_err_fd { - 1 } ;
2022-01-26 19:46:36 +00:00
pid_t child_pid { 0 } ;
2021-06-27 20:22:25 +00:00
} ;
2022-12-04 18:02:33 +00:00
DeprecatedString g_currently_running_test ;
2021-06-27 20:22:25 +00:00
class TestRunner : public : : Test : : TestRunner {
public :
2022-12-04 18:02:33 +00:00
TestRunner ( DeprecatedString test_root , Regex < PosixExtended > exclude_regex , NonnullRefPtr < Core : : ConfigFile > config , Regex < PosixExtended > skip_regex , bool run_skipped_tests , bool print_progress , bool print_json , bool print_all_output , bool print_times = true )
2021-06-27 20:22:25 +00:00
: : : Test : : TestRunner ( move ( test_root ) , print_times , print_progress , print_json )
, m_exclude_regex ( move ( exclude_regex ) )
, m_config ( move ( config ) )
2021-07-12 19:52:17 +00:00
, m_skip_regex ( move ( skip_regex ) )
2022-02-03 17:36:27 +00:00
, m_run_skipped_tests ( run_skipped_tests )
2021-06-27 20:22:25 +00:00
, m_print_all_output ( print_all_output )
{
2022-02-03 17:36:27 +00:00
if ( ! run_skipped_tests ) {
m_skip_directories = m_config - > read_entry ( " Global " , " SkipDirectories " , " " ) . split ( ' ' ) ;
m_skip_files = m_config - > read_entry ( " Global " , " SkipTests " , " " ) . split ( ' ' ) ;
}
2021-06-27 20:22:25 +00:00
}
virtual ~ TestRunner ( ) = default ;
protected :
2022-12-04 18:02:33 +00:00
virtual void do_run_single_test ( DeprecatedString const & test_path , size_t current_text_index , size_t num_tests ) override ;
virtual Vector < DeprecatedString > get_test_paths ( ) const override ;
virtual Vector < DeprecatedString > const * get_failed_test_names ( ) const override { return & m_failed_test_names ; }
2021-06-27 20:22:25 +00:00
2022-12-04 18:02:33 +00:00
virtual FileResult run_test_file ( DeprecatedString const & test_path ) ;
2021-06-27 20:22:25 +00:00
2022-04-01 17:58:27 +00:00
bool should_skip_test ( LexicalPath const & test_path ) ;
2021-06-27 20:22:25 +00:00
Regex < PosixExtended > m_exclude_regex ;
NonnullRefPtr < Core : : ConfigFile > m_config ;
2022-12-04 18:02:33 +00:00
Vector < DeprecatedString > m_skip_directories ;
Vector < DeprecatedString > m_skip_files ;
Vector < DeprecatedString > m_failed_test_names ;
2021-07-12 19:52:17 +00:00
Regex < PosixExtended > m_skip_regex ;
2022-02-03 17:36:27 +00:00
bool m_run_skipped_tests { false } ;
2021-06-27 20:22:25 +00:00
bool m_print_all_output { false } ;
} ;
2022-12-04 18:02:33 +00:00
Vector < DeprecatedString > TestRunner : : get_test_paths ( ) const
2021-06-27 20:22:25 +00:00
{
2022-12-04 18:02:33 +00:00
Vector < DeprecatedString > paths ;
Test : : iterate_directory_recursively ( m_test_root , [ & ] ( DeprecatedString const & file_path ) {
2021-06-27 20:22:25 +00:00
if ( access ( file_path . characters ( ) , R_OK | X_OK ) ! = 0 )
return ;
auto result = m_exclude_regex . match ( file_path , PosixFlags : : Global ) ;
if ( ! result . success ) // must NOT match the regex to be a valid test file
paths . append ( file_path ) ;
} ) ;
quick_sort ( paths ) ;
return paths ;
}
2022-04-01 17:58:27 +00:00
bool TestRunner : : should_skip_test ( LexicalPath const & test_path )
2021-06-27 20:22:25 +00:00
{
2022-02-03 17:36:27 +00:00
if ( m_run_skipped_tests )
return false ;
2022-12-04 18:02:33 +00:00
for ( DeprecatedString const & dir : m_skip_directories ) {
2021-06-27 20:22:25 +00:00
if ( test_path . dirname ( ) . contains ( dir ) )
return true ;
}
2022-12-04 18:02:33 +00:00
for ( DeprecatedString const & file : m_skip_files ) {
2021-06-27 20:22:25 +00:00
if ( test_path . basename ( ) . contains ( file ) )
return true ;
}
2021-07-12 19:52:17 +00:00
auto result = m_skip_regex . match ( test_path . basename ( ) , PosixFlags : : Global ) ;
if ( result . success )
return true ;
2021-06-27 20:22:25 +00:00
return false ;
}
2022-12-04 18:02:33 +00:00
void TestRunner : : do_run_single_test ( DeprecatedString const & test_path , size_t current_test_index , size_t num_tests )
2021-06-27 20:22:25 +00:00
{
g_currently_running_test = test_path ;
2021-08-19 01:16:23 +00:00
auto test_relative_path = LexicalPath : : relative_path ( test_path , m_test_root ) ;
outln ( " START {} ({}/{}) " , test_relative_path , current_test_index , num_tests ) ;
fflush ( stdout ) ; // we really want to see the start text in case the test hangs
2021-06-27 20:22:25 +00:00
auto test_result = run_test_file ( test_path ) ;
switch ( test_result . result ) {
case Test : : Result : : Pass :
+ + m_counts . tests_passed ;
break ;
case Test : : Result : : Skip :
+ + m_counts . tests_skipped ;
break ;
case Test : : Result : : Fail :
+ + m_counts . tests_failed ;
break ;
case Test : : Result : : Crashed :
+ + m_counts . tests_failed ; // FIXME: tests_crashed
break ;
}
if ( test_result . result ! = Test : : Result : : Skip )
+ + m_counts . files_total ;
m_total_elapsed_time_in_ms + = test_result . time_taken ;
bool crashed_or_failed = test_result . result = = Test : : Result : : Fail | | test_result . result = = Test : : Result : : Crashed ;
bool print_stdout_stderr = crashed_or_failed | | m_print_all_output ;
if ( crashed_or_failed ) {
2021-07-17 22:29:51 +00:00
m_failed_test_names . append ( test_path ) ;
2021-07-05 18:01:41 +00:00
print_modifiers ( { Test : : BG_RED , Test : : FG_BLACK , Test : : FG_BOLD } ) ;
2021-06-27 20:22:25 +00:00
out ( " {} " , test_result . result = = Test : : Result : : Fail ? " FAIL " : " CRASHED " ) ;
2021-07-05 18:01:41 +00:00
print_modifiers ( { Test : : CLEAR } ) ;
2022-01-26 19:46:36 +00:00
if ( test_result . result = = Test : : Result : : Crashed ) {
2022-12-04 18:02:33 +00:00
auto pid_search_string = DeprecatedString : : formatted ( " _{}_ " , test_result . child_pid ) ;
2022-01-26 19:46:36 +00:00
Core : : DirIterator iterator ( " /tmp/coredump " sv ) ;
if ( ! iterator . has_error ( ) ) {
while ( iterator . has_next ( ) ) {
auto path = iterator . next_full_path ( ) ;
if ( ! path . contains ( pid_search_string ) )
continue ;
auto reader = Coredump : : Reader : : create ( path ) ;
if ( ! reader )
break ;
dbgln ( " Last crash backtrace for {} (was pid {}): " , test_path , test_result . child_pid ) ;
reader - > for_each_thread_info ( [ & ] ( auto thread_info ) {
Coredump : : Backtrace thread_backtrace ( * reader , thread_info ) ;
auto tid = thread_info . tid ; // Note: Yoinking this out of the struct because we can't pass a reference to it (as it's a misaligned field in a packed struct)
dbgln ( " Thread {} " , tid ) ;
for ( auto const & entry : thread_backtrace . entries ( ) )
2022-12-06 01:12:49 +00:00
dbgln ( " - {} " , entry . to_deprecated_string ( true ) ) ;
2022-01-26 19:46:36 +00:00
return IterationDecision : : Continue ;
} ) ;
break ;
}
}
}
2021-06-27 20:22:25 +00:00
} else {
2021-07-05 18:01:41 +00:00
print_modifiers ( { Test : : BG_GREEN , Test : : FG_BLACK , Test : : FG_BOLD } ) ;
2021-08-19 01:16:23 +00:00
out ( " PASS " ) ;
2021-07-05 18:01:41 +00:00
print_modifiers ( { Test : : CLEAR } ) ;
2021-06-27 20:22:25 +00:00
}
2021-08-19 01:16:23 +00:00
out ( " {} " , test_relative_path ) ;
2021-06-27 20:22:25 +00:00
2021-07-05 18:01:41 +00:00
print_modifiers ( { Test : : CLEAR , Test : : ITALIC , Test : : FG_GRAY } ) ;
2021-06-27 20:22:25 +00:00
if ( test_result . time_taken < 1000 ) {
outln ( " ({}ms) " , static_cast < int > ( test_result . time_taken ) ) ;
} else {
outln ( " ({:3}s) " , test_result . time_taken / 1000.0 ) ;
}
2021-07-05 18:01:41 +00:00
print_modifiers ( { Test : : CLEAR } ) ;
2021-06-27 20:22:25 +00:00
if ( test_result . result ! = Test : : Result : : Pass ) {
2021-07-05 18:01:41 +00:00
print_modifiers ( { Test : : FG_GRAY , Test : : FG_BOLD } ) ;
2021-06-27 20:22:25 +00:00
out ( " Test: " ) ;
if ( crashed_or_failed ) {
2021-07-05 18:01:41 +00:00
print_modifiers ( { Test : : CLEAR , Test : : FG_RED } ) ;
2021-06-27 20:22:25 +00:00
outln ( " {} ({}) " , test_result . file_path . basename ( ) , test_result . result = = Test : : Result : : Fail ? " failed " : " crashed " ) ;
} else {
2021-07-05 18:01:41 +00:00
print_modifiers ( { Test : : CLEAR , Test : : FG_ORANGE } ) ;
2021-06-27 20:22:25 +00:00
outln ( " {} (skipped) " , test_result . file_path . basename ( ) ) ;
}
2021-07-05 18:01:41 +00:00
print_modifiers ( { Test : : CLEAR } ) ;
2021-06-27 20:22:25 +00:00
}
// Make sure our clear modifiers goes through before we dump file output via write(2)
fflush ( stdout ) ;
if ( print_stdout_stderr & & test_result . stdout_err_fd > 0 ) {
int ret = lseek ( test_result . stdout_err_fd , 0 , SEEK_SET ) ;
VERIFY ( ret = = 0 ) ;
for ( ; ; ) {
char buf [ 32768 ] ;
ssize_t nread = read ( test_result . stdout_err_fd , buf , sizeof ( buf ) ) ;
if ( nread = = 0 )
break ;
if ( nread < 0 ) {
perror ( " read " ) ;
break ;
}
size_t already_written = 0 ;
while ( already_written < ( size_t ) nread ) {
ssize_t nwritten = write ( STDOUT_FILENO , buf + already_written , nread - already_written ) ;
if ( nwritten < 0 ) {
perror ( " write " ) ;
break ;
}
already_written + = nwritten ;
}
}
}
close ( test_result . stdout_err_fd ) ;
}
2022-12-04 18:02:33 +00:00
FileResult TestRunner : : run_test_file ( DeprecatedString const & test_path )
2021-06-27 20:22:25 +00:00
{
double start_time = get_time_in_ms ( ) ;
auto path_for_test = LexicalPath ( test_path ) ;
if ( should_skip_test ( path_for_test ) ) {
return FileResult { move ( path_for_test ) , 0.0 , Test : : Result : : Skip , - 1 } ;
}
// FIXME: actual error handling, mark test as :yaksplode: if any are bad instead of VERIFY
posix_spawn_file_actions_t file_actions ;
posix_spawn_file_actions_init ( & file_actions ) ;
char child_out_err_path [ ] = " /tmp/run-tests.XXXXXX " ;
int child_out_err_file = mkstemp ( child_out_err_path ) ;
VERIFY ( child_out_err_file > = 0 ) ;
2022-12-04 18:02:33 +00:00
DeprecatedString dirname = path_for_test . dirname ( ) ;
DeprecatedString basename = path_for_test . basename ( ) ;
2021-06-30 09:19:21 +00:00
2021-06-27 20:22:25 +00:00
( void ) posix_spawn_file_actions_adddup2 ( & file_actions , child_out_err_file , STDOUT_FILENO ) ;
( void ) posix_spawn_file_actions_adddup2 ( & file_actions , child_out_err_file , STDERR_FILENO ) ;
2021-06-30 09:19:21 +00:00
( void ) posix_spawn_file_actions_addchdir ( & file_actions , dirname . characters ( ) ) ;
2021-06-27 20:22:25 +00:00
2022-04-01 17:58:27 +00:00
Vector < char const * , 4 > argv ;
2021-06-30 09:19:21 +00:00
argv . append ( basename . characters ( ) ) ;
2021-06-27 20:22:25 +00:00
auto extra_args = m_config - > read_entry ( path_for_test . basename ( ) , " Arguments " , " " ) . split ( ' ' ) ;
for ( auto & arg : extra_args )
argv . append ( arg . characters ( ) ) ;
argv . append ( nullptr ) ;
pid_t child_pid = - 1 ;
// FIXME: Do we really want to copy test runner's entire env?
2021-06-30 04:52:41 +00:00
int ret = posix_spawn ( & child_pid , test_path . characters ( ) , & file_actions , nullptr , const_cast < char * const * > ( argv . data ( ) ) , environ ) ;
2021-06-27 20:22:25 +00:00
VERIFY ( ret = = 0 ) ;
VERIFY ( child_pid > 0 ) ;
int wstatus ;
Test : : Result test_result = Test : : Result : : Fail ;
for ( size_t num_waits = 0 ; num_waits < 2 ; + + num_waits ) {
ret = waitpid ( child_pid , & wstatus , 0 ) ; // intentionally not setting WCONTINUED
if ( ret ! = child_pid )
break ; // we'll end up with a failure
if ( WIFEXITED ( wstatus ) ) {
if ( wstatus = = 0 ) {
test_result = Test : : Result : : Pass ;
}
break ;
} else if ( WIFSIGNALED ( wstatus ) ) {
test_result = Test : : Result : : Crashed ;
break ;
} else if ( WIFSTOPPED ( wstatus ) ) {
outln ( " {} was stopped unexpectedly, sending SIGCONT " , test_path ) ;
kill ( child_pid , SIGCONT ) ;
}
}
2021-06-30 04:52:41 +00:00
// Remove the child's stdout from /tmp. This does cause the temp file to be observable
// while the test is executing, but if it hangs that might even be a bonus :)
ret = unlink ( child_out_err_path ) ;
VERIFY ( ret = = 0 ) ;
2022-01-26 19:46:36 +00:00
return FileResult { move ( path_for_test ) , get_time_in_ms ( ) - start_time , test_result , child_out_err_file , child_pid } ;
2021-06-27 20:22:25 +00:00
}
2022-03-28 19:55:25 +00:00
ErrorOr < int > serenity_main ( Main : : Arguments arguments )
2021-06-27 20:22:25 +00:00
{
2022-01-05 10:17:45 +00:00
2022-03-28 19:55:25 +00:00
auto program_name = LexicalPath : : basename ( arguments . strings [ 0 ] ) ;
2021-06-27 20:22:25 +00:00
# ifdef SIGINFO
2022-03-28 19:55:25 +00:00
TRY ( Core : : System : : signal ( SIGINFO , [ ] ( int ) {
2021-06-27 20:22:25 +00:00
static char buffer [ 4096 ] ;
auto & counts = : : Test : : TestRunner : : the ( ) - > counts ( ) ;
int len = snprintf ( buffer , sizeof ( buffer ) , " Pass: %d, Fail: %d, Skip: %d \n Current test: %s \n " , counts . tests_passed , counts . tests_failed , counts . tests_skipped , g_currently_running_test . characters ( ) ) ;
write ( STDOUT_FILENO , buffer , len ) ;
2022-03-28 19:55:25 +00:00
} ) ) ;
2021-06-27 20:22:25 +00:00
# endif
bool print_progress =
2022-10-09 21:23:23 +00:00
# ifdef AK_OS_SERENITY
2021-06-27 20:22:25 +00:00
true ; // Use OSC 9 to print progress
# else
false ;
# endif
bool print_json = false ;
bool print_all_output = false ;
2022-01-05 10:17:45 +00:00
bool run_benchmarks = false ;
2022-02-03 17:36:27 +00:00
bool run_skipped_tests = false ;
2022-04-01 17:58:27 +00:00
char const * specified_test_root = nullptr ;
2022-12-04 18:02:33 +00:00
DeprecatedString test_glob ;
DeprecatedString exclude_pattern ;
DeprecatedString config_file ;
2021-06-27 20:22:25 +00:00
Core : : ArgsParser args_parser ;
args_parser . add_option ( Core : : ArgsParser : : Option {
2022-07-12 20:13:38 +00:00
. argument_mode = Core : : ArgsParser : : OptionArgumentMode : : Required ,
2021-06-27 20:22:25 +00:00
. help_string = " Show progress with OSC 9 (true, false) " ,
. long_name = " show-progress " ,
. short_name = ' p ' ,
. accept_value = [ & ] ( auto * str ) {
2021-07-04 09:08:46 +00:00
if ( " true " sv = = str )
2021-06-27 20:22:25 +00:00
print_progress = true ;
2021-07-04 09:08:46 +00:00
else if ( " false " sv = = str )
2021-06-27 20:22:25 +00:00
print_progress = false ;
else
return false ;
return true ;
} ,
} ) ;
args_parser . add_option ( print_json , " Show results as JSON " , " json " , ' j ' ) ;
args_parser . add_option ( print_all_output , " Show all test output " , " verbose " , ' v ' ) ;
2022-01-05 10:17:45 +00:00
args_parser . add_option ( run_benchmarks , " Run benchmarks as well " , " benchmarks " , ' b ' ) ;
2022-02-03 17:36:27 +00:00
args_parser . add_option ( run_skipped_tests , " Run all matching tests, even those marked as 'skip' " , " all " , ' a ' ) ;
2021-06-27 20:22:25 +00:00
args_parser . add_option ( test_glob , " Only run tests matching the given glob " , " filter " , ' f ' , " glob " ) ;
args_parser . add_option ( exclude_pattern , " Regular expression to use to exclude paths from being considered tests " , " exclude-pattern " , ' e ' , " pattern " ) ;
args_parser . add_option ( config_file , " Configuration file to use " , " config-file " , ' c ' , " filename " ) ;
args_parser . add_positional_argument ( specified_test_root , " Tests root directory " , " path " , Core : : ArgsParser : : Required : : No ) ;
2022-03-28 19:55:25 +00:00
args_parser . parse ( arguments ) ;
2021-06-27 20:22:25 +00:00
2022-12-04 18:02:33 +00:00
test_glob = DeprecatedString : : formatted ( " *{}* " , test_glob ) ;
2021-06-27 20:22:25 +00:00
if ( getenv ( " DISABLE_DBG_OUTPUT " ) ) {
AK : : set_debug_enabled ( false ) ;
}
2022-01-05 10:17:45 +00:00
// Make UBSAN deadly for all tests we run by default.
2022-07-11 17:32:29 +00:00
TRY ( Core : : System : : setenv ( " UBSAN_OPTIONS " sv , " halt_on_error=1 " sv , true ) ) ;
2022-01-05 10:17:45 +00:00
if ( ! run_benchmarks )
2022-07-11 17:32:29 +00:00
TRY ( Core : : System : : setenv ( " TESTS_ONLY " sv , " 1 " sv , true ) ) ;
2022-01-05 10:17:45 +00:00
2022-12-04 18:02:33 +00:00
DeprecatedString test_root ;
2021-06-27 20:22:25 +00:00
if ( specified_test_root ) {
2022-12-04 18:02:33 +00:00
test_root = DeprecatedString { specified_test_root } ;
2021-06-27 20:22:25 +00:00
} else {
test_root = " /usr/Tests " ;
}
if ( ! Core : : File : : is_directory ( test_root ) ) {
warnln ( " Test root is not a directory: {} " , test_root ) ;
return 1 ;
}
test_root = Core : : File : : real_path_for ( test_root ) ;
2022-03-28 19:55:25 +00:00
auto void_or_error = Core : : System : : chdir ( test_root ) ;
if ( void_or_error . is_error ( ) ) {
warnln ( " chdir failed: {} " , void_or_error . error ( ) ) ;
return void_or_error . release_error ( ) ;
2021-06-27 20:22:25 +00:00
}
2022-02-06 13:33:42 +00:00
auto config_or_error = config_file . is_empty ( ) ? Core : : ConfigFile : : open_for_app ( " Tests " ) : Core : : ConfigFile : : open ( config_file ) ;
if ( config_or_error . is_error ( ) ) {
warnln ( " Failed to open configuration file ({}): {} " , config_file . is_empty ( ) ? " User config for Tests " : config_file . characters ( ) , config_or_error . error ( ) ) ;
2022-03-28 19:55:25 +00:00
return config_or_error . release_error ( ) ;
2022-02-06 13:33:42 +00:00
}
auto config = config_or_error . release_value ( ) ;
2021-06-27 20:22:25 +00:00
if ( config - > num_groups ( ) = = 0 )
warnln ( " Empty configuration file ({}) loaded! " , config_file . is_empty ( ) ? " User config for Tests " : config_file . characters ( ) ) ;
if ( exclude_pattern . is_empty ( ) )
exclude_pattern = config - > read_entry ( " Global " , " NotTestsPattern " , " $^ " ) ; // default is match nothing (aka match end then beginning)
Regex < PosixExtended > exclude_regex ( exclude_pattern , { } ) ;
2021-11-06 09:33:46 +00:00
if ( exclude_regex . parser_result . error ! = regex : : Error : : NoError ) {
2021-06-27 20:22:25 +00:00
warnln ( " Exclude pattern \" {} \" is invalid " , exclude_pattern ) ;
return 1 ;
}
2021-07-12 19:52:17 +00:00
// we need to preconfigure this, because we can't autoinitialize Regex types
// in the Testrunner
auto skip_regex_pattern = config - > read_entry ( " Global " , " SkipRegex " , " $^ " ) ;
Regex < PosixExtended > skip_regex { skip_regex_pattern , { } } ;
2021-11-06 09:33:46 +00:00
if ( skip_regex . parser_result . error ! = regex : : Error : : NoError ) {
2021-07-12 19:52:17 +00:00
warnln ( " SkipRegex pattern \" {} \" is invalid " , skip_regex_pattern ) ;
return 1 ;
}
2022-02-03 17:36:27 +00:00
TestRunner test_runner ( test_root , move ( exclude_regex ) , move ( config ) , move ( skip_regex ) , run_skipped_tests , print_progress , print_json , print_all_output ) ;
2021-06-27 20:22:25 +00:00
test_runner . run ( test_glob ) ;
return test_runner . counts ( ) . tests_failed ;
}