Refactor listener specs.

- Introduce `listen_to`, `fixture` and `watch` spec helpers.
- Added docs to spec helper.
- Better separation of fixture setup/teardown and the actual expectation.
- Make line match within 120 characters.
This commit is contained in:
Michael Kessler 2011-09-28 15:06:33 +02:00
parent 14150889c5
commit 5325cbdea1
2 changed files with 359 additions and 252 deletions

View File

@ -2,32 +2,32 @@ require 'spec_helper'
describe Guard::Listener do
describe ".select_and_init" do
describe '.select_and_init' do
before(:each) { @target_os = RbConfig::CONFIG['target_os'] }
after(:each) { RbConfig::CONFIG['target_os'] = @target_os }
it "uses the Darwin listener on Mac OS X" do
it 'uses the Darwin listener on Mac OS X' do
RbConfig::CONFIG['target_os'] = 'darwin10.4.0'
Guard::Darwin.stub(:usable?).and_return(true)
Guard::Darwin.should_receive(:new)
described_class.select_and_init
end
it "uses the Windows listener on Windows" do
it 'uses the Windows listener on Windows' do
RbConfig::CONFIG['target_os'] = 'mingw'
Guard::Windows.stub(:usable?).and_return(true)
Guard::Windows.should_receive(:new)
described_class.select_and_init
end
it "uses the Linux listener on Linux" do
it 'uses the Linux listener on Linux' do
RbConfig::CONFIG['target_os'] = 'linux'
Guard::Linux.stub(:usable?).and_return(true)
Guard::Linux.should_receive(:new)
described_class.select_and_init
end
it "forwards its arguments to the constructor" do
it 'forwards its arguments to the constructor' do
described_class.stub!(:mac?).and_return(true)
Guard::Darwin.stub!(:usable?).and_return(true)
@ -37,246 +37,269 @@ describe Guard::Listener do
end
end
describe "#all_files" do
describe '#all_files' do
subject { described_class.new(@fixture_path) }
it "should return all files" do
subject.all_files.should =~ Dir.glob("#{@fixture_path}/**/*", File::FNM_DOTMATCH).select { |file| File.file?(file) }
it 'should return all files' do
subject.all_files.should =~ Dir.glob("#{ @fixture_path }/**/*", File::FNM_DOTMATCH).select { |file| File.file?(file) }
end
end
describe "#relativize_paths" do
describe '#relativize_paths' do
subject { described_class.new('/tmp') }
before :each do
@paths = %w( /tmp/a /tmp/a/b /tmp/a.b/c.d )
let(:paths) { %w( /tmp/a /tmp/a/b /tmp/a.b/c.d ) }
it 'should relativize paths to the configured directory' do
subject.relativize_paths(paths).should =~ %w( a a/b a.b/c.d )
end
it "should relativize paths to the configured directory" do
subject.relativize_paths(@paths).should =~ %w( a a/b a.b/c.d )
end
context "when set to false" do
context 'when set to false' do
subject { described_class.new('/tmp', :relativize_paths => false) }
it "can be disabled" do
subject.relativize_paths(@paths).should eql @paths
it 'can be disabled' do
subject.relativize_paths(paths).should eql paths
end
end
end
describe "#update_last_event" do
describe '#update_last_event' do
subject { described_class.new }
it "updates the last event to the current time" do
it 'updates the last event to the current time' do
time = Time.now
subject.update_last_event
subject.instance_variable_get(:@last_event).to_i.should >= time.to_i
end
end
describe "#modified_files" do
describe '#modified_files' do
subject { described_class.new }
let(:file1) { @fixture_path.join("folder1", "file1.txt") }
let(:file2) { @fixture_path.join("folder1", "folder2", "file2.txt") }
let(:file3) { @fixture_path.join("folder1", "deletedfile1.txt") }
let(:file4) { @fixture_path.join("folder1", "movedfile1.txt") }
let(:file5) { @fixture_path.join("folder1", "folder2", "movedfile1.txt") }
let(:file1) { fixture('folder1', 'file1.txt') }
let(:file2) { fixture('folder1', 'folder2', 'file2.txt') }
let(:file3) { fixture('folder1', 'deletedfile1.txt') }
let(:file4) { fixture('folder1', 'movedfile1.txt') }
let(:file5) { fixture('folder1', 'folder2', 'movedfile1.txt') }
before do
@listener = subject
end
before { listen_to subject }
context "without the :all option" do
it "finds modified files only in the directory supplied" do
start
FileUtils.touch([file1, file2, file3])
subject.modified_files([@fixture_path.join("folder1")], {}).should =~ ["spec/fixtures/folder1/deletedfile1.txt", "spec/fixtures/folder1/file1.txt"]
stop
context 'without the :all option' do
it 'finds modified files only in the directory supplied' do
watch do
FileUtils.touch([file1, file2, file3])
subject.modified_files([fixture('folder1')], {}).should =~
['spec/fixtures/folder1/deletedfile1.txt', 'spec/fixtures/folder1/file1.txt']
end
end
end
context "with the :all options" do
it "finds modified files within subdirectories" do
start
FileUtils.touch([file1, file2, file3])
subject.modified_files([@fixture_path.join("folder1")], { :all => true }).should =~ ["spec/fixtures/folder1/deletedfile1.txt", "spec/fixtures/folder1/file1.txt", "spec/fixtures/folder1/folder2/file2.txt"]
stop
context 'with the :all options' do
it 'finds modified files within subdirectories' do
watch do
FileUtils.touch([file1, file2, file3])
subject.modified_files([fixture('folder1')], { :all => true }).should =~
['spec/fixtures/folder1/deletedfile1.txt',
'spec/fixtures/folder1/file1.txt',
'spec/fixtures/folder1/folder2/file2.txt']
end
end
end
context "without updating the content" do
it "ignores the files for the second time" do
start
FileUtils.touch([file1, file2, file3])
subject.modified_files([@fixture_path.join("folder1")], {}).should =~ ["spec/fixtures/folder1/deletedfile1.txt", "spec/fixtures/folder1/file1.txt"]
context 'without updating the content' do
it 'ignores the files for the second time' do
watch do
FileUtils.touch([file1, file2, file3])
subject.modified_files([fixture('folder1')], {}).should =~
['spec/fixtures/folder1/deletedfile1.txt', 'spec/fixtures/folder1/file1.txt']
subject.update_last_event
FileUtils.touch([file1, file2, file3])
subject.modified_files([fixture('folder1')], {}).should be_empty
end
end
end
context 'with content that has changed' do
after { File.open(file1, 'w') { |f| f.write('') } }
it 'identifies the files for the second time' do
watch do
FileUtils.touch([file1, file2, file3])
subject.modified_files([fixture('folder1')], {}).should =~
['spec/fixtures/folder1/deletedfile1.txt', 'spec/fixtures/folder1/file1.txt']
subject.update_last_event
FileUtils.touch([file2, file3])
File.open(file1, 'w') { |f| f.write('changed content') }
subject.modified_files([fixture('folder1')], {}).should =~
['spec/fixtures/folder1/file1.txt']
end
end
end
context 'without the :watch_all_modifications option' do
after { FileUtils.touch(file3) }
it 'defaults to false' do
subject.instance_variable_get(:@watch_all_modifications).should eql false
end
it 'it should not track deleted files' do
watch do
FileUtils.touch([file1, file2, file3])
subject.modified_files([fixture('folder1')], {}).should =~
['spec/fixtures/folder1/deletedfile1.txt', 'spec/fixtures/folder1/file1.txt']
subject.update_last_event
FileUtils.rm(file3)
subject.modified_files([fixture('folder1')], {}).should =~ []
end
end
end
context 'with the :watch_all_modifications option' do
subject { described_class.new(Dir.pwd, :watch_all_modifications => true) }
before do
subject.timestamp_files
subject.update_last_event
FileUtils.touch([file1, file2, file3])
subject.modified_files([@fixture_path.join("folder1")], {}).should be_empty
stop
end
end
context "with content that has changed" do
after { File.open(file1, "w") { |f| f.write("") } }
it "identifies the files for the second time" do
start
after do
FileUtils.touch([file1, file2, file3])
subject.modified_files([@fixture_path.join("folder1")], {}).should =~ ["spec/fixtures/folder1/deletedfile1.txt", "spec/fixtures/folder1/file1.txt"]
subject.update_last_event
FileUtils.touch([file2, file3])
File.open(file1, "w") { |f| f.write("changed content") }
subject.modified_files([@fixture_path.join("folder1")], {}).should =~ ["spec/fixtures/folder1/file1.txt"]
stop
end
end
context "without watch_all_modifications" do
after { FileUtils.touch(file3) }
it 'should be true when set' do
subject.instance_variable_get(:@watch_all_modifications).should eql true
end
it "defaults to false" do
subject.instance_variable_get(:@watch_all_modifications).should eql false
it 'should track deleted files' do
watch do
FileUtils.touch([file1, file3])
subject.modified_files([fixture('folder1')], { }).should =~
['spec/fixtures/folder1/deletedfile1.txt', 'spec/fixtures/folder1/file1.txt']
subject.update_last_event
FileUtils.remove_file(file3)
subject.modified_files([fixture('folder1')], { }).should =~
['!spec/fixtures/folder1/deletedfile1.txt']
end
end
it "it should not track deleted files" do
FileUtils.touch([file1, file2, file3])
subject.modified_files([@fixture_path.join("folder1")], {}).should =~ ["spec/fixtures/folder1/deletedfile1.txt", "spec/fixtures/folder1/file1.txt"]
subject.update_last_event
FileUtils.rm(file3)
subject.modified_files([@fixture_path.join("folder1")], {}).should =~ []
sleep 1
it 'should track moved files' do
watch do
FileUtils.touch([file1, file3])
subject.modified_files([@fixture_path.join('folder1')], { }).should =~
['spec/fixtures/folder1/deletedfile1.txt', 'spec/fixtures/folder1/file1.txt']
subject.update_last_event
FileUtils.move(file1, file4)
subject.modified_files([@fixture_path.join('folder1')], { }).should =~
['!spec/fixtures/folder1/file1.txt', 'spec/fixtures/folder1/movedfile1.txt']
FileUtils.move(file4, file1)
end
end
end
context "with watch_all_modifications" do
subject { described_class.new(Dir.pwd, :watch_all_modifications=>true) }
before :each do
subject.timestamp_files
sleep 1
subject.update_last_event
it 'should track deleted files with all option' do
watch do
FileUtils.touch([file1, file2])
subject.modified_files([@fixture_path.join('folder1')], { :all => true }).should =~
['spec/fixtures/folder1/file1.txt', 'spec/fixtures/folder1/folder2/file2.txt']
subject.update_last_event
FileUtils.remove_file(file2)
subject.modified_files([@fixture_path.join('folder1')], { :all => true }).should =~
['!spec/fixtures/folder1/folder2/file2.txt']
end
end
after :each do
FileUtils.touch([file1, file2, file3])
end
it "should be true when set" do
subject.instance_variable_get(:@watch_all_modifications).should eql true
end
it "should track deleted files" do
FileUtils.touch([file1, file3])
subject.modified_files([@fixture_path.join("folder1")], {}).should =~
["spec/fixtures/folder1/deletedfile1.txt", "spec/fixtures/folder1/file1.txt"]
subject.update_last_event
FileUtils.remove_file(file3)
subject.modified_files([@fixture_path.join("folder1")], {}).should =~
["!spec/fixtures/folder1/deletedfile1.txt"]
end
it "should track moved files" do
FileUtils.touch([file1, file3])
subject.modified_files([@fixture_path.join("folder1")], {}).should =~
["spec/fixtures/folder1/deletedfile1.txt", "spec/fixtures/folder1/file1.txt"]
subject.update_last_event
FileUtils.move(file1, file4)
subject.modified_files([@fixture_path.join("folder1")], {}).should =~
["!spec/fixtures/folder1/file1.txt", "spec/fixtures/folder1/movedfile1.txt"]
FileUtils.move(file4, file1)
end
it "should track deleted files with all option" do
FileUtils.touch([file1, file2])
subject.modified_files([@fixture_path.join("folder1")], {:all=>true}).should =~
["spec/fixtures/folder1/file1.txt", "spec/fixtures/folder1/folder2/file2.txt"]
subject.update_last_event
FileUtils.remove_file(file2)
subject.modified_files([@fixture_path.join("folder1")], {:all=>true}).should =~
["!spec/fixtures/folder1/folder2/file2.txt"]
end
it "should track moved files with all option" do
FileUtils.touch([file1, file2])
subject.modified_files([@fixture_path.join("folder1")], {:all=>true}).should =~
["spec/fixtures/folder1/file1.txt", "spec/fixtures/folder1/folder2/file2.txt"]
subject.update_last_event
FileUtils.move(file1, file5)
subject.modified_files([@fixture_path.join("folder1")], {:all=>true}).should =~
["!spec/fixtures/folder1/file1.txt","spec/fixtures/folder1/folder2/movedfile1.txt"]
FileUtils.move(file5, file1)
it 'should track moved files with all option' do
watch do
FileUtils.touch([file1, file2])
subject.modified_files([@fixture_path.join('folder1')], { :all => true }).should =~
['spec/fixtures/folder1/file1.txt', 'spec/fixtures/folder1/folder2/file2.txt']
subject.update_last_event
FileUtils.move(file1, file5)
subject.modified_files([@fixture_path.join('folder1')], { :all => true }).should =~
['!spec/fixtures/folder1/file1.txt', 'spec/fixtures/folder1/folder2/movedfile1.txt']
FileUtils.move(file5, file1)
end
end
end
end
describe "working directory" do
context "unspecified" do
describe 'working directory' do
context 'unspecified' do
subject { described_class.new }
it "defaults to Dir.pwd" do
it 'defaults to Dir.pwd' do
subject.instance_variable_get(:@directory).should eql Dir.pwd
end
it "can be not changed" do
it 'can be not changed' do
subject.should_not respond_to(:directory=)
end
end
context "specified as first argument to ::new" do
subject { described_class.new @wd }
context 'specified as first argument to ::new' do
let(:working_directory) { fixture('folder1') }
before do
@wd = @fixture_path.join("folder1")
@listener = subject
subject { described_class.new working_directory }
before { listen_to subject }
it 'can be inspected' do
subject.instance_variable_get(:@directory).should eql working_directory.to_s
end
it "can be inspected" do
subject.instance_variable_get(:@directory).should eql @wd.to_s
end
it "can be not changed" do
it 'can be not changed' do
subject.should_not respond_to(:directory=)
end
it "will be used to watch" do
subject.should_receive(:watch).with(@wd.to_s)
it 'will be used to watch' do
subject.should_receive(:watch).with(working_directory.to_s)
start
stop
end
end
end
describe "#ignore_paths" do
it "defaults to the default ignore paths" do
describe '#ignore_paths' do
it 'defaults to the default ignore paths' do
described_class.new.ignore_paths.should == Guard::Listener::DEFAULT_IGNORE_PATHS
end
it "can be added to via :ignore_paths option" do
it 'can be added to via :ignore_paths option' do
listener = described_class.new 'path', :ignore_paths => ['foo', 'bar']
listener.ignore_paths.should include('foo', 'bar')
end
end
describe "#exclude_ignored_paths [<dirs>]" do
describe '#exclude_ignored_paths [<dirs>]' do
let(:ignore_paths) { nil }
subject { described_class.new(@fixture_path, {:ignore_paths => ignore_paths}) }
subject { described_class.new(@fixture_path, { :ignore_paths => ignore_paths }) }
it "returns children of <dirs>" do
subject.exclude_ignored_paths(["spec/fixtures"]).should =~ ["spec/fixtures/.dotfile", "spec/fixtures/folder1", "spec/fixtures/Guardfile"]
it 'returns children of <dirs>' do
subject.exclude_ignored_paths(['spec/fixtures']).should =~
['spec/fixtures/.dotfile', 'spec/fixtures/folder1', 'spec/fixtures/Guardfile']
end
describe "when ignore_paths set to some of <dirs> children" do
describe 'when ignore_paths set to some of <dirs> children' do
let(:ignore_paths) { ['Guardfile', '.dotfile'] }
it "excludes the ignored paths" do
subject.exclude_ignored_paths(["spec/fixtures"]).should =~ ["spec/fixtures/folder1"]
it 'excludes the ignored paths' do
subject.exclude_ignored_paths(['spec/fixtures']).should =~ ['spec/fixtures/folder1']
end
end
end

