require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
require 'socket'
require 'phusion_passenger/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
