require 'spec_helper' require 'puppet_spec/files' require 'puppet_spec/compiler' module Puppet::Pops module Lookup describe 'Puppet::Pops::Lookup::Context' do context 'an instance' do include PuppetSpec::Compiler it 'can be created' do code = "notice(type(Puppet::LookupContext.new('m')))" expect(eval_and_collect_notices(code)[0]).to match(/Object\[\{name => 'Puppet::LookupContext'/) end it 'returns its environment_name' do code = "notice(Puppet::LookupContext.new('m').environment_name)" expect(eval_and_collect_notices(code)[0]).to eql('production') end it 'returns its module_name' do code = "notice(Puppet::LookupContext.new('m').module_name)" expect(eval_and_collect_notices(code)[0]).to eql('m') end it 'can use an undef module_name' do code = "notice(type(Puppet::LookupContext.new(undef).module_name))" expect(eval_and_collect_notices(code)[0]).to eql('Undef') end it 'can store and retrieve a value using the cache' do code = <<-PUPPET.unindent $ctx = Puppet::LookupContext.new('m') $ctx.cache('ze_key', 'ze_value') notice($ctx.cached_value('ze_key')) PUPPET expect(eval_and_collect_notices(code)[0]).to eql('ze_value') end it 'the cache method returns the value that is cached' do code = <<-PUPPET.unindent $ctx = Puppet::LookupContext.new('m') notice($ctx.cache('ze_key', 'ze_value')) PUPPET expect(eval_and_collect_notices(code)[0]).to eql('ze_value') end it 'can store and retrieve a hash using the cache' do code = <<-PUPPET.unindent $ctx = Puppet::LookupContext.new('m') $ctx.cache_all({ 'v1' => 'first', 'v2' => 'second' }) $ctx.cached_entries |$key, $value| { notice($key); notice($value) } PUPPET expect(eval_and_collect_notices(code)).to eql(%w(v1 first v2 second)) end it 'can use the cache to merge hashes' do code = <<-PUPPET.unindent $ctx = Puppet::LookupContext.new('m') $ctx.cache_all({ 'v1' => 'first', 'v2' => 'second' }) $ctx.cache_all({ 'v3' => 'third', 'v4' => 'fourth' }) $ctx.cached_entries |$key, $value| { notice($key); notice($value) } PUPPET expect(eval_and_collect_notices(code)).to eql(%w(v1 first v2 second v3 third v4 fourth)) end it 'can use the cache to merge hashes and individual entries' do code = <<-PUPPET.unindent $ctx = Puppet::LookupContext.new('m') $ctx.cache_all({ 'v1' => 'first', 'v2' => 'second' }) $ctx.cache('v3', 'third') $ctx.cached_entries |$key, $value| { notice($key); notice($value) } PUPPET expect(eval_and_collect_notices(code)).to eql(%w(v1 first v2 second v3 third)) end it 'can iterate the cache using one argument block' do code = <<-PUPPET.unindent $ctx = Puppet::LookupContext.new('m') $ctx.cache_all({ 'v1' => 'first', 'v2' => 'second' }) $ctx.cached_entries |$entry| { notice($entry[0]); notice($entry[1]) } PUPPET expect(eval_and_collect_notices(code)).to eql(%w(v1 first v2 second)) end it 'can replace individual cached entries' do code = <<-PUPPET.unindent $ctx = Puppet::LookupContext.new('m') $ctx.cache_all({ 'v1' => 'first', 'v2' => 'second' }) $ctx.cache('v2', 'changed') $ctx.cached_entries |$key, $value| { notice($key); notice($value) } PUPPET expect(eval_and_collect_notices(code)).to eql(%w(v1 first v2 changed)) end it 'can replace multiple cached entries' do code = <<-PUPPET.unindent $ctx = Puppet::LookupContext.new('m') $ctx.cache_all({ 'v1' => 'first', 'v2' => 'second', 'v3' => 'third' }) $ctx.cache_all({ 'v1' => 'one', 'v3' => 'three' }) $ctx.cached_entries |$key, $value| { notice($key); notice($value) } PUPPET expect(eval_and_collect_notices(code)).to eql(%w(v1 one v2 second v3 three)) end it 'cached_entries returns an Iterable when called without a block' do code = <<-PUPPET.unindent $ctx = Puppet::LookupContext.new('m') $ctx.cache_all({ 'v1' => 'first', 'v2' => 'second' }) $iter = $ctx.cached_entries notice(type($iter, generalized)) $iter.each |$entry| { notice($entry[0]); notice($entry[1]) } PUPPET expect(eval_and_collect_notices(code)).to eql(['Iterator[Tuple[String, String, 2, 2]]', 'v1', 'first', 'v2', 'second']) end it 'will throw :no_such_key when not_found is called' do code = <<-PUPPET.unindent $ctx = Puppet::LookupContext.new('m') $ctx.not_found PUPPET expect { eval_and_collect_notices(code) }.to throw_symbol(:no_such_key) end context 'with cached_file_data' do include PuppetSpec::Files let(:code_dir) { Puppet[:environmentpath] } let(:env_name) { 'testing' } let(:env_dir) { File.join(code_dir, env_name) } let(:env) { Puppet::Node::Environment.create(env_name.to_sym, [File.join(populated_env_dir, 'modules')]) } let(:node) { Puppet::Node.new('test', :environment => env) } let(:data_yaml) { 'data.yaml' } let(:data_path) { File.join(populated_env_dir, 'data', data_yaml) } let(:populated_env_dir) do dir_contained_in(code_dir, { env_name => { 'data' => { data_yaml => <<-YAML.unindent a: value a YAML } } } ) PuppetSpec::Files.record_tmp(File.join(env_dir)) env_dir end it 'can use cached_file_data without a block' do code = <<-PUPPET.unindent $ctx = Puppet::LookupContext.new(nil) $yaml_data = $ctx.cached_file_data('#{data_path}') notice($yaml_data) PUPPET expect(eval_and_collect_notices(code, node)).to eql(["a: value a\n"]) end it 'can use cached_file_data with a block' do code = <<-PUPPET.unindent $ctx = Puppet::LookupContext.new(nil) $yaml_data = $ctx.cached_file_data('#{data_path}') |$content| { { 'parsed' => $content } } notice($yaml_data) PUPPET expect(eval_and_collect_notices(code, node)).to eql(["{parsed => a: value a\n}"]) end context 'and multiple compilations' do before(:each) { Puppet.settings[:environment_timeout] = 'unlimited' } it 'will reuse cached_file_data and not call block again' do code1 = <<-PUPPET.unindent $ctx = Puppet::LookupContext.new(nil) $yaml_data = $ctx.cached_file_data('#{data_path}') |$content| { { 'parsed' => $content } } notice($yaml_data) PUPPET code2 = <<-PUPPET.unindent $ctx = Puppet::LookupContext.new(nil) $yaml_data = $ctx.cached_file_data('#{data_path}') |$content| { { 'parsed' => 'should not be called' } } notice($yaml_data) PUPPET logs = [] Puppet::Util::Log.with_destination(Puppet::Test::LogCollector.new(logs)) do Puppet[:code] = code1 Puppet::Parser::Compiler.compile(node) Puppet[:code] = code2 Puppet::Parser::Compiler.compile(node) end logs = logs.select { |log| log.level == :notice }.map { |log| log.message } expect(logs.uniq.size).to eql(1) end it 'will invalidate cache if file changes size' do code = <<-PUPPET.unindent $ctx = Puppet::LookupContext.new(nil) $yaml_data = $ctx.cached_file_data('#{data_path}') |$content| { { 'parsed' => $content } } notice($yaml_data) PUPPET logs = [] Puppet::Util::Log.with_destination(Puppet::Test::LogCollector.new(logs)) do Puppet[:code] = code Puppet::Parser::Compiler.compile(node) # Change content size! File.write(data_path, "a: value is now A\n") Puppet::Parser::Compiler.compile(node) end logs = logs.select { |log| log.level == :notice }.map { |log| log.message } expect(logs).to eql(["{parsed => a: value a\n}", "{parsed => a: value is now A\n}"]) end it 'will invalidate cache if file changes mtime' do old_mtime = Puppet::FileSystem.stat(data_path).mtime code = <<-PUPPET.unindent $ctx = Puppet::LookupContext.new(nil) $yaml_data = $ctx.cached_file_data('#{data_path}') |$content| { { 'parsed' => $content } } notice($yaml_data) PUPPET logs = [] Puppet::Util::Log.with_destination(Puppet::Test::LogCollector.new(logs)) do Puppet[:code] = code Puppet::Parser::Compiler.compile(node) # Write content with the same size File.write(data_path, "a: value b\n") # Ensure mtime is at least 1 second ahead FileUtils.touch(data_path, mtime: old_mtime + 1) Puppet::Parser::Compiler.compile(node) end logs = logs.select { |log| log.level == :notice }.map { |log| log.message } expect(logs).to eql(["{parsed => a: value a\n}", "{parsed => a: value b\n}"]) end it 'will invalidate cache if file changes inode' do code = <<-PUPPET.unindent $ctx = Puppet::LookupContext.new(nil) $yaml_data = $ctx.cached_file_data('#{data_path}') |$content| { { 'parsed' => $content } } notice($yaml_data) PUPPET logs = [] Puppet::Util::Log.with_destination(Puppet::Test::LogCollector.new(logs)) do Puppet[:code] = code Puppet::Parser::Compiler.compile(node) # Change inode! File.delete(data_path); # Write content with the same size File.write(data_path, "a: value b\n") Puppet::Parser::Compiler.compile(node) end logs = logs.select { |log| log.level == :notice }.map { |log| log.message } expect(logs).to eql(["{parsed => a: value a\n}", "{parsed => a: value b\n}"]) end end end context 'when used in an Invocation' do let(:node) { Puppet::Node.new('test') } let(:compiler) { Puppet::Parser::Compiler.new(node) } let(:invocation) { Invocation.new(compiler.topscope) } let(:invocation_with_explain) { Invocation.new(compiler.topscope, {}, {}, true) } def compile_and_get_notices(code, scope_vars = {}) Puppet[:code] = code scope = compiler.topscope scope_vars.each_pair { |k,v| scope.setvar(k, v) } node.environment.check_for_reparse logs = [] Puppet::Util::Log.with_destination(Puppet::Test::LogCollector.new(logs)) do compiler.compile end logs = logs.select { |log| log.level == :notice }.map { |log| log.message } logs end it 'will not call explain unless explanations are active' do invocation.lookup('dummy', nil) do code = <<-PUPPET.unindent $ctx = Puppet::LookupContext.new('m') $ctx.explain || { notice('stop calling'); 'bad' } PUPPET expect(compile_and_get_notices(code)).to be_empty end end it 'will call explain when explanations are active' do invocation_with_explain.lookup('dummy', nil) do code = <<-PUPPET.unindent $ctx = Puppet::LookupContext.new('m') $ctx.explain || { notice('called'); 'good' } PUPPET expect(compile_and_get_notices(code)).to eql(['called']) end expect(invocation_with_explain.explainer.explain).to eql("good\n") end it 'will call interpolate to resolve interpolation' do invocation.lookup('dummy', nil) do code = <<-PUPPET.unindent $ctx = Puppet::LookupContext.new('m') notice($ctx.interpolate('-- %{testing} --')) PUPPET expect(compile_and_get_notices(code, { 'testing' => 'called' })).to eql(['-- called --']) end end end end end end end