################################################################################
#
# Copyright (c) 2002-2015 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;
$^W = 1;
BEGIN { plan tests => 116 }
my $CCCFG = require 'tests/include/config.pl';
#===================================================================
# create object (1 tests)
#===================================================================
eval { $p = new Convert::Binary::C };
ok($@,'',"failed to create Convert::Binary::C object");
#===================================================================
# try to parse empty file / empty code (2 tests)
#===================================================================
eval { $p->parse_file( 'tests/include/files/empty.h' ) };
ok($@,'',"failed to parse empty C-file");
eval { $p->parse( '' ) };
ok($@,'',"failed to parse empty C-code");
#===================================================================
# check that parse/parse_file return object references (4 tests)
#===================================================================
$p = eval { Convert::Binary::C->new->parse_file( 'tests/include/files/empty.h' ) };
ok($@,'',"failed to create Convert::Binary::C object");
ok(ref $p, 'Convert::Binary::C',
"object reference not blessed to Convert::Binary::C");
$p = eval { Convert::Binary::C->new->parse( '' ) };
ok($@,'',"failed to create Convert::Binary::C object");
ok(ref $p, 'Convert::Binary::C',
"object reference not blessed to Convert::Binary::C");
#===================================================================
# create object (1 tests)
#===================================================================
eval {
$p = new Convert::Binary::C %$CCCFG,
EnumSize => 0,
ShortSize => 2,
IntSize => 4,
LongSize => 4,
LongLongSize => 8,
PointerSize => 4,
FloatSize => 4,
DoubleSize => 8,
LongDoubleSize => 12;
};
ok($@,'',"failed to create Convert::Binary::C object");
#===================================================================
# try to parse a file with lots of includes (1 test)
#===================================================================
eval {
$p->parse_file( 'tests/include/include.c' );
};
ok($@,'',"failed to parse C-file");
#===================================================================
# check if context is correctly evaluated (10 tests)
# also do a quick check if the right stuff was parsed (12 tests)
#===================================================================
@enums = $p->enum;
@compounds = $p->compound;
@structs = $p->struct;
@unions = $p->union;
@typedefs = $p->typedef;
$s1 = @enums; $s2 = $p->enum;
ok($s1,$s2,"context not evaluated correctly in 'enum'");
ok($s1,35,"incorrect number of enums");
$s1 = @compounds; $s2 = $p->compound;
ok($s1,$s2,"context not evaluated correctly in 'compound'");
ok($s1,287,"incorrect number of compounds");
map {
push @{$_->{type} eq 'union' ? \@r_unions : \@r_structs}, $_
} @compounds;
$s1 = @structs; $s2 = $p->struct;
ok($s1,$s2,"context not evaluated correctly in 'struct'");
$s2 = @r_structs;
ok($s1,$s2,"direct/indirect counts differ in 'struct'");
ok($s1,200,"incorrect number of structs");
$s1 = @unions; $s2 = $p->union;
ok($s1,$s2,"context not evaluated correctly in 'union'");
$s2 = @r_unions;
ok($s1,$s2,"direct/indirect counts differ in 'union'");
ok($s1,87,"incorrect number of unions");
$s1 = @typedefs; $s2 = $p->typedef;
ok($s1,$s2,"context not evaluated correctly in 'typedef'");
ok($s1,334,"incorrect number of typedefs");
@enum_ids = $p->enum_names;
@compound_ids = $p->compound_names;
@struct_ids = $p->struct_names;
@union_ids = $p->union_names;
@typedef_ids = $p->typedef_names;
$s1 = @enum_ids; $s2 = $p->enum_names;
ok($s1,$s2,"context not evaluated correctly in 'enum_names'");
ok($s1,4,"incorrect number of enum identifiers");
$s1 = @compound_ids; $s2 = $p->compound_names;
ok($s1,$s2,"context not evaluated correctly in 'compound_names'");
ok($s1,146,"incorrect number of compound identifiers");
$s1 = @struct_ids; $s2 = $p->struct_names;
ok($s1,$s2,"context not evaluated correctly in 'struct_names'");
ok($s1,141,"incorrect number of struct identifiers");
$s1 = @union_ids; $s2 = $p->union_names;
ok($s1,$s2,"context not evaluated correctly in 'union_names'");
ok($s1,5,"incorrect number of union identifiers");
$s1 = @typedef_ids; $s2 = $p->typedef_names;
ok($s1,$s2,"context not evaluated correctly in 'typedef_names'");
ok($s1,329,"incorrect number of typedef identifiers");
# catch warnings
#===================================================================
# check if all sizes are correct (1 big test)
#===================================================================
do 'tests/include/sizeof.pl';
$max_size = 0;
@fail = ();
@success = ();
$SIG{__WARN__} = sub {
print "# unexpected warning: $_[0]";
push @fail, $_[0];
};
for my $t ( keys %size ) {
eval { $s = $p->sizeof($t) };
if( $@ ) {
print "# sizeof failed for '$t': $@\n";
}
elsif( $size{$t} != $s ) {
print "# incorrect size for '$t' (expected $size{$t}, got $s)\n";
}
else {
$max_size = $s if $s > $max_size;
push @success, $t;
next;
}
push @fail, $t unless $s == $size{$t}
}
ok(@fail == 0);
ok(@success > 0);
#===================================================================
# check if the def method works correctly (1 big test)
#===================================================================
$size{'_IO_lock_t'} = undef;
@names = ();
@fail = ();
push @names, map { { type => qr/^enum$/, id => $_ } }
map { $_->{identifier} || () } $p->enum;
push @names, map { { type => qr/^(?:struct|union)$/, id => $_ } }
map { $_->{identifier} || () } $p->compound;
push @names, map { { type => qr/^struct$/, id => $_ } }
map { $_->{identifier} || () } $p->struct;
push @names, map { { type => qr/^union$/, id => $_ } }
map { $_->{identifier} || () } $p->union;
push @names, map { { type => qr/^typedef$/,
id => $_->{declarator} =~ /(\w+)/ } } $p->typedef;
for( @names ) {
my $d = $p->def( $_->{id} );
unless( defined $d ) {
print "# def( '$_->{id}' ) = undef for existing type\n";
push @fail, $_->{id};
next;
}
if( $d xor exists $size{$_->{id}} ) {
print "# def( '$_->{id}' ) = $d\n";
push @fail, $_->{id};
next;
}
if( $d and not $d =~ $_->{type} ) {
unless( defined $p->$d( $_->{id} ) ) {
print "# def( '$_->{id}' ) = $d ($_->{type})\n";
push @fail, $_->{id};
next;
}
}
}
ok(@fail == 0);
#===================================================================
# and check if we can pack and unpack everything (1 big test)
#===================================================================
sub chkpack
{
my($orig, $pack) = @_;
for( my $i = 0; $i < length $pack; ++$i ) {
my $p = ord substr $pack, $i, 1;
if ($i < length $orig) {
my $o = ord substr $orig, $i, 1;
return 0 unless $p == $o or $p == 0;
}
else {
return 0 unless $p == 0;
}
}
return 1;
}
# don't use random data as it may cause failures
# for floating point values
$data = pack 'C*', map { $_ & 0xFF } 1 .. $max_size;
@fail = ();
for my $id ( @enum_ids, @compound_ids, @typedef_ids )
{
# skip long doubles
next if grep { $id eq $_ } qw( __convert_long_double float_t double_t );
eval { $x = $p->unpack( $id, $data ) };
if( $@ ) {
print "# unpack failed for '$id': $@\n";
push @fail, $id;
next;
}
eval { $packed = $p->pack( $id, $x ) };
if( $@ ) {
print "# pack failed for '$id': $@\n";
push @fail, $id;
next;
}
unless( chkpack( $data, $packed ) ) {
print "# inconsistent pack/unpack data for '$id'\n";
print "# \$data => @{[map { sprintf '%02X', $_ } unpack 'C*', substr $data, 0, $p->sizeof($id)]}\n";
print "# \$x => $x\n";
print "# \$packed => @{[map { sprintf '%02X', $_ } unpack 'C*', $packed]}\n";
push @fail, $id;
}
}
ok(@fail == 0);
#===================================================================
# check member and offsetof (1 big test)
#===================================================================
@fail = ();
foreach $type ( @compound_ids ) {
next unless exists $size{$type};
foreach( 0 .. $size{$type}-1 ) {
eval { $x = $p->member( $type, $_ ) };
if( $@ ) {
print "# member failed for '$type', offset $_: $@\n";
push @fail, $_;
}
if( $x !~ /\+\d+$/ ) {
eval { $o = $p->offsetof( $type, $x ) };
if( $@ ) {
print "# offsetof failed for '$type', member '$x': $@\n";
push @fail, $_;
}
if( $o != $_ ) {
print "# offsetof( '$type', '$x' ) = $o, expected $_\n";
push @fail, $_;
}
}
}
}
ok(@fail == 0);
#===================================================================
# check reference counts (38 tests)
#===================================================================
eval {
%rc = (
configure => $p->configure,
include => $p->Include,
enums_s => scalar $p->enum_names,
enums_a => [$p->enum_names],
compounds_s => scalar $p->compound_names,
compounds_a => [$p->compound_names],
structs_s => scalar $p->struct_names,
structs_a => [$p->struct_names],
unions_s => scalar $p->union_names,
unions_a => [$p->union_names],
typedefs_s => scalar $p->typedef_names,
typedefs_a => [$p->typedef_names],
enum_s => scalar $p->enum,
enum_a => [$p->enum],
compound_s => scalar $p->compound,
compound_a => [$p->compound],
struct_s => scalar $p->struct,
struct_a => [$p->struct],
union_s => scalar $p->union,
union_a => [$p->union],
typedef_s => scalar $p->typedef,
typedef_a => [$p->typedef],
enum_sx => scalar $p->enum( $p->enum_names ),
enum_ax => [$p->enum( $p->enum_names )],
compound_sx => scalar $p->compound( $p->compound_names ),
compound_ax => [$p->compound( $p->compound_names )],
struct_sx => scalar $p->struct( $p->struct_names ),
struct_ax => [$p->struct( $p->struct_names )],
union_sx => scalar $p->union( $p->union_names ),
union_ax => [$p->union( $p->union_names )],
typedef_sx => scalar $p->typedef( $p->typedef_names ),
typedef_ax => [$p->typedef( $p->typedef_names )],
sizeof => $p->sizeof( 'AMT' ),
typeof => $p->typeof( 'AMT.table[2]' ),
offsetof => $p->offsetof( 'AMT', 'table[2]' ),
member_sxx => scalar $p->member( 'AMT', 100 ),
member_axx => [$p->member( 'AMT', 100 )],
member_sx => scalar $p->member( 'AMT' ),
member_ax => [$p->member( 'AMT' )],
dependencies_s => scalar $p->dependencies,
dependencies_a => [$p->dependencies],
sourcify => $p->sourcify,
);
};
ok($@,'',"method call failed");
$debug = Convert::Binary::C::feature( 'debug' );
for( keys %rc ) {
$fail = $succ = 0;
if( $debug ) {
# print "# dumping $_\n";
my $r = Convert::Binary::C::__DUMP__( $rc{$_} );
# print "# checking $_\n";
while( $r =~ /REFCNT\s*=\s*(\d+)/g ) {
if( $1 == 1 ) { $succ++ }
else {
print "# REFCNT = $1, should be 1\n";
$fail++;
}
}
# print "# $_ (succ = $succ, fail = $fail)\n";
}
skip( $debug ? '' : 'no debugging', $fail == 0 && $succ > 0 );
}
#===================================================================
# check parser stack (2 tests)
#===================================================================
my @tests = (
{ level => 4997, error => '' },
{ level => 4998, error => qr/memory exhausted/ },
);
for my $t ( @tests ) {
my $c = new Convert::Binary::C;
my $pre = 'struct { ' x $t->{level};
my $post = ' x; }' x $t->{level};
eval { $c->parse("typedef $pre int $post deep;") };
ok( $@, $t->{error} );
# Don't try to unpack data (or even worse, call $c->typedef('deep')).
# Doing so can result in a stack overflow in perl during destruction.
}
#===================================================================
# check parse error recovery (9 tests)
#===================================================================
$c = new Convert::Binary::C IntSize => 4, EnumSize => 4;
eval {
$c->parse(<<ENDC);
struct foo {
int a, b, c;
};
}}} Whew!
ENDC
};
ok($@, qr/syntax error/);
# however, we should still be able to use 'foo'
$s = eval { $c->sizeof('foo') };
ok($@, '');
ok($s, 12);
$p = eval { $c->pack('foo', { a => 4711, b => -101, c => 42 }) };
ok($@, '');
ok(length $p, $s);
$u = eval { $c->unpack('foo', $p) };
ok($@, '');
ok($u->{a}, 4711);
ok($u->{b}, -101);
ok($u->{c}, 42);
# check that another parse updates parse info
eval {
$c->parse(<<ENDC);
struct bar {
int a;
int b;
};
enum enu { A, B, C };
typedef struct {
int x, y, z;
} t_bar;
ENDC
};
ok($@, '');
$s = eval { $c->sizeof('bar') };
ok($@, '');
ok($s, 8);
# check that all possible methods update the parse info
$c->ByteOrder('BigEndian')->ByteOrder('LittleEndian'); # reset
my $d = $c->clone; # also check cloning here
$s = eval { $c->sizeof('bar') };
ok($@, '');
ok($s, 8);
$s = eval { $d->sizeof('bar') };
ok($@, '');
ok($s, 8);
$c->ByteOrder('BigEndian')->ByteOrder('LittleEndian'); # reset
$s = eval { $c->compound('bar') };
ok($@, '');
ok($s->{size}, 8);
$c->ByteOrder('BigEndian')->ByteOrder('LittleEndian'); # reset
$s = eval { $c->def('bar.a') };
ok($@, '');
ok($s, 'member');
$c->ByteOrder('BigEndian')->ByteOrder('LittleEndian'); # reset
$s = eval { $c->enum('enu') };
ok($@, '');
ok($s->{size}, 4);
$c->ByteOrder('BigEndian')->ByteOrder('LittleEndian'); # reset
$s = eval { $c->initializer('bar', { a => 1, b => 2 }) };
ok($@, '');
ok($s, qr/^\s*\{\s*1,\s*2\s*\}\s*$/);
$c->ByteOrder('BigEndian')->ByteOrder('LittleEndian'); # reset
$s = eval { $c->member('bar', 5) };
ok($@, '');
ok($s, '.b+1');
$c->ByteOrder('BigEndian')->ByteOrder('LittleEndian'); # reset
$s = eval { $c->offsetof('bar', '.b') };
ok($@, '');
ok($s, 4);
$c->ByteOrder('BigEndian')->ByteOrder('LittleEndian'); # reset
$s = eval { $c->pack('bar', { a => 1, b => 2 }) };
ok($@, '');
ok($s, pack('VV', 1, 2));
$c->ByteOrder('BigEndian')->ByteOrder('LittleEndian'); # reset
$s = eval { $c->unpack('bar', pack('VV', 42, 4711)) };
ok($@, '');
ok($s->{a}, 42);
ok($s->{b}, 4711);
$c->ByteOrder('BigEndian')->ByteOrder('LittleEndian'); # reset
$s = eval { $c->typedef('t_bar') };
ok($@, '');
ok($s->{type}{size}, 12);