require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
require 'socket'
PhusionPassenger.require_passenger_lib 'message_channel'

module PhusionPassenger

describe MessageChannel do
  describe "scenarios with a single channel" do
    before :each do
      @reader_pipe, @writer_pipe = IO.pipe
      @reader = MessageChannel.new(@reader_pipe)
      @writer = MessageChannel.new(@writer_pipe)
    end

    after :each do
      @reader_pipe.close unless @reader_pipe.closed?
      @writer_pipe.close unless @writer_pipe.closed?
    end

    it "can read a single written array message" do
      @writer.write("hello")
      @reader.read.should == ["hello"]
    end

    it "can handle array messages that contain spaces" do
      @writer.write("hello world", "! ")
      @reader.read.should == ["hello world", "! "]
    end

    it "can handle array messages that have only a single empty string" do
      @writer.write("")
      @reader.read.should == [""]
    end

    it "can handle array messages with empty arguments" do
      @writer.write("hello", "", "world")
      @reader.read.should == ["hello", "", "world"]

      @writer.write("")
      @reader.read.should == [""]

      @writer.write(nil, "foo")
      @reader.read.should == ["", "foo"]
    end

    it "properly detects end-of-file when reading an array message" do
      @writer.close
      @reader.read.should be_nil
    end

    specify "#read_hash works" do
      @writer.write("hello", "world")
      @reader.read_hash.should == { "hello" => "world" }

      @writer.write("hello", "world", "foo", "bar", "", "...")
      @reader.read_hash.should == { "hello" => "world", "foo" => "bar", "" => "..." }
    end

    specify "#read_hash throws an exception if the array message doesn't have an even number of items" do
      @writer.write("foo")
      lambda { @reader.read_hash }.should raise_error(MessageChannel::InvalidHashError)

      @writer.write("foo", "bar", "baz")
      lambda { @reader.read_hash }.should raise_error(MessageChannel::InvalidHashError)
    end

    it "can read a single written scalar message" do
      @writer.write_scalar("hello world")
      @reader.read_scalar.should == "hello world"
    end

    it "can handle empty scalar messages" do
      @writer.write_scalar("")
      @reader.read_scalar.should == ""
    end

    it "properly detects end-of-file when reading a scalar message" do
      @writer.close
      @reader.read_scalar.should be_nil
    end

    it "puts the data into the given buffer" do
      buffer = ''
      @writer.write_scalar("x" * 100)
      result = @reader.read_scalar(buffer)
      result.object_id.should == buffer.object_id
      buffer.should == "x" * 100
    end

    it "raises SecurityError when a received scalar message's size is larger than a specified maximum" do
      @writer.write_scalar(" " * 100)
      lambda { @reader.read_scalar('', 99) }.should raise_error(SecurityError)
    end
  end

  describe "scenarios with 2 channels and 2 concurrent processes" do
    after :each do
      @parent_socket.close
      Process.waitpid(@pid) rescue nil
    end

    def spawn_process
      @parent_socket, @child_socket = UNIXSocket.pair
      @pid = fork do
        @parent_socket.close
        @channel = MessageChannel.new(@child_socket)
        begin
          yield
        rescue Exception => e
          print_exception("child", e)
        ensure
          @child_socket.close
          exit!
        end
      end
      @child_socket.close
      @channel = MessageChannel.new(@parent_socket)
    end

    it "both processes can read and write a single array message" do
      spawn_process do
        x = @channel.read
        @channel.write("#{x[0]}!")
      end
      @channel.write("hello")
      @channel.read.should == ["hello!"]
    end

    it "can handle scalar messages with arbitrary binary data" do
      garbage_files = ["garbage1.dat", "garbage2.dat", "garbage3.dat"]
      spawn_process do
        garbage_files.each do |name|
          data = File.binread("stub/#{name}")
          @channel.write_scalar(data)
        end
      end

      garbage_files.each do |name|
        data = File.binread("stub/#{name}")
        @channel.read_scalar.should == data
      end
    end

    it "supports IO object (file descriptor) passing" do
      spawn_process do
        writer = @channel.recv_io
        writer.write("it works")
        writer.close
      end
      reader, writer = IO.pipe
      @channel.send_io(writer)
      writer.close
      reader.read.should == "it works"
      reader.close
    end

    it "supports large amounts of data" do
      iterations = 1000
      blob = "123" * 1024
      spawn_process do
        iterations.times do |i|
          @channel.write(blob)
        end
      end
      iterations.times do
        @channel.read.should == [blob]
      end
    end

    it "has stream properties" do
      garbage = File.binread("stub/garbage1.dat")
      spawn_process do
        @channel.write("hello", "world")
        @channel.write_scalar(garbage)
        @channel.send_io(STDIN)
        @channel.write_scalar(":-)")

        a = @channel.read_scalar
        b = @channel.read
        b << a
        @channel.write(*b)
      end
      @channel.read.should == ["hello", "world"]
      @channel.read_scalar.should == garbage
      @channel.recv_io.close
      @channel.read_scalar.should == ":-)"

      @channel.write_scalar("TASTE MY WRATH! ULTIMATE SWORD TECHNIQUE!! DRAGON'S BREATH SL--")
      @channel.write("Uhm, watch your step.", "WAAHH?!", "Calm down, Motoko!!")
      @channel.read.should == ["Uhm, watch your step.", "WAAHH?!", "Calm down, Motoko!!",
        "TASTE MY WRATH! ULTIMATE SWORD TECHNIQUE!! DRAGON'S BREATH SL--"]
    end
  end
end

end # module PhusionPassenger
