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
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
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_path.join("folder1")], {}).should =~ ["spec/fixtures/folder1/deletedfile1.txt", "spec/fixtures/folder1/file1.txt"]
stop
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
context 'with the :all options' do
it 'finds modified files within subdirectories' do
watch do
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
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
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_path.join("folder1")], {}).should =~ ["spec/fixtures/folder1/deletedfile1.txt", "spec/fixtures/folder1/file1.txt"]
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_path.join("folder1")], {}).should be_empty
stop
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("") } }
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
it 'identifies the files for the second time' do
watch 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.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_path.join("folder1")], {}).should =~ ["spec/fixtures/folder1/file1.txt"]
stop
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 watch_all_modifications" do
context 'without the :watch_all_modifications option' do
after { FileUtils.touch(file3) }
it "defaults to false" do
it 'defaults to false' do
subject.instance_variable_get(:@watch_all_modifications).should eql false
end
it "it should not track deleted files" do
it 'it should not track deleted files' do
watch 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.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_path.join("folder1")], {}).should =~ []
sleep 1
subject.modified_files([fixture('folder1')], {}).should =~ []
end
end
end
context "with watch_all_modifications" do
context 'with the :watch_all_modifications option' do
subject { described_class.new(Dir.pwd, :watch_all_modifications => true) }
before :each do
before do
subject.timestamp_files
sleep 1
subject.update_last_event
end
after :each do
after do
FileUtils.touch([file1, file2, file3])
end
it "should be true when set" do
it 'should be true when set' do
subject.instance_variable_get(:@watch_all_modifications).should eql true
end
it "should track deleted files" do
it 'should track deleted 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.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_path.join("folder1")], {}).should =~
["!spec/fixtures/folder1/deletedfile1.txt"]
subject.modified_files([fixture('folder1')], { }).should =~
['!spec/fixtures/folder1/deletedfile1.txt']
end
end
it "should track moved files" do
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.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"]
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
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.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
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"]
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 }) }
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
shared_examples_for 'a listener that reacts to #on_change' do
before(:each) do
@listener = described_class.new
record_results
# 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
it "catches a new file" do
file = @fixture_path.join("newfile.rb")
if File.exists?(file)
begin
File.delete file
rescue
end
shared_examples_for 'a listener that reacts to #on_change' do
before do
listen_to described_class.new
end
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
start
watch do
FileUtils.touch file
stop
begin
File.delete file
rescue
end
results.should =~ ['spec/fixtures/newfile.rb']
end
end
it "catches a single file update" do
file = @fixture_path.join("folder1/file1.txt")
context 'for a single file update' do
let(:file) { fixture('folder1', 'file1.txt') }
it 'catches the update' do
File.exists?(file).should be_true
start
watch do
File.open(file, 'w') { |f| f.write('') }
stop
end
results.should =~ ['spec/fixtures/folder1/file1.txt']
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 =~ []
end
it "catches a dotfile update" do
file = @fixture_path.join(".dotfile")
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
start
watch do
File.chmod(0777, file)
end
results.should =~ []
end
end
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('') }
stop
end
results.should =~ ['spec/fixtures/.dotfile']
end
end
it "catches multiple file updates" do
file1 = @fixture_path.join("folder1/file1.txt")
file2 = @fixture_path.join("folder1/folder2/file2.txt")
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
start
watch do
File.open(file1, 'w') { |f| f.write('') }
File.open(file2, 'w') { |f| f.write('') }
stop
end
results.should =~ ['spec/fixtures/folder1/file1.txt', 'spec/fixtures/folder1/folder2/file2.txt']
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
it "not catches a moved file" do
file1 = @fixture_path.join("folder1/file1.txt")
file2 = @fixture_path.join("folder1/movedfile1.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
context 'for a moved file' do
let(:file1) { fixture('folder1', 'file1.txt') }
let(:file2) { fixture('folder1', 'movedfile1.txt') }
after { FileUtils.mv file2, file1 }
it 'does not catches the move' do
File.exists?(file1).should be_true
File.exists?(file2).should be_false
start
watch do
FileUtils.mv file1, file2
stop
FileUtils.mv file2, file1
results.should =~ []
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
watch do
FileUtils.touch new_file
File.open(modified, 'w') { |f| f.write('') }
stop
File.delete new_file
results.should =~ ["folder2/newfile.rb", 'file1.txt']
end
end
results.should =~ ['folder2/newfile.rb', 'file1.txt']
end
end