summaryrefslogtreecommitdiff
path: root/t/TestLib.pm
blob: 9a56a5c9eb5236980d6c19a59269852253e88242 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
#------------------------------------------------------------------------------
# File:         TestLib.pm
#
# Description:  Utility routines for testing ExifTool modules
#
# Revisions:    Feb. 19/04 - P. Harvey Created
#               Feb. 26/04 - P. Harvey Name temporary file ".failed" and erase
#                            it if the test passes
#               Feb. 27/04 - P. Harvey Change print format and allow ExifTool
#                            object to be passed instead of tags hash ref.
#               Oct. 30/04 - P. Harvey Split testCompare() into separate sub.
#               May  18/05 - P. Harvey Tolerate round-off errors in floats.
#               Feb. 02/08 - P. Harvey Allow different timezones in time values
#               Sep. 16/08 - P. Harvey Improve timezone testing
#               Jul. 14/10 - P. Harvey Added writeInfo()
#               Jan. 06/12 - P. Harvey Patched MirBSD leap second "feature"
#               Jun. 08/21 - PH Patched float compare to fix quadmath test failure
#------------------------------------------------------------------------------

package t::TestLib;

use strict;
require 5.002;
require Exporter;
use Image::ExifTool qw(ImageInfo);

use vars qw($VERSION @ISA @EXPORT);
$VERSION = '1.23';
@ISA = qw(Exporter);
@EXPORT = qw(check writeCheck writeInfo testCompare binaryCompare testVerbose notOK done);

my $noTimeLocal;
my $rtnCode = 0;

sub nearEnough($$);
sub nearTime($$$$);
sub formatValue($);
sub writeInfo($$;$$$);
sub notOK();

#------------------------------------------------------------------------------
# Compare 2 binary files
# Inputs: 0) file name 1, 1) file name 2
# Returns: 1 if files are identical
sub binaryCompare($$)
{
    my ($file1, $file2) = @_;
    my $success = 1;
    open(TESTFILE1, $file1) or return 0;
    unless (open(TESTFILE2, $file2)) {
        close(TESTFILE1);
        return 0;
    }
    binmode(TESTFILE1);
    binmode(TESTFILE2);
    my ($buf1, $buf2);
    while (read(TESTFILE1, $buf1, 65536)) {
        read(TESTFILE2, $buf2, 65536) or $success = 0, last;
        $buf1 eq $buf2 or $success = 0, last;
    }
    read(TESTFILE2, $buf2, 65536) and $success = 0;
    close(TESTFILE1);
    close(TESTFILE2);
    return $success
}

#------------------------------------------------------------------------------
# Compare 2 files and return true and erase the 2nd file if they are the same
# Inputs: 0) file1, 1) file2, 2) test number, 3) flag to not erase test file
# Returns: true if files are the same
sub testCompare($$$;$)
{
    my ($stdfile, $testfile, $testnum, $keep) = @_;
    my $success = 0;
    my $linenum;
    
    my $oldSep = $/;   
    $/ = "\x0a";        # set input line separator
    if (open(FILE1, $stdfile)) {
        if (open(FILE2, $testfile)) {
            $success = 1;
            my ($line1, $line2);
            my $linenum = 0;
            my $skip = 0;
            for (;;) {
                $line1 = <FILE1> unless $skip == 1;
                last unless defined $line1;
                ++$linenum;
                $line2 = <FILE2> unless $skip == 2;
                $skip = 0;
                if (defined $line2) {
                    next if $line1 eq $line2;
                    next if nearEnough($line1, $line2);
                    # ignore IPTCDigest warning if Digest::MD5 isn't available
                    if ($line1 =~ /Warning: IPTCDigest is not current/ and
                        not eval 'require Digest::MD5')
                    {
                        $skip = 2; 
                        next;
                    } elsif ($line2 =~ /Warning: IPTCDigest is not current/ and
                        not eval 'require Digest::MD5')
                    {
                        $skip = 1;
                        next;
                    }
                }
                $success = 0;
                last;
            }
            if ($success) {
                # make sure there is nothing left in file2
                $line2 = <FILE2>;
                if ($line2) {
                    ++$linenum;
                    $success = 0;
                }
            }
            unless ($success) {
                warn "\n  Test $testnum differs beginning at line $linenum:\n";
                defined $line1 or $line1 = '(null)';
                defined $line2 or $line2 = '(null)';
                chomp($line1,$line2);
                warn qq{    Test gave: "$line2"\n};
                warn qq{    Should be: "$line1"\n};
            }
            close(FILE2);
        }
        close(FILE1);
    }
    $/ = $oldSep;       # restore input line separator
    
    # erase .failed file if test was successful
    $success and not $keep and unlink $testfile;

    return $success
}

