04local.t   [plain text]


use strict;
use warnings;

use DateTime::TimeZone::Local;
use DateTime::TimeZone::Local::Unix;
use File::Basename qw( basename );
use File::Spec;
use Sys::Hostname qw( hostname );
use Test::More;

plan skip_all => 'HPUX is weird'
    if $^O eq 'hpux';

use lib File::Spec->catdir( File::Spec->curdir, 't' );

BEGIN { require 'check_datetime_version.pl' }

my $IsMaintainer = hostname() =~ /houseabsolute|quasar/ && -d '.svn';
my $CanWriteEtcLocaltime = -w '/etc/localtime' && -l '/etc/localtime';

my @aliases = sort keys %{ DateTime::TimeZone::links() };
my @names = DateTime::TimeZone::all_names();

plan tests => @aliases + @names + 31;


# Ensures that we can load our OS-specific subclass. Otherwise this
# might happen later in an eval, and the error will get lost.
DateTime::TimeZone::Local->_load_subclass();

{
    my %links = DateTime::TimeZone->links();

    for my $alias ( sort @aliases )
    {
        local $ENV{TZ} = $alias;
        my $tz = eval { DateTime::TimeZone::Local->TimeZone() };
        is( $tz->name(), $links{$alias},
            "$alias in \$ENV{TZ} for Local->TimeZone()" );
    }
}

{
    for my $name ( sort @names )
    {
        local $ENV{TZ} = $name;
        my $tz = eval { DateTime::TimeZone::Local->TimeZone() };
        is( $tz->name(), $name,
            "$name in \$ENV{TZ} for Local->TimeZone()" );
    }
}

{
    local $ENV{TZ} = 'this will not work';

    my $tz = DateTime::TimeZone::Local::Unix->FromEnv();
    is( $tz, undef,
        'invalid time zone name in $ENV{TZ} fails' );

    local $ENV{TZ} = '123/456';

    $tz = DateTime::TimeZone::Local::Unix->FromEnv();
    is( $tz, undef,
        'invalid time zone name in $ENV{TZ} fails' );
}

{
    local $ENV{TZ} = 'Africa/Kinshasa';

    my $tz = DateTime::TimeZone::Local::Unix->FromEnv();
    is( $tz->name(), 'Africa/Kinshasa', 'tz object name() is Africa::Kinshasa' );

    local $ENV{TZ} = 0;
    $tz = eval { DateTime::TimeZone::Local->TimeZone() };
    is( $tz->name(), 'UTC',
        "\$ENV{TZ} set to 0 returns UTC" );
}


{
    # This passes the _IsValidName() check but when passed to
    # DT::TZ->new() will throw an exception.
    {
        package Foo;
        use overload '""' => sub { "Foo" },
                     'eq' => sub { "$_[0]" eq "$_[1]" };
    }
    local $ENV{TZ} = bless [], 'Foo';

    DateTime::TimeZone::Local::Unix->FromEnv();
    is( $@, '', 'FromEnv does not leave $@ set' );
}

{
    local $^O = 'DoesNotExist';
    my @err;
    local $SIG{__DIE__} = sub { push @err, shift };

    eval { DateTime::TimeZone::Local->_load_subclass() };

    is_deeply( \@err, [],
               'error loading local time zone module is not seen by __DIE__ handler' );
}

no warnings 'redefine';

