vagrant-mirror 0.1.0.alpha

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,161 @@
1
+ describe Vagrant::Mirror::Config do
2
+
3
+ context "with no configuration" do
4
+
5
+ it "returns an empty array of mirrored folders" do
6
+ subject.folders.should eq([])
7
+ end
8
+
9
+ end
10
+
11
+ context "when a mirrored folder is added with no options" do
12
+
13
+ before(:each) do
14
+ subject.folder "foo", "/var/path"
15
+ end
16
+
17
+ it "adds it to the list of folders" do
18
+ subject.folders.count.should eq 1
19
+ subject.folders[0][:name].should eq 'foo'
20
+ subject.folders[0][:guest_path].should eq "/var/path"
21
+ end
22
+
23
+ it "does not beep by default" do
24
+ subject.folders[0][:beep].should be_false
25
+ end
26
+
27
+ it "does not delete by default" do
28
+ subject.folders[0][:delete].should be_false
29
+ end
30
+
31
+ it "does not exclude anything by default" do
32
+ subject.folders[0][:exclude].should eq([])
33
+ end
34
+
35
+ it "does not make any symlinks by default" do
36
+ subject.folders[0][:symlinks].should eq([])
37
+ end
38
+
39
+ end
40
+
41
+ it "provides a shortcut for mirroring the vagrant root to the guest" do
42
+ subject.vagrant_root "/var/vagrant"
43
+
44
+ subject.folders.count.should eq 1
45
+ subject.folders[0][:name].should eq 'v-root'
46
+ subject.folders[0][:guest_path].should eq "/var/vagrant"
47
+ end
48
+
49
+ it "can accept a folder with the delete option set" do
50
+ subject.vagrant_root "/var/vagrant", { :delete => true }
51
+
52
+ subject.folders[0][:delete].should be_true
53
+ end
54
+
55
+ it "can accept a folder with the beep option set" do
56
+ subject.vagrant_root "/var/vagrant", { :beep => true }
57
+
58
+ subject.folders[0][:beep].should be_true
59
+ end
60
+
61
+ it "can accept a folder with exclude patterns set" do
62
+ subject.vagrant_root "/var/vagrant", { :exclude => ['/docs'] }
63
+
64
+ subject.folders[0][:exclude].should eq(['/docs'])
65
+ end
66
+
67
+ context "when adding a folder with excludes and symlinks" do
68
+ before (:each) do
69
+ subject.vagrant_root "/var/vagrant", { :exclude => ['/docs'], :symlinks => ['/log'] }
70
+ end
71
+
72
+ it "stores the list of symlinks" do
73
+ subject.folders[0][:symlinks].should eq(['/log'])
74
+ end
75
+
76
+ it "adds the symlink to the rsync exclude list" do
77
+ subject.folders[0][:exclude].should eq(['/docs', '/log'])
78
+ end
79
+
80
+ end
81
+
82
+ context "when adding multiple folders" do
83
+ before (:each) do
84
+ subject.vagrant_root "/var/vagrant", { :exclude => ['/docs'], :symlinks => ['/log'] }
85
+ subject.folder "foo", "/var/foo", { :exclude => ['/docs2'], :symlinks => ['/log2'] }
86
+ end
87
+
88
+ it "builds a list of folders" do
89
+ subject.folders.count.should eq(2)
90
+ subject.folders[0][:name].should eq('v-root')
91
+ subject.folders[1][:name].should eq('foo')
92
+ end
93
+ end
94
+
95
+ context "when validating configuration" do
96
+
97
+ before :each do
98
+ @env = Vagrant::Environment.new
99
+ @errors = Vagrant::Config::ErrorRecorder.new
100
+ end
101
+
102
+ it "rejects folders with an empty shared folder name" do
103
+ subject.folder "", "/var/path"
104
+ subject.validate(@env, @errors)
105
+
106
+ @errors.errors.empty?.should be_false
107
+ end
108
+
109
+ it "rejects folders with an empty guest path" do
110
+ subject.folder "foo", ""
111
+ subject.validate(@env, @errors)
112
+
113
+ @errors.errors.empty?.should be_false
114
+ end
115
+
116
+ it "rejects folders which are not already a vagrant shared folder" do
117
+ pending
118
+ end
119
+
120
+ it "rejects folders with nil paths" do
121
+ subject.folder nil, nil
122
+ subject.validate(@env, @errors)
123
+ @errors.errors.count.should eq 2
124
+ end
125
+
126
+ it "rejects unknown options" do
127
+ subject.vagrant_root "/guest/path", { :foo => :bar }
128
+ subject.validate(@env, @errors)
129
+
130
+ @errors.errors.empty?.should be_false
131
+ end
132
+
133
+ it "validates with valid options" do
134
+ subject.vagrant_root "/var/guest", { :symlinks => [ 'logs'], :exclude => ['exclude'], :delete => true, :beep => true }
135
+ subject.validate(@env, @errors)
136
+
137
+ @errors.errors.empty?.should be_true
138
+ end
139
+
140
+ end
141
+
142
+ context "when merging configuration" do
143
+
144
+ it "combines the folders from each configuration source" do
145
+ subject.folder "low", "/low-level"
146
+ next_config = Vagrant::Mirror::Config.new
147
+ next_config.folder "top", "/top-level"
148
+ merged = subject.merge(next_config)
149
+
150
+ merged.should be_a Vagrant::Mirror::Config
151
+ merged.folders.should have(2).items
152
+
153
+ expected_result = [
154
+ subject.folders[0],
155
+ next_config.folders[0]
156
+ ]
157
+
158
+ merged.folders.should eq expected_result
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,96 @@
1
+ describe Vagrant::Mirror::Listener::Host do
2
+
3
+ let (:queue) { double("Queue").as_null_object }
4
+
5
+ subject { Vagrant::Mirror::Listener::Host.new('c:/host', queue) }
6
+
7
+ before (:each) do
8
+ Listen.stub(:to).as_null_object
9
+ end
10
+
11
+ describe "#listen!" do
12
+ it "listens to the host directory" do
13
+ Listen.should_receive(:to).with('c:/host', anything())
14
+
15
+ subject.listen!
16
+ end
17
+
18
+ it "requests relative paths" do
19
+ Listen.should_receive(:to)
20
+ .with(anything(), hash_including( :relative_paths => true ))
21
+
22
+ subject.listen!
23
+ end
24
+
25
+ context "when changes received" do
26
+ before(:each) do
27
+ Listen.stub(:to)
28
+ .and_yield(['modified'],[],[])
29
+ .and_yield([],['added'],[])
30
+ .and_yield([],[],['removed'])
31
+ .and_yield(['modified1'],['added1'],['removed1'])
32
+ end
33
+
34
+ it "pushes added files onto the queue" do
35
+ queue.should_receive(:"<<")
36
+ .with(hash_including(:added => ['added']))
37
+
38
+ subject.listen!
39
+ end
40
+
41
+ it "pushes modified files onto the queue" do
42
+ queue.should_receive(:"<<")
43
+ .with(hash_including(:modified => ['modified']))
44
+
45
+ subject.listen!
46
+ end
47
+
48
+ it "pushes removed files onto the queue" do
49
+ queue.should_receive(:"<<")
50
+ .with(hash_including(:removed => ['removed']))
51
+
52
+ subject.listen!
53
+ end
54
+
55
+ it "handles simultaneous changes" do
56
+ queue.should_receive(:"<<")
57
+ .with(hash_including({
58
+ :added => ['added1'],
59
+ :modified => ['modified1'],
60
+ :removed => ['removed1']
61
+ }))
62
+
63
+ subject.listen!
64
+ end
65
+
66
+ end
67
+
68
+ context "with a close signal" do
69
+ it "stops listening"
70
+ end
71
+ end
72
+
73
+ describe "#listen" do
74
+ let (:thread) { double("Thread") }
75
+
76
+ before (:each) do
77
+ Thread.stub(:new).and_yield.and_return(thread)
78
+ end
79
+
80
+ it "starts a new thread" do
81
+ Thread.should_receive(:new)
82
+
83
+ subject.listen
84
+ end
85
+
86
+ it "runs listen! in the new thread" do
87
+ Listen.should_receive(:to)
88
+
89
+ subject.listen
90
+ end
91
+
92
+ it "returns the thread" do
93
+ subject.listen.should eq thread
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,188 @@
1
+ describe Vagrant::Mirror::Middleware::Mirror do
2
+ # Shared mocks
3
+ let (:env) { Vagrant::Action::Environment.new }
4
+ let (:vm) { double("Vagrant::VM").as_null_object }
5
+ let (:ui) { double("Vagrant::UI::Interface").as_null_object }
6
+ let (:app) { double("Object").as_null_object }
7
+ let (:channel) { double("Vagrant::Communication::SSH").as_null_object }
8
+ let (:config) { double("Object").as_null_object }
9
+ let (:configmirror) { Vagrant::Mirror::Config.new }
10
+ let (:configvm) { Vagrant::Config::VMConfig.new }
11
+ let (:queue) { double("Queue").as_null_object }
12
+ let (:guard) { double("Vagrant::Mirror::Listener::Host").as_null_object }
13
+ let (:rsync) { double("Vagrant::Mirror::Rsync").as_null_object }
14
+
15
+ # Set basic stubs for shared mocks
16
+ before (:each) do
17
+ env[:vm] = vm
18
+ env[:ui] = ui
19
+ env[:root_path] = Dir.pwd
20
+
21
+ vm.stub(:config).and_return config
22
+ config.stub(:mirror).and_return configmirror
23
+ config.stub(:vm).and_return configvm
24
+
25
+ vm.stub(:channel).and_return channel
26
+
27
+ Vagrant::Mirror::Rsync.stub(:new).and_return rsync
28
+
29
+ app.stub(:call)
30
+
31
+ Vagrant::Mirror::Listener::Host.stub(:new).and_return(guard)
32
+ Queue.stub(:new).and_return(queue)
33
+ queue.stub(:pop).and_return({ :quit => true})
34
+ end
35
+
36
+ subject { Vagrant::Mirror::Middleware::Mirror.new(app, env) }
37
+
38
+ describe "#call" do
39
+ shared_examples "chained mirror middleware" do
40
+ it "calls the next middleware" do
41
+ app.should_receive(:call).with(env)
42
+
43
+ subject.call(env)
44
+ end
45
+ end
46
+
47
+ context "with no mirrored folders" do
48
+ before (:each) do
49
+ configmirror.stub(:folders).and_return([])
50
+ end
51
+
52
+ it_behaves_like "chained mirror middleware"
53
+ end
54
+
55
+ context "with a mirrored folder" do
56
+
57
+ before (:each) do
58
+ config.mirror.vagrant_root '/var/guest'
59
+ config.vm.share_folder("v-root", "/vagrant", ".")
60
+ end
61
+
62
+ it "logs the start of mirroring" do
63
+ ui.should_receive(:info).with("Beginning directory mirroring")
64
+
65
+ subject.call(env)
66
+ end
67
+
68
+ it "creates a Guard listener on the host path" do
69
+ Vagrant::Mirror::Listener::Host.should_receive(:new)
70
+ .with(env[:root_path], queue)
71
+
72
+ subject.call(env)
73
+ end
74
+
75
+ context "creates an rsync class for the folder pair" do
76
+ it "creates an rsync class for the folder pair" do
77
+ Vagrant::Mirror::Rsync.should_receive(:new)
78
+ .and_return(rsync)
79
+
80
+ subject.call(env)
81
+ end
82
+
83
+ it "passes the vm to rsync" do
84
+ Vagrant::Mirror::Rsync.should_receive(:new)
85
+ .with(vm, anything, anything, anything)
86
+ .and_return(rsync)
87
+
88
+ subject.call(env)
89
+ end
90
+
91
+ it "passes the guest shared folder path for the folder pair to rsync" do
92
+ Vagrant::Mirror::Rsync.should_receive(:new)
93
+ .with(anything, "/vagrant", anything, anything)
94
+ .and_return(rsync)
95
+
96
+ subject.call(env)
97
+ end
98
+
99
+ it "passes the host shared folder path for the folder pair to rsync" do
100
+ Vagrant::Mirror::Rsync.should_receive(:new)
101
+ .with(anything, anything, env[:root_path], anything)
102
+ .and_return(rsync)
103
+
104
+ subject.call(env)
105
+ end
106
+
107
+ it "passes the folder configuration to rsync" do
108
+ Vagrant::Mirror::Rsync.should_receive(:new)
109
+ .with(anything, anything, anything, config.mirror.folders[0])
110
+ .and_return(rsync)
111
+
112
+ subject.call(env)
113
+ end
114
+ end
115
+
116
+ it "listens for changes in the host path" do
117
+ guard.should_receive(:listen)
118
+
119
+ subject.call(env)
120
+ end
121
+
122
+ context "with notifications in the queue" do
123
+
124
+ let (:added) { ['added'] }
125
+ let (:modified) { ['modified','modified2'] }
126
+ let (:removed) { ['removed'] }
127
+ let (:rm_exists) { true }
128
+
129
+ before (:each) do
130
+ queue.stub(:pop).and_return(
131
+ { :added => added, :modified => modified, :removed => removed },
132
+ { :added => added, :modified => modified, :removed => removed },
133
+ { :quit => true })
134
+ File.stub(:exists?).with("#{env[:root_path]}/removed").and_return(rm_exists)
135
+ end
136
+
137
+ it "runs rsync one by one for each added and modified file" do
138
+ rsync.should_receive(:run).with("added")
139
+ rsync.should_receive(:run).with("modified")
140
+ rsync.should_receive(:run).with("modified2")
141
+
142
+ subject.call(env)
143
+ sleep 0.2
144
+ end
145
+
146
+ # Sometimes Guard flags a file as deleted that still exists - during certain types of
147
+ # atomic file writes, we think. So we should delete the file remotely if so, or sync if not.
148
+ context "if deleted files have been deleted" do
149
+ let (:rm_exists) { false }
150
+
151
+ it "deletes them by SSH" do
152
+ channel.should_receive(:sudo).with('rm /var/guest/removed')
153
+
154
+ subject.call(env)
155
+ sleep 0.2
156
+ end
157
+ end
158
+
159
+ context "if deleted files have not been deleted" do
160
+ let (:rm_exists) { true }
161
+
162
+ it "runs rsync on the file" do
163
+ rsync.should_receive(:run).with('removed')
164
+
165
+ subject.call(env)
166
+ sleep 0.2
167
+ end
168
+ end
169
+
170
+ end
171
+
172
+ shared_examples "finishing mirroring" do
173
+ it "signals the Guard listener to quit"
174
+ it "waits for the Guard listener to quit"
175
+ it "processes remaining jobs in the queue"
176
+ it "calls the next middleware"
177
+ end
178
+
179
+ context "when signaled to quit" do
180
+ it_behaves_like "finishing mirroring"
181
+ end
182
+
183
+ context "when user presses q on the console" do
184
+ it_behaves_like "finishing mirroring"
185
+ end
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,152 @@
1
+ describe Vagrant::Mirror::Middleware::Sync do
2
+ # Shared mocks
3
+ let (:env) { Vagrant::Action::Environment.new }
4
+ let (:vm) { double("Vagrant::VM").as_null_object }
5
+ let (:channel) { double("Vagrant::Communication::SSH").as_null_object }
6
+ let (:ui) { double("Vagrant::UI::Interface").as_null_object }
7
+ let (:app) { double("Object").as_null_object }
8
+ let (:config) { double("Object").as_null_object }
9
+ let (:configmirror) { Vagrant::Mirror::Config.new }
10
+ let (:configvm) { Vagrant::Config::VMConfig.new }
11
+ let (:rsync) { double("Vagrant::Mirror::Rsync").as_null_object }
12
+
13
+ # Set basic stubs for shared mocks
14
+ before (:each) do
15
+ env[:vm] = vm
16
+ env[:ui] = ui
17
+ env[:root_path] = Dir.pwd
18
+
19
+ vm.stub(:config).and_return config
20
+ config.stub(:mirror).and_return configmirror
21
+ config.stub(:vm).and_return configvm
22
+
23
+ vm.stub(:channel).and_return channel
24
+
25
+ Vagrant::Mirror::Rsync.stub(:new).and_return rsync
26
+
27
+ app.stub(:call)
28
+
29
+ end
30
+
31
+ subject { Vagrant::Mirror::Middleware::Sync.new(app, env) }
32
+
33
+ describe "#call" do
34
+
35
+ shared_examples "chained middleware" do
36
+ it "calls the next middleware" do
37
+ app.should_receive(:call).with(env)
38
+
39
+ subject.call(env)
40
+ end
41
+ end
42
+
43
+ context "with no mirrored folders" do
44
+ it_behaves_like "chained middleware"
45
+ end
46
+
47
+ context "with a mirrored folder" do
48
+ before (:each) do
49
+ config.mirror.vagrant_root '/var/guest'
50
+ config.vm.share_folder("v-root", "/vagrant", ".")
51
+ end
52
+
53
+ it_behaves_like "chained middleware"
54
+
55
+ it "creates an rsync class for the folder pair" do
56
+ Vagrant::Mirror::Rsync.should_receive(:new)
57
+ .and_return(rsync)
58
+
59
+ subject.call(env)
60
+ end
61
+
62
+ it "passes the vm to rsync" do
63
+ Vagrant::Mirror::Rsync.should_receive(:new)
64
+ .with(vm, anything, anything, anything)
65
+ .and_return(rsync)
66
+
67
+ subject.call(env)
68
+ end
69
+
70
+ it "passes the guest shared folder path for the folder pair to rsync" do
71
+ Vagrant::Mirror::Rsync.should_receive(:new)
72
+ .with(anything, "/vagrant", anything, anything)
73
+ .and_return(rsync)
74
+
75
+ subject.call(env)
76
+ end
77
+
78
+ it "passes the host shared folder path for the folder pair to rsync" do
79
+ Vagrant::Mirror::Rsync.should_receive(:new)
80
+ .with(anything, anything, env[:root_path], anything)
81
+ .and_return(rsync)
82
+
83
+ subject.call(env)
84
+ end
85
+
86
+ it "passes the folder configuration to rsync" do
87
+ Vagrant::Mirror::Rsync.should_receive(:new)
88
+ .with(anything, anything, anything, config.mirror.folders[0])
89
+ .and_return(rsync)
90
+
91
+ subject.call(env)
92
+ end
93
+
94
+ it "runs rsync for the root of the mirrored folder" do
95
+ rsync.should_receive(:run)
96
+ .with("/")
97
+
98
+ subject.call(env)
99
+ end
100
+ end
101
+
102
+ context "with two mirrored folders" do
103
+ before (:each) do
104
+ config.mirror.folder "foo", "/var/foo"
105
+ config.mirror.folder "bar", "/var/bar"
106
+ end
107
+
108
+ it "throws an exception as this is not yet supported" do
109
+ expect { subject.call(env) }.to raise_error(Vagrant::Mirror::Errors::MultipleFoldersNotSupported)
110
+ end
111
+ end
112
+
113
+ context "when the mirror config includes symlinks" do
114
+ let (:host_path) { File.join(Dir.pwd, 'logs') }
115
+
116
+ before (:each) do
117
+ config.mirror.vagrant_root "/var/vagrant", { :symlinks => ['logs'] }
118
+ config.vm.share_folder("v-root", "/vagrant", ".")
119
+
120
+ File.stub(:exists?).with(host_path).and_return true
121
+ end
122
+
123
+ context "if the host folder does not exist" do
124
+ let (:host_path) { File.join(Dir.pwd, 'logs') }
125
+
126
+ before (:each) do
127
+ File.stub(:exists?).with(host_path).and_return false
128
+ end
129
+
130
+ it "creates the folder on the host" do
131
+ FileUtils.stub(:mkdir_p).with(host_path)
132
+ end
133
+ end
134
+
135
+ it "creates a symlink on the guest" do
136
+ channel.should_receive(:sudo).with('rm -f /var/vagrant/logs && mkdir -p /var/vagrant && ln -s /vagrant/logs /var/vagrant/logs')
137
+
138
+ subject.call(env)
139
+ end
140
+ end
141
+
142
+ context "when the mirrored config includes invalid shared folders" do
143
+ before (:each) do
144
+ config.mirror.folder "unknown", "/var/whoops"
145
+ end
146
+
147
+ it "throws an exception" do
148
+ expect { subject.call(env) }.to raise_error(Vagrant::Mirror::Errors::SharedFolderNotMapped)
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,75 @@
1
+ describe Vagrant::Mirror::Rsync do
2
+
3
+ # Shared mocks
4
+ let (:vm) { double("Vagrant::VM").as_null_object }
5
+ let (:channel) { double("Vagrant::Communication::SSH").as_null_object }
6
+ let (:guest_sf_path) { '/vagrant' }
7
+ let (:host_path) { 'c:/vagrant' }
8
+ let (:cfg_delete) { false }
9
+ let (:cfg_exclude) { [] }
10
+ let (:cfg_guest) { '/var/vagrant' }
11
+ let (:mirror_config) { { :name => 'v-root', :guest_path => '/var/vagrant', :delete => false,
12
+ :exclude => [], :beep => false, :symlinks => [] } }
13
+
14
+ # Set basic stubs for shared mocks
15
+ before (:each) do
16
+ vm.stub(:channel).and_return channel
17
+
18
+ File.stub(:directory?).and_return(false)
19
+ File.stub(:directory?).with('c:/vagrant/dir').and_return(true)
20
+ end
21
+
22
+ subject { Vagrant::Mirror::Rsync.new(vm, guest_sf_path, host_path, mirror_config) }
23
+
24
+ describe "#run" do
25
+
26
+ shared_examples "running rsync to update the path" do | path, expect_source, expect_dest |
27
+ context "with no exclude paths" do
28
+ it "runs the expected rsync command" do
29
+ channel.should_receive(:sudo).with("rsync -av #{expect_source} #{expect_dest}")
30
+
31
+ subject.run(path)
32
+ end
33
+ end
34
+
35
+ context "with exclude paths" do
36
+ let (:mirror_config) { { :name => 'v-root', :guest_path => '/var/vagrant', :delete => false,
37
+ :exclude => ['.git', 'cache'], :beep => false, :symlinks => [] } }
38
+
39
+ it "runs the expected rsync command" do
40
+ channel.should_receive(:sudo).with("rsync -av --exclude '.git' --exclude 'cache' #{expect_source} #{expect_dest}")
41
+
42
+ subject.run(path)
43
+ end
44
+ end
45
+
46
+ context "with delete enabled" do
47
+ let (:mirror_config) { { :name => 'v-root', :guest_path => '/var/vagrant', :delete => true,
48
+ :exclude => [], :beep => false, :symlinks => [] } }
49
+
50
+ it "runs the expected rsync command" do
51
+ channel.should_receive(:sudo).with("rsync -av --del #{expect_source} #{expect_dest}")
52
+
53
+ subject.run(path)
54
+ end
55
+ end
56
+ end
57
+
58
+ context "when passed an empty path" do
59
+ it_behaves_like "running rsync to update the path", '', '/vagrant/', '/var/vagrant/'
60
+ end
61
+
62
+ context "when passed the root path" do
63
+ it_behaves_like "running rsync to update the path", '/', '/vagrant/', '/var/vagrant/'
64
+ end
65
+
66
+ context "when passed a file path" do
67
+ it_behaves_like "running rsync to update the path", 'file', '/vagrant/file', '/var/vagrant/file'
68
+ end
69
+
70
+ context "when passed a directory path" do
71
+ it_behaves_like "running rsync to update the path", 'dir', '/vagrant/dir/', '/var/vagrant/dir/'
72
+ end
73
+
74
+ end
75
+ end
@@ -0,0 +1,5 @@
1
+ require 'spec_helper'
2
+
3
+ describe Vagrant::Mirror do
4
+ it { should be_a(Module) }
5
+ end