#line 1 package Test::MockObject; use strict; use warnings; use vars qw( $VERSION $AUTOLOAD ); $VERSION = '1.08'; use Scalar::Util qw( blessed refaddr reftype weaken ); use UNIVERSAL::isa; use UNIVERSAL::can; use Test::Builder; my $Test = Test::Builder->new(); my (%calls, %subs); sub new { my ($class, $type) = @_; $type ||= {}; bless $type, $class; } sub mock { my ($self, $name, $sub) = @_; $sub ||= sub {}; # leading dash means unlog, otherwise do log _set_log( $self, $name, ( $name =~ s/^-// ? 0 : 1 ) ); _subs( $self )->{$name} = $sub; $self; } sub set_isa { my ($self, @supers) = @_; my $supers = _isas( $self ); $supers->{$_} = 1 for @supers; } sub set_always { my ($self, $name, $value) = @_; $self->mock( $name, sub { $value } ); } sub set_true { my $self = shift; for my $name ( @_ ) { $self->mock( $name, sub { 1 } ); } return $self; } sub set_false { my $self = shift; for my $name ( @_ ) { $self->mock( $name, sub {} ); } return $self; } sub set_list { my ($self, $name, @list) = @_; $self->mock( $name, sub { @{[ @list ]} } ); } sub set_series { my ($self, $name, @list) = @_; $self->mock( $name, sub { return unless @list; shift @list } ); } sub set_bound { my ($self, $name, $ref) = @_; my %bindings = ( SCALAR => sub { $$ref }, ARRAY => sub { @$ref }, HASH => sub { %$ref }, ); return unless exists $bindings{reftype( $ref )}; $self->mock( $name, $bindings{reftype( $ref )} ); } # hack around debugging mode being too smart for my sub names my $old_p; BEGIN { $old_p = $^P; $^P &= ~0x200; } BEGIN { for my $universal ( { sub => \&_subs, name => 'can' }, { sub => \&_isas, name => 'isa' } ) { my $sub = sub { my ($self, $sub) = @_; local *__ANON__ = $universal->{name}; # mockmethods are special cases, class methods are handled directly my $lookup = $universal->{sub}->( $self ); return $lookup->{$sub} if blessed $self and exists $lookup->{$sub}; my $parent = 'SUPER::' . $universal->{name}; return $self->$parent( $sub ); }; no strict 'refs'; *{ $universal->{name} } = $sub; } $^P = $old_p; } sub remove { my ($self, $sub) = @_; delete _subs( $self )->{$sub}; $self; } sub called { my ($self, $sub) = @_; for my $called (reverse @{ _calls( $self ) }) { return 1 if $called->[0] eq $sub; } return 0; } sub clear { my $self = shift; @{ _calls( $self ) } = (); $self; } sub call_pos { $_[0]->_call($_[1], 0); } sub call_args { return @{ $_[0]->_call($_[1], 1) }; } sub _call { my ($self, $pos, $type) = @_; my $calls = _calls( $self ); return if abs($pos) > @$calls; $pos-- if $pos > 0; return $calls->[$pos][$type]; } sub call_args_string { my $args = $_[0]->_call( $_[1], 1 ) or return; return join($_[2] || '', @$args); } sub call_args_pos { my ($self, $subpos, $argpos) = @_; my $args = $self->_call( $subpos, 1 ) or return; $argpos-- if $argpos > 0; return $args->[$argpos]; } sub next_call { my ($self, $num) = @_; $num ||= 1; my $calls = _calls( $self ); return unless @$calls >= $num; my ($call) = (splice(@$calls, 0, $num))[-1]; return wantarray() ? @$call : $call->[0]; } sub AUTOLOAD { my $self = shift; my $sub; { local $1; ($sub) = $AUTOLOAD =~ /::(\w+)\z/; } return if $sub eq 'DESTROY'; $self->dispatch_mocked_method( $sub, @_ ); } sub dispatch_mocked_method { my $self = $_[0]; my $sub = splice( @_, 1, 1 ); my $subs = _subs( $self ); if (exists $subs->{$sub}) { $self->log_call( $sub, @_ ); goto &{ $subs->{$sub} }; } else { require Carp; Carp::carp("Un-mocked method '$sub()' called"); } return; } sub log_call { my ($self, $sub, @call_args) = @_; return unless _logs( $self, $sub ); # prevent circular references with weaken for my $arg ( @call_args ) { next unless ref $arg; weaken( $arg ) if refaddr( $arg ) eq refaddr( $self ); } push @{ _calls( $self ) }, [ $sub, \@call_args ]; } sub called_ok { my ($self, $sub, $name) = @_; $name ||= "object called '$sub'"; $Test->ok( $self->called($sub), $name ); } sub called_pos_ok { my ($self, $pos, $sub, $name) = @_; $name ||= "object called '$sub' at position $pos"; my $called = $self->call_pos($pos, $sub); unless ($Test->ok( (defined $called and $called eq $sub), $name )) { $called = 'undef' unless defined $called; $Test->diag("Got:\n\t'$called'\nExpected:\n\t'$sub'\n"); } } sub called_args_string_is { my ($self, $pos, $sep, $expected, $name) = @_; $name ||= "object sent expected args to sub at position $pos"; $Test->is_eq( $self->call_args_string( $pos, $sep ), $expected, $name ); } sub called_args_pos_is { my ($self, $pos, $argpos, $arg, $name) = @_; $name ||= "object sent expected arg '$arg' to sub at position $pos"; $Test->is_eq( $self->call_args_pos( $pos, $argpos ), $arg, $name ); } sub fake_module { my ($class, $modname, %subs) = @_; if ($class->check_class_loaded( $modname ) and ! keys %subs) { require Carp; Carp::croak( "No mocked subs for loaded module '$modname'" ); } $modname =~ s!::!/!g; $INC{ $modname . '.pm' } = 1; my $warn = $SIG{__WARN__}; local $SIG{__WARN__} = sub { $warn->( $_[0] ) unless $_[0] =~ /redefined/ }; { no strict 'refs'; ${ $modname . '::' }{VERSION} ||= -1; } for my $sub (keys %subs) { my $type = reftype( $subs{ $sub } ) || ''; unless ( $type eq 'CODE' ) { require Carp; Carp::carp("'$sub' is not a code reference" ); next; } no strict 'refs'; *{ $_[1] . '::' . $sub } = $subs{ $sub }; } } sub check_class_loaded { my ($self, $class, $load_flag) = @_; (my $path = $class) =~ s{::}{/}g; return 1 if exists $INC{ $path . '.pm' }; my $symtable = \%main::; my $found = 1; for my $symbol ( split( '::', $class )) { unless (exists $symtable->{ $symbol . '::' }) { $found = 0; last; } $symtable = $symtable->{ $symbol . '::' }; } return $found; } sub fake_new { my ($self, $class) = @_; $self->fake_module( $class, new => sub { $self } ); } sub DESTROY { my $self = shift; $self->_clear_calls(); $self->_clear_subs(); $self->_clear_logs(); $self->_clear_isas(); } sub _get_key { my $invocant = shift; return blessed( $invocant ) ? refaddr( $invocant ) : $invocant; } { my %calls; sub _calls { $calls{ _get_key( shift ) } ||= []; } sub _clear_calls { delete $calls{ _get_key( shift ) }; } } { my %subs; sub _subs { $subs{ _get_key( shift ) } ||= {}; } sub _clear_subs { delete $subs{ _get_key( shift ) }; } } { my %logs; sub _set_log { my $key = _get_key( shift ); my ($name, $log) = @_; $logs{$key} ||= {}; if ($log) { $logs{$key}{$name} = 1; } else { delete $logs{$key}{$name}; } } sub _logs { my $key = _get_key( shift ); my ($name) = @_; return exists $logs{$key}{$name}; } sub _clear_logs { delete $logs{ _get_key( shift ) }; } } { my %isas; sub _isas { $isas{ _get_key( shift ) } ||= {}; } sub _clear_isas { delete $isas{ _get_key( shift ) }; } } 1; __END__ #line 884