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

require 'tmpdir'

require 'puppet/node/environment'
require 'puppet/util/execution'

describe Puppet::Node::Environment do
  include PuppetSpec::Files
  after do
    Puppet::Node::Environment.clear
  end

  it "should use the filetimeout for the ttl for the modulepath" do
    Puppet::Node::Environment.attr_ttl(:modulepath).should == Integer(Puppet[:filetimeout])
  end

  it "should use the filetimeout for the ttl for the module list" do
    Puppet::Node::Environment.attr_ttl(:modules).should == Integer(Puppet[:filetimeout])
  end

  it "should use the default environment if no name is provided while initializing an environment" do
    Puppet.settings.expects(:value).with(:environment).returns("one")
    Puppet::Node::Environment.new.name.should == :one
  end

  it "should treat environment instances as singletons" do
    Puppet::Node::Environment.new("one").should equal(Puppet::Node::Environment.new("one"))
  end

  it "should treat an environment specified as names or strings as equivalent" do
    Puppet::Node::Environment.new(:one).should equal(Puppet::Node::Environment.new("one"))
  end

  it "should return its name when converted to a string" do
    Puppet::Node::Environment.new(:one).to_s.should == "one"
  end

  it "should just return any provided environment if an environment is provided as the name" do
    one = Puppet::Node::Environment.new(:one)
    Puppet::Node::Environment.new(one).should equal(one)
  end

  describe "when managing known resource types" do
    before do
      @env = Puppet::Node::Environment.new("dev")
      @collection = Puppet::Resource::TypeCollection.new(@env)
      @env.stubs(:perform_initial_import).returns(Puppet::Parser::AST::Hostclass.new(''))
      Thread.current[:known_resource_types] = nil
    end

    it "should create a resource type collection if none exists" do
      Puppet::Resource::TypeCollection.expects(:new).with(@env).returns @collection
      @env.known_resource_types.should equal(@collection)
    end

    it "should reuse any existing resource type collection" do
      @env.known_resource_types.should equal(@env.known_resource_types)
    end

    it "should perform the initial import when creating a new collection" do
      @env = Puppet::Node::Environment.new("dev")
      @env.expects(:perform_initial_import).returns(Puppet::Parser::AST::Hostclass.new(''))
      @env.known_resource_types
    end

    it "should return the same collection even if stale if it's the same thread" do
      Puppet::Resource::TypeCollection.stubs(:new).returns @collection
      @env.known_resource_types.stubs(:stale?).returns true

      @env.known_resource_types.should equal(@collection)
    end

    it "should return the current thread associated collection if there is one" do
      Thread.current[:known_resource_types] = @collection

      @env.known_resource_types.should equal(@collection)
    end

    it "should give to all threads using the same environment the same collection if the collection isn't stale" do
      original_thread_type_collection = Puppet::Resource::TypeCollection.new(@env)
      Puppet::Resource::TypeCollection.expects(:new).with(@env).returns original_thread_type_collection
      @env.known_resource_types.should equal(original_thread_type_collection)

      original_thread_type_collection.expects(:require_reparse?).returns(false)
      Puppet::Resource::TypeCollection.stubs(:new).with(@env).returns @collection

      t = Thread.new {
        @env.known_resource_types.should equal(original_thread_type_collection)
      }
      t.join
    end

    it "should generate a new TypeCollection if the current one requires reparsing" do
      old_type_collection = @env.known_resource_types
      old_type_collection.stubs(:require_reparse?).returns true
      Thread.current[:known_resource_types] = nil
      new_type_collection = @env.known_resource_types

      new_type_collection.should be_a Puppet::Resource::TypeCollection
      new_type_collection.should_not equal(old_type_collection)
    end
  end

  it "should validate the modulepath directories" do
    real_file = tmpdir('moduledir')
    path = %W[/one /two #{real_file}].join(File::PATH_SEPARATOR)

    Puppet[:modulepath] = path

    env = Puppet::Node::Environment.new("testing")

    env.modulepath.should == [real_file]
  end

  it "should prefix the value of the 'PUPPETLIB' environment variable to the module path if present" do
    Puppet::Util::Execution.withenv("PUPPETLIB" => %w{/l1 /l2}.join(File::PATH_SEPARATOR)) do
      env = Puppet::Node::Environment.new("testing")
      module_path = %w{/one /two}.join(File::PATH_SEPARATOR)
      env.expects(:validate_dirs).with(%w{/l1 /l2 /one /two}).returns %w{/l1 /l2 /one /two}
      env.expects(:[]).with(:modulepath).returns module_path

      env.modulepath.should == %w{/l1 /l2 /one /two}
    end
  end

  describe "when validating modulepath or manifestdir directories" do
    before :each do
      @path_one = make_absolute('/one')
      @path_two = make_absolute('/two')
    end

    it "should not return non-directories" do
      env = Puppet::Node::Environment.new("testing")

      FileTest.expects(:directory?).with(@path_one).returns true
      FileTest.expects(:directory?).with(@path_two).returns false

      env.validate_dirs([@path_one, @path_two]).should == [@path_one]
    end

    it "should use the current working directory to fully-qualify unqualified paths" do
      FileTest.stubs(:directory?).returns true
      env = Puppet::Node::Environment.new("testing")

      two = File.expand_path(File.join(Dir.getwd, "two"))
      env.validate_dirs([@path_one, 'two']).should == [@path_one, two]
    end
  end

  describe "when modeling a specific environment" do
    it "should have a method for returning the environment name" do
      Puppet::Node::Environment.new("testing").name.should == :testing
    end

    it "should provide an array-like accessor method for returning any environment-specific setting" do
      env = Puppet::Node::Environment.new("testing")
      env.should respond_to(:[])
    end

    it "should ask the Puppet settings instance for the setting qualified with the environment name" do
      Puppet.settings.expects(:value).with("myvar", :testing).returns("myval")
      env = Puppet::Node::Environment.new("testing")
      env["myvar"].should == "myval"
    end

    it "should be able to return an individual module that exists in its module path" do
      env = Puppet::Node::Environment.new("testing")

      mod = mock 'module'
      Puppet::Module.expects(:new).with("one", env).returns mod
      mod.expects(:exist?).returns true

      env.module("one").should equal(mod)
    end

    it "should return nil if asked for a module that does not exist in its path" do
      env = Puppet::Node::Environment.new("testing")

      mod = mock 'module'
      Puppet::Module.expects(:new).with("one", env).returns mod
      mod.expects(:exist?).returns false

      env.module("one").should be_nil
    end

    it "should be able to return its modules" do
      Puppet::Node::Environment.new("testing").should respond_to(:modules)
    end

    describe ".modules" do
      it "should return a module named for every directory in each module path" do
        env = Puppet::Node::Environment.new("testing")
        env.expects(:modulepath).at_least_once.returns %w{/a /b}
        Dir.expects(:entries).with("/a").returns %w{foo bar}
        Dir.expects(:entries).with("/b").returns %w{bee baz}

        env.modules.collect{|mod| mod.name}.sort.should == %w{foo bar bee baz}.sort
      end

      it "should remove duplicates" do
        env = Puppet::Node::Environment.new("testing")
        env.expects(:modulepath).returns( %w{/a /b} ).at_least_once
        Dir.expects(:entries).with("/a").returns %w{foo}
        Dir.expects(:entries).with("/b").returns %w{foo}

        env.modules.collect{|mod| mod.name}.sort.should == %w{foo}
      end

      it "should ignore invalid modules" do
        env = Puppet::Node::Environment.new("testing")
        env.stubs(:modulepath).returns %w{/a}
        Dir.expects(:entries).with("/a").returns %w{foo bar}

        Puppet::Module.expects(:new).with { |name, env| name == "foo" }.returns mock("foomod", :name => "foo")
        Puppet::Module.expects(:new).with { |name, env| name == "bar" }.raises( Puppet::Module::InvalidName, "name is invalid" )

        env.modules.collect{|mod| mod.name}.sort.should == %w{foo}
      end

      it "should create modules with the correct environment" do
        env = Puppet::Node::Environment.new("testing")
        env.expects(:modulepath).at_least_once.returns %w{/a}
        Dir.expects(:entries).with("/a").returns %w{foo}

        env.modules.each {|mod| mod.environment.should == env }
      end

      it "should cache the module list" do
        env = Puppet::Node::Environment.new("testing")
        env.expects(:modulepath).at_least_once.returns %w{/a}
        Dir.expects(:entries).once.with("/a").returns %w{foo}

        env.modules
        env.modules
      end
    end
  end

  describe Puppet::Node::Environment::Helper do
    before do
      @helper = Object.new
      @helper.extend(Puppet::Node::Environment::Helper)
    end

    it "should be able to set and retrieve the environment" do
      @helper.environment = :foo
      @helper.environment.name.should == :foo
    end

    it "should accept an environment directly" do
      env = Puppet::Node::Environment.new :foo
      @helper.environment = env
      @helper.environment.name.should == :foo
    end

    it "should accept an environment as a string" do
      env = Puppet::Node::Environment.new "foo"
      @helper.environment = env
      @helper.environment.name.should == :foo
    end
  end

  describe "when performing initial import" do
    before do
      @parser = Puppet::Parser::Parser.new("test")
      Puppet::Parser::Parser.stubs(:new).returns @parser
      @env = Puppet::Node::Environment.new("env")
    end

    it "should set the parser's string to the 'code' setting and parse if code is available" do
      Puppet.settings[:code] = "my code"
      @parser.expects(:string=).with "my code"
      @parser.expects(:parse)
      @env.instance_eval { perform_initial_import }
    end

    it "should set the parser's file to the 'manifest' setting and parse if no code is available and the manifest is available" do
      filename = tmpfile('myfile')
      File.open(filename, 'w'){|f| }
      Puppet.settings[:manifest] = filename
      @parser.expects(:file=).with filename
      @parser.expects(:parse)
      @env.instance_eval { perform_initial_import }
    end

    it "should pass the manifest file to the parser even if it does not exist on disk" do
      filename = tmpfile('myfile')
      Puppet.settings[:code] = ""
      Puppet.settings[:manifest] = filename
      @parser.expects(:file=).with(filename).once
      @parser.expects(:parse).once
      @env.instance_eval { perform_initial_import }
    end

    it "should fail helpfully if there is an error importing" do
      File.stubs(:exist?).returns true
      @env.stubs(:known_resource_types).returns Puppet::Resource::TypeCollection.new(@env)
      @parser.expects(:file=).once
      @parser.expects(:parse).raises ArgumentError
      lambda { @env.instance_eval { perform_initial_import } }.should raise_error(Puppet::Error)
    end

    it "should not do anything if the ignore_import settings is set" do
      Puppet.settings[:ignoreimport] = true
      @parser.expects(:string=).never
      @parser.expects(:file=).never
      @parser.expects(:parse).never
      @env.instance_eval { perform_initial_import }
    end

    it "should mark the type collection as needing a reparse when there is an error parsing" do
      @parser.expects(:parse).raises Puppet::ParseError.new("Syntax error at ...")
      @env.stubs(:known_resource_types).returns Puppet::Resource::TypeCollection.new(@env)

      lambda { @env.instance_eval { perform_initial_import } }.should raise_error(Puppet::Error, /Syntax error at .../)
      @env.known_resource_types.require_reparse?.should be_true
    end
  end
end
