################################################################################
#
# $Project: /Convert-Binary-C $
# $Author: mhx $
# $Date: 2011/04/10 12:32:21 +0200 $
# $Revision: 40 $
# $Source: /tests/201_config.t $
#
################################################################################
#
# Copyright (c) 2002-2011 Marcus Holland-Moritz. All rights reserved.
# This program is free software; you can redistribute it and/or modify
# it under the same terms as Perl itself.
#
################################################################################

use Test;
use Convert::Binary::C @ARGV;

use constant SUCCEED => 1;
use constant FAIL    => 0;

$^W = 1;

BEGIN { plan tests => 2070 }

$debug = Convert::Binary::C::feature( 'debug' );

ok( defined $debug );

$RDBG = $debug ? '' : 'no debugging';

# passing references as options is not legal, so this is
# always checked for non-list options
@refs = (
  { in =>  [12], result => FAIL },
  { in =>  \123, result => FAIL },
  { in => {1,2}, result => FAIL },
);

$thisfile = quotemeta "at $0";

sub check_config
{
  my $opt = ref($_[0]) eq 'HASH' ? shift : {};
  my $reason = $opt->{skip} || '';
  my $option = shift;
  my $value;


  for my $config ( @_ ) {
    my @warn;

    {
      local $SIG{__WARN__} = sub { push @warn, shift };

      my $reference = $config->{out} || $config->{in};

      eval { $p = new Convert::Binary::C };
      skip($reason, $@, '', "failed to create Convert::Binary::C object");

      print "# \$p->configure( $option => $config->{in} )\n";
      eval { $p->configure( $option => $config->{in} ) };
      if( $@ ) {
        my $err = $@;
        $err =~ s/^/#   /g;
        print "# failed due to:\n$err";
      }
      skip( $reason, ($@ eq '' ? SUCCEED : FAIL), $config->{result},
            "$option => $config->{in}" );
      skip( $reason, $@, qr/$option must be.*not.*$thisfile/ ) if $config->{result} == FAIL;

      print "# \$p->$option( $config->{in} )\n";
      eval { $p->$option( $config->{in} ) };
      if( $@ ) {
        my $err = $@;
        $err =~ s/^/#   /g;
        print "# failed due to:\n$err";
      }
      skip( $reason, ($@ eq '' ? SUCCEED : FAIL), $config->{result},
            "$option => $config->{in}" );
      skip( $reason, $@, qr/$option must be.*not.*$thisfile/ ) if $config->{result} == FAIL;

      if( $config->{result} == SUCCEED ) {
        print "# \$value = \$p->configure( $option )\n";
        eval { $value = $p->configure( $option ) };
        skip( $reason, $@, '', "cannot get value for '$option' via configure" );
        skip( $reason, $value, $reference, "invalid value for '$option' via configure" );

        print "# \$value = \$p->$option\n";
        eval { $value = $p->$option() };
        skip( $reason, $@, '', "cannot get value for '$option' via $option" );
        skip( $reason, $value, $reference, "invalid value for '$option' via $option" );
      }
    }

    if( exists $config->{warnings} ) {
      my $fail = 0;
      for my $warning ( @warn ) {
        print "# $warning";
        my $expected = 0;
        $warning =~ $_ and $expected++ for @{$config->{warnings}};
        $expected == 1 or $fail++;
      }
      skip( $reason, $fail, 0, "unexpected warnings issued for option '$option'" );
    }
    else {
      for my $warning ( @warn ) {
        print "# unexpected warning: $warning";
      }
      skip( $reason, scalar @warn, 0, "warnings issued for option '$option'" );
    }
  }

  print "# \$p->configure( $option )\n";
  my @warn;
  {
    local $SIG{__WARN__} = sub { push @warn, shift };
    eval { $p->configure( $option ) };
  }
  skip( $reason, $@, '', "failed to call configure in void context" );
  if( @warn ) { print "# issued warnings:\n", map "#   $_", @warn }
  skip( $reason, scalar @warn, 1, "invalid number of warnings issued" );
  skip( $reason, $warn[0], qr/Useless use of configure in void context.*$thisfile/ );

  print "# \$p->$option\n";
  @warn = ();
  {
    local $SIG{__WARN__} = sub { push @warn, shift };
    eval { $p->$option() };
  }
  skip( $reason, $@, '', "failed to call $option in void context" );
  if( @warn ) { print "# issued warnings:\n", map "#   $_", @warn }
  skip( $reason, scalar @warn, 1, "invalid number of warnings issued" );
  skip( $reason, $warn[0], qr/Useless use of $option in void context.*$thisfile/ );
}

