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