diff --git a/lib/puppet/pal/pal_impl.rb b/lib/puppet/pal/pal_impl.rb index deabc48e85..1627432637 100644 --- a/lib/puppet/pal/pal_impl.rb +++ b/lib/puppet/pal/pal_impl.rb @@ -107,30 +107,6 @@ def self.with_script_compiler( Puppet[:code] = previous_code_value end - # Evaluates a Puppet Language script string. - # @param code_string [String] a snippet of Puppet Language source code - # @return [Object] what the Puppet Language code_string evaluates to - # @deprecated Use {#with_script_compiler} and then evaluate_string on the given compiler - to be removed in 1.0 version - # - def self.evaluate_script_string(code_string) - # prevent the default loading of Puppet[:manifest] which is the environment's manifest-dir by default settings - # by setting code_string to 'undef' - with_script_compiler do |compiler| - compiler.evaluate_string(code_string) - end - end - - # Evaluates a Puppet Language script (.pp) file. - # @param manifest_file [String] a file with Puppet Language source code - # @return [Object] what the Puppet Language manifest_file contents evaluates to - # @deprecated Use {#with_script_compiler} and then evaluate_file on the given compiler - to be removed in 1.0 version - # - def self.evaluate_script_manifest(manifest_file) - with_script_compiler do |compiler| - compiler.evaluate_file(manifest_file) - end - end - # Defines a context in which multiple operations in an env with a catalog producing compiler can be performed # in a given block. # The calls that takes place to PAL inside of the given block are all with the same instance of the compiler. diff --git a/spec/unit/puppet_pal_2pec.rb b/spec/unit/puppet_pal_2pec.rb deleted file mode 100644 index d4fd92df2d..0000000000 --- a/spec/unit/puppet_pal_2pec.rb +++ /dev/null @@ -1,1033 +0,0 @@ -require 'spec_helper' -require 'puppet_spec/files' -require 'puppet_pal' - -describe 'Puppet Pal' do - include PuppetSpec::Files - - let(:testing_env) do - { - 'pal_env' => { - 'functions' => functions, - 'lib' => { 'puppet' => lib_puppet }, - 'manifests' => manifests, - 'modules' => modules, - 'plans' => plans, - 'tasks' => tasks, - 'types' => types, - }, - 'other_env1' => { 'modules' => {} }, - 'other_env2' => { 'modules' => {} }, - } - end - - let(:functions) { {} } - let(:manifests) { {} } - let(:modules) { {} } - let(:plans) { {} } - let(:lib_puppet) { {} } - let(:tasks) { {} } - let(:types) { {} } - - let(:environments_dir) { Puppet[:environmentpath] } - - let(:testing_env_dir) do - dir_contained_in(environments_dir, testing_env) - env_dir = File.join(environments_dir, 'pal_env') - PuppetSpec::Files.record_tmp(env_dir) - PuppetSpec::Files.record_tmp(File.join(environments_dir, 'other_env1')) - PuppetSpec::Files.record_tmp(File.join(environments_dir, 'other_env2')) - env_dir - end - - let(:modules_dir) { File.join(testing_env_dir, 'modules') } - - # Without any facts - this speeds up the tests that do not require $facts to have any values - let(:node_facts) { Hash.new } - - # TODO: to be used in examples for running in an existing env - # let(:env) { Puppet::Node::Environment.create(:testing, [modules_dir]) } - - context 'in general - without code in modules or env' do - let(:modulepath) { [] } - - context 'deprecated PAL API methods work and' do - it '"evaluate_script_string" evaluates a code string in a given tmp environment' do - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| - ctx.evaluate_script_string('1+2+3') - end - expect(result).to eq(6) - end - - it '"evaluate_script_manifest" evaluates a manifest file in a given tmp environment' do - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| - manifest = file_containing('testing.pp', "1+2+3+4") - ctx.evaluate_script_manifest(manifest) - end - expect(result).to eq(10) - end - end - - context "with a script compiler" do - it 'errors if given both configured_by_env and manifest_file' do - expect { - Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| - ctx.with_script_compiler(configured_by_env: true, manifest_file: 'undef.pp') {|c| } - end - }.to raise_error(/manifest_file or code_string cannot be given when configured_by_env is true/) - end - - it 'errors if given both configured_by_env and code_string' do - expect { - Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| - ctx.with_script_compiler(configured_by_env: true, code_string: 'undef') {|c| } - end - }.to raise_error(/manifest_file or code_string cannot be given when configured_by_env is true/) - end - - context "evaluate_string method" do - it 'evaluates code string in a given tmp environment' do - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| - ctx.with_script_compiler {|c| c.evaluate_string('1+2+3') } - end - expect(result).to eq(6) - end - - it 'can be evaluated more than once in a given tmp environment - each in fresh compiler' do - Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| - expect( ctx.with_script_compiler {|c| c.evaluate_string('$a = 1+2+3')}).to eq(6) - expect { ctx.with_script_compiler {|c| c.evaluate_string('$a') }}.to raise_error(/Unknown variable: 'a'/) - end - end - - it 'instantiates definitions in the given code string' do - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |pal| - pal.with_script_compiler do |compiler| - compiler.evaluate_string(<<-CODE) - function run_me() { "worked1" } - run_me() - CODE - end - end - expect(result).to eq('worked1') - end - end - - context "evaluate_file method" do - it 'evaluates a manifest file in a given tmp environment' do - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| - manifest = file_containing('testing.pp', "1+2+3+4") - ctx.with_script_compiler {|c| c.evaluate_file(manifest) } - end - expect(result).to eq(10) - end - - it 'instantiates definitions in the given code string' do - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |pal| - pal.with_script_compiler do |compiler| - manifest = file_containing('testing.pp', (<<-CODE)) - function run_me() { "worked1" } - run_me() - CODE - pal.with_script_compiler {|c| c.evaluate_file(manifest) } - end - end - expect(result).to eq('worked1') - end - end - - context "variables are supported such that" do - it 'they can be set in any scope' do - vars = {'a'=> 10, 'x::y' => 20} - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts, variables: vars) do |ctx| - ctx.with_script_compiler {|c| c.evaluate_string("1+2+3+4+$a+$x::y")} - end - expect(result).to eq(40) - end - - it 'an error is raised if a variable name is illegal' do - vars = {'_a::b'=> 10} - expect do - Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts, variables: vars) do |ctx| - manifest = file_containing('testing.pp', "ok") - ctx.with_script_compiler {|c| c.evaluate_file(manifest) } - end - end.to raise_error(/has illegal name/) - end - - it 'an error is raised if variable value is not RichData compliant' do - vars = {'a'=> ArgumentError.new("not rich data")} - expect do - Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts, variables: vars) do |ctx| - ctx.with_script_compiler {|c| } - end - end.to raise_error(/has illegal type - got: ArgumentError/) - end - - it 'variable given to script_compiler overrides those given for environment' do - vars = {'a'=> 10, 'x::y' => 20} - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts, variables: vars) do |ctx| - ctx.with_script_compiler(variables: {'x::y' => 40}) {|c| c.evaluate_string("1+2+3+4+$a+$x::y")} - end - expect(result).to eq(60) - end - end - - context "functions are supported such that" do - it '"call_function" calls a function' do - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| - manifest = file_containing('afunc.pp', "function myfunc($a) { $a * 2 } ") - ctx.with_script_compiler(manifest_file: manifest) {|c| c.call_function('myfunc', 6) } - end - expect(result).to eq(12) - end - - it '"call_function" accepts a call with a ruby block' do - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| - ctx.with_script_compiler {|c| c.call_function('with', 6) {|x| x * 2} } - end - expect(result).to eq(12) - end - - it '"function_signature" returns a signature of a function' do - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| - manifest = file_containing('afunc.pp', "function myfunc(Integer $a) { $a * 2 } ") - ctx.with_script_compiler(manifest_file: manifest) do |c| - c.function_signature('myfunc') - end - end - expect(result.class).to eq(Puppet::Pal::FunctionSignature) - end - - it '"FunctionSignature#callable_with?" returns boolean if function is callable with given argument values' do - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| - manifest = file_containing('afunc.pp', "function myfunc(Integer $a) { $a * 2 } ") - ctx.with_script_compiler(manifest_file: manifest) do |c| - signature = c.function_signature('myfunc') - [ signature.callable_with?([10]), - signature.callable_with?(['nope']) - ] - end - end - expect(result).to eq([true, false]) - end - - it '"FunctionSignature#callable_with?" calls a given lambda if there is an error' do - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| - manifest = file_containing('afunc.pp', "function myfunc(Integer $a) { $a * 2 } ") - ctx.with_script_compiler(manifest_file: manifest) do |c| - signature = c.function_signature('myfunc') - local_result = 'not yay' - signature.callable_with?(['nope']) {|error| local_result = error } - local_result - end - end - expect(result).to match(/'myfunc' parameter 'a' expects an Integer value, got String/) - end - - it '"FunctionSignature#callable_with?" does not call a given lambda when there is no error' do - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| - manifest = file_containing('afunc.pp', "function myfunc(Integer $a) { $a * 2 } ") - ctx.with_script_compiler(manifest_file: manifest) do |c| - signature = c.function_signature('myfunc') - local_result = 'yay' - signature.callable_with?([10]) {|error| local_result = 'not yay' } - local_result - end - end - expect(result).to eq('yay') - end - - it '"function_signature" gets the signatures from a ruby function with multiple dispatch' do - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| - ctx.with_script_compiler {|c| c.function_signature('lookup') } - end - # check two different signatures of the lookup function - expect(result.callable_with?(['key'])).to eq(true) - expect(result.callable_with?(['key'], lambda() {|k| })).to eq(true) - end - - it '"function_signature" returns nil if function is not found' do - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| - ctx.with_script_compiler {|c| c.function_signature('no_where_to_be_found') } - end - expect(result).to eq(nil) - end - - it '"FunctionSignature#callables" returns an array of callables' do - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| - manifest = file_containing('afunc.pp', "function myfunc(Integer $a) { $a * 2 } ") - ctx.with_script_compiler(manifest_file: manifest) do |c| - c.function_signature('myfunc').callables - end - end - expect(result.class).to eq(Array) - expect(result.all? {|c| c.is_a?(Puppet::Pops::Types::PCallableType)}).to eq(true) - end - - it '"list_functions" returns an array with all function names that can be loaded' do - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| - ctx.with_script_compiler {|c| c.list_functions() } - end - expect(result.is_a?(Array)).to eq(true) - expect(result.all? {|s| s.is_a?(Puppet::Pops::Loader::TypedName) }).to eq(true) - # there are certainly more than 30 functions in puppet - (56 when writing this, but some refactoring - # may take place, so don't want an exact number here - jsut make sure it found "all of them" - expect(result.count).to be > 30 - end - - it '"list_functions" filters on name based on a given regexp' do - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| - ctx.with_script_compiler {|c| c.list_functions(/epp/) } - end - expect(result.is_a?(Array)).to eq(true) - expect(result.all? {|s| s.is_a?(Puppet::Pops::Loader::TypedName) }).to eq(true) - # there are two functions currently that have 'epp' in their name - expect(result.count).to eq(2) - end - - end - - context 'supports plans such that' do - it '"plan_signature" returns the signatures of a plan' do - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| - manifest = file_containing('afunc.pp', "plan myplan(Integer $a) { } ") - ctx.with_script_compiler(manifest_file: manifest) do |c| - signature = c.plan_signature('myplan') - [ signature.callable_with?({'a' => 10}), - signature.callable_with?({'a' => 'nope'}) - ] - end - end - expect(result).to eq([true, false]) - end - - it 'a PlanSignature.callable_with? calls a given lambda with any errors as a formatted string' do - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| - manifest = file_containing('afunc.pp', "plan myplan(Integer $a, Integer $b) { } ") - ctx.with_script_compiler(manifest_file: manifest) do |c| - signature = c.plan_signature('myplan') - local_result = nil - signature.callable_with?({'a' => 'nope'}) {|errors| local_result = errors } - local_result - end - end - # Note that errors are indented one space and on separate lines - # - expect(result).to eq(" parameter 'a' expects an Integer value, got String\n expects a value for parameter 'b'") - end - - it 'a PlanSignature.callable_with? does not call a given lambda if there are no errors' do - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| - manifest = file_containing('afunc.pp', "plan myplan(Integer $a) { } ") - ctx.with_script_compiler(manifest_file: manifest) do |c| - signature = c.plan_signature('myplan') - local_result = 'yay' - signature.callable_with?({'a' => 1}) {|errors| local_result = 'not yay' } - local_result - end - end - expect(result).to eq('yay') - end - - it '"plan_signature" returns nil if plan is not found' do - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| - ctx.with_script_compiler {|c| c.plan_signature('no_where_to_be_found') } - end - expect(result).to be(nil) - end - - it '"PlanSignature#params_type" returns a map of all parameters and their types' do - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| - manifest = file_containing('afunc.pp', "plan myplan(Integer $a, String $b) { } ") - ctx.with_script_compiler(manifest_file: manifest) do |c| - c.plan_signature('myplan').params_type - end - end - expect(result.class).to eq(Puppet::Pops::Types::PStructType) - expect(result.to_s).to eq("Struct[{'a' => Integer, 'b' => String}]") - end - end - - context 'supports puppet data types such that' do - it '"type" parses and returns a Type from a string specification' do - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx| - manifest = file_containing('main.pp', "type MyType = Float") - ctx.with_script_compiler(manifest_file: manifest) {|c| c.type('Variant[Integer, Boolean, MyType]') } - end - expect(result.is_a?(Puppet::Pops::Types::PVariantType)).to eq(true) - expect(result.types.size).to eq(3) - expect(result.instance?(3.14)).to eq(true) - end - - it '"create" creates a new object from a puppet data type and args' do - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx| - ctx.with_script_compiler { |c| c.create(Puppet::Pops::Types::PIntegerType::DEFAULT, '0x10') } - end - expect(result).to eq(16) - end - - it '"create" creates a new object from puppet data type in string form and args' do - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx| - ctx.with_script_compiler { |c| c.create('Integer', '010') } - end - expect(result).to eq(8) - end - end - end - - context 'supports parsing such that' do - it '"parse_string" parses a puppet language string' do - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx| - ctx.with_script_compiler { |c| c.parse_string('$a = 10') } - end - expect(result.class).to eq(Puppet::Pops::Model::Program) - end - - { nil => Puppet::Error, - '0xWAT' => Puppet::ParseErrorWithIssue, - '$0 = 1' => Puppet::ParseErrorWithIssue, - 'else 32' => Puppet::ParseErrorWithIssue, - }.each_pair do |input, error_class| - it "'parse_string' raises an error for invalid input: '#{input}'" do - expect { - Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx| - ctx.with_script_compiler { |c| c.parse_string(input) } - end - }.to raise_error(error_class) - end - end - - it '"parse_file" parses a puppet language string' do - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx| - manifest = file_containing('main.pp', "$a = 10") - ctx.with_script_compiler { |c| c.parse_file(manifest) } - end - expect(result.class).to eq(Puppet::Pops::Model::Program) - end - - it "'parse_file' raises an error for invalid input: 'else 32'" do - expect { - Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx| - manifest = file_containing('main.pp', "else 32") - ctx.with_script_compiler { |c| c.parse_file(manifest) } - end - }.to raise_error(Puppet::ParseErrorWithIssue) - end - - it "'parse_file' raises an error for invalid input, file is not a string" do - expect { - Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx| - ctx.with_script_compiler { |c| c.parse_file(42) } - end - }.to raise_error(Puppet::Error) - end - - it 'the "evaluate" method evaluates the parsed AST' do - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx| - ctx.with_script_compiler { |c| c.evaluate(c.parse_string('10 + 20')) } - end - expect(result).to eq(30) - end - - it 'the "evaluate" method instantiates definitions when given a Program' do - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx| - ctx.with_script_compiler { |c| c.evaluate(c.parse_string('function foo() { "yay"}; foo()')) } - end - expect(result).to eq('yay') - end - - it 'the "evaluate" method does not instantiates definitions when given ast other than Program' do - expect do - Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx| - ctx.with_script_compiler do |c| - program= c.parse_string('function foo() { "yay"}; foo()') - c.evaluate(program.body) - end - end - end.to raise_error(/Unknown function: 'foo'/) - end - - it 'the "evaluate_literal" method evaluates AST being a representation of a literal value' do - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx| - ctx.with_script_compiler { |c| c.evaluate_literal(c.parse_string('{10 => "hello"}')) } - end - expect(result).to eq({10 => 'hello'}) - end - - it 'the "evaluate_literal" method errors if ast is not representing a literal value' do - expect do - Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx| - ctx.with_script_compiler { |c| c.evaluate_literal(c.parse_string('{10+1 => "hello"}')) } - end - end.to raise_error(/does not represent a literal value/) - end - - it 'the "evaluate_literal" method errors if ast contains definitions' do - expect do - Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx| - ctx.with_script_compiler { |c| c.evaluate_literal(c.parse_string('function foo() { }; 42')) } - end - end.to raise_error(/does not represent a literal value/) - end - - end - end - - context 'with code in modules and env' do - let(:modulepath) { [modules_dir] } - - let(:metadata_json_a) { - { - 'name' => 'example/a', - 'version' => '0.1.0', - 'source' => 'git@github.com/example/example-a.git', - 'dependencies' => [{'name' => 'c', 'version_range' => '>=0.1.0'}], - 'author' => 'Bob the Builder', - 'license' => 'Apache-2.0' - } - } - - let(:metadata_json_b) { - { - 'name' => 'example/b', - 'version' => '0.1.0', - 'source' => 'git@github.com/example/example-b.git', - 'dependencies' => [{'name' => 'c', 'version_range' => '>=0.1.0'}], - 'author' => 'Bob the Builder', - 'license' => 'Apache-2.0' - } - } - - let(:metadata_json_c) { - { - 'name' => 'example/c', - 'version' => '0.1.0', - 'source' => 'git@github.com/example/example-c.git', - 'dependencies' => [], - 'author' => 'Bob the Builder', - 'license' => 'Apache-2.0' - } - } - - # TODO: there is something amiss with the metadata wrt dependencies - when metadata is present there is an error - # that dependencies could not be resolved. Metadata is therefore commented out. - # Dependency based visibility is probably something that we should remove... - let(:modules) { - { - 'a' => { - 'functions' => a_functions, - 'lib' => { 'puppet' => a_lib_puppet }, - 'plans' => a_plans, - 'tasks' => a_tasks, - 'types' => a_types, -# 'metadata.json' => metadata_json_a.to_json - }, - 'b' => { - 'functions' => b_functions, - 'lib' => b_lib, - 'plans' => b_plans, - 'tasks' => b_tasks, - 'types' => b_types, -# 'metadata.json' => metadata_json_b.to_json - }, - 'c' => { - 'types' => c_types, -# 'metadata.json' => metadata_json_c.to_json - }, - } - } - - let(:a_plans) { - { - 'aplan.pp' => <<-PUPPET.unindent, - plan a::aplan() { 'a::aplan value' } - PUPPET - } - } - - let(:a_types) { - { - 'atype.pp' => <<-PUPPET.unindent, - type A::Atype = Integer - PUPPET - } - } - - let(:a_tasks) { - { - 'atask' => '', - } - } - - let(:a_functions) { - { - 'afunc.pp' => 'function a::afunc() { "a::afunc value" }', - } - } - - let(:a_lib_puppet) { - { - 'functions' => { - 'a' => { - 'arubyfunc.rb' => <<-RUBY.unindent, - require 'stuff/something' - Puppet::Functions.create_function(:'a::arubyfunc') do - def arubyfunc - Stuff::SOMETHING - end - end - RUBY - 'myscriptcompilerfunc.rb' => <<-RUBY.unindent, - Puppet::Functions.create_function(:'a::myscriptcompilerfunc', Puppet::Functions::InternalFunction) do - dispatch :myscriptcompilerfunc do - script_compiler_param - param 'String',:name - end - - def myscriptcompilerfunc(script_compiler, name) - script_compiler.is_a?(Puppet::Pal::ScriptCompiler) ? name : 'no go' - end - end - RUBY - } - } - } - } - - let(:b_plans) { - { - 'aplan.pp' => <<-PUPPET.unindent, - plan b::aplan() {} - PUPPET - } - } - - let(:b_types) { - { - 'atype.pp' => <<-PUPPET.unindent, - type B::Atype = Integer - PUPPET - } - } - - let(:b_tasks) { - { - 'atask' => "# doing exactly nothing\n", - 'atask.json' => <<-JSONTEXT.unindent - { - "description": "test task b::atask", - "input_method": "stdin", - "parameters": { - "string_param": { - "description": "A string parameter", - "type": "String[1]" - }, - "int_param": { - "description": "An integer parameter", - "type": "Integer" - } - } - } - JSONTEXT - } - } - - let(:b_functions) { - { - 'afunc.pp' => 'function b::afunc() {}', - } - } - - let(:b_lib) { - { - 'puppet' => b_lib_puppet, - 'stuff' => { - 'something.rb' => "module Stuff; SOMETHING = 'something'; end" - } - } - } - - let(:b_lib_puppet) { - { - 'functions' => { - 'b' => { - 'arubyfunc.rb' => "Puppet::Functions.create_function(:'b::arubyfunc') { def arubyfunc; 'arubyfunc_value'; end }", - } - } - } - } - - let(:c_types) { - { - 'atype.pp' => <<-PUPPET.unindent, - type C::Atype = Integer - PUPPET - } - } - context 'configured as temporary environment such that' do - it 'modules are available' do - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| - ctx.with_script_compiler {|c| c.evaluate_string('a::afunc()') } - end - expect(result).to eq("a::afunc value") - end - - it 'libs in a given "modulepath" are added to the Ruby $LOAD_PATH' do - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| - ctx.with_script_compiler {|c| c.evaluate_string('a::arubyfunc()') } - end - expect(result).to eql('something') - end - - it 'errors if a block is not given to in_tmp_environment' do - expect do - Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) - end.to raise_error(/A block must be given to 'in_tmp_environment/) - end - - it 'errors if an env_name is given and is not a String[1]' do - expect do - Puppet::Pal.in_tmp_environment('', modulepath: modulepath, facts: node_facts) { |ctx| } - end.to raise_error(/temporary environment name has wrong type/) - - expect do - Puppet::Pal.in_tmp_environment(32, modulepath: modulepath, facts: node_facts) { |ctx| } - end.to raise_error(/temporary environment name has wrong type/) - end - - { 'a hash' => {'a' => 'hm'}, - 'an integer' => 32, - 'separated strings' => 'dir1;dir2', - 'empty string in array' => [''] - }.each_pair do |what, value| - it "errors if modulepath is #{what}" do - expect do - Puppet::Pal.in_tmp_environment('pal_env', modulepath: value, facts: node_facts) { |ctx| } - end.to raise_error(/modulepath has wrong type/) - end - end - - context 'facts are supported such that' do - it 'they are obtained if they are not given' do - facts = Puppet::Node::Facts.new(Puppet[:certname], 'puppetversion' => Puppet.version) - Puppet::Node::Facts.indirection.save(facts) - - testing_env_dir # creates the structure - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath ) do |ctx| - ctx.with_script_compiler {|c| c.evaluate_string("$facts =~ Hash and $facts[puppetversion] == '#{Puppet.version}'") } - end - expect(result).to eq(true) - end - - it 'can be given as a hash when creating the environment' do - testing_env_dir # creates the structure - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: { 'myfact' => 42 }) do |ctx| - ctx.with_script_compiler {|c| c.evaluate_string("$facts =~ Hash and $facts[myfact] == 42") } - end - expect(result).to eq(true) - end - - it 'can be overridden with a hash when creating a script compiler' do - testing_env_dir # creates the structure - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: { 'myfact' => 42 }) do |ctx| - ctx.with_script_compiler(facts: { 'myfact' => 43 }) {|c| c.evaluate_string("$facts =~ Hash and $facts[myfact] == 43") } - end - expect(result).to eq(true) - end - - it 'can be disabled with the :set_local_facts option' do - Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: { 'myfact' => 42}) do |ctx| - ctx.with_script_compiler(facts: { 'myfact' => 42 }, set_local_facts: false) do |compiler| - expect { compiler.evaluate_string('$facts') }.to raise_error( - Puppet::PreformattedError, - /Unknown variable: 'facts'/ - ) - end - end - end - end - - context 'supports tasks such that' do - it '"task_signature" returns the signatures of a generic task' do - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| - ctx.with_script_compiler do |c| - signature = c.task_signature('a::atask') - [ signature.runnable_with?('whatever' => 10), - signature.runnable_with?('anything_goes' => 'foo') - ] - end - end - expect(result).to eq([true, true]) - end - - it '"TaskSignature#runnable_with?" calls a given lambda if there is an error in a generic task' do - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| - ctx.with_script_compiler do |c| - signature = c.task_signature('a::atask') - local_result = 'not yay' - signature.runnable_with?('string_param' => /not data/) {|error| local_result = error } - local_result - end - end - expect(result).to match(/Task a::atask:\s+entry 'string_param' expects a Data value, got Regexp/m) - end - - it '"task_signature" returns the signatures of a task defined with metadata' do - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| - ctx.with_script_compiler do |c| - signature = c.task_signature('b::atask') - [ signature.runnable_with?('string_param' => 'foo', 'int_param' => 10), - signature.runnable_with?('anything_goes' => 'foo'), - signature.task_hash['name'], - signature.task_hash['metadata']['parameters']['string_param']['description'], - signature.task_hash['metadata']['description'], - signature.task_hash['metadata']['parameters']['int_param']['type'], - ] - end - end - expect(result).to eq([true, false, 'b::atask', 'A string parameter', 'test task b::atask', 'Integer']) - end - - it '"TaskSignature#runnable_with?" calls a given lambda if there is an error' do - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| - ctx.with_script_compiler do |c| - signature = c.task_signature('b::atask') - local_result = 'not yay' - signature.runnable_with?('string_param' => 10) {|error| local_result = error } - local_result - end - end - expect(result).to match(/Task b::atask:\s+parameter 'string_param' expects a String value, got Integer/m) - end - - it '"task_signature" returns nil if task is not found' do - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| - ctx.with_script_compiler {|c| c.task_signature('no_where_to_be_found') } - end - expect(result).to be(nil) - end - - it 'default task input_method is nil' do - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| - ctx.with_script_compiler do |c| - signature = c.task_signature('a::atask') - signature.task_hash - end - end - expect(result['metadata']['input_method']).to be_nil - end - - it 'task input_method is parsed from task metadata' do - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| - ctx.with_script_compiler do |c| - signature = c.task_signature('b::atask') - signature.task_hash - end - end - expect(result['metadata']['input_method']).to eq('stdin') - end - - it '"list_tasks" returns an array with all tasks that can be loaded' do - testing_env_dir # creates the structure - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| - ctx.with_script_compiler {|c| c.list_tasks() } - end - expect(result.is_a?(Array)).to eq(true) - expect(result.all? {|s| s.is_a?(Puppet::Pops::Loader::TypedName) }).to eq(true) - expect(result.map {|tn| tn.name}).to contain_exactly('a::atask', 'b::atask') - end - - it '"list_tasks" filters on name based on a given regexp' do - testing_env_dir # creates the structure - result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| - ctx.with_script_compiler {|c| c.list_tasks(/^a::/) } - end - expect(result.is_a?(Array)).to eq(true) - expect(result.all? {|s| s.is_a?(Puppet::Pops::Loader::TypedName) }).to eq(true) - expect(result.map {|tn| tn.name}).to eq(['a::atask']) - end - end - - end - - context 'configured as an existing given environment directory such that' do - it 'modules in it are available from its "modules" directory' do - result = Puppet::Pal.in_environment('pal_env', env_dir: testing_env_dir, facts: node_facts) do |ctx| - ctx.with_script_compiler {|c| c.evaluate_string('a::afunc()') } - end - expect(result).to eq("a::afunc value") - end - - it 'libs in a given "modulepath" are added to the Ruby $LOAD_PATH' do - result = Puppet::Pal.in_environment('pal_env', env_dir: testing_env_dir, facts: node_facts) do |ctx| - ctx.with_script_compiler {|c| c.evaluate_string('a::arubyfunc()') } - end - expect(result).to eql('something') - end - - it 'a given "modulepath" overrides the default' do - expect do - Puppet::Pal.in_environment('pal_env', env_dir: testing_env_dir, modulepath: [], facts: node_facts) do |ctx| - ctx.with_script_compiler {|c| c.evaluate_string('a::afunc()') } - end - end.to raise_error(/Unknown function: 'a::afunc'/) - end - - it 'a "pre_modulepath" is prepended and a "post_modulepath" is appended to the effective modulepath' do - other_modules1 = File.join(environments_dir, 'other_env1/modules') - other_modules2 = File.join(environments_dir, 'other_env2/modules') - result = Puppet::Pal.in_environment('pal_env', env_dir: testing_env_dir, - pre_modulepath: [other_modules1], - post_modulepath: [other_modules2], - facts: node_facts - ) do |ctx| - the_modulepath = Puppet.lookup(:environments).get('pal_env').modulepath - the_modulepath[0] == other_modules1 && the_modulepath[-1] == other_modules2 - end - expect(result).to be(true) - end - - it 'can set variables in any scope' do - vars = {'a'=> 10, 'x::y' => 20} - result = Puppet::Pal.in_environment('pal_env', env_dir: testing_env_dir, facts: node_facts, variables: vars) do |ctx| - ctx.with_script_compiler { |c| c.evaluate_string("1+2+3+4+$a+$x::y") } - end - expect(result).to eq(40) - end - - it 'errors in a meaningful way when a non existing env name is given' do - testing_env_dir # creates the structure - expect do - Puppet::Pal.in_environment('blah_env', env_dir: testing_env_dir.chop, facts: node_facts) { |ctx| } - end.to raise_error(/The environment directory '.*' does not exist/) - end - - it 'errors if an env_name is given and is not a String[1]' do - expect do - Puppet::Pal.in_environment('', env_dir: testing_env_dir, facts: node_facts) { |ctx| } - end.to raise_error(/env_name has wrong type/) - - expect do - Puppet::Pal.in_environment(32, env_dir: testing_env_dir, facts: node_facts) { |ctx| } - end.to raise_error(/env_name has wrong type/) - end - - { 'a hash' => {'a' => 'hm'}, - 'an integer' => 32, - 'separated strings' => 'dir1;dir2', - 'empty string in array' => [''] - }.each_pair do |what, value| - it "errors if modulepath is #{what}" do - expect do - Puppet::Pal.in_environment('pal_env', env_dir: testing_env_dir, modulepath: {'a' => 'hm'}, facts: node_facts) { |ctx| } - Puppet::Pal.in_tmp_environment('pal_env', modulepath: value, facts: node_facts) { |ctx| } - end.to raise_error(/modulepath has wrong type/) - end - end - - it 'errors if env_dir and envpath are both given' do - testing_env_dir # creates the structure - expect do - Puppet::Pal.in_environment('blah_env', env_dir: testing_env_dir, envpath: environments_dir, facts: node_facts) { |ctx| } - end.to raise_error(/Cannot use 'env_dir' and 'envpath' at the same time/) - end - end - - context 'configured as existing given envpath such that' do - it 'modules in it are available from its "modules" directory' do - testing_env_dir # creates the structure - result = Puppet::Pal.in_environment('pal_env', envpath: environments_dir, facts: node_facts) do |ctx| - ctx.with_script_compiler { |c| c.evaluate_string('a::afunc()') } - end - expect(result).to eq("a::afunc value") - end - - it 'a given "modulepath" overrides the default' do - testing_env_dir # creates the structure - expect do - Puppet::Pal.in_environment('pal_env', envpath: environments_dir, modulepath: [], facts: node_facts) do |ctx| - ctx.with_script_compiler { |c| c.evaluate_string('a::afunc()') } - end - end.to raise_error(/Unknown function: 'a::afunc'/) - end - - it 'a "pre_modulepath" is prepended and a "post_modulepath" is appended to the effective modulepath' do - testing_env_dir # creates the structure - other_modules1 = File.join(environments_dir, 'other_env1/modules') - other_modules2 = File.join(environments_dir, 'other_env2/modules') - result = Puppet::Pal.in_environment('pal_env', envpath: environments_dir, - pre_modulepath: [other_modules1], - post_modulepath: [other_modules2], - facts: node_facts - ) do |ctx| - the_modulepath = Puppet.lookup(:environments).get('pal_env').modulepath - the_modulepath[0] == other_modules1 && the_modulepath[-1] == other_modules2 - end - expect(result).to be(true) - end - - it 'the envpath can have multiple entries - that are searched for the given env' do - testing_env_dir # creates the structure - result = Puppet::Pal.in_environment('pal_env', envpath: environments_dir, facts: node_facts) do |ctx| - ctx.with_script_compiler {|c| c.evaluate_string('a::afunc()') } - end - expect(result).to eq("a::afunc value") - end - - it 'errors in a meaningful way when a non existing env name is given' do - testing_env_dir # creates the structure - expect do - Puppet::Pal.in_environment('blah_env', envpath: environments_dir, facts: node_facts) { |ctx| } - end.to raise_error(/No directory found for the environment 'blah_env' on the path '.*'/) - end - - it 'errors if a block is not given to in_environment' do - expect do - Puppet::Pal.in_environment('blah_env', envpath: environments_dir, facts: node_facts) - end.to raise_error(/A block must be given to 'in_environment/) - end - - it 'errors if envpath is something other than a string' do - testing_env_dir # creates the structure - expect do - Puppet::Pal.in_environment('blah_env', envpath: '', facts: node_facts) { |ctx| } - end.to raise_error(/envpath has wrong type/) - - expect do - Puppet::Pal.in_environment('blah_env', envpath: [environments_dir], facts: node_facts) { |ctx| } - end.to raise_error(/envpath has wrong type/) - end - - context 'with a script compiler' do - it 'uses configured manifest_file if configured_by_env is true and Puppet[:code] is unset' do - testing_env_dir # creates the structure - Puppet[:manifest] = file_containing('afunc.pp', "function myfunc(Integer $a) { $a * 2 } ") - result = Puppet::Pal.in_environment('pal_env', envpath: environments_dir, facts: node_facts) do |ctx| - ctx.with_script_compiler(configured_by_env: true) {|c| c.call_function('myfunc', 4)} - end - expect(result).to eql(8) - end - - it 'uses Puppet[:code] if configured_by_env is true and Puppet[:code] is set' do - testing_env_dir # creates the structure - Puppet[:manifest] = file_containing('amanifest.pp', "$a = 20") - Puppet[:code] = '$a = 40' - result = Puppet::Pal.in_environment('pal_env', envpath: environments_dir, facts: node_facts) do |ctx| - ctx.with_script_compiler(configured_by_env: true) {|c| c.evaluate_string('$a')} - end - expect(result).to eql(40) - end - - it 'makes the pal ScriptCompiler available as script_compiler_param to Function dispatcher' do - testing_env_dir # creates the structure - Puppet[:manifest] = file_containing('noop.pp', "undef") - result = Puppet::Pal.in_environment('pal_env', envpath: environments_dir, facts: node_facts) do |ctx| - ctx.with_script_compiler(configured_by_env: true) {|c| c.call_function('a::myscriptcompilerfunc', 'go')} - end - expect(result).to eql('go') - end - end - end - end -end diff --git a/spec/unit/puppet_pal_spec.rb b/spec/unit/puppet_pal_spec.rb index 0bd88277fb..b646972492 100644 --- a/spec/unit/puppet_pal_spec.rb +++ b/spec/unit/puppet_pal_spec.rb @@ -1,3 +1,1016 @@ require 'spec_helper' require 'puppet_spec/files' -require_relative 'puppet_pal_2pec' +require 'puppet_pal' + +describe 'Puppet Pal' do + include PuppetSpec::Files + + let(:testing_env) do + { + 'pal_env' => { + 'functions' => functions, + 'lib' => { 'puppet' => lib_puppet }, + 'manifests' => manifests, + 'modules' => modules, + 'plans' => plans, + 'tasks' => tasks, + 'types' => types, + }, + 'other_env1' => { 'modules' => {} }, + 'other_env2' => { 'modules' => {} }, + } + end + + let(:functions) { {} } + let(:manifests) { {} } + let(:modules) { {} } + let(:plans) { {} } + let(:lib_puppet) { {} } + let(:tasks) { {} } + let(:types) { {} } + + let(:environments_dir) { Puppet[:environmentpath] } + + let(:testing_env_dir) do + dir_contained_in(environments_dir, testing_env) + env_dir = File.join(environments_dir, 'pal_env') + PuppetSpec::Files.record_tmp(env_dir) + PuppetSpec::Files.record_tmp(File.join(environments_dir, 'other_env1')) + PuppetSpec::Files.record_tmp(File.join(environments_dir, 'other_env2')) + env_dir + end + + let(:modules_dir) { File.join(testing_env_dir, 'modules') } + + # Without any facts - this speeds up the tests that do not require $facts to have any values + let(:node_facts) { Hash.new } + + # TODO: to be used in examples for running in an existing env + # let(:env) { Puppet::Node::Environment.create(:testing, [modules_dir]) } + + context 'in general - without code in modules or env' do + let(:modulepath) { [] } + + context "with a script compiler" do + it 'errors if given both configured_by_env and manifest_file' do + expect { + Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| + ctx.with_script_compiler(configured_by_env: true, manifest_file: 'undef.pp') {|c| } + end + }.to raise_error(/manifest_file or code_string cannot be given when configured_by_env is true/) + end + + it 'errors if given both configured_by_env and code_string' do + expect { + Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| + ctx.with_script_compiler(configured_by_env: true, code_string: 'undef') {|c| } + end + }.to raise_error(/manifest_file or code_string cannot be given when configured_by_env is true/) + end + + context "evaluate_string method" do + it 'evaluates code string in a given tmp environment' do + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| + ctx.with_script_compiler {|c| c.evaluate_string('1+2+3') } + end + expect(result).to eq(6) + end + + it 'can be evaluated more than once in a given tmp environment - each in fresh compiler' do + Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| + expect( ctx.with_script_compiler {|c| c.evaluate_string('$a = 1+2+3')}).to eq(6) + expect { ctx.with_script_compiler {|c| c.evaluate_string('$a') }}.to raise_error(/Unknown variable: 'a'/) + end + end + + it 'instantiates definitions in the given code string' do + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |pal| + pal.with_script_compiler do |compiler| + compiler.evaluate_string(<<-CODE) + function run_me() { "worked1" } + run_me() + CODE + end + end + expect(result).to eq('worked1') + end + end + + context "evaluate_file method" do + it 'evaluates a manifest file in a given tmp environment' do + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| + manifest = file_containing('testing.pp', "1+2+3+4") + ctx.with_script_compiler {|c| c.evaluate_file(manifest) } + end + expect(result).to eq(10) + end + + it 'instantiates definitions in the given code string' do + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |pal| + pal.with_script_compiler do |compiler| + manifest = file_containing('testing.pp', (<<-CODE)) + function run_me() { "worked1" } + run_me() + CODE + pal.with_script_compiler {|c| c.evaluate_file(manifest) } + end + end + expect(result).to eq('worked1') + end + end + + context "variables are supported such that" do + it 'they can be set in any scope' do + vars = {'a'=> 10, 'x::y' => 20} + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts, variables: vars) do |ctx| + ctx.with_script_compiler {|c| c.evaluate_string("1+2+3+4+$a+$x::y")} + end + expect(result).to eq(40) + end + + it 'an error is raised if a variable name is illegal' do + vars = {'_a::b'=> 10} + expect do + Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts, variables: vars) do |ctx| + manifest = file_containing('testing.pp', "ok") + ctx.with_script_compiler {|c| c.evaluate_file(manifest) } + end + end.to raise_error(/has illegal name/) + end + + it 'an error is raised if variable value is not RichData compliant' do + vars = {'a'=> ArgumentError.new("not rich data")} + expect do + Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts, variables: vars) do |ctx| + ctx.with_script_compiler {|c| } + end + end.to raise_error(/has illegal type - got: ArgumentError/) + end + + it 'variable given to script_compiler overrides those given for environment' do + vars = {'a'=> 10, 'x::y' => 20} + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts, variables: vars) do |ctx| + ctx.with_script_compiler(variables: {'x::y' => 40}) {|c| c.evaluate_string("1+2+3+4+$a+$x::y")} + end + expect(result).to eq(60) + end + end + + context "functions are supported such that" do + it '"call_function" calls a function' do + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| + manifest = file_containing('afunc.pp', "function myfunc($a) { $a * 2 } ") + ctx.with_script_compiler(manifest_file: manifest) {|c| c.call_function('myfunc', 6) } + end + expect(result).to eq(12) + end + + it '"call_function" accepts a call with a ruby block' do + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| + ctx.with_script_compiler {|c| c.call_function('with', 6) {|x| x * 2} } + end + expect(result).to eq(12) + end + + it '"function_signature" returns a signature of a function' do + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| + manifest = file_containing('afunc.pp', "function myfunc(Integer $a) { $a * 2 } ") + ctx.with_script_compiler(manifest_file: manifest) do |c| + c.function_signature('myfunc') + end + end + expect(result.class).to eq(Puppet::Pal::FunctionSignature) + end + + it '"FunctionSignature#callable_with?" returns boolean if function is callable with given argument values' do + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| + manifest = file_containing('afunc.pp', "function myfunc(Integer $a) { $a * 2 } ") + ctx.with_script_compiler(manifest_file: manifest) do |c| + signature = c.function_signature('myfunc') + [ signature.callable_with?([10]), + signature.callable_with?(['nope']) + ] + end + end + expect(result).to eq([true, false]) + end + + it '"FunctionSignature#callable_with?" calls a given lambda if there is an error' do + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| + manifest = file_containing('afunc.pp', "function myfunc(Integer $a) { $a * 2 } ") + ctx.with_script_compiler(manifest_file: manifest) do |c| + signature = c.function_signature('myfunc') + local_result = 'not yay' + signature.callable_with?(['nope']) {|error| local_result = error } + local_result + end + end + expect(result).to match(/'myfunc' parameter 'a' expects an Integer value, got String/) + end + + it '"FunctionSignature#callable_with?" does not call a given lambda when there is no error' do + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| + manifest = file_containing('afunc.pp', "function myfunc(Integer $a) { $a * 2 } ") + ctx.with_script_compiler(manifest_file: manifest) do |c| + signature = c.function_signature('myfunc') + local_result = 'yay' + signature.callable_with?([10]) {|error| local_result = 'not yay' } + local_result + end + end + expect(result).to eq('yay') + end + + it '"function_signature" gets the signatures from a ruby function with multiple dispatch' do + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| + ctx.with_script_compiler {|c| c.function_signature('lookup') } + end + # check two different signatures of the lookup function + expect(result.callable_with?(['key'])).to eq(true) + expect(result.callable_with?(['key'], lambda() {|k| })).to eq(true) + end + + it '"function_signature" returns nil if function is not found' do + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| + ctx.with_script_compiler {|c| c.function_signature('no_where_to_be_found') } + end + expect(result).to eq(nil) + end + + it '"FunctionSignature#callables" returns an array of callables' do + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| + manifest = file_containing('afunc.pp', "function myfunc(Integer $a) { $a * 2 } ") + ctx.with_script_compiler(manifest_file: manifest) do |c| + c.function_signature('myfunc').callables + end + end + expect(result.class).to eq(Array) + expect(result.all? {|c| c.is_a?(Puppet::Pops::Types::PCallableType)}).to eq(true) + end + + it '"list_functions" returns an array with all function names that can be loaded' do + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| + ctx.with_script_compiler {|c| c.list_functions() } + end + expect(result.is_a?(Array)).to eq(true) + expect(result.all? {|s| s.is_a?(Puppet::Pops::Loader::TypedName) }).to eq(true) + # there are certainly more than 30 functions in puppet - (56 when writing this, but some refactoring + # may take place, so don't want an exact number here - jsut make sure it found "all of them" + expect(result.count).to be > 30 + end + + it '"list_functions" filters on name based on a given regexp' do + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| + ctx.with_script_compiler {|c| c.list_functions(/epp/) } + end + expect(result.is_a?(Array)).to eq(true) + expect(result.all? {|s| s.is_a?(Puppet::Pops::Loader::TypedName) }).to eq(true) + # there are two functions currently that have 'epp' in their name + expect(result.count).to eq(2) + end + + end + + context 'supports plans such that' do + it '"plan_signature" returns the signatures of a plan' do + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| + manifest = file_containing('afunc.pp', "plan myplan(Integer $a) { } ") + ctx.with_script_compiler(manifest_file: manifest) do |c| + signature = c.plan_signature('myplan') + [ signature.callable_with?({'a' => 10}), + signature.callable_with?({'a' => 'nope'}) + ] + end + end + expect(result).to eq([true, false]) + end + + it 'a PlanSignature.callable_with? calls a given lambda with any errors as a formatted string' do + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| + manifest = file_containing('afunc.pp', "plan myplan(Integer $a, Integer $b) { } ") + ctx.with_script_compiler(manifest_file: manifest) do |c| + signature = c.plan_signature('myplan') + local_result = nil + signature.callable_with?({'a' => 'nope'}) {|errors| local_result = errors } + local_result + end + end + # Note that errors are indented one space and on separate lines + # + expect(result).to eq(" parameter 'a' expects an Integer value, got String\n expects a value for parameter 'b'") + end + + it 'a PlanSignature.callable_with? does not call a given lambda if there are no errors' do + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| + manifest = file_containing('afunc.pp', "plan myplan(Integer $a) { } ") + ctx.with_script_compiler(manifest_file: manifest) do |c| + signature = c.plan_signature('myplan') + local_result = 'yay' + signature.callable_with?({'a' => 1}) {|errors| local_result = 'not yay' } + local_result + end + end + expect(result).to eq('yay') + end + + it '"plan_signature" returns nil if plan is not found' do + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| + ctx.with_script_compiler {|c| c.plan_signature('no_where_to_be_found') } + end + expect(result).to be(nil) + end + + it '"PlanSignature#params_type" returns a map of all parameters and their types' do + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| + manifest = file_containing('afunc.pp', "plan myplan(Integer $a, String $b) { } ") + ctx.with_script_compiler(manifest_file: manifest) do |c| + c.plan_signature('myplan').params_type + end + end + expect(result.class).to eq(Puppet::Pops::Types::PStructType) + expect(result.to_s).to eq("Struct[{'a' => Integer, 'b' => String}]") + end + end + + context 'supports puppet data types such that' do + it '"type" parses and returns a Type from a string specification' do + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx| + manifest = file_containing('main.pp', "type MyType = Float") + ctx.with_script_compiler(manifest_file: manifest) {|c| c.type('Variant[Integer, Boolean, MyType]') } + end + expect(result.is_a?(Puppet::Pops::Types::PVariantType)).to eq(true) + expect(result.types.size).to eq(3) + expect(result.instance?(3.14)).to eq(true) + end + + it '"create" creates a new object from a puppet data type and args' do + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx| + ctx.with_script_compiler { |c| c.create(Puppet::Pops::Types::PIntegerType::DEFAULT, '0x10') } + end + expect(result).to eq(16) + end + + it '"create" creates a new object from puppet data type in string form and args' do + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx| + ctx.with_script_compiler { |c| c.create('Integer', '010') } + end + expect(result).to eq(8) + end + end + end + + context 'supports parsing such that' do + it '"parse_string" parses a puppet language string' do + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx| + ctx.with_script_compiler { |c| c.parse_string('$a = 10') } + end + expect(result.class).to eq(Puppet::Pops::Model::Program) + end + + { nil => Puppet::Error, + '0xWAT' => Puppet::ParseErrorWithIssue, + '$0 = 1' => Puppet::ParseErrorWithIssue, + 'else 32' => Puppet::ParseErrorWithIssue, + }.each_pair do |input, error_class| + it "'parse_string' raises an error for invalid input: '#{input}'" do + expect { + Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx| + ctx.with_script_compiler { |c| c.parse_string(input) } + end + }.to raise_error(error_class) + end + end + + it '"parse_file" parses a puppet language string' do + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx| + manifest = file_containing('main.pp', "$a = 10") + ctx.with_script_compiler { |c| c.parse_file(manifest) } + end + expect(result.class).to eq(Puppet::Pops::Model::Program) + end + + it "'parse_file' raises an error for invalid input: 'else 32'" do + expect { + Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx| + manifest = file_containing('main.pp', "else 32") + ctx.with_script_compiler { |c| c.parse_file(manifest) } + end + }.to raise_error(Puppet::ParseErrorWithIssue) + end + + it "'parse_file' raises an error for invalid input, file is not a string" do + expect { + Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx| + ctx.with_script_compiler { |c| c.parse_file(42) } + end + }.to raise_error(Puppet::Error) + end + + it 'the "evaluate" method evaluates the parsed AST' do + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx| + ctx.with_script_compiler { |c| c.evaluate(c.parse_string('10 + 20')) } + end + expect(result).to eq(30) + end + + it 'the "evaluate" method instantiates definitions when given a Program' do + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx| + ctx.with_script_compiler { |c| c.evaluate(c.parse_string('function foo() { "yay"}; foo()')) } + end + expect(result).to eq('yay') + end + + it 'the "evaluate" method does not instantiates definitions when given ast other than Program' do + expect do + Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx| + ctx.with_script_compiler do |c| + program= c.parse_string('function foo() { "yay"}; foo()') + c.evaluate(program.body) + end + end + end.to raise_error(/Unknown function: 'foo'/) + end + + it 'the "evaluate_literal" method evaluates AST being a representation of a literal value' do + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx| + ctx.with_script_compiler { |c| c.evaluate_literal(c.parse_string('{10 => "hello"}')) } + end + expect(result).to eq({10 => 'hello'}) + end + + it 'the "evaluate_literal" method errors if ast is not representing a literal value' do + expect do + Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx| + ctx.with_script_compiler { |c| c.evaluate_literal(c.parse_string('{10+1 => "hello"}')) } + end + end.to raise_error(/does not represent a literal value/) + end + + it 'the "evaluate_literal" method errors if ast contains definitions' do + expect do + Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx| + ctx.with_script_compiler { |c| c.evaluate_literal(c.parse_string('function foo() { }; 42')) } + end + end.to raise_error(/does not represent a literal value/) + end + + end + end + + context 'with code in modules and env' do + let(:modulepath) { [modules_dir] } + + let(:metadata_json_a) { + { + 'name' => 'example/a', + 'version' => '0.1.0', + 'source' => 'git@github.com/example/example-a.git', + 'dependencies' => [{'name' => 'c', 'version_range' => '>=0.1.0'}], + 'author' => 'Bob the Builder', + 'license' => 'Apache-2.0' + } + } + + let(:metadata_json_b) { + { + 'name' => 'example/b', + 'version' => '0.1.0', + 'source' => 'git@github.com/example/example-b.git', + 'dependencies' => [{'name' => 'c', 'version_range' => '>=0.1.0'}], + 'author' => 'Bob the Builder', + 'license' => 'Apache-2.0' + } + } + + let(:metadata_json_c) { + { + 'name' => 'example/c', + 'version' => '0.1.0', + 'source' => 'git@github.com/example/example-c.git', + 'dependencies' => [], + 'author' => 'Bob the Builder', + 'license' => 'Apache-2.0' + } + } + + # TODO: there is something amiss with the metadata wrt dependencies - when metadata is present there is an error + # that dependencies could not be resolved. Metadata is therefore commented out. + # Dependency based visibility is probably something that we should remove... + let(:modules) { + { + 'a' => { + 'functions' => a_functions, + 'lib' => { 'puppet' => a_lib_puppet }, + 'plans' => a_plans, + 'tasks' => a_tasks, + 'types' => a_types, +# 'metadata.json' => metadata_json_a.to_json + }, + 'b' => { + 'functions' => b_functions, + 'lib' => b_lib, + 'plans' => b_plans, + 'tasks' => b_tasks, + 'types' => b_types, +# 'metadata.json' => metadata_json_b.to_json + }, + 'c' => { + 'types' => c_types, +# 'metadata.json' => metadata_json_c.to_json + }, + } + } + + let(:a_plans) { + { + 'aplan.pp' => <<-PUPPET.unindent, + plan a::aplan() { 'a::aplan value' } + PUPPET + } + } + + let(:a_types) { + { + 'atype.pp' => <<-PUPPET.unindent, + type A::Atype = Integer + PUPPET + } + } + + let(:a_tasks) { + { + 'atask' => '', + } + } + + let(:a_functions) { + { + 'afunc.pp' => 'function a::afunc() { "a::afunc value" }', + } + } + + let(:a_lib_puppet) { + { + 'functions' => { + 'a' => { + 'arubyfunc.rb' => <<-RUBY.unindent, + require 'stuff/something' + Puppet::Functions.create_function(:'a::arubyfunc') do + def arubyfunc + Stuff::SOMETHING + end + end + RUBY + 'myscriptcompilerfunc.rb' => <<-RUBY.unindent, + Puppet::Functions.create_function(:'a::myscriptcompilerfunc', Puppet::Functions::InternalFunction) do + dispatch :myscriptcompilerfunc do + script_compiler_param + param 'String',:name + end + + def myscriptcompilerfunc(script_compiler, name) + script_compiler.is_a?(Puppet::Pal::ScriptCompiler) ? name : 'no go' + end + end + RUBY + } + } + } + } + + let(:b_plans) { + { + 'aplan.pp' => <<-PUPPET.unindent, + plan b::aplan() {} + PUPPET + } + } + + let(:b_types) { + { + 'atype.pp' => <<-PUPPET.unindent, + type B::Atype = Integer + PUPPET + } + } + + let(:b_tasks) { + { + 'atask' => "# doing exactly nothing\n", + 'atask.json' => <<-JSONTEXT.unindent + { + "description": "test task b::atask", + "input_method": "stdin", + "parameters": { + "string_param": { + "description": "A string parameter", + "type": "String[1]" + }, + "int_param": { + "description": "An integer parameter", + "type": "Integer" + } + } + } + JSONTEXT + } + } + + let(:b_functions) { + { + 'afunc.pp' => 'function b::afunc() {}', + } + } + + let(:b_lib) { + { + 'puppet' => b_lib_puppet, + 'stuff' => { + 'something.rb' => "module Stuff; SOMETHING = 'something'; end" + } + } + } + + let(:b_lib_puppet) { + { + 'functions' => { + 'b' => { + 'arubyfunc.rb' => "Puppet::Functions.create_function(:'b::arubyfunc') { def arubyfunc; 'arubyfunc_value'; end }", + } + } + } + } + + let(:c_types) { + { + 'atype.pp' => <<-PUPPET.unindent, + type C::Atype = Integer + PUPPET + } + } + context 'configured as temporary environment such that' do + it 'modules are available' do + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| + ctx.with_script_compiler {|c| c.evaluate_string('a::afunc()') } + end + expect(result).to eq("a::afunc value") + end + + it 'libs in a given "modulepath" are added to the Ruby $LOAD_PATH' do + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| + ctx.with_script_compiler {|c| c.evaluate_string('a::arubyfunc()') } + end + expect(result).to eql('something') + end + + it 'errors if a block is not given to in_tmp_environment' do + expect do + Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) + end.to raise_error(/A block must be given to 'in_tmp_environment/) + end + + it 'errors if an env_name is given and is not a String[1]' do + expect do + Puppet::Pal.in_tmp_environment('', modulepath: modulepath, facts: node_facts) { |ctx| } + end.to raise_error(/temporary environment name has wrong type/) + + expect do + Puppet::Pal.in_tmp_environment(32, modulepath: modulepath, facts: node_facts) { |ctx| } + end.to raise_error(/temporary environment name has wrong type/) + end + + { 'a hash' => {'a' => 'hm'}, + 'an integer' => 32, + 'separated strings' => 'dir1;dir2', + 'empty string in array' => [''] + }.each_pair do |what, value| + it "errors if modulepath is #{what}" do + expect do + Puppet::Pal.in_tmp_environment('pal_env', modulepath: value, facts: node_facts) { |ctx| } + end.to raise_error(/modulepath has wrong type/) + end + end + + context 'facts are supported such that' do + it 'they are obtained if they are not given' do + facts = Puppet::Node::Facts.new(Puppet[:certname], 'puppetversion' => Puppet.version) + Puppet::Node::Facts.indirection.save(facts) + + testing_env_dir # creates the structure + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath ) do |ctx| + ctx.with_script_compiler {|c| c.evaluate_string("$facts =~ Hash and $facts[puppetversion] == '#{Puppet.version}'") } + end + expect(result).to eq(true) + end + + it 'can be given as a hash when creating the environment' do + testing_env_dir # creates the structure + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: { 'myfact' => 42 }) do |ctx| + ctx.with_script_compiler {|c| c.evaluate_string("$facts =~ Hash and $facts[myfact] == 42") } + end + expect(result).to eq(true) + end + + it 'can be overridden with a hash when creating a script compiler' do + testing_env_dir # creates the structure + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: { 'myfact' => 42 }) do |ctx| + ctx.with_script_compiler(facts: { 'myfact' => 43 }) {|c| c.evaluate_string("$facts =~ Hash and $facts[myfact] == 43") } + end + expect(result).to eq(true) + end + + it 'can be disabled with the :set_local_facts option' do + Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: { 'myfact' => 42}) do |ctx| + ctx.with_script_compiler(facts: { 'myfact' => 42 }, set_local_facts: false) do |compiler| + expect { compiler.evaluate_string('$facts') }.to raise_error( + Puppet::PreformattedError, + /Unknown variable: 'facts'/ + ) + end + end + end + end + + context 'supports tasks such that' do + it '"task_signature" returns the signatures of a generic task' do + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| + ctx.with_script_compiler do |c| + signature = c.task_signature('a::atask') + [ signature.runnable_with?('whatever' => 10), + signature.runnable_with?('anything_goes' => 'foo') + ] + end + end + expect(result).to eq([true, true]) + end + + it '"TaskSignature#runnable_with?" calls a given lambda if there is an error in a generic task' do + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| + ctx.with_script_compiler do |c| + signature = c.task_signature('a::atask') + local_result = 'not yay' + signature.runnable_with?('string_param' => /not data/) {|error| local_result = error } + local_result + end + end + expect(result).to match(/Task a::atask:\s+entry 'string_param' expects a Data value, got Regexp/m) + end + + it '"task_signature" returns the signatures of a task defined with metadata' do + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| + ctx.with_script_compiler do |c| + signature = c.task_signature('b::atask') + [ signature.runnable_with?('string_param' => 'foo', 'int_param' => 10), + signature.runnable_with?('anything_goes' => 'foo'), + signature.task_hash['name'], + signature.task_hash['metadata']['parameters']['string_param']['description'], + signature.task_hash['metadata']['description'], + signature.task_hash['metadata']['parameters']['int_param']['type'], + ] + end + end + expect(result).to eq([true, false, 'b::atask', 'A string parameter', 'test task b::atask', 'Integer']) + end + + it '"TaskSignature#runnable_with?" calls a given lambda if there is an error' do + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| + ctx.with_script_compiler do |c| + signature = c.task_signature('b::atask') + local_result = 'not yay' + signature.runnable_with?('string_param' => 10) {|error| local_result = error } + local_result + end + end + expect(result).to match(/Task b::atask:\s+parameter 'string_param' expects a String value, got Integer/m) + end + + it '"task_signature" returns nil if task is not found' do + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| + ctx.with_script_compiler {|c| c.task_signature('no_where_to_be_found') } + end + expect(result).to be(nil) + end + + it 'default task input_method is nil' do + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| + ctx.with_script_compiler do |c| + signature = c.task_signature('a::atask') + signature.task_hash + end + end + expect(result['metadata']['input_method']).to be_nil + end + + it 'task input_method is parsed from task metadata' do + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| + ctx.with_script_compiler do |c| + signature = c.task_signature('b::atask') + signature.task_hash + end + end + expect(result['metadata']['input_method']).to eq('stdin') + end + + it '"list_tasks" returns an array with all tasks that can be loaded' do + testing_env_dir # creates the structure + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| + ctx.with_script_compiler {|c| c.list_tasks() } + end + expect(result.is_a?(Array)).to eq(true) + expect(result.all? {|s| s.is_a?(Puppet::Pops::Loader::TypedName) }).to eq(true) + expect(result.map {|tn| tn.name}).to contain_exactly('a::atask', 'b::atask') + end + + it '"list_tasks" filters on name based on a given regexp' do + testing_env_dir # creates the structure + result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx| + ctx.with_script_compiler {|c| c.list_tasks(/^a::/) } + end + expect(result.is_a?(Array)).to eq(true) + expect(result.all? {|s| s.is_a?(Puppet::Pops::Loader::TypedName) }).to eq(true) + expect(result.map {|tn| tn.name}).to eq(['a::atask']) + end + end + + end + + context 'configured as an existing given environment directory such that' do + it 'modules in it are available from its "modules" directory' do + result = Puppet::Pal.in_environment('pal_env', env_dir: testing_env_dir, facts: node_facts) do |ctx| + ctx.with_script_compiler {|c| c.evaluate_string('a::afunc()') } + end + expect(result).to eq("a::afunc value") + end + + it 'libs in a given "modulepath" are added to the Ruby $LOAD_PATH' do + result = Puppet::Pal.in_environment('pal_env', env_dir: testing_env_dir, facts: node_facts) do |ctx| + ctx.with_script_compiler {|c| c.evaluate_string('a::arubyfunc()') } + end + expect(result).to eql('something') + end + + it 'a given "modulepath" overrides the default' do + expect do + Puppet::Pal.in_environment('pal_env', env_dir: testing_env_dir, modulepath: [], facts: node_facts) do |ctx| + ctx.with_script_compiler {|c| c.evaluate_string('a::afunc()') } + end + end.to raise_error(/Unknown function: 'a::afunc'/) + end + + it 'a "pre_modulepath" is prepended and a "post_modulepath" is appended to the effective modulepath' do + other_modules1 = File.join(environments_dir, 'other_env1/modules') + other_modules2 = File.join(environments_dir, 'other_env2/modules') + result = Puppet::Pal.in_environment('pal_env', env_dir: testing_env_dir, + pre_modulepath: [other_modules1], + post_modulepath: [other_modules2], + facts: node_facts + ) do |ctx| + the_modulepath = Puppet.lookup(:environments).get('pal_env').modulepath + the_modulepath[0] == other_modules1 && the_modulepath[-1] == other_modules2 + end + expect(result).to be(true) + end + + it 'can set variables in any scope' do + vars = {'a'=> 10, 'x::y' => 20} + result = Puppet::Pal.in_environment('pal_env', env_dir: testing_env_dir, facts: node_facts, variables: vars) do |ctx| + ctx.with_script_compiler { |c| c.evaluate_string("1+2+3+4+$a+$x::y") } + end + expect(result).to eq(40) + end + + it 'errors in a meaningful way when a non existing env name is given' do + testing_env_dir # creates the structure + expect do + Puppet::Pal.in_environment('blah_env', env_dir: testing_env_dir.chop, facts: node_facts) { |ctx| } + end.to raise_error(/The environment directory '.*' does not exist/) + end + + it 'errors if an env_name is given and is not a String[1]' do + expect do + Puppet::Pal.in_environment('', env_dir: testing_env_dir, facts: node_facts) { |ctx| } + end.to raise_error(/env_name has wrong type/) + + expect do + Puppet::Pal.in_environment(32, env_dir: testing_env_dir, facts: node_facts) { |ctx| } + end.to raise_error(/env_name has wrong type/) + end + + { 'a hash' => {'a' => 'hm'}, + 'an integer' => 32, + 'separated strings' => 'dir1;dir2', + 'empty string in array' => [''] + }.each_pair do |what, value| + it "errors if modulepath is #{what}" do + expect do + Puppet::Pal.in_environment('pal_env', env_dir: testing_env_dir, modulepath: {'a' => 'hm'}, facts: node_facts) { |ctx| } + Puppet::Pal.in_tmp_environment('pal_env', modulepath: value, facts: node_facts) { |ctx| } + end.to raise_error(/modulepath has wrong type/) + end + end + + it 'errors if env_dir and envpath are both given' do + testing_env_dir # creates the structure + expect do + Puppet::Pal.in_environment('blah_env', env_dir: testing_env_dir, envpath: environments_dir, facts: node_facts) { |ctx| } + end.to raise_error(/Cannot use 'env_dir' and 'envpath' at the same time/) + end + end + + context 'configured as existing given envpath such that' do + it 'modules in it are available from its "modules" directory' do + testing_env_dir # creates the structure + result = Puppet::Pal.in_environment('pal_env', envpath: environments_dir, facts: node_facts) do |ctx| + ctx.with_script_compiler { |c| c.evaluate_string('a::afunc()') } + end + expect(result).to eq("a::afunc value") + end + + it 'a given "modulepath" overrides the default' do + testing_env_dir # creates the structure + expect do + Puppet::Pal.in_environment('pal_env', envpath: environments_dir, modulepath: [], facts: node_facts) do |ctx| + ctx.with_script_compiler { |c| c.evaluate_string('a::afunc()') } + end + end.to raise_error(/Unknown function: 'a::afunc'/) + end + + it 'a "pre_modulepath" is prepended and a "post_modulepath" is appended to the effective modulepath' do + testing_env_dir # creates the structure + other_modules1 = File.join(environments_dir, 'other_env1/modules') + other_modules2 = File.join(environments_dir, 'other_env2/modules') + result = Puppet::Pal.in_environment('pal_env', envpath: environments_dir, + pre_modulepath: [other_modules1], + post_modulepath: [other_modules2], + facts: node_facts + ) do |ctx| + the_modulepath = Puppet.lookup(:environments).get('pal_env').modulepath + the_modulepath[0] == other_modules1 && the_modulepath[-1] == other_modules2 + end + expect(result).to be(true) + end + + it 'the envpath can have multiple entries - that are searched for the given env' do + testing_env_dir # creates the structure + result = Puppet::Pal.in_environment('pal_env', envpath: environments_dir, facts: node_facts) do |ctx| + ctx.with_script_compiler {|c| c.evaluate_string('a::afunc()') } + end + expect(result).to eq("a::afunc value") + end + + it 'errors in a meaningful way when a non existing env name is given' do + testing_env_dir # creates the structure + expect do + Puppet::Pal.in_environment('blah_env', envpath: environments_dir, facts: node_facts) { |ctx| } + end.to raise_error(/No directory found for the environment 'blah_env' on the path '.*'/) + end + + it 'errors if a block is not given to in_environment' do + expect do + Puppet::Pal.in_environment('blah_env', envpath: environments_dir, facts: node_facts) + end.to raise_error(/A block must be given to 'in_environment/) + end + + it 'errors if envpath is something other than a string' do + testing_env_dir # creates the structure + expect do + Puppet::Pal.in_environment('blah_env', envpath: '', facts: node_facts) { |ctx| } + end.to raise_error(/envpath has wrong type/) + + expect do + Puppet::Pal.in_environment('blah_env', envpath: [environments_dir], facts: node_facts) { |ctx| } + end.to raise_error(/envpath has wrong type/) + end + + context 'with a script compiler' do + it 'uses configured manifest_file if configured_by_env is true and Puppet[:code] is unset' do + testing_env_dir # creates the structure + Puppet[:manifest] = file_containing('afunc.pp', "function myfunc(Integer $a) { $a * 2 } ") + result = Puppet::Pal.in_environment('pal_env', envpath: environments_dir, facts: node_facts) do |ctx| + ctx.with_script_compiler(configured_by_env: true) {|c| c.call_function('myfunc', 4)} + end + expect(result).to eql(8) + end + + it 'uses Puppet[:code] if configured_by_env is true and Puppet[:code] is set' do + testing_env_dir # creates the structure + Puppet[:manifest] = file_containing('amanifest.pp', "$a = 20") + Puppet[:code] = '$a = 40' + result = Puppet::Pal.in_environment('pal_env', envpath: environments_dir, facts: node_facts) do |ctx| + ctx.with_script_compiler(configured_by_env: true) {|c| c.evaluate_string('$a')} + end + expect(result).to eql(40) + end + + it 'makes the pal ScriptCompiler available as script_compiler_param to Function dispatcher' do + testing_env_dir # creates the structure + Puppet[:manifest] = file_containing('noop.pp', "undef") + result = Puppet::Pal.in_environment('pal_env', envpath: environments_dir, facts: node_facts) do |ctx| + ctx.with_script_compiler(configured_by_env: true) {|c| c.call_function('a::myscriptcompilerfunc', 'go')} + end + expect(result).to eql('go') + end + end + end + end +end