test-kitchen 1.0.0.beta.4 → 1.0.0.rc.1

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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +50 -1
  3. data/Gemfile +1 -1
  4. data/README.md +18 -7
  5. data/Rakefile +8 -1
  6. data/features/kitchen_init_command.feature +90 -11
  7. data/features/step_definitions/git_steps.rb +3 -0
  8. data/lib/kitchen/busser.rb +79 -45
  9. data/lib/kitchen/cli.rb +14 -13
  10. data/lib/kitchen/config.rb +79 -138
  11. data/lib/kitchen/data_munger.rb +224 -0
  12. data/lib/kitchen/driver/base.rb +4 -31
  13. data/lib/kitchen/driver/ssh_base.rb +6 -16
  14. data/lib/kitchen/driver.rb +4 -0
  15. data/lib/kitchen/generator/init.rb +20 -9
  16. data/lib/kitchen/instance.rb +53 -58
  17. data/lib/kitchen/lazy_hash.rb +50 -0
  18. data/lib/kitchen/platform.rb +2 -31
  19. data/lib/kitchen/provisioner/base.rb +55 -9
  20. data/lib/kitchen/provisioner/chef/berkshelf.rb +76 -0
  21. data/lib/kitchen/provisioner/chef/librarian.rb +72 -0
  22. data/lib/kitchen/provisioner/chef_base.rb +159 -78
  23. data/lib/kitchen/provisioner/chef_solo.rb +6 -36
  24. data/lib/kitchen/provisioner/chef_zero.rb +70 -59
  25. data/lib/kitchen/provisioner/dummy.rb +28 -0
  26. data/lib/kitchen/provisioner.rb +6 -4
  27. data/lib/kitchen/shell_out.rb +2 -5
  28. data/lib/kitchen/ssh.rb +1 -1
  29. data/lib/kitchen/suite.rb +10 -79
  30. data/lib/kitchen/util.rb +2 -2
  31. data/lib/kitchen/version.rb +2 -2
  32. data/lib/kitchen.rb +5 -0
  33. data/spec/kitchen/config_spec.rb +84 -123
  34. data/spec/kitchen/data_munger_spec.rb +1412 -0
  35. data/spec/kitchen/driver/base_spec.rb +30 -0
  36. data/spec/kitchen/instance_spec.rb +868 -86
  37. data/spec/kitchen/lazy_hash_spec.rb +63 -0
  38. data/spec/kitchen/platform_spec.rb +0 -22
  39. data/spec/kitchen/provisioner/base_spec.rb +210 -0
  40. data/spec/kitchen/provisioner_spec.rb +70 -0
  41. data/spec/kitchen/suite_spec.rb +25 -38
  42. data/spec/spec_helper.rb +1 -0
  43. data/support/chef-client-zero.rb +51 -35
  44. data/support/dummy-validation.pem +27 -0
  45. data/templates/init/kitchen.yml.erb +10 -22
  46. data/test-kitchen.gemspec +1 -2
  47. metadata +20 -18
@@ -16,6 +16,8 @@
16
16
  # See the License for the specific language governing permissions and
17
17
  # limitations under the License.
18
18
 
19
+ require 'kitchen/lazy_hash'
20
+
19
21
  module Kitchen
20
22
 
21
23
  module Provisioner
@@ -27,10 +29,35 @@ module Kitchen
27
29
 
28
30
  include Logging
29
31
 
30
- def initialize(instance, config)
31
- @instance = instance
32
- @config = config
33
- @logger = instance.logger
32
+ attr_accessor :instance
33
+
34
+ def initialize(config = {})
35
+ @config = LazyHash.new(config, self)
36
+ self.class.defaults.each do |attr, value|
37
+ @config[attr] = value unless @config.has_key?(attr)
38
+ end
39
+ end
40
+
41
+ # Returns the name of this driver, suitable for display in a CLI.
42
+ #
43
+ # @return [String] name of this driver
44
+ def name
45
+ self.class.name.split('::').last
46
+ end
47
+
48
+ # Provides hash-like access to configuration keys.
49
+ #
50
+ # @param attr [Object] configuration key
51
+ # @return [Object] value at configuration key
52
+ def [](attr)
53
+ config[attr]
54
+ end
55
+
56
+ # Returns an array of configuration keys.
57
+ #
58
+ # @return [Array] array of configuration keys
59
+ def config_keys
60
+ config.keys
34
61
  end