#------------------------------------------------------------------------------
# Return true if two test lines are close enough
# Inputs: 0) line1, 1) line2
# Returns: true if lines are similar enough to pass test
sub nearEnough($$)
{
    my ($line1, $line2) = @_;

    # of course, the version number will change...
    return 1 if $line1 =~ /^(.*ExifTool.*)\b\d{1,2}\.\d{2}\b(.*)/s and
               ($line2 eq "$1$Image::ExifTool::VERSION$Image::ExifTool::RELEASE$2" or
                $line2 eq "$1$Image::ExifTool::VERSION$2");

    # allow different FileModifyDate, FileAccessDate, FileCreateDate/FileInodeChangeDate and FilePermissions
    return 1 if $line1 =~ /(File\s?(Modif.*Date|Access\s?Date|Inode\s?Change\s?Date|Permissions))/ and
               ($line2 =~ /$1/ or $line2 =~ /File\s?Creat.*Date/);

    # allow CurrentIPTCDigest to be zero if Digest::MD5 isn't installed
    return 1 if $line1 =~ /Current IPTC Digest/ and
                $line2 =~ /Current IPTC Digest: (0|#){32}/ and
                not eval 'require Digest::MD5';

    # analyze every token in the line, and allow rounding
    # or format differences in floating point numbers
    my @toks1 = split /\s+/, $line1;
    my @toks2 = split /\s+/, $line2;
    my $lenChanged = 0;
    my $i;
    for ($i=0; ; ++$i) {
        return 1 if $i >= @toks1 and $i >= @toks2;  # all tokens were OK
        my $tok1 = $toks1[$i];
        my $tok2 = $toks2[$i];
        last unless defined $tok1 and defined $tok2;
        next if $tok1 eq $tok2;
        # can't compare any more if either line was truncated (ie. ends with '[...]' or '[snip]')
        if ($tok1 =~ /\[(\.{3}|snip)\]$/ or $tok2 =~ /\[(\.{3}|snip)\]$/) {
            return 1 if $tok1=~ /^[-+]?\d+\./ or $tok2=~/^[-+]?\d+\./;  # check for float
            return $lenChanged
        }
        if ($tok1 =~ /^(\d{2}|\d{4}):\d{2}:\d{2}/ and $tok2 =~ /^(\d{2}|\d{4}):\d{2}:\d{2}/ and
            not eval { require Time::Local })
        {
            unless ($noTimeLocal) {
                warn "Ignored time difference(s) because Time::Local is not installed\n";
                $noTimeLocal = 1;
            }
            next;   # ignore times if Time::Local not available
        # account for different timezones
        } elsif ($tok1 =~ /^(\d{2}:\d{2}:\d{2})(Z|[-+]\d{2}:\d{2})$/i) {
            my $time = $1;  # remove timezone
            # timezone may be wrong if writing date/time value in a different timezone
            next if $tok2 =~ /^(\d{2}:\d{2}:\d{2})(Z|[-+]\d{2}:\d{2})$/i and $time eq $1;
            # date/time may be wrong to if converting GMT value to local time
            last unless $i and $toks1[$i-1] =~ /^\d{4}:\d{2}:\d{2}$/ and
                               $toks2[$i-1] =~ /^\d{4}:\d{2}:\d{2}$/;
            $tok1 = $toks1[$i-1] . ' ' . $tok1; # add date to give date/time value
            $tok2 = $toks2[$i-1] . ' ' . $tok2;
            last unless nearTime($tok1, $tok2, $line1, $line2);
        # date may be different if timezone shifted into next day
        } elsif ($tok1 =~ /^\d{4}:\d{2}:\d{2}$/ and $tok2 =~ /^\d{4}:\d{2}:\d{2}$/ and
                 defined $toks1[$i+1] and defined $toks2[$i+1] and
                 $toks1[$i+1] =~ /^(\d{2}:\d{2}:\d{2})(Z|[-+]\d{2}:\d{2})$/i and
                 $toks2[$i+1] =~ /^(\d{2}:\d{2}:\d{2})(Z|[-+]\d{2}:\d{2})$/i)
        {
            ++$i;
            $tok1 .= ' ' . $toks1[$i];      # add time to give date/time value
            $tok2 .= ' ' . $toks2[$i];
            last unless nearTime($tok1, $tok2, $line1, $line2);
        # handle floating point numbers filtered by ExifTool test 29
        } elsif ($tok1 =~ s/(\.#)#*(e[-+]\#+)?/$1/g or $tok2 =~ s/(\.#)#*(e[-+]\#+)?/$1/g) {
            $tok2 =~ s/(\.#)#*(e[-+]\#+)?/$1/g;
            last if $tok1 ne $tok2;
        } else {
            # check to see if both tokens are floating point numbers (with decimal points!)
            if ($tok1 =~ s/([^\d.]+)$//) {  # remove trailing units
                my $a = $1;
                last unless $tok2 =~ s/\Q$a\E$//;
            }
            if ($tok1 =~ s/^(\d+:\d+:)//) { # remove leading HH:MM:
                my $a = $1;
                last unless $tok2 =~ s/^\Q$a//;
            }
            if ($tok1 =~ s/^'//) {          # remove leading quote
                last unless $tok2 =~ s/^'//;
            }
            last unless Image::ExifTool::IsFloat($tok1) and
                        Image::ExifTool::IsFloat($tok2);
            last if $tok1 == 0 or $tok2 == 0;
            # numbers are bad if not the same to 5 significant figures
            if (abs(($tok1-$tok2)/($tok1+$tok2)) > 1e-5) {
                # (but allow last digit to be different due to round-off errors)
                my ($int1, $int2);
                ($int1 = $tok1) =~ tr/0-9//dc;
                ($int2 = $tok2) =~ tr/0-9//dc;
                my $dlen = length($int1) - length($int2);
                if ($dlen > 0) {
                    $int2 .= '0' x $dlen;
                } elsif ($dlen < 0) {
                    $int1 .= '0' x (-$dlen);
                }
                last if abs($int1-$int2) > 1.00001;
            }
        }
        # set flag if length changed
        $lenChanged = 1 if length($tok1) ne length($tok2);
    }
    return 0;
}

#------------------------------------------------------------------------------
# Check two time strings to see if they are the same
# Inputs: 0) time1, 1) time2, 2) line1, 3) line2
# Returns: true on success
sub nearTime($$$$)
{
    my ($tok1, $tok2, $line1, $line2) = @_;
    my $t1 = Image::ExifTool::GetUnixTime($tok1, 'local') or return 0;
    my $t2 = Image::ExifTool::GetUnixTime($tok2, 'local') or return 0;
    my $td = $t2 - $t1;
    if ($td) {
        # patch for the MirBSD leap-second unconformity
        # (120 leap seconds should cover us until _well_ into the future)
        return 0 unless $^O eq 'mirbsd' and $td < 0 and $td > -120;
        warn "\n  Ignoring $td second error due to MirBSD leap-second \"feature\":\n";
        chomp($line1,$line2);
        warn qq{    Test gave: "$line2"\n};
        warn qq{    Should be: "$line1"\n};
    }
    return 1;
}

#------------------------------------------------------------------------------
# Format value for printing
# Inputs: 0) value
# Returns: string for printing
sub formatValue($)
{
    local $_;
    my $val = shift;
    my ($str, @a);
    if (ref $val eq 'SCALAR') {
        if ($$val =~ /^Binary data/) {
            $str = "($$val)";
        } else {
            $str = '(Binary data ' . length($$val) . ' bytes)';
        }
    } elsif (ref $val eq 'ARRAY') {
        foreach (@$val) {
            push @a, formatValue($_);
        }
        $str = '[' . join(',', @a) . ']';
    } elsif (ref $val eq 'HASH') {
        my $key;
        foreach $key (sort keys %$val) {
            push @a, $key . '=' . formatValue($$val{$key});
        }
        $str = '{' . join(',', @a) . '}';
    } elsif (defined $val) {
        # make sure there are no linefeeds in output
        ($str = $val) =~ tr/\x0a\x0d/;/;
        # translate unknown characters
       # $str =~ tr/\x01-\x1f\x80-\xff/\./;
        $str =~ tr/\x01-\x1f\x7f/./;
        # remove NULL chars
        $str =~ s/\x00//g;
    } else {
        $str = '';
    }
    return $str;
}

#------------------------------------------------------------------------------
# Compare extracted information against a standard output file
# Inputs: 0) [optional] ExifTool object reference
#         1) tag hash reference, 2) test name, 3) test number
#         4) test number for comparison file (if different than this test)
#         5) top group number to test (2 by default)
# Returns: 1 if check passed
sub check($$$;$$$)
{
    my $exifTool = shift if ref $_[0] ne 'HASH';
    my ($info, $testname, $testnum, $stdnum, $topGroup) = @_;
    return 0 unless $info;
    $stdnum = $testnum unless defined $stdnum;
    my $testfile = "t/${testname}_$testnum.failed";
    my $stdfile = "t/${testname}_$stdnum.out";
    open(FILE, ">$testfile") or return 0;
    
    # use one type of linefeed so this test works across platforms
    my $oldSep = $\;
    $\ = "\x0a";        # set output line separator
    
    # get a list of found tags
    my @tags;
    if ($exifTool) {
        if ($$exifTool{NO_SORT}) {
            @tags = $exifTool->GetFoundTags();
        } else {
            # sort tags by group to make it a bit prettier
            @tags = $exifTool->GetTagList($info, 'Group0');
        }
    } else {
        @tags = sort keys %$info;
    }
#
# Write information to file (with filename "TESTNAME_#.failed")
#
    foreach (@tags) {
        my $val = formatValue($$info{$_});
        # (no "\n" needed since we set the output line separator above)
        if ($exifTool) {
            my @groups = $exifTool->GetGroup($_);
            my $groups = join ', ', @groups[0..($topGroup||2)];
            my $tagID = $exifTool->GetTagID($_);
            my $desc = $exifTool->GetDescription($_);
            print FILE "[$groups] $tagID - $desc: $val";
        } else {
            print FILE "$_: $val";
        }
    }
    close(FILE);
    
    $\ = $oldSep;       # restore output line separator
#
# Compare the output file to the output from the standard test (TESTNAME_#.out)
#
    return testCompare($stdfile, $testfile, $testnum);
}

#------------------------------------------------------------------------------
# Test writing feature by writing specified information to JPEG file
# Inputs: 0) list reference to lists of SetNewValue arguments
#         1) test name, 2) test number, 3) optional source file name,
#         4) true to only check tags which were written (or list ref for tags to check)
#         5) flag set if nothing is expected to change in the output file
#         6) true to ignore warnings
# Returns: 1 if check passed
sub writeCheck($$$;$$$$)
{
    my ($writeInfo, $testname, $testnum, $srcfile, $onlyWritten, $same, $ignore) = @_;
    $srcfile or $srcfile = "t/images/$testname.jpg";
    my ($ext) = ($srcfile =~ /\.(.+?)$/);
    my $testfile = "t/${testname}_${testnum}_failed.$ext";
    my $exifTool = Image::ExifTool->new;
    my @tags;
    if (ref $onlyWritten eq 'ARRAY') {
        @tags = @$onlyWritten;
        undef $onlyWritten;
    }
    foreach (@$writeInfo) {
        $exifTool->SetNewValue(@$_);
        push @tags, $$_[0] if $onlyWritten;
    }
    unlink $testfile;
    my $ok = writeInfo($exifTool, $srcfile, $testfile, $same, $ignore);
    my $info = $exifTool->ImageInfo($testfile,{Duplicates=>1,Unknown=>1},@tags);
    my $rtnVal = check($exifTool, $info, $testname, $testnum);
    return 0 unless $ok and $rtnVal;
    unlink $testfile;
    return 1;
}

#------------------------------------------------------------------------------
# Call Image::ExifTool::WriteInfo with error checking
# Inputs: 0) ExifTool ref, 1) src file, 2) dst file, 3) true if nothing should change
#         4) true to ignore warnings
# Return: true on success
sub writeInfo($$;$$$)
{
    my ($exifTool, $src, $dst, $same, $ignore) = @_;
    # erase temporary file created by WriteInfo() if no destination file is given
    # (may be left over from previous crashed tests)
    unlink "${src}_exiftool_tmp" if not defined $dst and not ref $src;
    my $result = $exifTool->WriteInfo($src, $dst);
    my $err = '';
    $err .= "  Error: WriteInfo() returned $result\n" if $result != ($same ? 2 : 1);
    my $info = $exifTool->GetInfo('Warning', 'Error');
    foreach (sort keys %$info) {
        next if $ignore and $_ =~ /^Warning/;
        my $tag = Image::ExifTool::GetTagName($_);
        $err .= "  $tag: $$info{$_}\n";
    }
    return 1 unless $err;
    warn "\n$err";
    return 0;
}

#------------------------------------------------------------------------------
# Test verbose output
# Inputs: 0) test name, 1) test number, 2) Input file, 3) verbose level
# Returns: true if test passed
sub testVerbose($$$$)
{
    my ($testname, $testnum, $infile, $verbose) = @_;
    my $testfile = "t/${testname}_$testnum";
    # capture verbose output by redirecting STDOUT
    return 0 unless open(TMPFILE,">$testfile.tmp");
    ImageInfo($infile, { Verbose => $verbose, TextOut => \*TMPFILE });
    close(TMPFILE);
    # re-write output file to change newlines to be same as standard test file
    # (if I was a Perl guru, maybe I would know a better way to do this)
    open(TMPFILE,"$testfile.tmp");
    open(TESTFILE,">$testfile.failed");
    my $oldSep = $\;
    $\ = "\x0a";        # set output line separator
    while (<TMPFILE>) {
        chomp;          # remove existing newline
        print TESTFILE $_;  # re-write line using \x0a for newlines
    }
    $\ = $oldSep;       # restore output line separator
    close(TESTFILE);
    close(TMPFILE);
    unlink("$testfile.tmp");
    return testCompare("$testfile.out","$testfile.failed",$testnum);
}

#------------------------------------------------------------------------------
# One of the tests failed
sub notOK()
{
    print 'not ';
    $rtnCode = 1;
}

#------------------------------------------------------------------------------
# Done tests and exit
sub done()
{
    exit $rtnCode;
}

1; #end