#!/usr/bin/perl -w
# vim: ts=2 sw=2 filetype=perl expandtab

use strict;

use Test::More tests => 17;
use POE::Pipe::OneWay;

BEGIN { use_ok("POE::Driver::SysRW") }

# Start with some errors.

eval { my $d = POE::Driver::SysRW->new( BlockSize => 0 ) };
ok(
  $@ && $@ =~ /BlockSize must be greater than 0/,
  "disallow zero or negative block sizes"
);

eval { my $d = POE::Driver::SysRW->new( 0 ) };
ok(
  $@ && $@ =~ /requires an even number of parameters/,
  "disallow zero or negative block sizes"
);

eval { my $d = POE::Driver::SysRW->new( Booga => 1 ) };
ok(
  $@ && $@ =~ /unknown parameter.*Booga/,
  "disallow unknown parameters"
);

# This block of tests also exercises the driver with its default
# constructor parameters.

{ my $d = POE::Driver::SysRW->new();

  use Symbol qw(gensym);
  my $fh = gensym();
  open $fh, ">deleteme.now" or die $!;

  $! = 0;

  open(SAVE_STDERR, ">&STDERR") or die $!;
  close(STDERR) or die $!;

  my $get_ret = $d->get($fh);
  ok(!defined($get_ret), "get() returns undef on error");
  ok($!, "get() sets \$! on error ($!)");

  open(STDERR, ">&SAVE_STDERR") or die $!;
  close(SAVE_STDERR) or die $!;

  close $fh;
  unlink "deleteme.now";
}

my $d = POE::Driver::SysRW->new( BlockSize => 1024 );

# Empty put().

{ my $octets_left = $d->put([ ]);
  ok( $octets_left == 0, "buffered 0 octets on empty put()" );
}

ok( $d->get_out_messages_buffered() == 0, "no messages buffered" );

# The number of octets we expect in the driver's put() buffer.
my $expected = 0;

# Put() returns the correct number of octets.

{ my $string_to_put = "test" x 10;
  my $length_to_put = length($string_to_put);
  $expected += $length_to_put;

  my $octets_left = $d->put([ $string_to_put ]);
  ok(
    $octets_left == $expected,
    "first put: buffer contains $octets_left octets (should be $expected)"
  );
}

# Only one message buffered.

ok( $d->get_out_messages_buffered() == 1, "one message buffered" );

# Put() returns the correct number of octets on a subsequent call.

{ my $string_to_put = "more test" x 5;
  my $length_to_put = length($string_to_put);
  $expected += $length_to_put;

  my $octets_left = $d->put([ $string_to_put ]);
  ok(
    $octets_left == $expected,
    "second put: buffer contains $octets_left octets (should be $expected)"
  );
}

# Remaining tests require some live handles.

my ($r, $w) = POE::Pipe::OneWay->new();
die "can't open a pipe: $!" unless $r;

nonblocking($w);
nonblocking($r);

# Number of flushed octets == number of read octets.

{ my ($flushed_count, $full) = write_until_pipe_is_full($d, $w);
  my ($read_count)           = read_until_pipe_is_empty($d, $r);

  ok(
    $flushed_count == $read_count,
    "flushed $flushed_count octets == read $read_count octets"
  );
}

# Flush the buffer and the pipe.

while (flush_remaining_buffer($d, $w)) {
  read_until_pipe_is_empty($d, $r);
}

{
  my $out_messages = $d->get_out_messages_buffered();
  ok($out_messages == 0, "buffer exhausted (got $out_messages wanted 0)");
}

# Get() returns undef ($! == 0) on EOF.

{ write_until_pipe_is_full($d, $w);
  close($w);

  open(SAVE_STDERR, ">&STDERR") or die $!;
  close(STDERR) or die $!;

  while (1) {
    $! = 1;
    last unless defined $d->get($r);
  }

  pass("driver returns undef on eof");
  ok($! == 0, "\$! is clear on eof");

  open(STDERR, ">&SAVE_STDERR") or die $!;
  close(SAVE_STDERR) or die $!;
}

# Flush() returns the number of octets remaining, and sets $! to
# nonzero on major error.

{ open(SAVE_STDERR, ">&STDERR") or die $!;
  close(STDERR) or die $!;

  # Make sure $w is closed.  Sometimes, like on Cygwin, it isn't.
  close $w;

  $! = 0;
  my $error_left = $d->flush($w);

  ok($error_left, "put() returns octets left on error");
  ok($!, "put() sets \$! nonzero on error");

  open(STDERR, ">&SAVE_STDERR") or die $!;
  close(SAVE_STDERR) or die $!;
}

exit 0;

# Buffer data, and flush it, until the pipe refuses to hold more data.
# This should also cause the driver to experience an EAGAIN or
# EWOULDBLOCK on write.

sub write_until_pipe_is_full {
  my ($driver, $handle) = @_;

  # Hopefully bigger than any system buffer ever.
  my $big_chunk = "*" x (1024 * 1024);

  my $flushed   = 0;
  my $full      = 0;

  while (1) {
    # Put a big chunk into the buffer.
    my $buffered = $driver->put([ $big_chunk ]);

    # Try to flush it.
    my $after_flush = $driver->flush($handle);

    # How much was flushed?
    $flushed += $buffered - $after_flush;

    # If there's data left, then this flush failed.
    last if $after_flush;
  }

  if (wantarray) {
    return ($flushed, $full);
  }
  return $flushed;
}

# Assume the driven has buffered data.  This makes sure it's flushed,
# or at least the pipe is clogged.  Combine it with
# read_until_pipe_is_empty() to flush the driver and the pipe.

sub flush_remaining_buffer {
  my ($driver, $handle) = @_;

  my $before_flush = $driver->get_out_messages_buffered();
  $driver->flush($handle);
  return $before_flush;
}

# Read until there's nothing left to read from the pipe.  This should
# exercise the driver's EAGAIN/EWOULDBLOCK code on the read side.

sub read_until_pipe_is_empty {
  my ($driver, $handle) = @_;

  my $read_octets = 0;

  # SunOS catalogue1 5.11 snv_101b i86pc i386 i86pc
  # Sometimes returns "empty" when there's data in the pipe.
  # Looping again seems to fetch the remaining data, though.
  for (1..3) {
    while (1) {
      my $data = $driver->get($handle);
      last unless defined($data) and @$data;
      $read_octets += length() foreach @$data;
    }
  }

  return $read_octets;
}

# Portable nonblocking sub.  blocking(0) doesn't do it all the time,
# everywhere, and it sucks.
#
# This sub sucks, too.  The code is lifted almost verbatim from
# POE::Resource::FileHandles.  That code should probably be made a
# library function, but where should it go?

sub nonblocking {
  my $handle = shift;

  # For DOSISH systems like OS/2.  Wrapped in eval{} in case it's a
  # tied handle that doesn't support binmode.
  eval { binmode *$handle };

  # Turn off blocking.
  eval { $handle->blocking(0); $handle->blocking(); };

  # Turn off buffering.
  CORE::select((CORE::select($handle), $| = 1)[0]);
}