35
62
 
36
63
  def install_command ; end
@@ -45,19 +72,38 @@ module Kitchen
45
72
 
46
73
  def cleanup_sandbox ; end
47
74
 
48
- def home_path ; end
49
-
50
75
  protected
51
76
 
52
- attr_reader :instance, :logger, :config, :tmpdir
77
+ attr_reader :config
78
+
79
+ def logger
80
+ instance ? instance.logger : Kitchen.logger
81
+ end
53
82
 
54
83
  def sudo(script)
55
84
  config[:sudo] ? "sudo -E #{script}" : script
56
85
  end
57
86
 
58
- def kitchen_root
59
- config[:kitchen_root]
87
+ def self.defaults
88
+ @defaults ||= Hash.new.merge(super_defaults)
89
+ end
90
+
91
+ def self.super_defaults
92
+ klass = self.superclass
93
+
94
+ if klass.respond_to?(:defaults)
95
+ klass.defaults
96
+ else
97
+ Hash.new
98
+ end
99
+ end
100
+
101
+ def self.default_config(attr, value = nil, &block)
102
+ defaults[attr] = block_given? ? block : value
60
103
  end
104
+
105
+ default_config :root_path, "/tmp/kitchen"
106
+ default_config :sudo, true
61
107
  end
62
108
  end
63
109
  end
@@ -0,0 +1,76 @@
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
4
+ #
5
+ # Copyright (C) 2013, Fletcher Nichol
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+
19
+ require 'kitchen/errors'
20
+ require 'kitchen/logging'
21
+
22
+ module Kitchen
23
+
24
+ module Provisioner
25
+
26
+ module Chef
27
+
28
+ # Chef cookbook resolver that uses Berkshelf and a Berksfile to calculate
29
+ # dependencies.
30
+ #
31
+ # @author Fletcher Nichol <fnichol@nichol.ca>
32
+ class Berkshelf
33
+
34
+ include Logging
35
+
36
+ def initialize(berksfile, path, logger = Kitchen.logger)
37
+ @berksfile = berksfile
38
+ @path = path
39
+ @logger = logger
40
+ end
41
+
42
+ def resolve
43
+ info("Resolving cookbook dependencies with Berkshelf...")
44
+ debug("Using Berksfile from #{berksfile}")
45
+
46
+ load_berkshelf!
47
+
48
+ ::Berkshelf.ui.mute do
49
+ if ::Berkshelf::Berksfile.method_defined?(:vendor)
50
+ # Berkshelf 3.0 requires the directory to not exist
51
+ FileUtils.rm_rf(path)
52
+ ::Berkshelf::Berksfile.from_file(berksfile).vendor(path)
53
+ else
54
+ ::Berkshelf::Berksfile.from_file(berksfile).install(path: path)
55
+ end
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ attr_reader :berksfile, :path, :logger
62
+
63
+ def load_berkshelf!
64
+ require 'berkshelf'
65
+ rescue LoadError => e
66
+ fatal("The `berkshelf' gem is missing and must be installed" +
67
+ " or cannot be properly activated. Run" +
68
+ " `gem install berkshelf` or add the following to your" +
69
+ " Gemfile if you are using Bundler: `gem 'berkshelf'`.")
70
+ raise UserError,
71
+ "Could not load or activate Berkshelf (#{e.message})"
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,72 @@
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
4
+ #
5
+ # Copyright (C) 2013, Fletcher Nichol
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+
19
+ require 'kitchen/errors'
20
+ require 'kitchen/logging'
21
+
22
+ module Kitchen
23
+
24
+ module Provisioner
25
+
26
+ module Chef
27
+
28
+ # Chef cookbook resolver that uses Librarian-Chef and a Cheffile to
29
+ # calculate # dependencies.
30
+ #
31
+ # @author Fletcher Nichol <fnichol@nichol.ca>
32
+ class Librarian
33
+
34
+ include Logging
35
+
36
+ def initialize(cheffile, path, logger = Kitchen.logger)
37
+ @cheffile = cheffile
38
+ @path = path
39
+ @logger = logger
40
+ end
41
+
42
+ def resolve
43
+ info("Resolving cookbook dependencies with Librarian-Chef")
44
+ debug("Using Cheffile from #{cheffile}")
45
+
46
+ load_librarian!
47
+
48
+ env = ::Librarian::Chef::Environment.new(
49
+ :project_path => File.dirname(cheffile))
50
+ env.config_db.local["path"] = path
51
+ ::Librarian::Action::Resolve.new(env).run
52
+ ::Librarian::Action::Install.new(env).run
53
+ end
54
+
55
+ attr_reader :cheffile, :path, :logger
56
+
57
+ def load_librarian!
58
+ require 'librarian/chef/environment'
59
+ require 'librarian/action/resolve'
60
+ require 'librarian/action/install'
61
+ rescue LoadError => e
62
+ fatal("The `librarian-chef' gem is missing and must be installed" +
63
+ " or cannot be properly activated. Run" +
64
+ " `gem install librarian-chef` or add the following to your" +
65
+ " Gemfile if you are using Bundler: `gem 'librarian-chef'`.")
66
+ raise UserError,
67
+ "Could not load or activate Librarian-Chef (#{e.message})"
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -16,10 +16,12 @@
16
16
  # See the License for the specific language governing permissions and