SKIP:
{
    skip "/etc/localtime is not a symlink", 6
        unless -l '/etc/localtime';

    local *DateTime::TimeZone::Local::Unix::_Readlink = sub { '/usr/share/zoneinfo/US/Eastern' };

    my $tz;
    eval { $tz = DateTime::TimeZone::Local::Unix->FromEtcLocaltime() };
    is( $@, '', 'valid time zone name in /etc/localtime symlink should not die' );
    is( $tz->name(), 'America/New_York',
        'FromEtchLocaltime() with _Readlink returning /usr/share/zoneinfo/US/Eastern' );

    local *DateTime::TimeZone::Local::Unix::_Readlink = sub { '/usr/share/zoneinfo/Foo/Bar' };

    $tz = DateTime::TimeZone::Local::Unix->FromEtcLocaltime() ;
    is( $@, '', 'valid time zone name in /etc/localtime symlink should not leave $@ set' );
    ok( ! $tz, 'no time zone was found' );


    local *DateTime::TimeZone::Local::Unix::_Readlink = sub { undef };
    local *DateTime::TimeZone::Local::Unix::_FindMatchingZoneinfoFile = sub { 'America/Los_Angeles' };

    eval { $tz = DateTime::TimeZone::Local::Unix->FromEtcLocaltime() };
    is( $@, '', 'fall back to _FindMatchZoneinfoFile if _Readlink finds nothing' );
    is( $tz->name(), 'America/Los_Angeles',
        'FromEtchLocaltime() with _FindMatchingZoneinfoFile returning America/Los_Angeles' );
}

SKIP:
{
    skip "cannot read /etc/sysconfig/clock", 2
        unless -r '/etc/sysconfig/clock' && -f _;

    local *DateTime::TimeZone::Local::Unix::_ReadEtcSysconfigClock = sub { 'US/Eastern' };

    my $tz;
    eval { $tz = DateTime::TimeZone::Local::Unix->FromEtcSysconfigClock() };
    is( $@, '', 'valid time zone name in /etc/sysconfig/clock should not die' );
    is( $tz->name(), 'America/New_York',
        'FromEtcSysConfigClock() with _ReadEtcSysconfigClock returning US/Eastern' );
}

SKIP:
{
    skip "cannot read /etc/default/init", 2
        unless -r '/etc/default/init' && -f _;

    local *DateTime::TimeZone::Local::Unix::_ReadEtcDefaultInit = sub { 'Asia/Tokyo' };

    my $tz;
    eval { $tz = DateTime::TimeZone::Local::Unix->FromEtcDefaultInit() };
    is( $@, '', 'valid time zone name in /etc/default/init should not die' );
    is( $tz->name(), 'Asia/Tokyo',
      'FromEtcDefaultInit with _ReadEtcDefaultInit returning Asia/Tokyo');
}

SKIP:
{
    skip "Cannot run these tests without explicitly knowing local time zone first (only runs on developers' machine)", 6
        unless $IsMaintainer;

    {
        local $ENV{TZ} = '';

        my $tz;
        eval { $tz = DateTime::TimeZone::Local->TimeZone() };
        is( $@, '', 'valid time zone name in /etc/localtime should not die' );
        is( $tz->name(), 'America/Chicago',
            '/etc/localtime should link to America/Chicago' );
    }

    {
        local *DateTime::TimeZone::Local::Unix::FromEtcLocaltime = sub { undef };

        my $tz;
        eval { $tz = DateTime::TimeZone::Local->TimeZone() };
        is( $@, '', 'valid time zone name in /etc/timezone should not die' );
        is( $tz->name(), 'America/Chicago',
            '/etc/timezone should contain America/Chicago' );
    }

    {
        # requires that /etc/default/init contain
        # TZ=Australia/Melbourne to work.
        local *DateTime::TimeZone::Local::Unix::FromEtcLocaltime = sub { undef };
        local *DateTime::TimeZone::Local::Unix::FromEtcTimezone = sub { undef };
        local *DateTime::TimeZone::Local::Unix::FromEtcTIMEZONE = sub { undef };

        my $tz;
        eval { $tz = DateTime::TimeZone::Local->TimeZone() };
        is( $@, '', '/etc/default/init contains TZ=Australia/Melbourne' );
        is( $tz->name(), 'Australia/Melbourne',
            '/etc/default/init should contain Australia/Melbourne' );
    }
}

SKIP:
{
    my $file = '/etc/timezone';

    skip "Cannot write this test unless we can write to /etc/timezone", 1
        unless $IsMaintainer && -w $file;

    open my $fh, '>', $file
        or die "Cannot write to $file: $!";
    print $fh 'Foo/Bar';
    close $fh;

    DateTime::TimeZone::Local::Unix->FromEtcTimezone();
    is( $@, '', 'calling FromEtcTimezone when it contains a bad name should not leave $@ set' );

    open $fh, '>', $file
        or die "Cannot write to $file: $!";
    print $fh 'America/Chicago';
    close $fh;
}