sub check_config_bool
{
  my $opt = ref($_[0]) eq 'HASH' ? shift : {};
  my $option = shift;

  my @tests = (
     { in =>     0, out => 0, result => SUCCEED },
     { in =>     1, out => 1, result => SUCCEED },
     { in =>  4711, out => 1, result => SUCCEED },
     { in =>   -42, out => 1, result => SUCCEED },
     @refs
  );

  check_config( $opt, $option, @tests );
}

sub check_option_strlist
{
  my $option = shift;
  my @warn;
  my @tests = (
    { in => \4711,             result => FAIL, error => qr/$option wants an array reference/ },
    { in => [],                result => SUCCEED },
    { in => { key => 'val' },  result => FAIL, error => qr/$option wants an array reference/ },
    { in => ['const', 'void'], result => SUCCEED },
  );

  local $SIG{__WARN__} = sub { push @warn, shift };

  for my $config ( @tests ) {
    @warn = ();

    eval { $p = new Convert::Binary::C };
    ok($@, '', "failed to create Convert::Binary::C object");

    print "# \$p->configure( $option => $config->{in} )\n";
    eval { $p->configure( $option => $config->{in} ) };
    if( $@ ) {
      my $err = $@;
      $err =~ s/^/#   /g;
      print "# failed due to:\n$err";
    }
    ok( ($@ eq '' ? SUCCEED : FAIL), $config->{result},
        "$option => $config->{in}" );
    ok( $@, $config->{error} ) if $config->{result} == FAIL;

    print "# \$p->$option( $config->{in} )\n";
    eval { $p->$option( $config->{in} ) };
    if( $@ ) {
      my $err = $@;
      $err =~ s/^/#   /g;
      print "# failed due to:\n$err";
    }
    ok( ($@ eq '' ? SUCCEED : FAIL), $config->{result},
        "$option => $config->{in}" );
    ok( $@, $config->{error} ) if $config->{result} == FAIL;

    if( $config->{result} == SUCCEED ) {
      print "# \$value = \$p->configure( $option )\n";
      eval { $value = $p->configure( $option ) };
      ok( $@, '', "cannot get value for '$option' via configure" );
      ok( "@$value", "@{$config->{in}}", "invalid value for '$option' via configure" );

      print "# \$value = \$p->$option\n";
      eval { $value = $p->$option() };
      ok( $@, '', "cannot get value for '$option' via $option" );
      ok( "@$value", "@{$config->{in}}", "invalid value for '$option' via $option" );
    }

    for my $warning ( @warn ) {
      print "# unexpected warning: $warning";
    }
    ok( scalar @warn, 0, "warnings issued for option '$option'" );
  }

  @warn = ();
  print "# \$p->configure( $option )\n";
  eval { $p->configure( $option ) };
  ok( $@, '', "failed to call configure in void context" );
  if( @warn ) { print "# issued warnings:\n", map "#   $_", @warn }
  ok( scalar @warn, 1, "invalid number of warnings issued" );
  ok( $warn[0], qr/Useless use of configure in void context.*$thisfile/ );

  @warn = ();
  print "# \$p->$option\n";
  eval { $p->$option() };
  ok( $@, '', "failed to call $option in void context" );
  if( @warn ) { print "# issued warnings:\n", map "#   $_", @warn }
  ok( scalar @warn, 1, "invalid number of warnings issued" );
  ok( $warn[0], qr/Useless use of $option in void context.*$thisfile/ );
}