17
17
  # limitations under the License.
18
18
 
19
- require 'buff/ignore'
20
19
  require 'fileutils'
21
20
  require 'pathname'
22
21
  require 'json'
22
+
23
+ require 'kitchen/provisioner/chef/berkshelf'
24
+ require 'kitchen/provisioner/chef/librarian'
23
25
  require 'kitchen/util'
24
26
 
25
27
  module Kitchen
@@ -31,10 +33,47 @@ module Kitchen
31
33
  # @author Fletcher Nichol <fnichol@nichol.ca>
32
34
  class ChefBase < Base
33
35
 
36
+ default_config :require_chef_omnibus, true
37
+ default_config :chef_omnibus_url, "https://www.opscode.com/chef/install.sh"
38
+ default_config :run_list, []
39
+ default_config :attributes, {}
40
+ default_config :cookbook_files_glob, %w[README.* metadata.{json,rb}
41
+ attributes/**/* definitions/**/* files/**/* libraries/**/*
42
+ providers/**/* recipes/**/* resources/**/* templates/**/*].join(",")
43
+
44
+ default_config :data_path do |provisioner|
45
+ provisioner.calculate_path("data")
46
+ end
47
+
48
+ default_config :data_bags_path do |provisioner|
49
+ provisioner.calculate_path("data_bags")
50
+ end
51
+
52
+ default_config :environments_path do |provisioner|
53
+ provisioner.calculate_path("environments")
54
+ end
55
+
56
+ default_config :nodes_path do |provisioner|
57
+ provisioner.calculate_path("nodes")
58
+ end
59
+
60
+ default_config :roles_path do |provisioner|
61
+ provisioner.calculate_path("roles")
62
+ end
63
+
64
+ default_config :encrypted_data_bag_secret_key_path do |provisioner|
65
+ provisioner.calculate_path("encrypted_data_bag_secret", :file)
66
+ end
67
+
68
+ def instance=(instance)
69
+ @instance = instance
70
+ expand_paths!
71
+ end
72
+
34
73
  def install_command
35
- return nil unless config[:require_chef_omnibus]
74
+ return unless config[:require_chef_omnibus]
36
75
 