SKIP:
{
    my $file = '/etc/TIMEZONE';

    skip "Cannot write this test unless we can write to /etc/TIMEZONE", 1
        unless $IsMaintainer && -w '/etc';

    open my $fh, '>', $file
        or die "Cannot write to $file: $!";
    print $fh "TZ = Foo/Bar\n";
    close $fh;

    DateTime::TimeZone::Local::Unix->FromEtcTIMEZONE();
    is( $@, '', 'calling FromEtcTIMEZONE when it contains a bad name should not leave $@ set' );

    unlink $file
        or die "Cannot unlink $file: $!";
}

SKIP:
{
    skip "These tests are too dangerous to run on someone else's machine ;)", 5
        unless $IsMaintainer;

    skip "These tests can only be run if we can overwrite /etc/localtime", 5
        unless $CanWriteEtcLocaltime;

    my $tz_file = readlink '/etc/localtime';

    unlink '/etc/localtime' or die "Cannot unlink /etc/localtime: $!";

    require File::Copy;
    File::Copy::copy( '/usr/share/zoneinfo/Asia/Calcutta', '/etc/localtime' )
        or die "Cannot copy /usr/share/zoneinfo/Asia/Calcutta to '/etc/localtime': $!";

    {
        local $ENV{TZ} = '';

        require Cwd;
        my $cwd = Cwd::cwd();

        my $tz;
        eval { $tz = DateTime::TimeZone::Local->TimeZone() };
        is( $@, '', 'copy of zoneinfo file at /etc/localtime' );
        is( $tz->name(), 'Asia/Calcutta',
            '/etc/localtime should be a copy of Asia/Calcutta' );

        is( Cwd::cwd(), $cwd, 'cwd should not change after finding local time zone' );

        $tz = DateTime::TimeZone::Local->TimeZone();
        is( $@, '', 'calling _FindMatchZoneinfoFile does not leave $@ set' );
    }

    {
        local $ENV{TZ} = '';

        # Make sure that a die handler does not break our use of die
        # to escape from File::Find::find()
        local $SIG{__DIE__} = sub { die 'haha'; };

        my $tz;
        eval { $tz = DateTime::TimeZone::Local->TimeZone() };
        is( $tz->name(), 'Asia/Calcutta',
            'a __DIE__ handler did not interfere with our use of File::Find' );
    }

    unlink '/etc/localtime' or die "Cannot unlink /etc/localtime: $!";
    symlink $tz_file, '/etc/localtime'
        or die "Cannot symlink $tz_file to '/etc/localtime': $!";
}

{
    local $ENV{TZ} = 'Australia/Melbourne';
    my $tz = eval { DateTime::TimeZone->new( name => 'local' ) };
    is( $tz->name(), 'Australia/Melbourne',
        q|DT::TZ->new( name => 'local' )| );
}

SKIP:
{
    skip "These tests require File::Temp", 1
        unless require File::Temp;
    skip "These tests require a filesystem which support symlinks", 1
        unless eval { symlink '', '' ; 1 };

    my $tempdir = File::Temp::tempdir( CLEANUP => 1 );

    my $first = File::Spec->catfile( $tempdir, 'first' );
    open my $fh, '>', $first
        or die "Cannot open $first: $!";
    close $fh;

    my $second = File::Spec->catfile( $tempdir, 'second' );
    symlink $first => $second
        or die "Cannot symlink $first => $second: $!";

    my $third = File::Spec->catfile( $tempdir, 'third' );
    symlink $second => $third
        or die "Cannot symlink $first => $second: $!";

    # It seems that on some systems (OSX, others?) the temp directory
    # returned by File::Temp may be a symlink (/tmp is a link to
    # /private/tmp), so when abs_path folows that link, we end up with
    # a different path to the "first" file.
    is( basename( DateTime::TimeZone::Local::Unix->_Readlink( $third ) ),
        basename( $first ),
        '_Readlink follows multiple levels of symlinking' );
}