sub check_option_strlist_args {
  my $option = shift;
  my @warn;
  eval {
    $p = new Convert::Binary::C;
    $p->$option( [qw(foo bar)] );
    $p->$option( 'include' );
    $p->$option( qw(a b c) );
    $value = $p->$option();
  };
  ok( $@, '', "failed to call $option with various arguments" );
  if( @warn ) { print "# issued warnings:\n", map "#   $_", @warn }
  ok( scalar @warn, 0, "invalid number of warnings issued" );
  ok( "@$value", "@{[qw(foo bar include a b c)]}", "invalid value for '$option'" );
}

sub compare_config
{
  my($cfg1, $cfg2) = @_;
  my $fail = 0;
  scalar keys %$cfg1 == scalar keys %$cfg2 or $fail++;
  for my $key ( keys %$cfg1 ) {
    if( ref $cfg1->{$key} eq 'ARRAY' ) {
      "@{$cfg1->{$key}}" eq "@{$cfg2->{$key}}" or $fail++;
    }
    elsif( ref $cfg1->{$key} eq 'HASH' ) {
      "@{[sort keys %{$cfg1->{$key}}]}" eq "@{[sort keys %{$cfg1->{$key}}]}" or $fail++;
      for( sort keys %{$cfg1->{$key}} ) {
        if( defined( $cfg1->{$key}{$_} ) != defined( $cfg2->{$key}{$_} ) ) {
          $fail++;
        }
        if( defined( $cfg1->{$key}{$_} ) and defined( $cfg2->{$key}{$_} )
            and $cfg1->{$key}{$_} ne $cfg2->{$key}{$_} ) {
          $fail++;
        }
      }
    }
    else {
      if (defined($cfg1->{$key}) && defined($cfg2->{$key})) {
        $cfg1->{$key} eq $cfg2->{$key} or $fail++;
      }
      else {
        defined($cfg1->{$key}) == defined($cfg2->{$key}) or $fail++;
      }
    }
  }
  return $fail == 0;
}

sub checkrc
{
  my $rc = shift;
  my $fail = 0;
  my $succ = 0;
  while( $rc =~ /SV\s*=\s*(\S+).*?REFCNT\s*=\s*(\d+)/g ) {
    if( $2 == 1 ) {
      $succ++
    }
    elsif ($1 eq 'NULL' && $2 > 10000) { # we hit &PL_sv_undef...
      $succ++
    }
    else {
      print "# REFCNT = $2 for Sv$1, should be 1\n";
      $fail++;
    }
  }
  return $succ > 0 && $fail == 0;
}

@tests = (
  { in => -2,  result => FAIL    },
  { in => -1,  result => SUCCEED },
  { in =>  0,  result => SUCCEED },
  { in =>  1,  result => SUCCEED },
  { in =>  2,  result => SUCCEED },
  { in =>  3,  result => FAIL    },
  { in =>  4,  result => SUCCEED },
  { in =>  5,  result => FAIL    },
  { in =>  6,  result => FAIL    },
  { in =>  7,  result => FAIL    },
  { in =>  8,  result => SUCCEED },
  { in =>  9,  result => FAIL    },
  @refs
);

check_config( 'EnumSize', @tests );

@tests = (
  { in => -1,  result => FAIL    },
  { in =>  0,  result => SUCCEED },
  { in =>  1,  result => SUCCEED },
  { in =>  2,  result => SUCCEED },
  { in =>  3,  result => FAIL    },
  { in =>  4,  result => SUCCEED },
  { in =>  5,  result => FAIL    },
  { in =>  6,  result => FAIL    },
  { in =>  7,  result => FAIL    },
  { in =>  8,  result => SUCCEED },
  { in =>  9,  result => FAIL    },
  @refs
);