37
- url = config[:chef_omnibus_url] || "https://www.opscode.com/chef/install.sh"
76
+ url = config[:chef_omnibus_url]
38
77
  flag = config[:require_chef_omnibus]
39
78
  version = if flag.is_a?(String) && flag != "latest"
40
79
  "-v #{flag.downcase}"
@@ -63,7 +102,9 @@ module Kitchen
63
102
  end
64
103
 
65
104
  def init_command
66
- "#{sudo('rm')} -rf #{home_path}"
105
+ dirs = %w{cookbooks data data_bags environments roles}.
106
+ map { |dir| File.join(config[:root_path], dir) }.join(" ")
107
+ "#{sudo('rm')} -rf #{dirs} ; mkdir -p #{config[:root_path]}"
67
108
  end
68
109
 
69
110
  def cleanup_sandbox
@@ -73,8 +114,59 @@ module Kitchen
73
114
  FileUtils.rmtree(tmpdir)
74
115
  end
75
116
 
117
+ def calculate_path(path, type = :directory)
118
+ base = config[:test_base_path]
119
+ candidates = []
120
+ candidates << File.join(base, instance.suite.name, path)
121
+ candidates << File.join(base, path)
122
+ candidates << File.join(Dir.pwd, path)
123
+
124
+ candidates.find do |c|
125
+ type == :directory ? File.directory?(c) : File.file?(c)
126
+ end
127
+ end
128
+
76
129
  protected
77
130
 
131
+ attr_reader :tmpdir
132
+
133
+ def expand_paths!
134
+ paths = %w{test_base data data_bags environments nodes roles}
135
+ paths.map{ |p| "#{p}_path".to_sym }.each do |key|
136
+ unless config[key].nil?
137
+ config[key] = File.expand_path(config[key], config[:kitchen_root])
138
+ end
139
+ end
140
+ end
141
+
142
+ def format_config_file(data)
143
+ data.each.map { |attr, value|
144
+ [attr, (value.is_a?(Array) ? value.to_s : %{"#{value}"})].join(" ")
145
+ }.join("\n")
146
+ end
147
+
148
+ def default_config_rb
149
+ root = config[:root_path]
150
+
151
+ {
152
+ :node_name => instance.name,
153
+ :checksum_path => "#{root}/checksums",
154
+ :file_cache_path => "#{root}/cache",
155
+ :file_backup_path => "#{root}/backup",
156
+ :cookbook_path => ["#{root}/cookbooks", "#{root}/site-cookbooks"],
157
+ :data_bag_path => "#{root}/data_bags",
158
+ :environment_path => "#{root}/environments",
159
+ :node_path => "#{root}/nodes",
160
+ :role_path => "#{root}/roles",
161
+ :client_path => "#{root}/clients",
162
+ :user_path => "#{root}/users",
163
+ :validation_key => "#{root}/validation.pem",
164
+ :client_key => "#{root}/client.pem",
165
+ :chef_server_url => "http://127.0.0.1:8889",
166
+ :encrypted_data_bag_secret => "#{root}/encrypted_data_bag_secret",
167
+ }
168
+ end
169
+
78
170
  def create_chef_sandbox
79
171
  @tmpdir = Dir.mktmpdir("#{instance.name}-sandbox-")
80
172
  File.chmod(0755, @tmpdir)
@@ -82,22 +174,36 @@ module Kitchen
82
174
 
83
175
  yield if block_given?
84
176
  prepare_json
177
+ prepare_cache
178
+ prepare_cookbooks
179
+ prepare_data
85
180
  prepare_data_bags
86
- prepare_roles
87
- prepare_nodes
88
181
  prepare_environments
182
+ prepare_nodes
183
+ prepare_roles
89
184
  prepare_secret
90
- prepare_cache
91
- prepare_cookbooks
92
185
  tmpdir
93
186
  end
94
187
 
95
188
  def prepare_json
189
+ dna = config[:attributes].merge({ :run_list => config[:run_list] })
190
+
96
191
  File.open(File.join(tmpdir, "dna.json"), "wb") do |file|