View File

@ -1,9 +1,27 @@
private
# Set the sleep time around start/stop the listener. This defaults
# to one second but can be overridden by setting the environment
# variable `GUARD_SLEEP`.
#
def sleep_time
@sleep_time ||= ENV['GUARD_SLEEP'] ? ENV['GUARD_SLEEP'].to_f : 1
end
# Make the spec listen to a specific listener.
# This automatically starts to record results for the supplied listener.
#
# @param [Guard::Listener] listener the Guard listener
#
def listen_to(listener)
@listener = listener
record_results
end
# Start the listener. Normally you use {#watch} to wrap
# the code block that should be listen to instead of starting
# it manually.
#
def start
sleep(sleep_time)
@listener.update_last_event
@ -11,131 +29,197 @@ private
sleep(sleep_time)
end
def record_results
noise = %r|\.sw.$| # don't fail specs due to editor swap files, etc.
@results = []
@listener.on_change do |files|
@results += files.reject { |f| f =~ noise }
end
end
# Stop the listener. Normally you use {#watch} to wrap
# the code block that should be listen to instead of stopping
# it manually.
#
def stop
sleep(sleep_time)
@listener.stop
sleep(sleep_time)
end
# Watch file changes in a code block.
#
# @example Watch file changes
# watch do
# File.mv file1, file2
# end
#
# @yield The block to listen for file changes
#
def watch
start
yield if block_given?
stop
end
# Start recording results from the current listener.
# You may want to use {#listen_to} to set a listener
# instead of set it up manually.
#
def record_results
# Don't fail specs due to editor swap files, etc.
noise = %r|\.sw.$|
@results = []
@listener.on_change do |files|
@results += files.reject { |f| f =~ noise }
end
end
# Get the recorded result from the listener.
#
# @return [Array<String>] the result files
#
def results
@results.flatten
end
# Define a file absolute to the fixture path.
#
# @param [String, Array<String>] file the relative file name, separated by segment
# @return [String] the absolute file
#
def fixture(*file)
@fixture_path.join(*file)
end
shared_examples_for 'a listener that reacts to #on_change' do
before(:each) do
@listener = described_class.new
record_results
before do
listen_to described_class.new
end
it "catches a new file" do
file = @fixture_path.join("newfile.rb")
if File.exists?(file)
begin
File.delete file
rescue
context 'for a new file' do
let(:file) { fixture('newfile.rb') }
before { File.delete(file) if File.exists?(file) }
after { File.delete file }
it 'catches the new file' do
File.exists?(file).should be_false
watch do
FileUtils.touch file
end
results.should =~ ['spec/fixtures/newfile.rb']
end
File.exists?(file).should be_false
start
FileUtils.touch file
stop
begin
File.delete file
rescue
end
context 'for a single file update' do
let(:file) { fixture('folder1', 'file1.txt') }
it 'catches the update' do
File.exists?(file).should be_true
watch do
File.open(file, 'w') { |f| f.write('') }
end
results.should =~ ['spec/fixtures/folder1/file1.txt']
end
results.should =~ ['spec/fixtures/newfile.rb']
end
it "catches a single file update" do
file = @fixture_path.join("folder1/file1.txt")
File.exists?(file).should be_true
start
File.open(file, 'w') { |f| f.write('') }
stop
results.should =~ ['spec/fixtures/folder1/file1.txt']
context 'for a single file chmod update' do
let(:file) { fixture('folder1/file1.txt') }
it 'does not catch the update' do
File.exists?(file).should be_true
watch do
File.chmod(0777, file)
end
results.should =~ []
end
end
it "not catches a single file chmod update" do
file = @fixture_path.join("folder1/file1.txt")
File.exists?(file).should be_true
start
File.chmod(0777, file)
stop
results.should =~ []
context 'for a dotfile update' do
let(:file) { fixture('.dotfile') }
it "catches the update" do
File.exists?(file).should be_true
watch do
File.open(file, 'w') { |f| f.write('') }
end
results.should =~ ['spec/fixtures/.dotfile']
end
end
it "catches a dotfile update" do
file = @fixture_path.join(".dotfile")
File.exists?(file).should be_true
start
File.open(file, 'w') { |f| f.write('') }
stop
results.should =~ ['spec/fixtures/.dotfile']
context 'for multiple file updates' do
let(:file1) { fixture('folder1', 'file1.txt') }
let(:file2) { fixture('folder1', 'folder2', 'file2.txt') }
it 'catches the updates' do
File.exists?(file1).should be_true
File.exists?(file2).should be_true
watch do
File.open(file1, 'w') { |f| f.write('') }
File.open(file2, 'w') { |f| f.write('') }
end
results.should =~ ['spec/fixtures/folder1/file1.txt', 'spec/fixtures/folder1/folder2/file2.txt']
end
end
it "catches multiple file updates" do
file1 = @fixture_path.join("folder1/file1.txt")
file2 = @fixture_path.join("folder1/folder2/file2.txt")
File.exists?(file1).should be_true
File.exists?(file2).should be_true
start
File.open(file1, 'w') { |f| f.write('') }
File.open(file2, 'w') { |f| f.write('') }
stop
results.should =~ ['spec/fixtures/folder1/file1.txt', 'spec/fixtures/folder1/folder2/file2.txt']
context 'for a deleted file' do
let(:file) { fixture('folder1', 'file1.txt') }
after { FileUtils.touch file }
it 'does not catch the deletion' do
File.exists?(file).should be_true
watch do
File.delete file
end
results.should =~ []
end
end
it "not catches a deleted file" do
file = @fixture_path.join("folder1/file1.txt")
File.exists?(file).should be_true
start
File.delete file
stop
FileUtils.touch file
results.should =~ []
end
context 'for a moved file' do
let(:file1) { fixture('folder1', 'file1.txt') }
let(:file2) { fixture('folder1', 'movedfile1.txt') }
it "not catches a moved file" do
file1 = @fixture_path.join("folder1/file1.txt")
file2 = @fixture_path.join("folder1/movedfile1.txt")
File.exists?(file1).should be_true
File.exists?(file2).should be_false
start
FileUtils.mv file1, file2
stop
FileUtils.mv file2, file1
results.should =~ []
end
after { FileUtils.mv file2, file1 }
it 'does not catches the move' do
File.exists?(file1).should be_true
File.exists?(file2).should be_false
watch do
FileUtils.mv file1, file2
end
results.should =~ []
end
end
end
shared_examples_for "a listener scoped to a specific directory" do
before :each do
@wd = @fixture_path.join("folder1")
@listener = described_class.new @wd
end
it "should base paths within this directory" do
record_results
new_file = @wd.join("folder2/newfile.rb")
modified = @wd.join("file1.txt")
let(:work_directory) { fixture('folder1') }
let(:new_file) { work_directory.join('folder2', 'newfile.rb') }
let(:modified) { work_directory.join('file1.txt') }
before { listen_to described_class.new(work_directory) }
after { File.delete new_file }
it 'should base paths within this directory' do
File.exists?(modified).should be_true
File.exists?(new_file).should be_false
start
FileUtils.touch new_file
File.open(modified, 'w') { |f| f.write('') }
stop
File.delete new_file
results.should =~ ["folder2/newfile.rb", 'file1.txt']
watch do
FileUtils.touch new_file
File.open(modified, 'w') { |f| f.write('') }
end
results.should =~ ['folder2/newfile.rb', 'file1.txt']
end
end