require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
require 'phusion_passenger/utils/file_system_watcher'

module PhusionPassenger

describe Utils::FileSystemWatcher do
	before :each do
		@tmpdir = "tmp.fs_watcher"
		@tmpdir2 = "tmp.fs_watcher2"
		@tmpdir3 = "tmp.fs_watcher3"
		@term_pipe = IO.pipe
		[@tmpdir, @tmpdir2, @tmpdir3].each do |dir|
			remove_dir_tree(dir)
			Dir.mkdir(dir)
		end
	end
	
	after :each do
		if @thread
			@term_pipe[1].write("x")
			@thread.join
		end
		@watcher.close if @watcher
		@term_pipe[0].close
		@term_pipe[1].close
		[@tmpdir, @tmpdir2, @tmpdir3].each do |dir|
			remove_dir_tree(dir)
		end
	end
	
	def create(*args)
		@watcher = Utils::FileSystemWatcher.new(*args)
		@watcher.poll_interval = 0.1 if @watcher.respond_to?(:poll_interval=)
		return @watcher
	end
	
	describe "#wait_for_change blocks until" do
		def test_block(filenames)
			create(filenames, @term_pipe[0])
			result = nil
			thread = Thread.new do
				result = @watcher.wait_for_change
			end
			yield if block_given?
			eventually do
				!thread.alive?
			end
			return result
		ensure
			if thread
				@term_pipe[1].write("x")
				thread.join
			end
			@watcher.close
		end
		
		specify "a subdirectory has been created in one of the watched directories" do
			result = test_block([@tmpdir, @tmpdir2]) do
				Dir.mkdir("#{@tmpdir}/foo")
			end
			result.should be_true
		end
		
		specify "a subdirectory has been removed in one of the watched directories" do
			Dir.mkdir("#{@tmpdir2}/foo")
			result = test_block([@tmpdir, @tmpdir2]) do
				Dir.rmdir("#{@tmpdir2}/foo")
			end
			result.should be_true
		end
		
		specify "a subdirectory has been renamed in one of the watched directories" do
			Dir.mkdir("#{@tmpdir}/foo")
			result = test_block([@tmpdir, @tmpdir2]) do
				File.rename("#{@tmpdir}/foo", "#{@tmpdir3}/bar")
			end
			result.should be_true
		end
		
		specify "a file has been created in one of the watched directories" do
			result = test_block([@tmpdir, @tmpdir2]) do
				File.touch("#{@tmpdir}/foo")
			end
			result.should be_true
		end
		
		specify "a file has been removed in one of the watched directories" do
			File.touch("#{@tmpdir2}/foo")
			result = test_block([@tmpdir, @tmpdir2]) do
				File.unlink("#{@tmpdir2}/foo")
			end
			result.should be_true
		end
		
		specify "a file has been renamed in one of the watched directories" do
			File.touch("#{@tmpdir}/foo")
			result = test_block([@tmpdir, @tmpdir2]) do
				File.rename("#{@tmpdir}/foo", "#{@tmpdir3}/bar")
			end
			result.should be_true
		end
		
		specify "a watched file has been written to" do
			File.touch("#{@tmpdir}/foo")
			result = test_block(["#{@tmpdir}/foo"]) do
				File.write("#{@tmpdir}/foo", "bar")
			end
		end
		
		specify "a watched file has been truncated" do
			File.write("#{@tmpdir}/foo", "contents")
			result = test_block(["#{@tmpdir}/foo"]) do
				if RUBY_PLATFORM =~ /darwin/
					# OS X kernel bug in kqueue... sigh...
					File.open("#{@tmpdir}/foo", "w") do |f|
						f.write("a")
						f.truncate(0)
					end
				else
					File.open("#{@tmpdir}/foo", "w").close
				end
			end
		end
		
		specify "a watched file has been removed" do
			File.touch("#{@tmpdir}/foo")
			result = test_block(["#{@tmpdir}/foo"]) do
				File.unlink("#{@tmpdir}/foo")
			end
		end
		
		specify "a watched file has been renamed" do
			File.touch("#{@tmpdir}/foo")
			result = test_block(["#{@tmpdir}/foo"]) do
				File.rename("#{@tmpdir}/foo", "#{@tmpdir}/bar")
			end
		end
		
		specify "the termination pipe became readable" do
			result = test_block([@tmpdir]) do
				@term_pipe[1].write("x")
			end
			result.should be_nil
		end
		
		specify "one of the watched files or directories could not be statted while constructing the object" do
			test_block([@tmpdir, "#{@tmpdir}/foo"]).should be_false
			
			when_not_running_as_root do
				Dir.mkdir("#{@tmpdir}/foo")
				File.touch("#{@tmpdir}/foo/file")
				Dir.mkdir("#{@tmpdir}/foo/dir")
				File.chmod(0000, "#{@tmpdir}/foo")
				
				test_block([@tmpdir, "#{@tmpdir}/foo/file"]).should be_false
				test_block([@tmpdir, "#{@tmpdir}/foo/dir"]).should be_false
			end
		end
		
		if Utils::FileSystemWatcher.opens_files?
		when_not_running_as_root do
			specify "one of the watched files or directories could not be opened while constructing the object" do
				File.touch("#{@tmpdir}/file")
				File.chmod(0000, "#{@tmpdir}/file")
				test_block([@tmpdir, "#{@tmpdir}/file"]).should be_false
				
				Dir.mkdir("#{@tmpdir}/dir")
				File.chmod(0000, "#{@tmpdir}/dir")
				test_block([@tmpdir, "#{@tmpdir}/dir"]).should be_false
			end
		end
		end # if
	end
	
	describe "#wait_for_change does not return if" do
		def test_block(filenames)
			create(filenames, @term_pipe[0])
			@thread = Thread.new do
				@watcher.wait_for_change
			end
			yield
			should_never_happen(0.4) do
				!@thread.alive?
			end
		end
		
		specify "nothing happened in one of its watched files or directories" do
			test_block([@tmpdir, @tmpdir2]) do
				File.touch("#{@tmpdir3}/file")
				Dir.mkdir("#{@tmpdir3}/dir")
			end
		end
		
		specify "something happened in a subdirectory that isn't on the watch list" do
			# In other words it does not watch subdirectories recursively.
			Dir.mkdir("#{@tmpdir}/subdir")
			test_block([@tmpdir, @tmpdir2]) do
				File.touch("#{@tmpdir}/subdir/file")
			end
		end
		
		specify "a file in a watched directory is merely modified" do
			File.touch("#{@tmpdir}/hello", 10)
			test_block([@tmpdir, @tmpdir2]) do
				File.touch("#{@tmpdir}/hello", 4567)
				File.write("#{@tmpdir}/hello", "foobar")
			end
		end
	end
	
	specify "#wait_for_change notices events that have occurred after object construction but before #wait_for_change has been called" do
		create([@tmpdir, @tmpdir2], @term_pipe[0])
		@thread = Thread.new do
			@watcher.wait_for_change
		end
		File.touch("#{@tmpdir}/foo", Time.now - 10)
		eventually do
			!@thread.alive?
		end
	end
	
	it "can be closed multiple times" do
		create([@tmpdir])
		@watcher.close
		@watcher.close
	end
end

end # module PhusionPassenger