97
- file.write(instance.dna.to_json)
192
+ file.write(dna.to_json)
98
193
  end
99
194
  end
100
195
 
196
+ def prepare_data
197
+ return unless data
198
+
199
+ info("Preparing data")
200
+ debug("Using data from #{data}")
201
+
202
+ tmpdata_dir = File.join(tmpdir, "data")
203
+ FileUtils.mkdir_p(tmpdata_dir)
204
+ FileUtils.cp_r(Dir.glob("#{data}/*"), tmpdata_dir)
205
+ end
206
+
101
207
  def prepare_data_bags
102
208
  return unless data_bags
103
209
 
@@ -165,65 +271,71 @@ module Kitchen
165
271
  elsif File.exists?(metadata_rb)
166
272
  cp_this_cookbook
167
273
  else
168
- FileUtils.rmtree(tmpdir)
169
- fatal("Berksfile, Cheffile, cookbooks/, or metadata.rb" +
170
- " must exist in #{kitchen_root}")
171
- raise UserError, "Cookbooks could not be found"
274
+ make_fake_cookbook
172
275
  end
173
276
 
174
- remove_ignored_files
277
+ filter_only_cookbook_files
175
278
  end
176
279
 
177
- def remove_ignored_files
178
- cookbooks_in_tmpdir do |cookbook_path|
179
- chefignore = File.join(cookbook_path, "chefignore")
180
- if File.exist? chefignore
181
- ignores = Buff::Ignore::IgnoreFile.new(chefignore)
182
- cookbook_files = Dir.glob(File.join(cookbook_path, "**/*"), File::FNM_DOTMATCH).
183
- select { |fn| File.file?(fn) && fn != '.' && fn != '..' }
184
- cookbook_files.each { |file| FileUtils.rm(file) if ignores.ignored?(file) }
185
- end
186
- end
280
+ def filter_only_cookbook_files
281
+ info("Removing non-cookbook files in sandbox")
282
+ FileUtils.rm(all_files_in_cookbooks - only_cookbook_files)
283
+ end
284
+
285
+ def all_files_in_cookbooks
286
+ Dir.glob(File.join(tmpbooks_dir, "**/*"), File::FNM_DOTMATCH).
287
+ select { |fn| File.file?(fn) && ! %w{. ..}.include?(fn) }
288
+ end
289
+
290
+ def only_cookbook_files
291
+ glob = File.join(tmpbooks_dir, "*", "{#{config[:cookbook_files_glob]}}")
292
+
293
+ Dir.glob(glob, File::FNM_DOTMATCH).
294
+ select { |fn| File.file?(fn) && ! %w{. ..}.include?(fn) }
187
295
  end
188
296
 
189
297
  def berksfile
190
- File.join(kitchen_root, "Berksfile")
298
+ File.join(config[:kitchen_root], "Berksfile")
191
299
  end
192
300
 
193
301
  def cheffile
194
- File.join(kitchen_root, "Cheffile")
302
+ File.join(config[:kitchen_root], "Cheffile")
195
303
  end
196
304
 
197
305
  def metadata_rb
198
- File.join(kitchen_root, "metadata.rb")
306
+ File.join(config[:kitchen_root], "metadata.rb")
199
307
  end
200
308
 
201
309
  def cookbooks_dir
202
- File.join(kitchen_root, "cookbooks")
310
+ File.join(config[:kitchen_root], "cookbooks")
203
311
  end
204
312
 
205
313
  def site_cookbooks_dir
206
- File.join(kitchen_root, "site-cookbooks")
314
+ File.join(config[:kitchen_root], "site-cookbooks")
207
315
  end
208
316
 
209
317
  def data_bags
210
- instance.suite.data_bags_path
318
+ config[:data_bags_path]
211
319
  end
212
320
 
213
321
  def roles
214
- instance.suite.roles_path
322
+ config[:roles_path]
215
323
  end
216
324
 
217
325
  def nodes
