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