1# -- 2# Copyright (C) 2001-2020 OTRS AG, https://otrs.com/ 3# -- 4# This software comes with ABSOLUTELY NO WARRANTY. For details, see 5# the enclosed file COPYING for license information (GPL). If you 6# did not receive this file, see https://www.gnu.org/licenses/gpl-3.0.txt. 7# -- 8 9package Kernel::System::UnitTest::Driver; 10 11use strict; 12use warnings; 13 14use Storable(); 15use Term::ANSIColor(); 16use Text::Diff; 17use Time::HiRes(); 18 19# UnitTest helper must be loaded to override the builtin time functions! 20use Kernel::System::UnitTest::Helper; 21 22use Kernel::System::VariableCheck qw(DataIsDifferent); 23 24our @ObjectDependencies = ( 25 'Kernel::Config', 26 'Kernel::System::Log', 27 'Kernel::System::Main', 28); 29 30=head1 NAME 31 32Kernel::System::UnitTest::Driver - unit test file execution wrapper 33 34=head1 PUBLIC INTERFACE 35 36=head2 new() 37 38create unit test driver object. Do not use it directly, instead use: 39 40 my $Driver = $Kernel::OM->Create( 41 'Kernel::System::UnitTest::Driver', 42 ObjectParams => { 43 Verbose => $Self->{Verbose}, 44 ANSI => $Self->{ANSI}, 45 }, 46 ); 47 48=cut 49 50sub new { 51 my ( $Type, %Param ) = @_; 52 53 # allocate new hash for object 54 my $Self = {}; 55 bless( $Self, $Type ); 56 57 $Self->{ANSI} = $Param{ANSI}; 58 $Self->{Verbose} = $Param{Verbose}; 59 $Self->{DataDiffType} = ucfirst( lc( $Param{DataDiffType} || 'Table' ) ); 60 61 # We use an output buffering mechanism if Verbose is not set. Only failed tests will be output in this case. 62 63 # Make sure stuff is always flushed to keep it in the right order. 64 *STDOUT->autoflush(1); 65 *STDERR->autoflush(1); 66 $Self->{OriginalSTDOUT} = *STDOUT; 67 $Self->{OriginalSTDOUT}->autoflush(1); 68 $Self->{OutputBuffer} = ''; 69 70 # Report results via file. 71 $Self->{ResultDataFile} = $Kernel::OM->Get('Kernel::Config')->Get('Home') . '/var/tmp/UnitTest.dump'; 72 unlink $Self->{ResultDataFile}; # purge if exists 73 74 return $Self; 75} 76 77=head2 Run() 78 79executes a single unit test file and provides it with an empty environment (fresh C<ObjectManager> instance). 80 81This method assumes that it runs in a dedicated child process just for this one unit test. 82This process forking is done in L<Kernel::System::UnitTest>, which creates one child process per test file. 83 84All results will be collected and written to a C<var/tmp/UnitTest.dump> file that the main process will 85load to collect all results. 86 87=cut 88 89sub Run { 90 my ( $Self, %Param ) = @_; 91 92 my $File = $Param{File}; 93 94 my $UnitTestFile = $Kernel::OM->Get('Kernel::System::Main')->FileRead( 95 Location => $File, 96 ); 97 98 if ( !$UnitTestFile ) { 99 $Self->True( 0, "ERROR: $!: $File" ); 100 print STDERR "ERROR: $!: $File\n"; 101 $Self->_SaveResults(); 102 return; 103 } 104 105 print "+-------------------------------------------------------------------+\n"; 106 print ' ' . $Self->_Color( 'yellow', $File ) . ":\n"; 107 print "+-------------------------------------------------------------------+\n"; 108 109 my $StartTime = [ Time::HiRes::gettimeofday() ]; 110 111 # Create a new scope to be sure to destroy local object of the test files. 112 { 113 # Make sure every UT uses its own clean environment. 114 ## nofilter(TidyAll::Plugin::OTRS::Perl::ObjectManagerCreation) 115 local $Kernel::OM = Kernel::System::ObjectManager->new( 116 'Kernel::System::Log' => { 117 LogPrefix => 'OTRS-otrs.UnitTest', 118 }, 119 ); 120 121 # Provide $Self as 'Kernel::System::UnitTest' for convenience. 122 $Kernel::OM->ObjectInstanceRegister( 123 Package => 'Kernel::System::UnitTest::Driver', 124 Object => $Self, 125 Dependencies => [], 126 ); 127 128 $Self->{OutputBuffer} = ''; 129 local *STDOUT = *STDOUT; 130 local *STDERR = *STDERR; 131 if ( !$Self->{Verbose} ) { 132 undef *STDOUT; 133 undef *STDERR; 134 open STDOUT, '>:utf8', \$Self->{OutputBuffer}; ## no critic 135 open STDERR, '>:utf8', \$Self->{OutputBuffer}; ## no critic 136 } 137 138 # HERE the actual tests are run. 139 my $TestSuccess = eval ${$UnitTestFile}; ## no critic 140 141 if ( !$TestSuccess ) { 142 if ($@) { 143 $Self->True( 0, "ERROR: Error in $File: $@" ); 144 } 145 else { 146 $Self->True( 0, "ERROR: $File did not return a true value." ); 147 } 148 } 149 } 150 151 $Self->{ResultData}->{Duration} = sprintf( '%.3f', Time::HiRes::tv_interval($StartTime) ); 152 153 if ( $Self->{SeleniumData} ) { 154 $Self->{ResultData}->{SeleniumData} = $Self->{SeleniumData}; 155 } 156 157 print { $Self->{OriginalSTDOUT} } "\n" if !$Self->{Verbose}; 158 159 my $TestCountTotal = $Self->{ResultData}->{TestOk} // 0; 160 $TestCountTotal += $Self->{ResultData}->{TestNotOk} // 0; 161 162 printf( 163 "%s ran %s test(s) in %s.\n\n", 164 $File, 165 $Self->_Color( 'yellow', $TestCountTotal ), 166 $Self->_Color( 'yellow', "$Self->{ResultData}->{Duration}s" ), 167 ); 168 169 return $Self->_SaveResults(); 170} 171 172=head2 True() 173 174test for a scalar value that evaluates to true. 175 176Send a scalar value to this function along with the test's name: 177 178 $UnitTestObject->True(1, 'Test Name'); 179 180 $UnitTestObject->True($ParamA, 'Test Name'); 181 182Internally, the function receives this value and evaluates it to see 183if it's true, returning 1 in this case or undef, otherwise. 184 185 my $TrueResult = $UnitTestObject->True( 186 $TestValue, 187 'Test Name', 188 ); 189 190=cut 191 192sub True { 193 my ( $Self, $True, $Name ) = @_; 194 195 if ( !$Name ) { 196 return $Self->_Print( 0, 'Error: test name was not provided.' ); 197 } 198 199 if ($True) { 200 return $Self->_Print( 1, $Name ); 201 } 202 else { 203 return $Self->_Print( 0, $Name ); 204 } 205} 206 207=head2 False() 208 209test for a scalar value that evaluates to false. 210 211It has the same interface as L</True()>, but tests 212for a false value instead. 213 214=cut 215 216sub False { 217 my ( $Self, $False, $Name ) = @_; 218 219 if ( !$Name ) { 220 return $Self->_Print( 0, 'Error: test name was not provided.' ); 221 } 222 223 if ( !$False ) { 224 return $Self->_Print( 1, $Name ); 225 } 226 else { 227 return $Self->_Print( 0, $Name ); 228 } 229} 230 231=head2 Is() 232 233compares two scalar values for equality. 234 235To this function you must send a pair of scalar values to compare them, 236and the name that the test will take, this is done as shown in the examples 237below. 238 239 $UnitTestObject->Is($A, $B, 'Test Name'); 240 241Returns 1 if the values were equal, or undef otherwise. 242 243 my $IsResult = $UnitTestObject->Is( 244 $ValueFromFunction, # test data 245 1, # expected value 246 'Test Name', 247 ); 248 249=cut 250 251sub Is { 252 my ( $Self, $Test, $ShouldBe, $Name ) = @_; 253 254 if ( !$Name ) { 255 return $Self->_Print( 0, 'Error: test name was not provided.' ); 256 } 257 258 if ( !defined $Test && !defined $ShouldBe ) { 259 return $Self->_Print( 1, $Name ); 260 } 261 elsif ( !defined $Test && defined $ShouldBe ) { 262 return $Self->_Print( 0, "$Name (is 'undef' should be '$ShouldBe')" ); 263 } 264 elsif ( defined $Test && !defined $ShouldBe ) { 265 return $Self->_Print( 0, "$Name (is '$Test' should be 'undef')" ); 266 } 267 elsif ( $Test eq $ShouldBe ) { 268 return $Self->_Print( 1, $Name ); 269 } 270 else { 271 return $Self->_Print( 0, "$Name (is '$Test' should be '$ShouldBe')" ); 272 } 273} 274 275=head2 IsNot() 276 277compares two scalar values for inequality. 278 279It has the same interface as L</Is()>, but tests 280for inequality instead. 281 282=cut 283 284sub IsNot { 285 my ( $Self, $Test, $ShouldBe, $Name ) = @_; 286 287 if ( !$Name ) { 288 return $Self->_Print( 0, 'Error: test name was not provided.' ); 289 } 290 291 if ( !defined $Test && !defined $ShouldBe ) { 292 return $Self->_Print( 0, "$Name (is 'undef')" ); 293 } 294 elsif ( !defined $Test && defined $ShouldBe ) { 295 return $Self->_Print( 1, $Name ); 296 } 297 elsif ( defined $Test && !defined $ShouldBe ) { 298 return $Self->_Print( 1, $Name ); 299 } 300 if ( $Test ne $ShouldBe ) { 301 return $Self->_Print( 1, $Name ); 302 } 303 else { 304 return $Self->_Print( 0, "$Name (is '$Test' should not be '$ShouldBe')" ); 305 } 306} 307 308=head2 IsDeeply() 309 310compares complex data structures for equality. 311 312To this function you must send the references to two data structures to be compared, 313and the name that the test will take, this is done as shown in the examples 314below. 315 316 $UnitTestObject-> IsDeeply($ParamA, $ParamB, 'Test Name'); 317 318Where $ParamA and $ParamB must be references to a structure (scalar, list or hash). 319 320Returns 1 if the data structures are the same, or undef otherwise. 321 322 my $IsDeeplyResult = $UnitTestObject->IsDeeply( 323 \%ResultHash, # test data 324 \%ExpectedHash, # expected value 325 'Dummy Test Name', 326 ); 327 328=cut 329 330sub IsDeeply { 331 my ( $Self, $Test, $ShouldBe, $Name ) = @_; 332 333 if ( !$Name ) { 334 $Self->_Print( 0, 'Error: test name was not provided.' ); 335 return; 336 } 337 338 my $Diff = DataIsDifferent( 339 Data1 => $Test, 340 Data2 => $ShouldBe, 341 ); 342 343 if ( !defined $Test && !defined $ShouldBe ) { 344 return $Self->_Print( 1, $Name ); 345 } 346 elsif ( !defined $Test && defined $ShouldBe ) { 347 return $Self->_Print( 0, "$Name (is 'undef' should be defined)" ); 348 } 349 elsif ( defined $Test && !defined $ShouldBe ) { 350 return $Self->_Print( 0, "$Name (is defined should be 'undef')" ); 351 } 352 elsif ( !$Diff ) { 353 return $Self->_Print( 1, $Name ); 354 } 355 else { 356 my $TestDump = $Kernel::OM->Get('Kernel::System::Main')->Dump($Test); 357 my $ShouldBeDump = $Kernel::OM->Get('Kernel::System::Main')->Dump($ShouldBe); 358 local $ENV{DIFF_OUTPUT_UNICODE} = 1; 359 my $Diff = Text::Diff::diff( 360 \$TestDump, 361 \$ShouldBeDump, 362 { 363 STYLE => $Self->{DataDiffType}, 364 FILENAME_A => 'Actual data', 365 FILENAME_B => 'Expected data', 366 } 367 ); 368 369 # Provide colored diff. 370 if ( $Self->{ANSI} ) { 371 my @DiffLines = split( m{\n}, $Diff ); 372 $Diff = ''; 373 374 for my $DiffLine (@DiffLines) { 375 376 # Diff type "Table" 377 if ( $Self->{DataDiffType} eq 'Table' ) { 378 379 # Line changed 380 if ( substr( $DiffLine, 0, 1 ) eq '*' && substr( $DiffLine, -1, 1 ) eq '*' ) { 381 $DiffLine = $Self->_Color( 'yellow', $DiffLine ); 382 } 383 384 # Line added 385 elsif ( substr( $DiffLine, 0, 1 ) eq '|' && substr( $DiffLine, -1, 1 ) eq '*' ) { 386 $DiffLine = $Self->_Color( 'green', $DiffLine ); 387 } 388 389 # Line removed 390 elsif ( substr( $DiffLine, 0, 1 ) eq '*' && substr( $DiffLine, -1, 1 ) eq '|' ) { 391 $DiffLine = $Self->_Color( 'red', $DiffLine ); 392 } 393 } 394 395 # Diff type "Unified" 396 else { 397 # Line added 398 if ( substr( $DiffLine, 0, 1 ) eq '+' && substr( $DiffLine, 0, 4 ) ne '+++ ' ) { 399 $DiffLine = $Self->_Color( 'green', $DiffLine ); 400 } 401 402 # Line removed 403 elsif ( substr( $DiffLine, 0, 1 ) eq '-' && substr( $DiffLine, 0, 4 ) ne '--- ' ) { 404 $DiffLine = $Self->_Color( 'red', $DiffLine ); 405 } 406 } 407 $Diff .= $DiffLine . "\n"; 408 } 409 } 410 411 my $Output; 412 $Output .= $Self->_Color( 'yellow', "Diff" ) . ":\n$Diff\n"; 413 $Output .= $Self->_Color( 'yellow', "Actual data" ) . ":\n$TestDump\n"; 414 $Output .= $Self->_Color( 'yellow', "Expected data" ) . ":\n$ShouldBeDump\n"; 415 416 return $Self->_Print( 0, "$Name (is not equal, see below)\n$Output" ); 417 } 418} 419 420=head2 IsNotDeeply() 421 422compares two data structures for inequality. 423 424It has the same interface as L</IsDeeply()>, but tests 425for inequality instead. 426 427=cut 428 429sub IsNotDeeply { 430 my ( $Self, $Test, $ShouldBe, $Name ) = @_; 431 432 if ( !$Name ) { 433 $Self->_Print( 0, 'Error: test name was not provided.' ); 434 return; 435 } 436 437 my $Diff = DataIsDifferent( 438 Data1 => $Test, 439 Data2 => $ShouldBe, 440 ); 441 442 if ( !defined $Test && !defined $ShouldBe ) { 443 return $Self->_Print( 0, "$Name (is 'undef')" ); 444 } 445 elsif ( !defined $Test && defined $ShouldBe ) { 446 return $Self->_Print( 1, $Name ); 447 } 448 elsif ( defined $Test && !defined $ShouldBe ) { 449 return $Self->_Print( 1, $Name ); 450 } 451 452 if ($Diff) { 453 return $Self->_Print( 1, $Name ); 454 } 455 else { 456 my $TestDump = $Kernel::OM->Get('Kernel::System::Main')->Dump($Test); 457 my $Output = $Self->_Color( 'yellow', "Actual data" ) . ":\n$TestDump\n"; 458 return $Self->_Print( 0, "$Name (the structures are wrongly equal, see below)\n$Output" ); 459 } 460} 461 462=head2 AttachSeleniumScreenshot() 463 464attach a screenshot taken during Selenium error handling. These will be sent to the server 465together with the test results. 466 467 $Driver->AttachSeleniumScreenshot( 468 Filename => $Filename, 469 Content => $Data # raw image data 470 ); 471 472=cut 473 474sub AttachSeleniumScreenshot { 475 my ( $Self, %Param ) = @_; 476 477 push @{ $Self->{ResultData}->{Results}->{ $Self->{TestCount} }->{Screenshots} }, 478 { 479 Filename => $Param{Filename}, 480 Content => $Param{Content}, 481 }; 482 483 return; 484} 485 486=begin Internal: 487 488=cut 489 490sub _SaveResults { 491 my ($Self) = @_; 492 493 if ( !$Self->{ResultData} ) { 494 $Self->True( 0, 'No result data found.' ); 495 } 496 497 my $Success = Storable::nstore( $Self->{ResultData}, $Self->{ResultDataFile} ); 498 if ( !$Success ) { 499 print STDERR $Self->_Color( 'red', "Could not store result data in $Self->{ResultDataFile}\n" ); 500 return 0; 501 } 502 503 return 1; 504} 505 506sub _Print { 507 my ( $Self, $ResultOk, $Message ) = @_; 508 509 $Message ||= '->>No Name!<<-'; 510 511 my $ShortMessage = $Message; 512 if ( length $ShortMessage > 2_000 && !$Self->{Verbose} ) { 513 $ShortMessage = substr( $ShortMessage, 0, 2_000 ) . "[...]"; 514 } 515 516 if ( $Self->{Verbose} || !$ResultOk ) { 517 518 # Work around problem with leading \0 bytes in the output buffer 519 # which breaks the unicode output. The reason is not certain, maybe because of 520 # Perl's exception handling. 521 $Self->{OutputBuffer} =~ s{\0}{}g; 522 print { $Self->{OriginalSTDOUT} } $Self->{OutputBuffer}; 523 } 524 $Self->{OutputBuffer} = ''; 525 526 $Self->{TestCount}++; 527 if ($ResultOk) { 528 if ( $Self->{Verbose} ) { 529 print { $Self->{OriginalSTDOUT} } " " 530 . $Self->_Color( 'green', "ok" ) 531 . " $Self->{TestCount} - $ShortMessage\n"; 532 } 533 else { 534 print { $Self->{OriginalSTDOUT} } $Self->_Color( 'green', "." ); 535 } 536 537 $Self->{ResultData}->{TestOk}++; 538 return 1; 539 } 540 else { 541 if ( !$Self->{Verbose} ) { 542 print { $Self->{OriginalSTDOUT} } "\n"; 543 } 544 print { $Self->{OriginalSTDOUT} } " " 545 . $Self->_Color( 'red', "not ok" ) 546 . " $Self->{TestCount} - $ShortMessage\n"; 547 $Self->{ResultData}->{TestNotOk}++; 548 $Self->{ResultData}->{Results}->{ $Self->{TestCount} }->{Status} = 'not ok'; 549 $Self->{ResultData}->{Results}->{ $Self->{TestCount} }->{Message} = $Message; 550 551 # Failure summary: only the first line 552 my $TestFailureDetails = ( split m/\r?\n/, $Message )[0]; 553 554 # And only without details 555 $TestFailureDetails =~ s{\s*\(.+\Z}{}; 556 if ( length $TestFailureDetails > 100 ) { 557 $TestFailureDetails = substr( $TestFailureDetails, 0, 100 ) . "[...]"; 558 } 559 560 # Store information about failed tests, but only if we are running in a toplevel unit test object 561 # that is actually processing files, and not in an embedded object that just runs individual tests. 562 push @{ $Self->{ResultData}->{NotOkInfo} }, sprintf "#%s - %s", $Self->{TestCount}, 563 $TestFailureDetails; 564 565 return; 566 } 567} 568 569=head2 _Color() 570 571this will color the given text (see Term::ANSIColor::color()) if 572ANSI output is available and active, otherwise the text stays unchanged. 573 574 my $PossiblyColoredText = $CommandObject->_Color('green', $Text); 575 576=cut 577 578sub _Color { 579 my ( $Self, $Color, $Text ) = @_; 580 581 return $Text if !$Self->{ANSI}; 582 return Term::ANSIColor::color($Color) . $Text . Term::ANSIColor::color('reset'); 583} 584 5851; 586 587=end Internal: 588 589=head1 TERMS AND CONDITIONS 590 591This software is part of the OTRS project (L<https://otrs.org/>). 592 593This software comes with ABSOLUTELY NO WARRANTY. For details, see 594the enclosed file COPYING for license information (GPL). If you 595did not receive this file, see L<https://www.gnu.org/licenses/gpl-3.0.txt>. 596 597=cut 598