check_config( $_, @tests ) for qw( PointerSize
                                   IntSize
                                   CharSize
                                   ShortSize
                                   LongSize
                                   LongLongSize );

@tests = (
  { in => -1, result => FAIL    },
  { in =>  0, result => SUCCEED },
  { in =>  1, result => SUCCEED },
  { in =>  2, result => SUCCEED },
  { in =>  3, result => FAIL    },
  { in =>  4, result => SUCCEED },
  { in =>  5, result => FAIL    },
  { in =>  6, result => FAIL    },
  { in =>  7, result => FAIL    },
  { in =>  8, result => SUCCEED },
  { in =>  9, result => FAIL    },
  { in => 10, result => FAIL    },
  { in => 11, result => FAIL    },
  { in => 12, result => SUCCEED },
  { in => 13, result => FAIL    },
  { in => 14, result => FAIL    },
  { in => 15, result => FAIL    },
  { in => 16, result => SUCCEED },
  { in => 17, result => FAIL    },
  @refs
);

check_config( $_, @tests ) for qw( FloatSize
                                   DoubleSize
                                   LongDoubleSize );

@tests = (
  { in => -1, result => FAIL    },
  { in =>  0, result => SUCCEED },
  { in =>  1, result => SUCCEED },
  { in =>  2, result => SUCCEED },
  { in =>  3, result => FAIL    },
  { in =>  4, result => SUCCEED },
  { in =>  5, result => FAIL    },
  { in =>  6, result => FAIL    },
  { in =>  7, result => FAIL    },
  { in =>  8, result => SUCCEED },
  { in =>  9, result => FAIL    },
  { in => 10, result => FAIL    },
  { in => 11, result => FAIL    },
  { in => 12, result => FAIL    },
  { in => 13, result => FAIL    },
  { in => 14, result => FAIL    },
  { in => 15, result => FAIL    },
  { in => 16, result => SUCCEED },
  { in => 17, result => FAIL    },
  @refs
);

check_config( $_, @tests ) for qw( Alignment CompoundAlignment );

check_config( 'ByteOrder',
  { in => 'BigEndian',    result => SUCCEED },
  { in => 'LittleEndian', result => SUCCEED },
  { in => 'NoEndian',     result => FAIL    },
  @refs
);

check_config( 'EnumType',
  { in => 'Integer', result => SUCCEED },
  { in => 'String',  result => SUCCEED },
  { in => 'Both',    result => SUCCEED },
  { in => 'None',    result => FAIL    },
  @refs
);

check_config_bool( $_ ) for qw( UnsignedBitfields
                                UnsignedChars
                                Warnings
                                HasCPPComments
                                HasMacroVAARGS );

check_option_strlist( $_ ) for qw( Include
                                   Define
                                   Assert
                                   DisabledKeywords );

check_option_strlist_args( $_ ) for qw( Include
                                        Define
                                        Assert);

{
  my @warn;

  eval { require Tie::Hash::Indexed };
  $@ and eval { require Tie::IxHash };
  $@ and push @warn, qr/^Couldn't load a module for member ordering.*$thisfile/;

  @tests = (
     { in =>     0, out => 0, result => SUCCEED, warnings => [] },
     { in =>     1, out => 1, result => SUCCEED, warnings => \@warn },
     { in =>  4711, out => 1, result => SUCCEED, warnings => \@warn },
     { in =>   -42, out => 1, result => SUCCEED, warnings => \@warn },
     @refs
  );

  check_config( 'OrderMembers', @tests );
}

#===================================================================
# check DisabledKeywords option
#===================================================================

eval {
  $p = new Convert::Binary::C;
  $p->configure( DisabledKeywords => ['void', 'foo', 'const'] );
};
ok( $@, qr/Cannot disable unknown keyword 'foo'.*$thisfile/ );

