right_publish 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.rspec +4 -0
- data/Gemfile +20 -0
- data/Gemfile.lock +114 -0
- data/README.rdoc +113 -0
- data/Rakefile +48 -0
- data/VERSION +1 -0
- data/bin/right_publish +8 -0
- data/lib/right_publish/profile.rb +96 -0
- data/lib/right_publish/repo.rb +159 -0
- data/lib/right_publish/repos/apt.rb +178 -0
- data/lib/right_publish/repos/gem.rb +44 -0
- data/lib/right_publish/repos/yum.rb +167 -0
- data/lib/right_publish/storage.rb +90 -0
- data/lib/right_publish/stores/local.rb +29 -0
- data/lib/right_publish/stores/s3.rb +35 -0
- data/lib/right_publish.rb +97 -0
- data/right_publish.gemspec +101 -0
- data/spec/repo_manager_spec.rb +65 -0
- data/spec/repo_spec.rb +74 -0
- data/spec/repos/apt_spec.rb +288 -0
- data/spec/repos/gem_spec.rb +79 -0
- data/spec/repos/yum_spec.rb +282 -0
- data/spec/spec_helper.rb +30 -0
- data/spec/storage_manager_spec.rb +71 -0
- data/spec/storage_spec.rb +317 -0
- data/spec/stores/local_spec.rb +56 -0
- data/spec/stores/s3_spec.rb +44 -0
- metadata +274 -0
@@ -0,0 +1,282 @@
|
|
1
|
+
require 'right_publish/repo'
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe RightPublish::YumRepo do
|
5
|
+
|
6
|
+
def pkg_parts(path)
|
7
|
+
result = {:name=>nil, :version=>nil, :arch=>nil}
|
8
|
+
if /([A-Za-z\-+]+)-([0-9][A-Za-z0-9\-\.]*)\.(#{RightPublish::YumRepo::PKG_TYPES.join('|')})\.rpm\Z/.match(path)
|
9
|
+
result[:name] = $1
|
10
|
+
result[:version] = $2
|
11
|
+
result[:arch] = $3
|
12
|
+
end
|
13
|
+
result
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
before(:each) do
|
18
|
+
@repo_subdir = 'fake/'
|
19
|
+
@repo = flexmock(RightPublish::RepoManager.get_repository(:yum_repo)) do |obj|
|
20
|
+
obj.should_receive(:fetch)
|
21
|
+
obj.should_receive(:store)
|
22
|
+
obj.should_receive(:do_in_subdir).and_return {|dir,block| block.call}
|
23
|
+
obj.should_receive(:yum_prune)
|
24
|
+
obj.should_receive(:pkg_parts).and_return { |pkg| pkg_parts(pkg) }
|
25
|
+
end
|
26
|
+
|
27
|
+
flexmock(RightPublish::Profile) do |obj|
|
28
|
+
obj.should_receive(:log).and_return { |str,dbg| }
|
29
|
+
end
|
30
|
+
|
31
|
+
flexmock(File) { |obj| obj.should_receive(:file?).and_return(true) }
|
32
|
+
end
|
33
|
+
|
34
|
+
[false, true].each do |should_sign|
|
35
|
+
context "#publish#{" and sign" if should_sign}" do
|
36
|
+
|
37
|
+
# Test the EPEL (not flat) repo profile type
|
38
|
+
context "EPEL layout" do
|
39
|
+
|
40
|
+
def build_expectations(pkgs, expected_dists)
|
41
|
+
pkgs = Array(pkgs)
|
42
|
+
|
43
|
+
pkgs.each do |pkg|
|
44
|
+
pkg_arch = pkg_parts(pkg)[:arch]
|
45
|
+
dest_archs = case pkg_arch
|
46
|
+
when RightPublish::YumRepo::SRC_ALL_ARCH
|
47
|
+
[RightPublish::YumRepo::SRC_ALL_PATH]
|
48
|
+
when RightPublish::YumRepo::BIN_ALL_ARCH
|
49
|
+
RightPublish::YumRepo::ARCHITECTURES
|
50
|
+
else
|
51
|
+
[pkg_arch]
|
52
|
+
end
|
53
|
+
|
54
|
+
expected_dists.each_pair do |dist,releases|
|
55
|
+
releases.each do |release|
|
56
|
+
dest_archs.each do |arch|
|
57
|
+
dist_path = File.join(@repo_subdir, @epel_version.to_s, dist.to_s, release, arch)
|
58
|
+
@install_expectations[dist_path] ||= []
|
59
|
+
@install_expectations[dist_path] << pkg
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
@repo.should_receive(:install_file).and_return do |pkg,dir|
|
65
|
+
@install_expectations.should be_has_key(dir)
|
66
|
+
@install_expectations[dir].delete(pkg)
|
67
|
+
@install_expectations.delete(dir) if @install_expectations[dir].empty?
|
68
|
+
end
|
69
|
+
system_calls = @install_expectations.size
|
70
|
+
system_sign_calls = (@profile.config[:yum_repo][:gpg_key_id]) ? 1 : 0
|
71
|
+
@repo.should_receive(:system).times(system_calls).and_return(0)
|
72
|
+
@repo.should_receive(:shellout_with_password).times(system_sign_calls).and_return(0)
|
73
|
+
end
|
74
|
+
|
75
|
+
before(:each) do
|
76
|
+
@epel_dists = {:el=>['5','6'],:fc=>['12','13']}
|
77
|
+
@epel_version = 1
|
78
|
+
@install_expectations = {}
|
79
|
+
@profile = flexmock(RightPublish::Profile) do |obj|
|
80
|
+
obj.should_receive(:config).and_return {
|
81
|
+
{ :verbose=>false,
|
82
|
+
:remote_storage=>{:provider=>'mockremote'},
|
83
|
+
:local_storage=>{:provider=>'mocklocal'},
|
84
|
+
:yum_repo=>{
|
85
|
+
:epel=>@epel_version,
|
86
|
+
:subdir=>@repo_subdir,
|
87
|
+
:dists=>@epel_dists,
|
88
|
+
:gpg_key_id=>should_sign,
|
89
|
+
:gpg_password=>'fakepass'}
|
90
|
+
}
|
91
|
+
}
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
after(:each) do
|
96
|
+
@install_expectations.should be_empty
|
97
|
+
end
|
98
|
+
|
99
|
+
# Test for publish noarch packages (multicasting)
|
100
|
+
context "NoArch Packages" do
|
101
|
+
it "installs a single noarch rpm file to all distribution/architectures" do
|
102
|
+
rpm_file = 'foo-1.noarch.rpm'
|
103
|
+
build_expectations(rpm_file,@epel_dists)
|
104
|
+
@repo.publish(rpm_file, nil)
|
105
|
+
end
|
106
|
+
|
107
|
+
it "installs a single src rpm file to all dists" do
|
108
|
+
rpm_file = 'foo-1.src.rpm'
|
109
|
+
build_expectations(rpm_file,@epel_dists)
|
110
|
+
@repo.publish(rpm_file, nil)
|
111
|
+
end
|
112
|
+
|
113
|
+
it "installs a list of noarch rpm files to all distribution/architectures" do
|
114
|
+
rpm_files = ['foo-1.noarch.rpm', 'bar-2.noarch.rpm']
|
115
|
+
build_expectations(rpm_files,@epel_dists)
|
116
|
+
@repo.publish(rpm_files, nil)
|
117
|
+
end
|
118
|
+
|
119
|
+
it "globs and installs a directory of noarch rpm files to all distributions/architectures" do
|
120
|
+
rpm_files = ['foo-1.noarch.rpm', 'bar-2.src.rpm']
|
121
|
+
build_expectations(rpm_files,@epel_dists)
|
122
|
+
flexmock(File) { |obj| obj.should_receive(:directory?).and_return(true) }
|
123
|
+
flexmock(Dir) { |obj| obj.should_receive(:glob).and_return(rpm_files) }
|
124
|
+
@repo.publish('somedir/', nil)
|
125
|
+
end
|
126
|
+
|
127
|
+
it "fails with no files in a dir" do
|
128
|
+
flexmock(File) { |obj| obj.should_receive(:directory?).and_return(true) }
|
129
|
+
flexmock(Dir) { |obj| obj.should_receive(:glob).and_return([]) }
|
130
|
+
@repo.should_receive(:system).never
|
131
|
+
expect { @repo.publish('some_dir/', nil) }.to raise_error(RuntimeError)
|
132
|
+
end
|
133
|
+
|
134
|
+
it "fails with files missing a rpm extenstion" do
|
135
|
+
rpm_files = ['foo-1.noarch.rpm', 'bar-2.noarch.deb']
|
136
|
+
@repo.should_receive(:system).never
|
137
|
+
expect { @repo.publish(rpm_files, nil) }.to raise_error(RuntimeError)
|
138
|
+
end
|
139
|
+
|
140
|
+
it "fails with a mix of noarch and binary rpms" do
|
141
|
+
rpm_files = ['foo-1.noarch.rpm', 'bar-2.x86_64.rpm']
|
142
|
+
@repo.should_receive(:system).never
|
143
|
+
expect { @repo.publish(rpm_files, nil) }.to raise_error(RuntimeError)
|
144
|
+
end
|
145
|
+
end # end NoArch context
|
146
|
+
|
147
|
+
context "Architecture Dependant Packages" do
|
148
|
+
before(:each) do
|
149
|
+
@target_distribution_map = {:fc=>['12']}
|
150
|
+
@target_distribution_str = 'fc/12'
|
151
|
+
end
|
152
|
+
|
153
|
+
it "installs a single x86_64 rpm file to a dist" do
|
154
|
+
rpm_file = 'foo-1.x86_64.rpm'
|
155
|
+
build_expectations(rpm_file,@target_distribution_map)
|
156
|
+
@repo.publish(rpm_file, @target_distribution_str)
|
157
|
+
end
|
158
|
+
|
159
|
+
it "installs a list of x86_64 rpms to a dist" do
|
160
|
+
rpm_files = ['foo-1.x86_64.rpm', 'bar-2.x86_64.rpm']
|
161
|
+
build_expectations(rpm_files,@target_distribution_map)
|
162
|
+
@repo.publish(rpm_files, @target_distribution_str)
|
163
|
+
end
|
164
|
+
|
165
|
+
it "installs a directory mixed of x86_64 and i386 rpms to a dist" do
|
166
|
+
rpm_files = ['foo-1.x86_64.rpm', 'bar-2.i386.rpm']
|
167
|
+
build_expectations(rpm_files,@target_distribution_map)
|
168
|
+
flexmock(File) { |obj| obj.should_receive(:directory?).and_return(true) }
|
169
|
+
flexmock(Dir) { |obj| obj.should_receive(:glob).and_return(rpm_files) }
|
170
|
+
@repo.publish('some_dir/', @target_distribution_str)
|
171
|
+
end
|
172
|
+
|
173
|
+
it "fails with no files in a dir" do
|
174
|
+
flexmock(File) { |obj| obj.should_receive(:directory?).and_return(true) }
|
175
|
+
flexmock(Dir) { |obj| obj.should_receive(:glob).and_return([]) }
|
176
|
+
@repo.should_receive(:system).never
|
177
|
+
expect { @repo.publish('some_dir/', @target_distribution_str) }.to raise_error(RuntimeError)
|
178
|
+
end
|
179
|
+
|
180
|
+
it "fails with files missing a rpm extension" do
|
181
|
+
rpm_files = ['foo-1.x86_64.rpm', 'bar-2.i386.deb']
|
182
|
+
@repo.should_receive(:system).never
|
183
|
+
expect { @repo.publish(rpm_files, @target_distribution_str) }.to raise_error(RuntimeError)
|
184
|
+
end
|
185
|
+
|
186
|
+
it "fails with unknown architectures" do
|
187
|
+
rpm_files = ['foo-1.ppc.rpm', 'bar-2.ppc.rpm']
|
188
|
+
@repo.should_receive(:system).never
|
189
|
+
expect { @repo.publish(rpm_files, @target_distribution_str) }.to raise_error(RuntimeError)
|
190
|
+
end
|
191
|
+
|
192
|
+
it "fails when not provided with a target" do
|
193
|
+
rpm_files = ['foo-1.x86_64.rpm', 'bar-2.i386.rpm']
|
194
|
+
@repo.should_receive(:system).never
|
195
|
+
expect { @repo.publish(rpm_files, nil) }.to raise_error(RuntimeError)
|
196
|
+
end
|
197
|
+
end # end Architecture Dependant context
|
198
|
+
end # end EPEL context
|
199
|
+
|
200
|
+
# Test the Flat repo profile type
|
201
|
+
context "Flat layout" do
|
202
|
+
def build_expectations(pkgs)
|
203
|
+
@install_expectations = Array(pkgs)
|
204
|
+
@repo.should_receive(:install_file).and_return do |pkg,dir|
|
205
|
+
@install_expectations.delete(pkg)
|
206
|
+
end
|
207
|
+
system_calls = 1
|
208
|
+
system_sign_calls = (@profile.config[:yum_repo][:gpg_key_id]) ? 1 : 0
|
209
|
+
@repo.should_receive(:system).times(system_calls).and_return(0)
|
210
|
+
@repo.should_receive(:shellout_with_password).times(system_sign_calls).and_return(0)
|
211
|
+
end
|
212
|
+
|
213
|
+
before(:each) do
|
214
|
+
@epel_version = false
|
215
|
+
@install_expectations = []
|
216
|
+
@profile = flexmock(RightPublish::Profile) do |obj|
|
217
|
+
obj.should_receive(:config).and_return {
|
218
|
+
{ :verbose=>false,
|
219
|
+
:remote_storage=>{:provider=>'mockremote'},
|
220
|
+
:local_storage=>{:provider=>'mocklocal'},
|
221
|
+
:yum_repo=>{
|
222
|
+
:epel=>@epel_version,
|
223
|
+
:subdir=>@repo_subdir,
|
224
|
+
:gpg_key_id=>should_sign,
|
225
|
+
:gpg_password=>'fakepass'}
|
226
|
+
}
|
227
|
+
}
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
after(:each) do
|
232
|
+
@install_expectations.should be_empty
|
233
|
+
end
|
234
|
+
|
235
|
+
it "installs a single rpm file to repo" do
|
236
|
+
rpm_file = 'foo-1.noarch.rpm'
|
237
|
+
build_expectations(rpm_file)
|
238
|
+
@repo.publish(rpm_file, nil)
|
239
|
+
end
|
240
|
+
|
241
|
+
it "installs a single src rpm file to repo" do
|
242
|
+
rpm_file = 'foo-1.src.rpm'
|
243
|
+
build_expectations(rpm_file)
|
244
|
+
@repo.publish(rpm_file, nil)
|
245
|
+
end
|
246
|
+
|
247
|
+
it "installs a list of rpm files to repo" do
|
248
|
+
rpm_files = ['foo-1.x86_64.rpm', 'bar-2.x86_64.rpm']
|
249
|
+
build_expectations(rpm_files)
|
250
|
+
@repo.publish(rpm_files, nil)
|
251
|
+
end
|
252
|
+
|
253
|
+
it "globs and installs a directory of rpm files to repo" do
|
254
|
+
rpm_files = ['foo-1.noarch.rpm', 'bar-2.src.rpm']
|
255
|
+
build_expectations(rpm_files)
|
256
|
+
flexmock(File) { |obj| obj.should_receive(:directory?).and_return(true) }
|
257
|
+
flexmock(Dir) { |obj| obj.should_receive(:glob).and_return(rpm_files) }
|
258
|
+
@repo.publish('somedir/', nil)
|
259
|
+
end
|
260
|
+
|
261
|
+
it "succeeds with a mix of noarch and binary rpms" do
|
262
|
+
rpm_files = ['foo-1.noarch.rpm', 'bar-2.x86_64.rpm']
|
263
|
+
build_expectations(rpm_files)
|
264
|
+
@repo.publish(rpm_files, nil)
|
265
|
+
end
|
266
|
+
|
267
|
+
it "fails with no files in a dir" do
|
268
|
+
flexmock(File) { |obj| obj.should_receive(:directory?).and_return(true) }
|
269
|
+
flexmock(Dir) { |obj| obj.should_receive(:glob).and_return([]) }
|
270
|
+
@repo.should_receive(:system).never
|
271
|
+
expect { @repo.publish('some_dir/', nil) }.to raise_error(RuntimeError)
|
272
|
+
end
|
273
|
+
|
274
|
+
it "fails with files missing a rpm extenstion" do
|
275
|
+
rpm_files = ['foo-1.i386.rpm', 'bar-2.noarch.deb']
|
276
|
+
@repo.should_receive(:system).never
|
277
|
+
expect { @repo.publish('some_dir/', nil) }.to raise_error(RuntimeError)
|
278
|
+
end
|
279
|
+
end # end Flat context
|
280
|
+
end # end #publish context
|
281
|
+
end
|
282
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# Copyright (c) 2012- RightScale Inc
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
# a copy of this software and associated documentation files (the
|
5
|
+
# "Software"), to deal in the Software without restriction, including
|
6
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
# the following conditions:
|
10
|
+
#
|
11
|
+
# The above copyright notice and this permission notice shall be
|
12
|
+
# included in all copies or substantial portions of the Software.
|
13
|
+
#
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
|
+
|
22
|
+
require 'rubygems'
|
23
|
+
require 'bundler/setup'
|
24
|
+
require 'flexmock'
|
25
|
+
require 'ruby-debug'
|
26
|
+
require 'simplecov' unless RUBY_VERSION =~ /1\.8/
|
27
|
+
|
28
|
+
RSpec.configure do |config|
|
29
|
+
config.mock_with :flexmock
|
30
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'right_publish/storage'
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe RightPublish::StorageManager do
|
5
|
+
|
6
|
+
def is_registered?(name, key)
|
7
|
+
@storage_types = RightPublish::StorageManager.storage_types
|
8
|
+
@storage_types.should have_key(name)
|
9
|
+
@storage_types[name].should equal(key)
|
10
|
+
RightPublish::StorageManager.get_storage(key)::STORAGE_KEY.should be_equal(key)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should provide a hash of known storage types" do
|
14
|
+
RightPublish::StorageManager.storage_types.should be_instance_of(Hash)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "returns nil for unknown storage types" do
|
18
|
+
RightPublish::StorageManager.get_storage(:doesnotexist_storage).should be_equal(nil)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should know about the local storage" do
|
22
|
+
is_registered?('local', :local_storage)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should know about the s3 storage" do
|
26
|
+
is_registered?('s3', :s3_storage)
|
27
|
+
end
|
28
|
+
|
29
|
+
context ".register_storage" do
|
30
|
+
it "rejects storage without a STORAGE_KEY" do
|
31
|
+
class MissingKey
|
32
|
+
end
|
33
|
+
expect { RightPublish::StorageManager.register_storage(MissingKey) }.to raise_error(NameError)
|
34
|
+
end
|
35
|
+
|
36
|
+
it "rejects storage whose key does not end in _storage" do
|
37
|
+
class BadKey
|
38
|
+
STORAGE_KEY = :bad_key
|
39
|
+
end
|
40
|
+
expect { RightPublish::StorageManager.register_storage(BadKey) }.to raise_error(TypeError)
|
41
|
+
end
|
42
|
+
|
43
|
+
it "rejects storage who do not provide .get_directories" do
|
44
|
+
class NoDirectories
|
45
|
+
STORAGE_KEY = :nooptions_storage
|
46
|
+
end
|
47
|
+
expect { RightPublish::StorageManager.register_storage(NoDirectories) }.to raise_error(TypeError)
|
48
|
+
end
|
49
|
+
|
50
|
+
it "rejects storage who do not provide options" do
|
51
|
+
class NoOptions
|
52
|
+
STORAGE_KEY = :nooptions_storage
|
53
|
+
def self.get_directories()
|
54
|
+
end
|
55
|
+
end
|
56
|
+
expect { RightPublish::StorageManager.register_storage(NoOptions) }.to raise_error(NameError)
|
57
|
+
end
|
58
|
+
|
59
|
+
it "accpets storage who are properly defined" do
|
60
|
+
class GoodStorage
|
61
|
+
STORAGE_KEY = :good_storage
|
62
|
+
STORAGE_OPTIONS = {}
|
63
|
+
def self.get_directories()
|
64
|
+
end
|
65
|
+
end
|
66
|
+
RightPublish::StorageManager.register_storage(GoodStorage).should be_equal(nil)
|
67
|
+
is_registered?('good', :good_storage)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
@@ -0,0 +1,317 @@
|
|
1
|
+
require 'right_publish/storage'
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe RightPublish::Storage do
|
5
|
+
|
6
|
+
# Create a File entity.
|
7
|
+
def make_file(name, expects, copy, destroy)
|
8
|
+
file = flexmock("File:#{name}")
|
9
|
+
|
10
|
+
# We can set some expectations on it like if we believe we're
|
11
|
+
# going to be removed from Storage.
|
12
|
+
destroy_calls = 0
|
13
|
+
munge_body = false
|
14
|
+
if expects.instance_of? Hash
|
15
|
+
destroy_calls = (expects.has_key?(:destroy) && expects[:destroy] && 1) || 0
|
16
|
+
munge_body = (expects.has_key?(:munge_body) && expects[:munge_body]) || false
|
17
|
+
end
|
18
|
+
|
19
|
+
# For simplicity use the filename as the "contents" but provide a
|
20
|
+
# way to corrupt it to force testing content, not name matching.
|
21
|
+
file.should_receive(:body).and_return { (munge_body && 'data_mismatch') || name }
|
22
|
+
|
23
|
+
# Use the copy callback to insert a copy of ourselves at a different
|
24
|
+
# path (Remote duplication).
|
25
|
+
file.should_receive(:copy).and_return do |dir_key,dest|
|
26
|
+
new_file = make_file(dest, nil, copy, destroy)
|
27
|
+
copy.call(new_file)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Use the destroy callback to remove outselves from the Files obj.
|
31
|
+
file.should_receive(:destroy).times(destroy_calls).and_return { destroy.call(name) }
|
32
|
+
file.should_receive(:key).and_return(name)
|
33
|
+
file
|
34
|
+
end
|
35
|
+
|
36
|
+
# Create a Fog Storage Mock
|
37
|
+
def make_storage(path, file_hash, expectations)
|
38
|
+
# List of File mocks bound to this storage instance
|
39
|
+
files = []
|
40
|
+
|
41
|
+
# These procedures are used as callbacks for the File mocks to copy
|
42
|
+
# or destroy themselves and have a side-effect on the about Array
|
43
|
+
create_proc = Proc.new { |file| files << file }
|
44
|
+
delete_proc = Proc.new { |file| files.delete_if { |f| f.key == file } }
|
45
|
+
|
46
|
+
# Create the File mocks and store in the Array
|
47
|
+
file_hash.each_pair do |name,expects|
|
48
|
+
files << make_file(name, expects, create_proc, delete_proc )
|
49
|
+
end
|
50
|
+
|
51
|
+
# Fog has a weird notion of a 'Files' and 'Directory' object being
|
52
|
+
# distinct. We care here because create is called on the 'Files'
|
53
|
+
# rather then on the 'Directory' and it is Enumerable.
|
54
|
+
file_list = flexmock(files)
|
55
|
+
file_list.should_receive(:create).
|
56
|
+
times(expectations[:create_call]).
|
57
|
+
and_return do |args|
|
58
|
+
new_file = make_file( args[:key], nil, create_proc, delete_proc )
|
59
|
+
files << new_file
|
60
|
+
new_file
|
61
|
+
end
|
62
|
+
|
63
|
+
# The only thing ever called on the Storage object is the
|
64
|
+
# #get_directories interface, or at least that's all we use.
|
65
|
+
obj = flexmock('Storage')
|
66
|
+
obj.should_receive(:get_directories).and_return do
|
67
|
+
# The Directory object is synonymous to an S3 bucket object
|
68
|
+
# and is responsible for providing the Files enumerable and
|
69
|
+
# calculating the MD5 hash of a given file.
|
70
|
+
f = flexmock('Directory')
|
71
|
+
f.should_receive(:compute_md5).and_return {|file| Digest::MD5.hexdigest(File.basename(file.body))}
|
72
|
+
f.should_receive(:files).and_return(file_list)
|
73
|
+
f.should_receive(:key).and_return(path)
|
74
|
+
f
|
75
|
+
end
|
76
|
+
obj
|
77
|
+
end
|
78
|
+
|
79
|
+
# In order to match we just make sure that the sizes of the Files
|
80
|
+
# objects match and that every File within A is also within B. We don't
|
81
|
+
# have to check the reverse since the sizes implies it.
|
82
|
+
def storage_match?(a,b)
|
83
|
+
a_files = a.get_directories.files
|
84
|
+
b_files = b.get_directories.files
|
85
|
+
|
86
|
+
return false if a_files.count != b_files.count
|
87
|
+
a_files.all? { |a_f|
|
88
|
+
name = a_f.key
|
89
|
+
b_files.any? { |b_f| name == b_f.key }
|
90
|
+
}
|
91
|
+
end
|
92
|
+
|
93
|
+
before(:each) do
|
94
|
+
flexmock(RightPublish::Profile) do |obj|
|
95
|
+
obj.should_receive(:log).and_return { |str,debug|
|
96
|
+
}
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
context ".sync_dirs" do
|
101
|
+
it "handles source AND destination storages that are empty" do
|
102
|
+
src_storage = make_storage(
|
103
|
+
'foo',
|
104
|
+
{},
|
105
|
+
{:create_call=>0} )
|
106
|
+
|
107
|
+
# First copy of file1 will use create, second will be duped
|
108
|
+
dest_storage = make_storage(
|
109
|
+
'bar',
|
110
|
+
{},
|
111
|
+
{:create_call=>0} )
|
112
|
+
|
113
|
+
RightPublish::Storage.sync_dirs(src_storage, dest_storage, :sweep=>true)
|
114
|
+
storage_match?(src_storage, dest_storage).should be_true
|
115
|
+
end
|
116
|
+
|
117
|
+
it "does nothing with identical directories" do
|
118
|
+
src_storage = make_storage(
|
119
|
+
'foo',
|
120
|
+
{ 'dir1/file1'=>nil,
|
121
|
+
'dir1/file2'=>nil,
|
122
|
+
'file3'=>nil },
|
123
|
+
{:create_call=>0} )
|
124
|
+
|
125
|
+
dest_storage = make_storage(
|
126
|
+
'bar',
|
127
|
+
{ 'dir1/file1'=>nil,
|
128
|
+
'dir1/file2'=>nil,
|
129
|
+
'file3'=>nil },
|
130
|
+
{:create_call=>0} )
|
131
|
+
|
132
|
+
RightPublish::Storage.sync_dirs(src_storage, dest_storage)
|
133
|
+
storage_match?(src_storage, dest_storage).should be_true
|
134
|
+
end
|
135
|
+
|
136
|
+
it "copies a new file to the destination" do
|
137
|
+
src_storage = make_storage(
|
138
|
+
'foo',
|
139
|
+
{ 'dir1/file1'=>nil,
|
140
|
+
'dir1/file2'=>nil,
|
141
|
+
'file3'=>nil,
|
142
|
+
'file4'=>nil },
|
143
|
+
{:create_call=>0} )
|
144
|
+
|
145
|
+
dest_storage = make_storage(
|
146
|
+
'bar',
|
147
|
+
{ 'dir1/file1'=>nil,
|
148
|
+
'dir1/file2'=>nil,
|
149
|
+
'file3'=>nil },
|
150
|
+
{:create_call=>1} )
|
151
|
+
|
152
|
+
RightPublish::Storage.sync_dirs(src_storage, dest_storage)
|
153
|
+
storage_match?(src_storage, dest_storage).should be_true
|
154
|
+
end
|
155
|
+
|
156
|
+
it "handles source storages that are empty" do
|
157
|
+
src_storage = make_storage(
|
158
|
+
'foo',
|
159
|
+
{},
|
160
|
+
{:create_call=>0} )
|
161
|
+
|
162
|
+
# First copy of file1 will use create, second will be duped
|
163
|
+
dest_storage = make_storage(
|
164
|
+
'bar',
|
165
|
+
{ 'dir1/file1'=>{:destroy=>true} },
|
166
|
+
{:create_call=>0} )
|
167
|
+
|
168
|
+
RightPublish::Storage.sync_dirs(src_storage, dest_storage, :sweep=>true)
|
169
|
+
storage_match?(src_storage, dest_storage).should be_true
|
170
|
+
end
|
171
|
+
|
172
|
+
it "handles destination storages that are empty" do
|
173
|
+
src_storage = make_storage(
|
174
|
+
'foo',
|
175
|
+
{ 'dir1/file1'=>nil },
|
176
|
+
{:create_call=>0} )
|
177
|
+
|
178
|
+
# First copy of file1 will use create, second will be duped
|
179
|
+
dest_storage = make_storage(
|
180
|
+
'bar',
|
181
|
+
{},
|
182
|
+
{:create_call=>1} )
|
183
|
+
|
184
|
+
RightPublish::Storage.sync_dirs(src_storage, dest_storage, :sweep=>true)
|
185
|
+
storage_match?(src_storage, dest_storage).should be_true
|
186
|
+
end
|
187
|
+
|
188
|
+
it "leaves an extraneous file in the destination w/o :sweep" do
|
189
|
+
src_storage = make_storage(
|
190
|
+
'foo',
|
191
|
+
{ 'dir1/file1'=>nil,
|
192
|
+
'dir1/file2'=>nil,
|
193
|
+
'file3'=>nil },
|
194
|
+
{:create_call=>0} )
|
195
|
+
|
196
|
+
dest_storage = make_storage(
|
197
|
+
'bar',
|
198
|
+
{ 'dir1/file1'=>nil,
|
199
|
+
'dir1/file2'=>nil,
|
200
|
+
'file3'=>nil,
|
201
|
+
'file4'=>nil },
|
202
|
+
{:create_call=>0} )
|
203
|
+
|
204
|
+
RightPublish::Storage.sync_dirs(src_storage, dest_storage)
|
205
|
+
|
206
|
+
# We should NOT match here. Not sure what the usecase of this
|
207
|
+
# is, but it's tested anyways.
|
208
|
+
storage_match?(src_storage, dest_storage).should be_false
|
209
|
+
end
|
210
|
+
|
211
|
+
it "destroys an extraneous file from the destination w/ :sweep" do
|
212
|
+
src_storage = make_storage(
|
213
|
+
'foo',
|
214
|
+
{ 'dir1/file1'=>nil,
|
215
|
+
'dir1/file2'=>nil,
|
216
|
+
'file3'=>nil },
|
217
|
+
{:create_call=>0} )
|
218
|
+
|
219
|
+
dest_storage = make_storage(
|
220
|
+
'bar',
|
221
|
+
{ 'dir1/file1'=>nil,
|
222
|
+
'dir1/file2'=>nil,
|
223
|
+
'file3'=>nil,
|
224
|
+
'file4'=>{:destroy=>true} },
|
225
|
+
{:create_call=>0} )
|
226
|
+
|
227
|
+
RightPublish::Storage.sync_dirs(src_storage, dest_storage, :sweep=>true)
|
228
|
+
storage_match?(src_storage, dest_storage).should be_true
|
229
|
+
end
|
230
|
+
|
231
|
+
it "destroys and copies files as needed to synchronize directories" do
|
232
|
+
src_storage = make_storage(
|
233
|
+
'foo',
|
234
|
+
{ 'dir1/file1'=>nil,
|
235
|
+
'dir1/file2'=>nil,
|
236
|
+
'file3'=>nil,
|
237
|
+
'file4'=>nil,
|
238
|
+
'file6'=>nil },
|
239
|
+
{:create_call=>0} )
|
240
|
+
|
241
|
+
dest_storage = make_storage(
|
242
|
+
'bar',
|
243
|
+
{ 'dir1/file1'=>nil,
|
244
|
+
'dir1/file2'=>nil,
|
245
|
+
'file3'=>nil,
|
246
|
+
'file5'=>{:destroy=>true},
|
247
|
+
'dir2/file2'=>{:destroy=>true} },
|
248
|
+
{:create_call=>2} )
|
249
|
+
|
250
|
+
RightPublish::Storage.sync_dirs(src_storage, dest_storage, :sweep=>true)
|
251
|
+
storage_match?(src_storage, dest_storage).should be_true
|
252
|
+
end
|
253
|
+
|
254
|
+
it "uses the remote copy facilities when making duplicates" do
|
255
|
+
src_storage = make_storage(
|
256
|
+
'foo',
|
257
|
+
{ 'dir1/file1'=>nil,
|
258
|
+
'dir1/file2'=>nil,
|
259
|
+
'dir2/file1'=>nil,
|
260
|
+
'file4'=>nil },
|
261
|
+
{:create_call=>0} )
|
262
|
+
|
263
|
+
# There are two duplicates, 'file1', in the local store. To save
|
264
|
+
# time/data we just call copy on the file that already exists in
|
265
|
+
# the remote, notice the '0' create_calls.
|
266
|
+
dest_storage = make_storage(
|
267
|
+
'bar',
|
268
|
+
{ 'dir1/file1'=>nil,
|
269
|
+
'dir1/file2'=>nil,
|
270
|
+
'file4'=>nil },
|
271
|
+
{:create_call=>0} )
|
272
|
+
|
273
|
+
RightPublish::Storage.sync_dirs(src_storage, dest_storage, :sweep=>true)
|
274
|
+
storage_match?(src_storage, dest_storage).should be_true
|
275
|
+
end
|
276
|
+
|
277
|
+
it "uses the content of the file, not the name to determine equality" do
|
278
|
+
src_storage = make_storage(
|
279
|
+
'foo',
|
280
|
+
{ 'dir1/file1'=>nil,
|
281
|
+
'dir1/file2'=>nil,
|
282
|
+
'file4'=>nil },
|
283
|
+
{:create_call=>0} )
|
284
|
+
|
285
|
+
# :munge_body cause the MD5 sum to mismatch the file name
|
286
|
+
dest_storage = make_storage(
|
287
|
+
'bar',
|
288
|
+
{ 'dir1/file1'=>{:munge_body=>true,:destroy=>true},
|
289
|
+
'dir1/file2'=>nil,
|
290
|
+
'file4'=>nil },
|
291
|
+
{:create_call=>1} )
|
292
|
+
|
293
|
+
RightPublish::Storage.sync_dirs(src_storage, dest_storage, :sweep=>true)
|
294
|
+
storage_match?(src_storage, dest_storage).should be_true
|
295
|
+
end
|
296
|
+
|
297
|
+
it "uses create AND duplicate routines with multiple new identical files" do
|
298
|
+
src_storage = make_storage(
|
299
|
+
'foo',
|
300
|
+
{ 'dir1/file1'=>nil,
|
301
|
+
'dir1/file2'=>nil,
|
302
|
+
'dir2/dir3/file2'=>nil,
|
303
|
+
'file2'=>nil },
|
304
|
+
{:create_call=>0} )
|
305
|
+
|
306
|
+
# First copy of file1 will use create, the rest will be duped
|
307
|
+
dest_storage = make_storage(
|
308
|
+
'bar',
|
309
|
+
{ 'dir1/file1'=>nil },
|
310
|
+
{:create_call=>1} )
|
311
|
+
|
312
|
+
RightPublish::Storage.sync_dirs(src_storage, dest_storage, :sweep=>true)
|
313
|
+
storage_match?(src_storage, dest_storage).should be_true
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
end
|