218
- instance.suite.nodes_path
326
+ config[:nodes_path]
327
+ end
328
+
329
+ def data
330
+ config[:data_path]
219
331
  end
220
332
 
221
333
  def environments
222
- instance.suite.environments_path
334
+ config[:environments_path]
223
335
  end
224
336
 
225
337
  def secret
226
- instance.suite.encrypted_data_bag_secret_key_path
338
+ config[:encrypted_data_bag_secret_key_path]
227
339
  end
228
340
 
229
341
  def tmpbooks_dir
@@ -260,63 +372,32 @@ module Kitchen
260
372
 
261
373
  cb_path = File.join(tmpbooks_dir, cb_name)
262
374
 
263
- glob = Dir.glob("#{kitchen_root}/**")
375
+ glob = Dir.glob("#{config[:kitchen_root]}/**")
264
376
 
265
377
  FileUtils.mkdir_p(cb_path)
266
378
  FileUtils.cp_r(glob, cb_path)
267
379
  end
268
380
 
269
- def resolve_with_berkshelf
270
- info("Resolving cookbook dependencies with Berkshelf")
271
- debug("Using Berksfile from #{berksfile}")
272
-
273
- begin
274
- require 'berkshelf'
275
- rescue LoadError => e
276
- fatal("The `berkshelf' gem is missing and must be installed" +
277
- " or cannot be properly activated. Run" +
278
- " `gem install berkshelf` or add the following to your" +
279
- " Gemfile if you are using Bundler: `gem 'berkshelf'`.")
280
- raise UserError,
281
- "Could not load or activate Berkshelf (#{e.message})"
381
+ def make_fake_cookbook
382
+ info("Berksfile, Cheffile, cookbooks/, or metadata.rb not found " +
383
+ "so Chef will run with effectively no cookbooks. Is this intended?")
384
+ name = File.basename(config[:kitchen_root])
385
+ fake_cb = File.join(tmpbooks_dir, name)
386
+ FileUtils.mkdir_p(fake_cb)
387
+ File.open(File.join(fake_cb, "metadata.rb"), "wb") do |file|
388
+ file.write(%{name "#{name}\n"})
282
389
  end
390
+ end
283
391
 
392
+ def resolve_with_berkshelf
284
393
  Kitchen.mutex.synchronize do
285
- Berkshelf::Berksfile.from_file(berksfile).
286
- install(:path => tmpbooks_dir)
394
+ Chef::Berkshelf.new(berksfile, tmpbooks_dir, logger).resolve
287
395
  end
288
396
  end
289
397
 
290
398
  def resolve_with_librarian
291
- info("Resolving cookbook dependencies with Librarian-Chef")
292
- debug("Using Cheffile from #{cheffile}")
293
-
294
- begin
295
- require 'librarian/chef/environment'
296
- require 'librarian/action/resolve'
297
- require 'librarian/action/install'
298
- rescue LoadError => e
299
- fatal("The `librarian-chef' gem is missing and must be installed" +
300
- " or cannot be properly activated. Run" +
301
- " `gem install librarian-chef` or add the following to your" +
302
- " Gemfile if you are using Bundler: `gem 'librarian-chef'`.")
303
- raise UserError,
304
- "Could not load or activate Librarian-Chef (#{e.message})"
305
- end
306
-
307
399
  Kitchen.mutex.synchronize do
308
- env = Librarian::Chef::Environment.new(:project_path => kitchen_root)
309
- env.config_db.local["path"] = tmpbooks_dir
310
- Librarian::Action::Resolve.new(env).run
311
- Librarian::Action::Install.new(env).run
312
- end
313
- end
314
-
315
- private
316
-
317
- def cookbooks_in_tmpdir
318
- Dir.glob(File.join(tmpbooks_dir, "*/")).each do |cookbook_path|
319
- yield cookbook_path if block_given?
400
+ Chef::Librarian.new(cheffile, tmpbooks_dir, logger).resolve
320
401
  end
321
402
  end
322
403
  end