eval {
  $p = new Convert::Binary::C;
  $p->DisabledKeywords( 'void', 'foo', 'const' );
};
ok( $@, qr/DisabledKeywords cannot take more than one argument.*$thisfile/ );

eval {
  $p = new Convert::Binary::C;
  $p->DisabledKeywords( ['auto', 'enum'] );
  $p->DisabledKeywords( ['void', 'while', 'register'] );
};
ok( $@, qr/Cannot disable unknown keyword 'while'.*$thisfile/ );
$kw = $p->DisabledKeywords;
ok( "@$kw", "auto enum", 'DisabledKeywords did not preserve configuration' );

#===================================================================
# check KeywordMap option
#===================================================================

eval {
  $p = new Convert::Binary::C;
  $p->configure( KeywordMap => 5 );
};
ok( $@, qr/KeywordMap wants a hash reference.*$thisfile/ );

eval {
  $p = new Convert::Binary::C;
  $p->configure( KeywordMap => [ __xxx__ => 'foo' ] );
};
ok( $@, qr/KeywordMap wants a hash reference.*$thisfile/ );

eval {
  $p = new Convert::Binary::C;
  $p->KeywordMap( { '' => 'int' } );
};
ok( $@, qr/Cannot use empty string as a keyword.*$thisfile/ );

eval {
  $p = new Convert::Binary::C;
  $p->KeywordMap( { '1_d' => 'int' } );
};
ok( $@, qr/Cannot use '1_d' as a keyword.*$thisfile/ );

eval {
  $p = new Convert::Binary::C;
  $p->KeywordMap( { '_d' => [] } );
};
ok( $@, qr/Cannot use a reference as a keyword.*$thisfile/ );

eval {
  $p = new Convert::Binary::C;
  $p->KeywordMap( { '_d' => 'foo' } );
};
ok( $@, qr/Cannot use 'foo' as a keyword.*$thisfile/ );

eval {
  $p = new Convert::Binary::C;
  $p->KeywordMap( {'__const' => 'const', '__restrict' => undef} );
  $p->KeywordMap( {'__volatile' => 'volatile', '__foo' => 'foo'} );
};
ok( $@, qr/Cannot use 'foo' as a keyword.*$thisfile/ );
$kw = $p->KeywordMap;
ok( "@{[sort keys %$kw]}", "__const __restrict", 'KeywordMap did not preserve configuration' );

#===================================================================
# check invalid configuration
#===================================================================
@tests = (
  { value => [1, 2, 3], result => FAIL, error => qr/Invalid number of arguments to configure.*$thisfile/ },
  { value => [[1], 2],  result => FAIL, error => qr/Option name must be a string, not a reference.*$thisfile/ },
);
foreach $config ( @tests )
{
  eval {
    $p = new Convert::Binary::C;
    $p->configure( @{$config->{value}} );
  };
  ok( ($@ eq '' ? SUCCEED : FAIL), $config->{result},
      "invalid configuration: " . join(', ', @{$config->{value}}) );
  ok( $@, $config->{error} ) if exists $config->{error};
}

#===================================================================
# check invalid option
#===================================================================
eval {
  $p = new Convert::Binary::C;
  $p->configure(
    Something => 'xxx',
    ByteOrder => 'BigEndian',
    EnumSize  => 0,
  );
};
ok( $@, qr/Invalid option 'Something'.*$thisfile/ );

#===================================================================
# check invalid method
#===================================================================
eval {
  $p = new Convert::Binary::C;
  $p->some_method( 1, 2, 3 );
};
ok( $@, qr/Invalid method some_method called.*$thisfile/ );

#===================================================================
# check configure returning the whole configuration
#===================================================================

