From 7a299ae2de64de71df75ff5e8cce73c912224819 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 23 Jun 2012 19:56:31 -0700 Subject: [PATCH] Configuration loader can handle upgrading. The basic process for this is to: 1. Load the configuration using the proper loader for that version. i.e. if you're loading V1 config, then use the V1 loader. 2. If we just loaded a version that isn't current (imagine we're currently at V3), then we need to upgrade that config. So we first ask the V2 loader to upgrade the V1 config to V2, then we ask the V3 loader to upgrade the V2 config to V3. We keep track of warnings and errors throughout this process. 3. Finally, we have a current config, so we merge it into the in-process configuration that is being loaded. --- lib/vagrant/config/loader.rb | 37 ++++- lib/vagrant/config/version_base.rb | 18 +++ test/unit/vagrant/config/loader_test.rb | 178 ++++++++++++++---------- 3 files changed, 157 insertions(+), 76 deletions(-) diff --git a/lib/vagrant/config/loader.rb b/lib/vagrant/config/loader.rb index 8eac2e747..dc9739e35 100644 --- a/lib/vagrant/config/loader.rb +++ b/lib/vagrant/config/loader.rb @@ -81,7 +81,7 @@ module Vagrant end # Get the current version config class to use - current_version = @version_order.last + current_version = @version_order.last current_config_klass = @versions.get(current_version) # This will hold our result @@ -93,7 +93,40 @@ module Vagrant @sources[key].each do |version, proc| if !@config_cache.has_key?(proc) @logger.debug("Loading from: #{key} (evaluating)") - @config_cache[proc] = current_config_klass.load(proc) + + # Get the proper version loader for this version and load + version_loader = @versions.get(version) + version_config = version_loader.load(proc) + + # If this version is not the current version, then we need + # to upgrade to the latest version. + if version != current_version + @logger.debug("Upgrading config from #{version} to #{current_version}") + version_index = @version_order.index(version) + current_index = @version_order.index(current_version) + + (version_index + 1).upto(current_index) do |index| + next_version = @version_order[index] + @logger.debug("Upgrading config to version #{next_version}") + + # Get the loader of this version and ask it to upgrade + loader = @versions.get(next_version) + upgrade_result = loader.upgrade(version_config) + + # XXX: Do something with the warning/error messages + warnings = upgrade_result[1] + errors = upgrade_result[2] + @logger.debug("Upgraded to version #{next_version} with " + + "#{warnings.length} warnings and " + + "#{errors.length} errors") + + # Store the new upgraded version + version_config = upgrade_result[0] + end + end + + # Cache the results for this proc + @config_cache[proc] = version_config else @logger.debug("Loading from: #{key} (cache)") end diff --git a/lib/vagrant/config/version_base.rb b/lib/vagrant/config/version_base.rb index f1e0551fd..c3a8375c3 100644 --- a/lib/vagrant/config/version_base.rb +++ b/lib/vagrant/config/version_base.rb @@ -57,6 +57,24 @@ module Vagrant def self.merge(old, new) raise NotImplementedError end + + # This is called if a previous version of configuration needs to be + # upgraded to this version. Each version of configuration should know + # how to upgrade the version immediately prior to it. This should be + # a best effort upgrade that makes many assumptions. The goal is for + # this to work in almost every case, but perhaps with some warnings. + # The return value for this is a 3-tuple: `[object, warnings, errors]`, + # where `object` is the upgraded configuration object, `warnings` is + # an array of warning messages, and `errors` is an array of error + # messages. + # + # @param [Object] old The version of the configuration object just + # prior to this one. + # @return [Array] The 3-tuple result. Please see the above documentation + # for more information on the exact structure of this object. + def self.upgrade(old) + raise NotImplementedError + end end end end diff --git a/test/unit/vagrant/config/loader_test.rb b/test/unit/vagrant/config/loader_test.rb index a6fa096b4..dd8bef56f 100644 --- a/test/unit/vagrant/config/loader_test.rb +++ b/test/unit/vagrant/config/loader_test.rb @@ -38,95 +38,125 @@ describe Vagrant::Config::Loader do let(:instance) { described_class.new(versions, version_order) } - it "should ignore non-existent load order keys" do - instance.load_order = [:foo] - instance.load - end - - it "should load and return the configuration" do - proc = Proc.new do |config| - config[:foo] = "yep" + describe "basic loading" do + it "should ignore non-existent load order keys" do + instance.load_order = [:foo] + instance.load end - instance.load_order = [:proc] - instance.set(:proc, [[current_version, proc]]) - config = instance.load + it "should load and return the configuration" do + proc = Proc.new do |config| + config[:foo] = "yep" + end - config[:foo].should == "yep" - end + instance.load_order = [:proc] + instance.set(:proc, [[current_version, proc]]) + config = instance.load - it "should finalize the configuration" do - # Create the finalize method on our loader - def test_loader.finalize(obj) - obj[:finalized] = true - obj - end - - # Basic configuration proc - proc = lambda do |config| - config[:foo] = "yep" - end - - # Run the actual configuration and assert that we get the proper result - instance.load_order = [:proc] - instance.set(:proc, [[current_version, proc]]) - config = instance.load - config[:foo].should == "yep" - config[:finalized].should == true - end - - it "should only run the same proc once" do - count = 0 - proc = Proc.new do |config| - config[:foo] = "yep" - count += 1 - end - - instance.load_order = [:proc] - instance.set(:proc, [[current_version, proc]]) - - 5.times do - result = instance.load - - # Verify the config result - result[:foo].should == "yep" - - # Verify the count is only one - count.should == 1 + config[:foo].should == "yep" end end - it "should only load configuration files once" do - $_config_data = 0 + describe "finalization" do + it "should finalize the configuration" do + # Create the finalize method on our loader + def test_loader.finalize(obj) + obj[:finalized] = true + obj + end - # We test both setting a file multiple times as well as multiple - # loads, since both should not cache the data. - file = temporary_file("$_config_data += 1") - instance.load_order = [:file] - 5.times { instance.set(:file, file) } - 5.times { instance.load } + # Basic configuration proc + proc = lambda do |config| + config[:foo] = "yep" + end - $_config_data.should == 1 + # Run the actual configuration and assert that we get the proper result + instance.load_order = [:proc] + instance.set(:proc, [[current_version, proc]]) + config = instance.load + config[:foo].should == "yep" + config[:finalized].should == true + end end - it "should not clear the cache if setting to the same value multiple times" do - $_config_data = 0 + describe "upgrading" do + it "should do an upgrade to the latest version" do + test_loader_v2 = Class.new(test_loader) do + def self.upgrade(old) + new = old.dup + new[:v2] = true - file = temporary_file("$_config_data += 1") + [new, [], []] + end + end - instance.load_order = [:proc] - instance.set(:proc, file) - 5.times { instance.load } + versions.register("2") { test_loader_v2 } + version_order << "2" - instance.set(:proc, file) - 5.times { instance.load } - - $_config_data.should == 1 + # Load a version 1 proc, and verify it is upgraded to version 2 + proc = lambda { |config| config[:foo] = "yep" } + instance.load_order = [:proc] + instance.set(:proc, [["1", proc]]) + config = instance.load + config[:foo].should == "yep" + config[:v2].should == true + end end - it "should raise proper error if there is a syntax error in a Vagrantfile" do - instance.load_order = [:file] - expect { instance.set(:file, temporary_file("Vagrant:^Config")) }. - to raise_exception(Vagrant::Errors::VagrantfileSyntaxError) + describe "loading edge cases" do + it "should only run the same proc once" do + count = 0 + proc = Proc.new do |config| + config[:foo] = "yep" + count += 1 + end + + instance.load_order = [:proc] + instance.set(:proc, [[current_version, proc]]) + + 5.times do + result = instance.load + + # Verify the config result + result[:foo].should == "yep" + + # Verify the count is only one + count.should == 1 + end + end + + it "should only load configuration files once" do + $_config_data = 0 + + # We test both setting a file multiple times as well as multiple + # loads, since both should not cache the data. + file = temporary_file("$_config_data += 1") + instance.load_order = [:file] + 5.times { instance.set(:file, file) } + 5.times { instance.load } + + $_config_data.should == 1 + end + + it "should not clear the cache if setting to the same value multiple times" do + $_config_data = 0 + + file = temporary_file("$_config_data += 1") + + instance.load_order = [:proc] + instance.set(:proc, file) + 5.times { instance.load } + + instance.set(:proc, file) + 5.times { instance.load } + + $_config_data.should == 1 + end + + it "should raise proper error if there is a syntax error in a Vagrantfile" do + instance.load_order = [:file] + expect { instance.set(:file, temporary_file("Vagrant:^Config")) }. + to raise_exception(Vagrant::Errors::VagrantfileSyntaxError) + end end end