#! /usr/bin/env ruby
require 'spec_helper'

require 'yaml'
require 'fileutils'
require 'puppet/util/storage'

describe Puppet::Util::Storage do
  include PuppetSpec::Files

  before(:each) do
    @basepath = File.expand_path("/somepath")
  end

  describe "when caching a symbol" do
    it "should return an empty hash" do
      Puppet::Util::Storage.cache(:yayness).should == {}
      Puppet::Util::Storage.cache(:more_yayness).should == {}
    end

    it "should add the symbol to its internal state" do
      Puppet::Util::Storage.cache(:yayness)
      Puppet::Util::Storage.state.should == {:yayness=>{}}
    end

    it "should not clobber existing state when caching additional objects" do
      Puppet::Util::Storage.cache(:yayness)
      Puppet::Util::Storage.state.should == {:yayness=>{}}
      Puppet::Util::Storage.cache(:bubblyness)
      Puppet::Util::Storage.state.should == {:yayness=>{},:bubblyness=>{}}
    end
  end

  describe "when caching a Puppet::Type" do
    before(:each) do
      @file_test = Puppet::Type.type(:file).new(:name => @basepath+"/yayness", :audit => %w{checksum type})
      @exec_test = Puppet::Type.type(:exec).new(:name => @basepath+"/bin/ls /yayness")
    end

    it "should return an empty hash" do
      Puppet::Util::Storage.cache(@file_test).should == {}
      Puppet::Util::Storage.cache(@exec_test).should == {}
    end

    it "should add the resource ref to its internal state" do
      Puppet::Util::Storage.state.should == {}
      Puppet::Util::Storage.cache(@file_test)
      Puppet::Util::Storage.state.should == {"File[#{@basepath}/yayness]"=>{}}
      Puppet::Util::Storage.cache(@exec_test)
      Puppet::Util::Storage.state.should == {"File[#{@basepath}/yayness]"=>{}, "Exec[#{@basepath}/bin/ls /yayness]"=>{}}
    end
  end

  describe "when caching something other than a resource or symbol" do
    it "should cache by converting to a string" do
      data = Puppet::Util::Storage.cache(42)
      data[:yay] = true
      Puppet::Util::Storage.cache("42")[:yay].should be_true
    end
  end

  it "should clear its internal state when clear() is called" do
    Puppet::Util::Storage.cache(:yayness)
    Puppet::Util::Storage.state.should == {:yayness=>{}}
    Puppet::Util::Storage.clear
    Puppet::Util::Storage.state.should == {}
  end

  describe "when loading from the state file" do
    before do
      Puppet.settings.stubs(:use).returns(true)
    end

    describe "when the state file/directory does not exist" do
      before(:each) do
        @path = tmpfile('storage_test')
      end

      it "should not fail to load" do
        Puppet::FileSystem.exist?(@path).should be_false
        Puppet[:statedir] = @path
        Puppet::Util::Storage.load
        Puppet[:statefile] = @path
        Puppet::Util::Storage.load
      end

      it "should not lose its internal state when load() is called" do
        Puppet::FileSystem.exist?(@path).should be_false

        Puppet::Util::Storage.cache(:yayness)
        Puppet::Util::Storage.state.should == {:yayness=>{}}

        Puppet[:statefile] = @path
        Puppet::Util::Storage.load

        Puppet::Util::Storage.state.should == {:yayness=>{}}
      end
    end

    describe "when the state file/directory exists" do
      before(:each) do
        @state_file = tmpfile('storage_test')
        FileUtils.touch(@state_file)
        Puppet[:statefile] = @state_file
      end

      def write_state_file(contents)
        File.open(@state_file, 'w') { |f| f.write(contents) }
      end

      it "should overwrite its internal state if load() is called" do
        # Should the state be overwritten even if Puppet[:statefile] is not valid YAML?
        Puppet::Util::Storage.cache(:yayness)
        Puppet::Util::Storage.state.should == {:yayness=>{}}

        Puppet::Util::Storage.load

        Puppet::Util::Storage.state.should == {}
      end

      it "should restore its internal state if the state file contains valid YAML" do
        test_yaml = {'File["/yayness"]'=>{"name"=>{:a=>:b,:c=>:d}}}
        write_state_file(test_yaml.to_yaml)

        Puppet::Util::Storage.load

        Puppet::Util::Storage.state.should == test_yaml
      end

      it "should initialize with a clear internal state if the state file does not contain valid YAML" do
        write_state_file('{ invalid')

        Puppet::Util::Storage.load

        Puppet::Util::Storage.state.should == {}
      end

      it "should initialize with a clear internal state if the state file does not contain a hash of data" do
        write_state_file("not_a_hash")

        Puppet::Util::Storage.load

        Puppet::Util::Storage.state.should == {}
      end

      it "should raise an error if the state file does not contain valid YAML and cannot be renamed" do
        write_state_file('{ invalid')

        File.expects(:rename).raises(SystemCallError)

        expect { Puppet::Util::Storage.load }.to raise_error(Puppet::Error, /Could not rename/)
      end

      it "should attempt to rename the state file if the file is corrupted" do
        write_state_file('{ invalid')

        File.expects(:rename).at_least_once

        Puppet::Util::Storage.load
      end

      it "should fail gracefully on load() if the state file is not a regular file" do
        FileUtils.rm_f(@state_file)
        Dir.mkdir(@state_file)

        Puppet::Util::Storage.load
      end
    end
  end

  describe "when storing to the state file" do
    before(:each) do
      @state_file = tmpfile('storage_test')
      @saved_statefile = Puppet[:statefile]
      Puppet[:statefile] = @state_file
    end

    it "should create the state file if it does not exist" do
      Puppet::FileSystem.exist?(Puppet[:statefile]).should be_false
      Puppet::Util::Storage.cache(:yayness)

      Puppet::Util::Storage.store

      Puppet::FileSystem.exist?(Puppet[:statefile]).should be_true
    end

    it "should raise an exception if the state file is not a regular file" do
      Dir.mkdir(Puppet[:statefile])
      Puppet::Util::Storage.cache(:yayness)

      expect { Puppet::Util::Storage.store }.to raise_error

      Dir.rmdir(Puppet[:statefile])
    end

    it "should load() the same information that it store()s" do
      Puppet::Util::Storage.cache(:yayness)
      Puppet::Util::Storage.state.should == {:yayness=>{}}

      Puppet::Util::Storage.store
      Puppet::Util::Storage.clear

      Puppet::Util::Storage.state.should == {}

      Puppet::Util::Storage.load

      Puppet::Util::Storage.state.should == {:yayness=>{}}
    end
  end
end