%config = (
  'KeywordMap' => {},
  'DisabledKeywords' => [],
  'UnsignedBitfields' => 0,
  'UnsignedChars' => 0,
  'CharSize' => 1,
  'ShortSize' => 2,
  'EnumType' => 'Integer',
  'EnumSize' => 4,
  'Include' => [ '/usr/include' ],
  'DoubleSize' => 4,
  'FloatSize' => 4,
  'HasCPPComments' => 1,
  'Alignment' => 1,
  'CompoundAlignment' => 1,
  'Define' => [ 'DEBUGGING', 'FOO=123' ],
  'HasMacroVAARGS' => 1,
  'LongSize' => 4,
  'Warnings' => 0,
  'ByteOrder' => 'LittleEndian',
  'Assert' => [],
  'IntSize' => 4,
  'PointerSize' => 4,
  'LongLongSize' => 8,
  'LongDoubleSize' => 12,
  'OrderMembers' => 0,
  'Bitfields' => { Engine => 'Simple', BlockSize => 2 },
  'StdCVersion' => undef,
  'HostedC' => 0,
);

eval {
  $p = new Convert::Binary::C %config;
  $cfg = $p->configure;
};
ok( $@, '', "failed to retrieve configuration" );

ok( compare_config( \%config, $cfg ) );

#===================================================================
# check option chaining
#===================================================================

%newcfg = (
  'KeywordMap' => {'__signed__' => 'signed', '__restrict' => undef},
  'DisabledKeywords' => ['const', 'register'],
  'UnsignedBitfields' => 1,
  'UnsignedChars' => 1,
  'CharSize' => 2,
  'ShortSize' => 4,
  'EnumType' => 'Both',
  'EnumSize' => 0,
  'Include' => [ '/usr/local/include', '/usr/include', '/include' ],
  'DoubleSize' => 8,
  'FloatSize' => 8,
  'HasCPPComments' => 1,
  'Alignment' => 2,
  'CompoundAlignment' => 4,
  'Define' => [ 'DEBUGGING', 'FOO=123', 'BAR=456' ],
  'HasMacroVAARGS' => 1,
  'LongSize' => 4,
  'Warnings' => 1,
  'ByteOrder' => 'BigEndian',
  'Assert' => [],
  'IntSize' => 4,
  'PointerSize' => 2,
  'LongLongSize' => 8,
  'LongDoubleSize' => 12,
  'OrderMembers' => 0,
  'Bitfields' => { Engine => 'Simple', BlockSize => 4 },
  'StdCVersion' => 199901,
  'HostedC' => undef,
);

@warn = ();

eval {
  local $SIG{__WARN__} = sub { push @warn, shift };

  $p = new Convert::Binary::C %config;

  $p->UnsignedChars( 1 )->configure( ShortSize => 4, EnumType => 'Both', EnumSize => 0 )
    ->Include( ['/usr/local/include'] )->DoubleSize( 8 )
    ->CompoundAlignment( 4 );

  $p->FloatSize( 8 )->Include( qw( /usr/include /include ) )->DisabledKeywords( [qw( const register )] )
    ->Alignment( 2 )->Define( qw( BAR=456 ) )->configure( ByteOrder => 'BigEndian' );

  $p->configure( PointerSize => 2 )->Warnings( 1 )->UnsignedBitfields( 1 )
    ->KeywordMap( {'__signed__' => 'signed', '__restrict' => undef} );

  $p->CharSize(2);

  $p->Bitfields( { BlockSize => 4 } );

  $p->configure(StdCVersion => 199901);

  $p->HostedC(undef);

  $cfg = $p->configure;
};
ok( $@, '', "failed to configure object" );

if( @warn ) { print "# issued warnings:\n", map "#   $_", @warn }
ok( scalar @warn, 0, "invalid number of warnings issued" );

ok( compare_config( \%newcfg, $cfg ) );

$debug and $result = checkrc( Convert::Binary::C::__DUMP__( $cfg ) );
skip( $RDBG, $result );

