diff --git a/lib/puppet/pops/model/factory.rb b/lib/puppet/pops/model/factory.rb index a47a7040f..3e22c0fe8 100644 --- a/lib/puppet/pops/model/factory.rb +++ b/lib/puppet/pops/model/factory.rb @@ -1,922 +1,922 @@ # Factory is a helper class that makes construction of a Pops Model # much more convenient. It can be viewed as a small internal DSL for model # constructions. # For usage see tests using the factory. # # @todo All those uppercase methods ... they look bad in one way, but stand out nicely in the grammar... # decide if they should change into lower case names (some of the are lower case)... # class Puppet::Pops::Model::Factory Model = Puppet::Pops::Model attr_accessor :current alias_method :model, :current # Shared build_visitor, since there are many instances of Factory being used @@build_visitor = Puppet::Pops::Visitor.new(self, "build") @@interpolation_visitor = Puppet::Pops::Visitor.new(self, "interpolate") # Initialize a factory with a single object, or a class with arguments applied to build of # created instance # def initialize o, *args @current = case o when Model::PopsObject o when Puppet::Pops::Model::Factory o.current else build(o, *args) end end # Polymorphic build def build(o, *args) begin @@build_visitor.visit_this(self, o, *args) rescue =>e - # require 'debugger'; debugger # enable this when in trouble... + # debug here when in trouble... raise e end end # Polymorphic interpolate def interpolate() begin @@interpolation_visitor.visit_this_0(self, current) rescue =>e - # require 'debugger'; debugger # enable this when in trouble... + # debug here when in trouble... raise e end end # Building of Model classes def build_ArithmeticExpression(o, op, a, b) o.operator = op build_BinaryExpression(o, a, b) end def build_AssignmentExpression(o, op, a, b) o.operator = op build_BinaryExpression(o, a, b) end def build_AttributeOperation(o, name, op, value) o.operator = op o.attribute_name = name.to_s # BOOLEAN is allowed in the grammar o.value_expr = build(value) o end def build_AccessExpression(o, left, *keys) o.left_expr = to_ops(left) keys.each {|expr| o.addKeys(to_ops(expr)) } o end def build_BinaryExpression(o, left, right) o.left_expr = to_ops(left) o.right_expr = to_ops(right) o end def build_BlockExpression(o, *args) args.each {|expr| o.addStatements(to_ops(expr)) } o end def build_CollectExpression(o, type_expr, query_expr, attribute_operations) o.type_expr = to_ops(type_expr) o.query = build(query_expr) attribute_operations.each {|op| o.addOperations(build(op)) } o end def build_ComparisonExpression(o, op, a, b) o.operator = op build_BinaryExpression(o, a, b) end def build_ConcatenatedString(o, *args) args.each {|expr| o.addSegments(build(expr)) } o end def build_CreateTypeExpression(o, name, super_name = nil) o.name = name o.super_name = super_name o end def build_CreateEnumExpression(o, *args) o.name = args.slice(0) if args.size == 2 o.values = build(args.last) o end def build_CreateAttributeExpression(o, name, datatype_expr) o.name = name o.type = to_ops(datatype_expr) o end def build_HeredocExpression(o, name, expr) o.syntax = name o.text_expr = build(expr) o end # @param name [String] a valid classname # @param parameters [Array] may be empty # @param parent_class_name [String, nil] a valid classname referencing a parent class, optional. # @param body [Array, Expression, nil] expression that constitute the body # @return [Model::HostClassDefinition] configured from the parameters # def build_HostClassDefinition(o, name, parameters, parent_class_name, body) build_NamedDefinition(o, name, parameters, body) o.parent_class = parent_class_name if parent_class_name o end def build_ResourceOverrideExpression(o, resources, attribute_operations) o.resources = build(resources) attribute_operations.each {|ao| o.addOperations(build(ao)) } o end def build_KeyedEntry(o, k, v) o.key = build(k) o.value = build(v) o end def build_LiteralHash(o, *keyed_entries) keyed_entries.each {|entry| o.addEntries build(entry) } o end def build_LiteralList(o, *values) values.each {|v| o.addValues build(v) } o end def build_LiteralFloat(o, val) o.value = val o end def build_LiteralInteger(o, val, radix) o.value = val o.radix = radix o end def build_IfExpression(o, t, ift, els) o.test = build(t) o.then_expr = build(ift) o.else_expr= build(els) o end def build_MatchExpression(o, op, a, b) o.operator = op build_BinaryExpression(o, a, b) end # Builds body :) from different kinds of input # @overload f_build_body(nothing) # @param nothing [nil] unchanged, produces nil # @overload f_build_body(array) # @param array [Array] turns into a BlockExpression # @overload f_build_body(expr) # @param expr [Expression] produces the given expression # @overload f_build_body(obj) # @param obj [Object] produces the result of calling #build with body as argument def f_build_body(body) case body when NilClass nil when Array Puppet::Pops::Model::Factory.new(Model::BlockExpression, *body) else build(body) end end def build_LambdaExpression(o, parameters, body) parameters.each {|p| o.addParameters(build(p)) } b = f_build_body(body) o.body = b.current if b o end def build_NamedDefinition(o, name, parameters, body) parameters.each {|p| o.addParameters(build(p)) } b = f_build_body(body) o.body = b.current if b o.name = name o end # @param o [Model::NodeDefinition] # @param hosts [Array] host matches # @param parent [Expression] parent node matcher # @param body [Object] see {#f_build_body} def build_NodeDefinition(o, hosts, parent, body) hosts.each {|h| o.addHost_matches(build(h)) } o.parent = build(parent) if parent # no nop here b = f_build_body(body) o.body = b.current if b o end def build_Parameter(o, name, expr) o.name = name o.value = build(expr) if expr # don't build a nil/nop o end def build_QualifiedReference(o, name) o.value = name.to_s.downcase o end def build_RelationshipExpression(o, op, a, b) o.operator = op build_BinaryExpression(o, a, b) end def build_ResourceExpression(o, type_name, bodies) o.type_name = build(type_name) bodies.each {|b| o.addBodies(build(b)) } o end def build_RenderStringExpression(o, string) o.value = string; o end def build_ResourceBody(o, title_expression, attribute_operations) o.title = build(title_expression) attribute_operations.each {|ao| o.addOperations(build(ao)) } o end def build_ResourceDefaultsExpression(o, type_ref, attribute_operations) o.type_ref = build(type_ref) attribute_operations.each {|ao| o.addOperations(build(ao)) } o end def build_SelectorExpression(o, left, *selectors) o.left_expr = to_ops(left) selectors.each {|s| o.addSelectors(build(s)) } o end # Builds a SubLocatedExpression - this wraps the expression in a sublocation configured # from the given token # A SubLocated holds its own locator that is used for subexpressions holding positions relative # to what it describes. # def build_SubLocatedExpression(o, token, expression) o.expr = build(expression) o.offset = token.offset o.length = token.length locator = token.locator o.locator = locator o.leading_line_count = locator.leading_line_count o.leading_line_offset = locator.leading_line_offset # Index is held in sublocator's parent locator - needed to be able to reconstruct o.line_offsets = locator.locator.line_index o end def build_SelectorEntry(o, matching, value) o.matching_expr = build(matching) o.value_expr = build(value) o end def build_QueryExpression(o, expr) ops = to_ops(expr) o.expr = ops unless Puppet::Pops::Model::Factory.nop? ops o end def build_UnaryExpression(o, expr) ops = to_ops(expr) o.expr = ops unless Puppet::Pops::Model::Factory.nop? ops o end def build_Program(o, body, definitions, locator) o.body = to_ops(body) # non containment definitions.each { |d| o.addDefinitions(d) } o.source_ref = locator.file o.source_text = locator.string o.line_offsets = locator.line_index o.locator = locator o end def build_QualifiedName(o, name) o.value = name.to_s o end # Puppet::Pops::Model::Factory helpers def f_build_unary(klazz, expr) Puppet::Pops::Model::Factory.new(build(klazz.new, expr)) end def f_build_binary_op(klazz, op, left, right) Puppet::Pops::Model::Factory.new(build(klazz.new, op, left, right)) end def f_build_binary(klazz, left, right) Puppet::Pops::Model::Factory.new(build(klazz.new, left, right)) end def f_build_vararg(klazz, left, *arg) Puppet::Pops::Model::Factory.new(build(klazz.new, left, *arg)) end def f_arithmetic(op, r) f_build_binary_op(Model::ArithmeticExpression, op, current, r) end def f_comparison(op, r) f_build_binary_op(Model::ComparisonExpression, op, current, r) end def f_match(op, r) f_build_binary_op(Model::MatchExpression, op, current, r) end # Operator helpers def in(r) f_build_binary(Model::InExpression, current, r); end def or(r) f_build_binary(Model::OrExpression, current, r); end def and(r) f_build_binary(Model::AndExpression, current, r); end def not(); f_build_unary(Model::NotExpression, self); end def minus(); f_build_unary(Model::UnaryMinusExpression, self); end def text(); f_build_unary(Model::TextExpression, self); end def var(); f_build_unary(Model::VariableExpression, self); end def [](*r); f_build_vararg(Model::AccessExpression, current, *r); end def dot r; f_build_binary(Model::NamedAccessExpression, current, r); end def + r; f_arithmetic(:+, r); end def - r; f_arithmetic(:-, r); end def / r; f_arithmetic(:/, r); end def * r; f_arithmetic(:*, r); end def % r; f_arithmetic(:%, r); end def << r; f_arithmetic(:<<, r); end def >> r; f_arithmetic(:>>, r); end def < r; f_comparison(:<, r); end def <= r; f_comparison(:<=, r); end def > r; f_comparison(:>, r); end def >= r; f_comparison(:>=, r); end def == r; f_comparison(:==, r); end def ne r; f_comparison(:'!=', r); end def =~ r; f_match(:'=~', r); end def mne r; f_match(:'!~', r); end def paren(); f_build_unary(Model::ParenthesizedExpression, current); end def relop op, r f_build_binary_op(Model::RelationshipExpression, op.to_sym, current, r) end def select *args Puppet::Pops::Model::Factory.new(build(Model::SelectorExpression, current, *args)) end # For CaseExpression, setting the default for an already build CaseExpression def default r current.addOptions(Puppet::Pops::Model::Factory.WHEN(:default, r).current) self end def lambda=(lambda) current.lambda = lambda.current self end # Assignment = def set(r) f_build_binary_op(Model::AssignmentExpression, :'=', current, r) end # Assignment += def plus_set(r) f_build_binary_op(Model::AssignmentExpression, :'+=', current, r) end # Assignment -= def minus_set(r) f_build_binary_op(Model::AssignmentExpression, :'-=', current, r) end def attributes(*args) args.each {|a| current.addAttributes(build(a)) } self end # Catch all delegation to current def method_missing(meth, *args, &block) if current.respond_to?(meth) current.send(meth, *args, &block) else super end end def respond_to?(meth, include_all=false) current.respond_to?(meth, include_all) || super end # Records the position (start -> end) and computes the resulting length. # def record_position(start_locatable, end_locatable) from = start_locatable.is_a?(Puppet::Pops::Model::Factory) ? start_locatable.current : start_locatable to = end_locatable.is_a?(Puppet::Pops::Model::Factory) ? end_locatable.current : end_locatable to = from if to.nil? o = current # record information directly in the Model::Positioned object o.offset = from.offset o.length ||= to.offset - from.offset + to.length self end # @return [Puppet::Pops::Adapters::SourcePosAdapter] with location information def loc() Puppet::Pops::Adapters::SourcePosAdapter.adapt(current) end # Returns symbolic information about an expected share of a resource expression given the LHS of a resource expr. # # * `name { }` => `:resource`, create a resource of the given type # * `Name { }` => ':defaults`, set defaults for the referenced type # * `Name[] { }` => `:override`, overrides instances referenced by LHS # * _any other_ => ':error', all other are considered illegal # def self.resource_shape(expr) expr = expr.current if expr.is_a?(Puppet::Pops::Model::Factory) case expr when Model::QualifiedName :resource when Model::QualifiedReference :defaults when Model::AccessExpression :override when 'class' :class else :error end end # Factory starting points def self.literal(o); new(o); end def self.minus(o); new(o).minus; end def self.var(o); new(o).var; end def self.block(*args); new(Model::BlockExpression, *args); end def self.string(*args); new(Model::ConcatenatedString, *args); end def self.text(o); new(o).text; end def self.IF(test_e,then_e,else_e); new(Model::IfExpression, test_e, then_e, else_e); end def self.UNLESS(test_e,then_e,else_e); new(Model::UnlessExpression, test_e, then_e, else_e); end def self.CASE(test_e,*options); new(Model::CaseExpression, test_e, *options); end def self.WHEN(values_list, block); new(Model::CaseOption, values_list, block); end def self.MAP(match, value); new(Model::SelectorEntry, match, value); end def self.TYPE(name, super_name=nil); new(Model::CreateTypeExpression, name, super_name); end def self.ATTR(name, type_expr=nil); new(Model::CreateAttributeExpression, name, type_expr); end def self.ENUM(*args); new(Model::CreateEnumExpression, *args); end def self.KEY_ENTRY(key, val); new(Model::KeyedEntry, key, val); end def self.HASH(entries); new(Model::LiteralHash, *entries); end # TODO_HEREDOC def self.HEREDOC(name, expr); new(Model::HeredocExpression, name, expr); end def self.SUBLOCATE(token, expr) new(Model::SubLocatedExpression, token, expr); end def self.LIST(entries); new(Model::LiteralList, *entries); end def self.PARAM(name, expr=nil); new(Model::Parameter, name, expr); end def self.NODE(hosts, parent, body); new(Model::NodeDefinition, hosts, parent, body); end # Creates a QualifiedName representation of o, unless o already represents a QualifiedName in which # case it is returned. # def self.fqn(o) o = o.current if o.is_a?(Puppet::Pops::Model::Factory) o = new(Model::QualifiedName, o) unless o.is_a? Model::QualifiedName o end # Creates a QualifiedName representation of o, unless o already represents a QualifiedName in which # case it is returned. # def self.fqr(o) o = o.current if o.is_a?(Puppet::Pops::Model::Factory) o = new(Model::QualifiedReference, o) unless o.is_a? Model::QualifiedReference o end def self.TEXT(expr) new(Model::TextExpression, new(expr).interpolate) end # TODO_EPP def self.RENDER_STRING(o) new(Model::RenderStringExpression, o) end def self.RENDER_EXPR(expr) new(Model::RenderExpression, expr) end def self.EPP(parameters, body) new(Model::EppExpression, parameters, body) end # TODO: This is the same a fqn factory method, don't know if callers to fqn and QNAME can live with the # same result or not yet - refactor into one method when decided. # def self.QNAME(name) new(Model::QualifiedName, name) end def self.NUMBER(name_or_numeric) if n_radix = Puppet::Pops::Utils.to_n_with_radix(name_or_numeric) val, radix = n_radix if val.is_a?(Float) new(Model::LiteralFloat, val) else new(Model::LiteralInteger, val, radix) end else # Bad number should already have been caught by lexer - this should never happen raise ArgumentError, "Internal Error, NUMBER token does not contain a valid number, #{name_or_numeric}" end end # Convert input string to either a qualified name, a LiteralInteger with radix, or a LiteralFloat # def self.QNAME_OR_NUMBER(name) if n_radix = Puppet::Pops::Utils.to_n_with_radix(name) val, radix = n_radix if val.is_a?(Float) new(Model::LiteralFloat, val) else new(Model::LiteralInteger, val, radix) end else new(Model::QualifiedName, name) end end def self.QREF(name) new(Model::QualifiedReference, name) end def self.VIRTUAL_QUERY(query_expr) new(Model::VirtualQuery, query_expr) end def self.EXPORTED_QUERY(query_expr) new(Model::ExportedQuery, query_expr) end def self.ATTRIBUTE_OP(name, op, expr) new(Model::AttributeOperation, name, op, expr) end def self.CALL_NAMED(name, rval_required, argument_list) unless name.kind_of?(Model::PopsObject) name = Puppet::Pops::Model::Factory.fqn(name) unless name.is_a?(Puppet::Pops::Model::Factory) end new(Model::CallNamedFunctionExpression, name, rval_required, *argument_list) end def self.CALL_METHOD(functor, argument_list) new(Model::CallMethodExpression, functor, true, nil, *argument_list) end def self.COLLECT(type_expr, query_expr, attribute_operations) new(Model::CollectExpression, type_expr, query_expr, attribute_operations) end def self.NAMED_ACCESS(type_name, bodies) new(Model::NamedAccessExpression, type_name, bodies) end def self.RESOURCE(type_name, bodies) new(Model::ResourceExpression, type_name, bodies) end def self.RESOURCE_DEFAULTS(type_name, attribute_operations) new(Model::ResourceDefaultsExpression, type_name, attribute_operations) end def self.RESOURCE_OVERRIDE(resource_ref, attribute_operations) new(Model::ResourceOverrideExpression, resource_ref, attribute_operations) end def self.RESOURCE_BODY(resource_title, attribute_operations) new(Model::ResourceBody, resource_title, attribute_operations) end def self.PROGRAM(body, definitions, locator) new(Model::Program, body, definitions, locator) end # Builds a BlockExpression if args size > 1, else the single expression/value in args def self.block_or_expression(*args) if args.size > 1 new(Model::BlockExpression, *args) else new(args[0]) end end def self.HOSTCLASS(name, parameters, parent, body) new(Model::HostClassDefinition, name, parameters, parent, body) end def self.DEFINITION(name, parameters, body) new(Model::ResourceTypeDefinition, name, parameters, body) end def self.LAMBDA(parameters, body) new(Model::LambdaExpression, parameters, body) end def self.nop? o o.nil? || o.is_a?(Puppet::Pops::Model::Nop) end # Transforms an array of expressions containing literal name expressions to calls if followed by an # expression, or expression list. # def self.transform_calls(expressions) expressions.reduce([]) do |memo, expr| expr = expr.current if expr.is_a?(Puppet::Pops::Model::Factory) name = memo[-1] if name.is_a? Model::QualifiedName memo[-1] = Puppet::Pops::Model::Factory.CALL_NAMED(name, false, expr.is_a?(Array) ? expr : [expr]) if expr.is_a?(Model::CallNamedFunctionExpression) # Patch statement function call to expression style # This is needed because it is first parsed as a "statement" and the requirement changes as it becomes # an argument to the name to call transform above. expr.rval_required = true end else memo << expr if expr.is_a?(Model::CallNamedFunctionExpression) # Patch rvalue expression function call to statement style. # This is not really required but done to be AST model compliant expr.rval_required = false end end memo end end # Building model equivalences of Ruby objects # Allows passing regular ruby objects to the factory to produce instructions # that when evaluated produce the same thing. def build_String(o) x = Model::LiteralString.new x.value = o; x end def build_NilClass(o) x = Model::Nop.new x end def build_TrueClass(o) x = Model::LiteralBoolean.new x.value = o x end def build_FalseClass(o) x = Model::LiteralBoolean.new x.value = o x end def build_Fixnum(o) x = Model::LiteralInteger.new x.value = o; x end def build_Float(o) x = Model::LiteralFloat.new x.value = o; x end def build_Regexp(o) x = Model::LiteralRegularExpression.new x.value = o; x end # If building a factory, simply unwrap the model oject contained in the factory. def build_Factory(o) o.current end # Creates a String literal, unless the symbol is one of the special :undef, or :default # which instead creates a LiterlUndef, or a LiteralDefault. def build_Symbol(o) case o when :undef Model::LiteralUndef.new when :default Model::LiteralDefault.new else build_String(o.to_s) end end # Creates a LiteralList instruction from an Array, where the entries are built. def build_Array(o) x = Model::LiteralList.new o.each { |v| x.addValues(build(v)) } x end # Create a LiteralHash instruction from a hash, where keys and values are built # The hash entries are added in sorted order based on key.to_s # def build_Hash(o) x = Model::LiteralHash.new (o.sort_by {|k,v| k.to_s}).each {|k,v| x.addEntries(build(Model::KeyedEntry.new, k, v)) } x end # @param rval_required [Boolean] if the call must produce a value def build_CallExpression(o, functor, rval_required, *args) o.functor_expr = to_ops(functor) o.rval_required = rval_required args.each {|x| o.addArguments(to_ops(x)) } o end def build_CallMethodExpression(o, functor, rval_required, lambda, *args) build_CallExpression(o, functor, rval_required, *args) o.lambda = lambda o end def build_CaseExpression(o, test, *args) o.test = build(test) args.each {|opt| o.addOptions(build(opt)) } o end def build_CaseOption(o, value_list, then_expr) value_list = [value_list] unless value_list.is_a? Array value_list.each { |v| o.addValues(build(v)) } b = f_build_body(then_expr) o.then_expr = to_ops(b) if b o end # Build a Class by creating an instance of it, and then calling build on the created instance # with the given arguments def build_Class(o, *args) build(o.new(), *args) end def interpolate_Factory(o) interpolate(o.current) end def interpolate_LiteralInteger(o) # convert number to a variable self.class.new(o).var end def interpolate_Object(o) o end def interpolate_QualifiedName(o) self.class.new(o).var end # rewrite left expression to variable if it is name, number, and recurse if it is an access expression # this is for interpolation support in new lexer (${NAME}, ${NAME[}}, ${NUMBER}, ${NUMBER[]} - all # other expressions requires variables to be preceded with $ # def interpolate_AccessExpression(o) if is_interop_rewriteable?(o.left_expr) o.left_expr = to_ops(self.class.new(o.left_expr).interpolate) end o end def interpolate_NamedAccessExpression(o) if is_interop_rewriteable?(o.left_expr) o.left_expr = to_ops(self.class.new(o.left_expr).interpolate) end o end # Rewrite method calls on the form ${x.each ...} to ${$x.each} def interpolate_CallMethodExpression(o) if is_interop_rewriteable?(o.functor_expr) o.functor_expr = to_ops(self.class.new(o.functor_expr).interpolate) end o end def is_interop_rewriteable?(o) case o when Model::AccessExpression, Model::QualifiedName, Model::NamedAccessExpression, Model::CallMethodExpression true when Model::LiteralInteger # Only decimal integers can represent variables, else it is a number o.radix == 10 else false end end # Checks if the object is already a model object, or build it def to_ops(o, *args) case o when Model::PopsObject o when Puppet::Pops::Model::Factory o.current else build(o, *args) end end def self.concat(*args) new(args.map do |e| e = e.current if e.is_a?(self) case e when Model::LiteralString e.value when String e else raise ArgumentError, "can only concatenate strings, got #{e.class}" end end.join('')) end end diff --git a/lib/puppet/pops/model/model_tree_dumper.rb b/lib/puppet/pops/model/model_tree_dumper.rb index adb4eb471..2e49092ce 100644 --- a/lib/puppet/pops/model/model_tree_dumper.rb +++ b/lib/puppet/pops/model/model_tree_dumper.rb @@ -1,373 +1,377 @@ # Dumps a Pops::Model in reverse polish notation; i.e. LISP style # The intention is to use this for debugging output # TODO: BAD NAME - A DUMP is a Ruby Serialization # class Puppet::Pops::Model::ModelTreeDumper < Puppet::Pops::Model::TreeDumper def dump_Array o o.collect {|e| do_dump(e) } end def dump_LiteralFloat o o.value.to_s end def dump_LiteralInteger o case o.radix when 10 o.value.to_s when 8 "0%o" % o.value when 16 "0x%X" % o.value else "bad radix:" + o.value.to_s end end def dump_LiteralValue o o.value.to_s end def dump_Factory o do_dump(o.current) end def dump_ArithmeticExpression o [o.operator.to_s, do_dump(o.left_expr), do_dump(o.right_expr)] end # x[y] prints as (slice x y) def dump_AccessExpression o if o.keys.size <= 1 ["slice", do_dump(o.left_expr), do_dump(o.keys[0])] else ["slice", do_dump(o.left_expr), do_dump(o.keys)] end end def dump_MatchesExpression o [o.operator.to_s, do_dump(o.left_expr), do_dump(o.right_expr)] end def dump_CollectExpression o result = ["collect", do_dump(o.type_expr), :indent, :break, do_dump(o.query), :indent] o.operations do |ao| result << :break << do_dump(ao) end result += [:dedent, :dedent ] result end def dump_EppExpression o result = ["epp"] result << ["parameters"] + o.parameters.collect {|p| do_dump(p) } if o.parameters.size() > 0 if o.body result << do_dump(o.body) else result << [] end result end def dump_ExportedQuery o result = ["<<| |>>"] result += dump_QueryExpression(o) unless is_nop?(o.expr) result end def dump_VirtualQuery o result = ["<| |>"] result += dump_QueryExpression(o) unless is_nop?(o.expr) result end def dump_QueryExpression o [do_dump(o.expr)] end def dump_ComparisonExpression o [o.operator.to_s, do_dump(o.left_expr), do_dump(o.right_expr)] end def dump_AndExpression o ["&&", do_dump(o.left_expr), do_dump(o.right_expr)] end def dump_OrExpression o ["||", do_dump(o.left_expr), do_dump(o.right_expr)] end def dump_InExpression o ["in", do_dump(o.left_expr), do_dump(o.right_expr)] end def dump_AssignmentExpression o [o.operator.to_s, do_dump(o.left_expr), do_dump(o.right_expr)] end # Produces (name => expr) or (name +> expr) def dump_AttributeOperation o [o.attribute_name, o.operator, do_dump(o.value_expr)] end def dump_LiteralList o ["[]"] + o.values.collect {|x| do_dump(x)} end def dump_LiteralHash o ["{}"] + o.entries.collect {|x| do_dump(x)} end def dump_KeyedEntry o [do_dump(o.key), do_dump(o.value)] end def dump_MatchExpression o [o.operator.to_s, do_dump(o.left_expr), do_dump(o.right_expr)] end def dump_LiteralString o "'#{o.value}'" end def dump_LambdaExpression o result = ["lambda"] result << ["parameters"] + o.parameters.collect {|p| do_dump(p) } if o.parameters.size() > 0 if o.body result << do_dump(o.body) else result << [] end result end def dump_LiteralDefault o ":default" end def dump_LiteralUndef o ":undef" end def dump_LiteralRegularExpression o "/#{o.value.source}/" end def dump_Nop o ":nop" end def dump_NamedAccessExpression o [".", do_dump(o.left_expr), do_dump(o.right_expr)] end def dump_NilClass o "()" end def dump_NotExpression o ['!', dump(o.expr)] end def dump_VariableExpression o "$#{dump(o.expr)}" end # Interpolation (to string) shown as (str expr) def dump_TextExpression o ["str", do_dump(o.expr)] end def dump_UnaryMinusExpression o ['-', do_dump(o.expr)] end def dump_BlockExpression o ["block"] + o.statements.collect {|x| do_dump(x) } end # Interpolated strings are shown as (cat seg0 seg1 ... segN) def dump_ConcatenatedString o ["cat"] + o.segments.collect {|x| do_dump(x)} end def dump_HeredocExpression(o) result = ["@(#{o.syntax})", :indent, :break, do_dump(o.text_expr), :dedent, :break] end def dump_HostClassDefinition o result = ["class", o.name] result << ["inherits", o.parent_class] if o.parent_class result << ["parameters"] + o.parameters.collect {|p| do_dump(p) } if o.parameters.size() > 0 if o.body result << do_dump(o.body) else result << [] end result end def dump_NodeDefinition o result = ["node"] result << ["matches"] + o.host_matches.collect {|m| do_dump(m) } result << ["parent", do_dump(o.parent)] if o.parent if o.body result << do_dump(o.body) else result << [] end result end def dump_ResourceTypeDefinition o result = ["define", o.name] result << ["parameters"] + o.parameters.collect {|p| do_dump(p) } if o.parameters.size() > 0 if o.body result << do_dump(o.body) else result << [] end result end def dump_ResourceOverrideExpression o result = ["override", do_dump(o.resources), :indent] o.operations.each do |p| result << :break << do_dump(p) end result << :dedent result end # Produces parameters as name, or (= name value) def dump_Parameter o if o.value ["=", o.name, do_dump(o.value)] else o.name end end def dump_ParenthesizedExpression o do_dump(o.expr) end # Hides that Program exists in the output (only its body is shown), the definitions are just # references to contained classes, resource types, and nodes def dump_Program(o) dump(o.body) end def dump_IfExpression o result = ["if", do_dump(o.test), :indent, :break, ["then", :indent, do_dump(o.then_expr), :dedent]] result += [:break, ["else", :indent, do_dump(o.else_expr), :dedent], :dedent] unless is_nop? o.else_expr result end def dump_UnlessExpression o result = ["unless", do_dump(o.test), :indent, :break, ["then", :indent, do_dump(o.then_expr), :dedent]] result += [:break, ["else", :indent, do_dump(o.else_expr), :dedent], :dedent] unless is_nop? o.else_expr result end # Produces (invoke name args...) when not required to produce an rvalue, and # (call name args ... ) otherwise. # def dump_CallNamedFunctionExpression o result = [o.rval_required ? "call" : "invoke", do_dump(o.functor_expr)] o.arguments.collect {|a| result << do_dump(a) } result end # def dump_CallNamedFunctionExpression o # result = [o.rval_required ? "call" : "invoke", do_dump(o.functor_expr)] # o.arguments.collect {|a| result << do_dump(a) } # result # end def dump_CallMethodExpression o result = [o.rval_required ? "call-method" : "invoke-method", do_dump(o.functor_expr)] o.arguments.collect {|a| result << do_dump(a) } result << do_dump(o.lambda) if o.lambda result end def dump_CaseExpression o result = ["case", do_dump(o.test), :indent] o.options.each do |s| result << :break << do_dump(s) end result << :dedent end def dump_CaseOption o result = ["when"] result << o.values.collect {|x| do_dump(x) } result << ["then", do_dump(o.then_expr) ] result end def dump_RelationshipExpression o [o.operator.to_s, do_dump(o.left_expr), do_dump(o.right_expr)] end def dump_RenderStringExpression o ["render-s", " '#{o.value}'"] end def dump_RenderExpression o ["render", do_dump(o.expr)] end def dump_ResourceBody o result = [do_dump(o.title), :indent] o.operations.each do |p| result << :break << do_dump(p) end result << :dedent result end def dump_ResourceDefaultsExpression o result = ["resource-defaults", do_dump(o.type_ref), :indent] o.operations.each do |p| result << :break << do_dump(p) end result << :dedent result end def dump_ResourceExpression o form = o.form == :regular ? '' : o.form.to_s + "-" result = [form+"resource", do_dump(o.type_name), :indent] o.bodies.each do |b| result << :break << do_dump(b) end result << :dedent result end def dump_SelectorExpression o ["?", do_dump(o.left_expr)] + o.selectors.collect {|x| do_dump(x) } end def dump_SelectorEntry o [do_dump(o.matching_expr), "=>", do_dump(o.value_expr)] end + def dump_SubLocatedExpression o + ["sublocated", do_dump(o.expr)] + end + def dump_Object o [o.class.to_s, o.to_s] end def is_nop? o o.nil? || o.is_a?(Puppet::Pops::Model::Nop) end end diff --git a/lib/puppet/util/instance_loader.rb b/lib/puppet/util/instance_loader.rb index fe0036b82..c6d6b71d4 100644 --- a/lib/puppet/util/instance_loader.rb +++ b/lib/puppet/util/instance_loader.rb @@ -1,82 +1,81 @@ require 'puppet/util/autoload' require 'puppet/util' # A module that can easily autoload things for us. Uses an instance # of Puppet::Util::Autoload module Puppet::Util::InstanceLoader include Puppet::Util # Are we instance-loading this type? def instance_loading?(type) defined?(@autoloaders) and @autoloaders.include?(type.intern) end # Define a new type of autoloading. def instance_load(type, path, options = {}) @autoloaders ||= {} @instances ||= {} type = type.intern @instances[type] = {} @autoloaders[type] = Puppet::Util::Autoload.new(self, path, options) # Now define our new simple methods unless respond_to?(type) meta_def(type) do |name| loaded_instance(type, name) end end end # Return a list of the names of all instances def loaded_instances(type) @instances[type].keys end # Collect the docs for all of our instances. def instance_docs(type) docs = "" # Load all instances. instance_loader(type).loadall # Use this method so they all get loaded loaded_instances(type).sort { |a,b| a.to_s <=> b.to_s }.each do |name| mod = self.loaded_instance(name) docs << "#{name}\n#{"-" * name.to_s.length}\n" docs << Puppet::Util::Docs.scrub(mod.doc) << "\n\n" end docs end # Return the instance hash for our type. def instance_hash(type) @instances[type.intern] end # Return the Autoload object for a given type. def instance_loader(type) @autoloaders[type.intern] end # Retrieve an alread-loaded instance, or attempt to load our instance. def loaded_instance(type, name) name = name.intern return nil unless instances = instance_hash(type) unless instances.include? name -# require 'debugger'; debugger if instance_loader(type).load(name) unless instances.include? name Puppet.warning( "Loaded #{type} file for #{name} but #{type} was not defined" ) return nil end else return nil end end instances[name] end end diff --git a/lib/puppetx/puppetlabs/syntax_checkers/json.rb b/lib/puppetx/puppetlabs/syntax_checkers/json.rb index 6af6c59c6..c12e0e52e 100644 --- a/lib/puppetx/puppetlabs/syntax_checkers/json.rb +++ b/lib/puppetx/puppetlabs/syntax_checkers/json.rb @@ -1,40 +1,37 @@ # A syntax checker for JSON. # @api public require 'puppetx/puppet/syntax_checker' class Puppetx::Puppetlabs::SyntaxCheckers::Json < Puppetx::Puppet::SyntaxChecker # Checks the text for JSON syntax issues and reports them to the given acceptor. # This implementation is abstract, it raises {NotImplementedError} since a subclass should have implemented the # method. # + # Error messages from the checker are capped at 100 chars from the source text. + # # @param text [String] The text to check # @param syntax [String] The syntax identifier in mime style (e.g. 'json', 'json-patch+json', 'xml', 'myapp+xml' - # @option location_info [String] :file The filename where the string originates - # @option location_info [Integer] :line The line number identifying the location where the string is being used/checked - # @option location_info [Integer] :position The position on the line identifying the location where the string is being used/checked - # @return [Boolean] Whether the checked string had issues (warnings and/or errors) or not. + # @param acceptor [#accept] A Diagnostic acceptor + # @param source_pos [Puppet::Pops::Adapters::SourcePosAdapter] A source pos adapter with location information # @api public # def check(text, syntax, acceptor, source_pos) raise ArgumentError.new("Json syntax checker: the text to check must be a String.") unless text.is_a?(String) raise ArgumentError.new("Json syntax checker: the syntax identifier must be a String, e.g. json, data+json") unless syntax.is_a?(String) raise ArgumentError.new("Json syntax checker: invalid Acceptor, got: '#{acceptor.class.name}'.") unless acceptor.is_a?(Puppet::Pops::Validation::Acceptor) #raise ArgumentError.new("Json syntax checker: location_info must be a Hash") unless location_info.is_a?(Hash) begin JSON.parse(text) rescue => e # Cap the message to 100 chars and replace newlines msg = "JSON syntax checker: Cannot parse invalid JSON string. \"#{e.message().slice(0,100).gsub(/\r?\n/, "\\n")}\"" # TODO: improve the pops API to allow simpler diagnostic creation while still maintaining capabilities # and the issue code. (In this case especially, where there is only a single error message being issued). # issue = Puppet::Pops::Issues::issue(:ILLEGAL_JSON) { msg } -# source_pos = Puppet::Pops::Adapters::SourcePosAdapter.new() -# source_pos.line = location_info[:line] -# source_pos.pos = location_info[:pos] acceptor.accept(Puppet::Pops::Validation::Diagnostic.new(:error, issue, source_pos.locator.file, source_pos, {})) end end end diff --git a/spec/unit/pops/evaluator/evaluating_parser_spec.rb b/spec/unit/pops/evaluator/evaluating_parser_spec.rb index 127427dc8..4667de4bb 100644 --- a/spec/unit/pops/evaluator/evaluating_parser_spec.rb +++ b/spec/unit/pops/evaluator/evaluating_parser_spec.rb @@ -1,977 +1,1045 @@ -#! /usr/bin/env ruby require 'spec_helper' require 'puppet/pops' require 'puppet/pops/evaluator/evaluator_impl' require 'puppet_spec/pops' require 'puppet_spec/scope' require 'puppet/parser/e4_parser_adapter' # relative to this spec file (./) does not work as this file is loaded by rspec #require File.join(File.dirname(__FILE__), '/evaluator_rspec_helper') describe 'Puppet::Pops::Evaluator::EvaluatorImpl' do include PuppetSpec::Pops include PuppetSpec::Scope before(:each) do Puppet[:strict_variables] = true # These must be set since the is 3x logic that triggers on these even if the tests are explicit # about selection of parser and evaluator # Puppet[:parser] = 'future' Puppet[:evaluator] = 'future' + # Puppetx cannot be loaded until the correct parser has been set (injector is turned off otherwise) + require 'puppetx' end - let(:parser) { Puppet::Pops::Parser::EvaluatingParser::Transitional.new } + let(:parser) { Puppet::Pops::Parser::EvaluatingParser::Transitional.new } let(:node) { 'node.example.com' } let(:scope) { s = create_test_scope_for_node(node); s } types = Puppet::Pops::Types::TypeFactory context "When evaluator evaluates literals" do { "1" => 1, "010" => 8, "0x10" => 16, "3.14" => 3.14, "0.314e1" => 3.14, "31.4e-1" => 3.14, "'1'" => '1', "'banana'" => 'banana', '"banana"' => 'banana', "banana" => 'banana', "banana::split" => 'banana::split', "false" => false, "true" => true, "Array" => types.array_of_data(), "/.*/" => /.*/ }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end end context "When the evaluator evaluates Lists and Hashes" do { "[]" => [], "[1,2,3]" => [1,2,3], "[1,[2.0, 2.1, [2.2]],[3.0, 3.1]]" => [1,[2.0, 2.1, [2.2]],[3.0, 3.1]], "[2 + 2]" => [4], "[1,2,3] == [1,2,3]" => true, "[1,2,3] != [2,3,4]" => true, "[1,2,3] == [2,2,3]" => false, "[1,2,3] != [1,2,3]" => false, "[1,2,3][2]" => 3, "[1,2,3] + [4,5]" => [1,2,3,4,5], "[1,2,3] + [[4,5]]" => [1,2,3,[4,5]], "[1,2,3] + 4" => [1,2,3,4], "[1,2,3] << [4,5]" => [1,2,3,[4,5]], "[1,2,3] << {'a' => 1, 'b'=>2}" => [1,2,3,{'a' => 1, 'b'=>2}], "[1,2,3] << 4" => [1,2,3,4], "[1,2,3,4] - [2,3]" => [1,4], "[1,2,3,4] - [2,5]" => [1,3,4], "[1,2,3,4] - 2" => [1,3,4], "[1,2,3,[2],4] - 2" => [1,3,[2],4], "[1,2,3,[2,3],4] - [[2,3]]" => [1,2,3,4], "[1,2,3,3,2,4,2,3] - [2,3]" => [1,4], "[1,2,3,['a',1],['b',2]] - {'a' => 1, 'b'=>2}" => [1,2,3], "[1,2,3,{'a'=>1,'b'=>2}] - [{'a' => 1, 'b'=>2}]" => [1,2,3], }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "[1,2,3] + {'a' => 1, 'b'=>2}" => [1,2,3,['a',1],['b',2]], }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do # This test must be done with match_array since the order of the hash # is undefined and Ruby 1.8.7 and 1.9.3 produce different results. expect(parser.evaluate_string(scope, source, __FILE__)).to match_array(result) end end { "[1,2,3][a]" => :error, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end { "{}" => {}, "{'a'=>1,'b'=>2}" => {'a'=>1,'b'=>2}, "{'a'=>1,'b'=>{'x'=>2.1,'y'=>2.2}}" => {'a'=>1,'b'=>{'x'=>2.1,'y'=>2.2}}, "{'a'=> 2 + 2}" => {'a'=> 4}, "{'a'=> 1, 'b'=>2} == {'a'=> 1, 'b'=>2}" => true, "{'a'=> 1, 'b'=>2} != {'x'=> 1, 'b'=>2}" => true, "{'a'=> 1, 'b'=>2} == {'a'=> 2, 'b'=>3}" => false, "{'a'=> 1, 'b'=>2} != {'a'=> 1, 'b'=>2}" => false, "{a => 1, b => 2}[b]" => 2, "{2+2 => sum, b => 2}[4]" => 'sum', "{'a'=>1, 'b'=>2} + {'c'=>3}" => {'a'=>1,'b'=>2,'c'=>3}, "{'a'=>1, 'b'=>2} + {'b'=>3}" => {'a'=>1,'b'=>3}, "{'a'=>1, 'b'=>2} + ['c', 3, 'b', 3]" => {'a'=>1,'b'=>3, 'c'=>3}, "{'a'=>1, 'b'=>2} + [['c', 3], ['b', 3]]" => {'a'=>1,'b'=>3, 'c'=>3}, "{'a'=>1, 'b'=>2} - {'b' => 3}" => {'a'=>1}, "{'a'=>1, 'b'=>2, 'c'=>3} - ['b', 'c']" => {'a'=>1}, "{'a'=>1, 'b'=>2, 'c'=>3} - 'c'" => {'a'=>1, 'b'=>2}, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "{'a' => 1, 'b'=>2} << 1" => :error, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end end context "When the evaluator perform comparisons" do { "'a' == 'a'" => true, "'a' == 'b'" => false, "'a' != 'a'" => false, "'a' != 'b'" => true, "'a' < 'b' " => true, "'a' < 'a' " => false, "'b' < 'a' " => false, "'a' <= 'b'" => true, "'a' <= 'a'" => true, "'b' <= 'a'" => false, "'a' > 'b' " => false, "'a' > 'a' " => false, "'b' > 'a' " => true, "'a' >= 'b'" => false, "'a' >= 'a'" => true, "'b' >= 'a'" => true, "'a' == 'A'" => true, "'a' != 'A'" => false, "'a' > 'A'" => false, "'a' >= 'A'" => true, "'A' < 'a'" => false, "'A' <= 'a'" => true, "1 == 1" => true, "1 == 2" => false, "1 != 1" => false, "1 != 2" => true, "1 < 2 " => true, "1 < 1 " => false, "2 < 1 " => false, "1 <= 2" => true, "1 <= 1" => true, "2 <= 1" => false, "1 > 2 " => false, "1 > 1 " => false, "2 > 1 " => true, "1 >= 2" => false, "1 >= 1" => true, "2 >= 1" => true, "1 == 1.0 " => true, "1 < 1.1 " => true, "'1' < 1.1" => true, "1.0 == 1 " => true, "1.0 < 2 " => true, "'1.0' < 1.1" => true, "'1.0' < 'a'" => true, "'1.0' < '' " => true, "'1.0' < ' '" => true, "'a' > '1.0'" => true, "/.*/ == /.*/ " => true, "/.*/ != /a.*/" => true, "true == true " => true, "false == false" => true, "true == false" => false, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "'a' =~ /.*/" => true, "'a' =~ '.*'" => true, "/.*/ != /a.*/" => true, "'a' !~ /b.*/" => true, "'a' !~ 'b.*'" => true, '$x = a; a =~ "$x.*"' => true, "a =~ Pattern['a.*']" => true, "a =~ Regexp['a.*']" => true, "$x = /a.*/ a =~ $x" => true, "$x = Pattern['a.*'] a =~ $x" => true, "1 =~ Integer" => true, "1 !~ Integer" => false, "[1,2,3] =~ Array[Integer[1,10]]" => true, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "666 =~ /6/" => :error, "[a] =~ /a/" => :error, "{a=>1} =~ /a/" => :error, "/a/ =~ /a/" => :error, "Array =~ /A/" => :error, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end { "1 in [1,2,3]" => true, "4 in [1,2,3]" => false, "a in {x=>1, a=>2}" => true, "z in {x=>1, a=>2}" => false, "ana in bananas" => true, "xxx in bananas" => false, "/ana/ in bananas" => true, "/xxx/ in bananas" => false, "ANA in bananas" => false, # ANA is a type, not a String "'ANA' in bananas" => true, "ana in 'BANANAS'" => true, "/ana/ in 'BANANAS'" => false, "/ANA/ in 'BANANAS'" => true, "xxx in 'BANANAS'" => false, "[2,3] in [1,[2,3],4]" => true, "[2,4] in [1,[2,3],4]" => false, "[a,b] in ['A',['A','B'],'C']" => true, "[x,y] in ['A',['A','B'],'C']" => false, "a in {a=>1}" => true, "x in {a=>1}" => false, "'A' in {a=>1}" => true, "'X' in {a=>1}" => false, "a in {'A'=>1}" => true, "x in {'A'=>1}" => false, "/xxx/ in {'aaaxxxbbb'=>1}" => true, "/yyy/ in {'aaaxxxbbb'=>1}" => false, "15 in [1, 0xf]" => true, "15 in [1, '0xf']" => true, "'15' in [1, 0xf]" => true, "15 in [1, 115]" => false, "1 in [11, '111']" => false, "'1' in [11, '111']" => false, "Array[Integer] in [2, 3]" => false, "Array[Integer] in [2, [3, 4]]" => true, "Array[Integer] in [2, [a, 4]]" => false, "Integer in { 2 =>'a'}" => true, "Integer[5,10] in [1,5,3]" => true, "Integer[5,10] in [1,2,3]" => false, "Integer in {'a'=>'a'}" => false, "Integer in {'a'=>1}" => false, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { 'Object' => ['Data', 'Scalar', 'Numeric', 'Integer', 'Float', 'Boolean', 'String', 'Pattern', 'Collection', 'Array', 'Hash', 'CatalogEntry', 'Resource', 'Class', 'Undef', 'File', 'NotYetKnownResourceType'], # Note, Data > Collection is false (so not included) 'Data' => ['Scalar', 'Numeric', 'Integer', 'Float', 'Boolean', 'String', 'Pattern', 'Array', 'Hash',], 'Scalar' => ['Numeric', 'Integer', 'Float', 'Boolean', 'String', 'Pattern'], 'Numeric' => ['Integer', 'Float'], 'CatalogEntry' => ['Class', 'Resource', 'File', 'NotYetKnownResourceType'], 'Integer[1,10]' => ['Integer[2,3]'], }.each do |general, specials| specials.each do |special | it "should compute that #{general} > #{special}" do parser.evaluate_string(scope, "#{general} > #{special}", __FILE__).should == true end it "should compute that #{special} < #{general}" do parser.evaluate_string(scope, "#{special} < #{general}", __FILE__).should == true end it "should compute that #{general} != #{special}" do parser.evaluate_string(scope, "#{special} != #{general}", __FILE__).should == true end end end { 'Integer[1,10] > Integer[2,3]' => true, 'Integer[1,10] == Integer[2,3]' => false, 'Integer[1,10] > Integer[0,5]' => false, 'Integer[1,10] > Integer[1,10]' => false, 'Integer[1,10] >= Integer[1,10]' => true, 'Integer[1,10] == Integer[1,10]' => true, }.each do |source, result| it "should parse and evaluate the integer range comparison expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end end context "When the evaluator performs arithmetic" do context "on Integers" do { "2+2" => 4, "2 + 2" => 4, "7 - 3" => 4, "6 * 3" => 18, "6 / 3" => 2, "6 % 3" => 0, "10 % 3" => 1, "-(6/3)" => -2, "-6/3 " => -2, "8 >> 1" => 4, "8 << 1" => 16, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end context "on Floats" do { "2.2 + 2.2" => 4.4, "7.7 - 3.3" => 4.4, "6.1 * 3.1" => 18.91, "6.6 / 3.3" => 2.0, "-(6.0/3.0)" => -2.0, "-6.0/3.0 " => -2.0, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "3.14 << 2" => :error, "3.14 >> 2" => :error, "6.6 % 3.3" => 0.0, "10.0 % 3.0" => 1.0, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end end context "on strings requiring boxing to Numeric" do { "'2' + '2'" => 4, "'2.2' + '2.2'" => 4.4, "'0xF7' + '010'" => 0xFF, "'0xF7' + '0x8'" => 0xFF, "'0367' + '010'" => 0xFF, "'012.3' + '010'" => 20.3, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "'0888' + '010'" => :error, "'0xWTF' + '010'" => :error, "'0x12.3' + '010'" => :error, "'0x12.3' + '010'" => :error, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end end end end # arithmetic context "When the evaluator evaluates assignment" do { "$a = 5" => 5, "$a = 5; $a" => 5, "$a = 5; $b = 6; $a" => 5, "$a = $b = 5; $a == $b" => true, "$a = [1,2,3]; [x].map |$x| { $a += x; $a }" => [[1,2,3,'x']], "$a = [a,x,c]; [x].map |$x| { $a -= x; $a }" => [['a','c']], }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "[a,b,c] = [1,2,3]; $a == 1 and $b == 2 and $c == 3" => :error, "[a,b,c] = {b=>2,c=>3,a=>1}; $a == 1 and $b == 2 and $c == 3" => :error, "$a = [1,2,3]; [x].collect |$x| { [a] += x; $a }" => :error, "$a = [a,x,c]; [x].collect |$x| { [a] -= x; $a }" => :error, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do expect { parser.evaluate_string(scope, source, __FILE__)}.to raise_error(Puppet::ParseError) end end end context "When the evaluator evaluates conditionals" do { "if true {5}" => 5, "if false {5}" => nil, "if false {2} else {5}" => 5, "if false {2} elsif true {5}" => 5, "if false {2} elsif false {5}" => nil, "unless false {5}" => 5, "unless true {5}" => nil, "unless true {2} else {5}" => 5, "$a = if true {5} $a" => 5, "$a = if false {5} $a" => nil, "$a = if false {2} else {5} $a" => 5, "$a = if false {2} elsif true {5} $a" => 5, "$a = if false {2} elsif false {5} $a" => nil, "$a = unless false {5} $a" => 5, "$a = unless true {5} $a" => nil, "$a = unless true {2} else {5} $a" => 5, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "case 1 { 1 : { yes } }" => 'yes', "case 2 { 1,2,3 : { yes} }" => 'yes', "case 2 { 1,3 : { no } 2: { yes} }" => 'yes', "case 2 { 1,3 : { no } 5: { no } default: { yes }}" => 'yes', "case 2 { 1,3 : { no } 5: { no } }" => nil, "case 'banana' { 1,3 : { no } /.*ana.*/: { yes } }" => 'yes', "case 'banana' { /.*(ana).*/: { $1 } }" => 'ana', "case [1] { Array : { yes } }" => 'yes', "case [1] { Array[String] : { no } Array[Integer]: { yes } }" => 'yes', "case 1 { Integer : { yes } Type[Integer] : { no } }" => 'yes', "case Integer { Integer : { no } Type[Integer] : { yes } }" => 'yes', }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "2 ? { 1 => no, 2 => yes}" => 'yes', "3 ? { 1 => no, 2 => no}" => nil, "3 ? { 1 => no, 2 => no, default => yes }" => 'yes', "3 ? { 1 => no, default => yes, 3 => no }" => 'yes', "'banana' ? { /.*(ana).*/ => $1 }" => 'ana', "[2] ? { Array[String] => yes, Array => yes}" => 'yes', }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end end context "When evaluator performs [] operations" do { "[1,2,3][0]" => 1, "[1,2,3][2]" => 3, "[1,2,3][3]" => nil, "[1,2,3][-1]" => 3, "[1,2,3][-2]" => 2, "[1,2,3][-4]" => nil, "[1,2,3,4][0,2]" => [1,2], "[1,2,3,4][1,3]" => [2,3,4], "[1,2,3,4][-2,2]" => [3,4], "[1,2,3,4][-3,2]" => [2,3], "[1,2,3,4][3,5]" => [4], "[1,2,3,4][5,2]" => [], "[1,2,3,4][0,-1]" => [1,2,3,4], "[1,2,3,4][0,-2]" => [1,2,3], "[1,2,3,4][0,-4]" => [1], "[1,2,3,4][0,-5]" => [], "[1,2,3,4][-5,2]" => [1], "[1,2,3,4][-5,-3]" => [1,2], "[1,2,3,4][-6,-3]" => [1,2], "[1,2,3,4][2,-3]" => [], }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "{a=>1, b=>2, c=>3}[a]" => 1, "{a=>1, b=>2, c=>3}[c]" => 3, "{a=>1, b=>2, c=>3}[x]" => nil, "{a=>1, b=>2, c=>3}[c,b]" => [3,2], "{a=>1, b=>2, c=>3}[a,b,c]" => [1,2,3], "{a=>{b=>{c=>'it works'}}}[a][b][c]" => 'it works', "$a = {undef => 10} $a[free_lunch]" => nil, "$a = {undef => 10} $a[undef]" => 10, "$a = {undef => 10} $a[$a[free_lunch]]" => 10, "$a = {} $a[free_lunch] == undef" => true, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "'abc'[0]" => 'a', "'abc'[2]" => 'c', "'abc'[-1]" => 'c', "'abc'[-2]" => 'b', "'abc'[-3]" => 'a', "'abc'[-4]" => '', "'abc'[3]" => '', "abc[0]" => 'a', "abc[2]" => 'c', "abc[-1]" => 'c', "abc[-2]" => 'b', "abc[-3]" => 'a', "abc[-4]" => '', "abc[3]" => '', "'abcd'[0,2]" => 'ab', "'abcd'[1,3]" => 'bcd', "'abcd'[-2,2]" => 'cd', "'abcd'[-3,2]" => 'bc', "'abcd'[3,5]" => 'd', "'abcd'[5,2]" => '', "'abcd'[0,-1]" => 'abcd', "'abcd'[0,-2]" => 'abc', "'abcd'[0,-4]" => 'a', "'abcd'[0,-5]" => '', "'abcd'[-5,2]" => 'a', "'abcd'[-5,-3]" => 'ab', "'abcd'[-6,-3]" => 'ab', "'abcd'[2,-3]" => '', }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end # Type operations (full set tested by tests covering type calculator) { "Array[Integer]" => types.array_of(types.integer), "Array[Integer,1]" => types.constrain_size(types.array_of(types.integer),1, :default), "Array[Integer,1,2]" => types.constrain_size(types.array_of(types.integer),1, 2), "Array[Integer,Integer[1,2]]" => types.constrain_size(types.array_of(types.integer),1, 2), "Array[Integer,Integer[1]]" => types.constrain_size(types.array_of(types.integer),1, :default), "Hash[Integer,Integer]" => types.hash_of(types.integer, types.integer), "Hash[Integer,Integer,1]" => types.constrain_size(types.hash_of(types.integer, types.integer),1, :default), "Hash[Integer,Integer,1,2]" => types.constrain_size(types.hash_of(types.integer, types.integer),1, 2), "Hash[Integer,Integer,Integer[1,2]]" => types.constrain_size(types.hash_of(types.integer, types.integer),1, 2), "Hash[Integer,Integer,Integer[1]]" => types.constrain_size(types.hash_of(types.integer, types.integer),1, :default), "Resource[File]" => types.resource('File'), "Resource['File']" => types.resource(types.resource('File')), "File[foo]" => types.resource('file', 'foo'), "File[foo, bar]" => [types.resource('file', 'foo'), types.resource('file', 'bar')], "Pattern[a, /b/, Pattern[c], Regexp[d]]" => types.pattern('a', 'b', 'c', 'd'), "String[1,2]" => types.constrain_size(types.string,1, 2), "String[Integer[1,2]]" => types.constrain_size(types.string,1, 2), "String[Integer[1]]" => types.constrain_size(types.string,1, :default), }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end # LHS where [] not supported, and missing key(s) { "Array[]" => :error, "'abc'[]" => :error, "Resource[]" => :error, "File[]" => :error, "String[]" => :error, "1[]" => :error, "3.14[]" => :error, "/.*/[]" => :error, "$a=[1] $a[]" => :error, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do expect { parser.evaluate_string(scope, source, __FILE__)}.to raise_error(/Syntax error/) end end # Errors when wrong number/type of keys are used { "Array[0]" => 'Array-Type[] arguments must be types. Got Fixnum', "Hash[0]" => 'Hash-Type[] arguments must be types. Got Fixnum', "Hash[Integer, 0]" => 'Hash-Type[] arguments must be types. Got Fixnum', "Array[Integer,1,2,3]" => 'Array-Type[] accepts 1 to 3 arguments. Got 4', "Array[Integer,String]" => "A Type's size constraint arguments must be a single Integer type, or 1-2 integers (or default). Got a String-Type", "Hash[Integer,String, 1,2,3]" => 'Hash-Type[] accepts 1 to 4 arguments. Got 5', "'abc'[x]" => "The value 'x' cannot be converted to Numeric", "'abc'[1.0]" => "A String[] cannot use Float where Integer is expected", "'abc'[1,2,3]" => "String supports [] with one or two arguments. Got 3", "Resource[0]" => 'First argument to Resource[] must be a resource type or a String. Got Fixnum', "Resource[a, 0]" => 'Error creating type specialization of a Resource-Type, Cannot use Fixnum where String is expected', "File[0]" => 'Error creating type specialization of a File-Type, Cannot use Fixnum where String is expected', "String[a]" => "A Type's size constraint arguments must be a single Integer type, or 1-2 integers (or default). Got a String", "Pattern[0]" => 'Error creating type specialization of a Pattern-Type, Cannot use Fixnum where String or Regexp or Pattern-Type or Regexp-Type is expected', "Regexp[0]" => 'Error creating type specialization of a Regexp-Type, Cannot use Fixnum where String or Regexp is expected', "Regexp[a,b]" => 'A Regexp-Type[] accepts 1 argument. Got 2', "true[0]" => "Operator '[]' is not applicable to a Boolean", "1[0]" => "Operator '[]' is not applicable to an Integer", "3.14[0]" => "Operator '[]' is not applicable to a Float", "/.*/[0]" => "Operator '[]' is not applicable to a Regexp", "[1][a]" => "The value 'a' cannot be converted to Numeric", "[1][0.0]" => "An Array[] cannot use Float where Integer is expected", "[1]['0.0']" => "An Array[] cannot use Float where Integer is expected", "[1,2][1, 0.0]" => "An Array[] cannot use Float where Integer is expected", "[1,2][1.0, -1]" => "An Array[] cannot use Float where Integer is expected", "[1,2][1, -1.0]" => "An Array[] cannot use Float where Integer is expected", }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do expect { parser.evaluate_string(scope, source, __FILE__)}.to raise_error(Regexp.new(Regexp.quote(result))) end end context "on catalog types" do it "[n] gets resource parameter [n]" do source = "notify { 'hello': message=>'yo'} Notify[hello][message]" parser.evaluate_string(scope, source, __FILE__).should == 'yo' end it "[n] gets class parameter [n]" do source = "class wonka($produces='chocolate'){ } include wonka Class[wonka][produces]" # This is more complicated since it needs to run like 3.x and do an import_ast adapted_parser = Puppet::Parser::E4ParserAdapter.new adapted_parser.file = __FILE__ ast = adapted_parser.parse(source) scope.known_resource_types.import_ast(ast, '') ast.code.safeevaluate(scope).should == 'chocolate' end # Resource default and override expressions and resource parameter access with [] { "notify { id: message=>explicit} Notify[id][message]" => "explicit", "Notify { message=>by_default} notify {foo:} Notify[foo][message]" => "by_default", "notify {foo:} Notify[foo]{message =>by_override} Notify[foo][message]" => "by_override", }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end # Resource default and override expressions and resource parameter access error conditions { "notify { xid: message=>explicit} Notify[id][message]" => /Resource not found/, "notify { id: message=>explicit} Notify[id][mustard]" => /does not have a parameter called 'mustard'/, }.each do |source, result| it "should parse '#{source}' and raise error matching #{result}" do expect { parser.evaluate_string(scope, source, __FILE__)}.to raise_error(result) end end context 'with errors' do { "Class['fail-whale']" => /Illegal name/, "Class[0]" => /An Integer cannot be used where a String is expected/, "Class[/.*/]" => /A Regexp cannot be used where a String is expected/, "Class[4.1415]" => /A Float cannot be used where a String is expected/, "Class[Integer]" => /An Integer-Type cannot be used where a String is expected/, "Class[File['tmp']]" => /A File\['tmp'\] Resource-Reference cannot be used where a String is expected/, }.each do | source, error_pattern| it "an error is flagged for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__)}.to raise_error(error_pattern) end end end end # end [] operations end context "When the evaluator performs boolean operations" do { "true and true" => true, "false and true" => false, "true and false" => false, "false and false" => false, "true or true" => true, "false or true" => true, "true or false" => true, "false or false" => false, "! true" => false, "!! true" => true, "!! false" => false, "! 'x'" => false, "! ''" => true, "! undef" => true, "! [a]" => false, "! []" => false, "! {a=>1}" => false, "! {}" => false, "true and false and '0xwtf' + 1" => false, "false or true or '0xwtf' + 1" => true, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "false || false || '0xwtf' + 1" => :error, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end end context "When evaluator performs operations on literal undef" do it "computes non existing hash lookup as undef" do parser.evaluate_string(scope, "{a => 1}[b] == undef", __FILE__).should == true parser.evaluate_string(scope, "undef == {a => 1}[b]", __FILE__).should == true end end context "When evaluator performs calls" do let(:populate) do parser.evaluate_string(scope, "$a = 10 $b = [1,2,3]") end { 'sprintf( "x%iy", $a )' => "x10y", '"x%iy".sprintf( $a )' => "x10y", '$b.reduce |$memo,$x| { $memo + $x }' => 6, 'reduce($b) |$memo,$x| { $memo + $x }' => 6, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do populate parser.evaluate_string(scope, source, __FILE__).should == result end end { '"value is ${a*2} yo"' => :error, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end end context "When evaluator performs string interpolation" do let(:populate) do parser.evaluate_string(scope, "$a = 10 $b = [1,2,3]") end { '"value is $a yo"' => "value is 10 yo", '"value is \$a yo"' => "value is $a yo", '"value is ${a} yo"' => "value is 10 yo", '"value is \${a} yo"' => "value is ${a} yo", '"value is ${$a} yo"' => "value is 10 yo", '"value is ${$a*2} yo"' => "value is 20 yo", '"value is ${sprintf("x%iy",$a)} yo"' => "value is x10y yo", '"value is ${"x%iy".sprintf($a)} yo"' => "value is x10y yo", '"value is ${[1,2,3]} yo"' => "value is [1, 2, 3] yo", '"value is ${/.*/} yo"' => "value is /.*/ yo", '$x = undef "value is $x yo"' => "value is yo", '$x = default "value is $x yo"' => "value is default yo", '$x = Array[Integer] "value is $x yo"' => "value is Array[Integer] yo", '"value is ${Array[Integer]} yo"' => "value is Array[Integer] yo", }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do populate parser.evaluate_string(scope, source, __FILE__).should == result end end it "should parse and evaluate an interpolation of a hash" do source = '"value is ${{a=>1,b=>2}} yo"' # This test requires testing against two options because a hash to string # produces a result that is unordered hashstr = {'a' => 1, 'b' => 2}.to_s alt_results = ["value is {a => 1, b => 2} yo", "value is {b => 2, a => 1} yo" ] populate parse_result = parser.evaluate_string(scope, source, __FILE__) alt_results.include?(parse_result).should == true end { '"value is ${a*2} yo"' => :error, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end end context "When evaluating variables" do context "that are non existing an error is raised for" do it "unqualified variable" do expect { parser.evaluate_string(scope, "$quantum_gravity", __FILE__) }.to raise_error(/Unknown variable/) end it "qualified variable" do expect { parser.evaluate_string(scope, "$quantum_gravity::graviton", __FILE__) }.to raise_error(/Unknown variable/) end end it "a lex error should be raised for '$foo::::bar'" do expect { parser.evaluate_string(scope, "$foo::::bar") }.to raise_error(Puppet::LexError, /Illegal fully qualified name at line 1:7/) end { '$a = $0' => nil, '$a = $1' => nil, }.each do |source, value| it "it is ok to reference numeric unassigned variables '#{source}'" do parser.evaluate_string(scope, source, __FILE__).should == value end end { '$00 = 0' => /must be a decimal value/, '$0xf = 0' => /must be a decimal value/, '$0777 = 0' => /must be a decimal value/, '$123a = 0' => /must be a decimal value/, }.each do |source, error_pattern| it "should raise an error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(error_pattern) end end context "an initial underscore in the last segment of a var name is allowed" do { '$_a = 1' => 1, '$__a = 1' => 1, }.each do |source, value| it "as in this example '#{source}'" do parser.evaluate_string(scope, source, __FILE__).should == value end end end end context "When evaluating relationships" do it 'should form a relation with File[a] -> File[b]' do source = "File[a] -> File[b]" parser.evaluate_string(scope, source, __FILE__) scope.compiler.should have_relationship(['File', 'a', '->', 'File', 'b']) end it 'should form a relation with resource -> resource' do source = "notify{a:} -> notify{b:}" parser.evaluate_string(scope, source, __FILE__) scope.compiler.should have_relationship(['Notify', 'a', '->', 'Notify', 'b']) end it 'should form a relation with [File[a], File[b]] -> [File[x], File[y]]' do source = "[File[a], File[b]] -> [File[x], File[y]]" parser.evaluate_string(scope, source, __FILE__) scope.compiler.should have_relationship(['File', 'a', '->', 'File', 'x']) scope.compiler.should have_relationship(['File', 'b', '->', 'File', 'x']) scope.compiler.should have_relationship(['File', 'a', '->', 'File', 'y']) scope.compiler.should have_relationship(['File', 'b', '->', 'File', 'y']) end it 'should tolerate (eliminate) duplicates in operands' do source = "[File[a], File[a]] -> File[x]" parser.evaluate_string(scope, source, __FILE__) scope.compiler.should have_relationship(['File', 'a', '->', 'File', 'x']) scope.compiler.relationships.size.should == 1 end it 'should form a relation with <-' do source = "File[a] <- File[b]" parser.evaluate_string(scope, source, __FILE__) scope.compiler.should have_relationship(['File', 'b', '->', 'File', 'a']) end it 'should form a relation with <-' do source = "File[a] <~ File[b]" parser.evaluate_string(scope, source, __FILE__) scope.compiler.should have_relationship(['File', 'b', '~>', 'File', 'a']) end end + context "When evaluating heredoc" do + it "evaluates plain heredoc" do + src = "@(END)\nThis is\nheredoc text\nEND\n" + parser.evaluate_string(scope, src).should == "This is\nheredoc text\n" + end + + it "parses heredoc with margin" do + src = [ + "@(END)", + " This is", + " heredoc text", + " | END", + "" + ].join("\n") + parser.evaluate_string(scope, src).should == "This is\nheredoc text\n" + end + + it "parses heredoc with margin and right newline trim" do + src = [ + "@(END)", + " This is", + " heredoc text", + " |- END", + "" + ].join("\n") + parser.evaluate_string(scope, src).should == "This is\nheredoc text" + end + + it "parses escape specification" do + src = <<-CODE + @(END/t) + Tex\\tt\\n + |- END + CODE + parser.evaluate_string(scope, src).should == "Tex\tt\\n" + end + + it "parses syntax checked specification" do + src = <<-CODE + @(END:json) + ["foo", "bar"] + |- END + CODE + parser.evaluate_string(scope, src).should == '["foo", "bar"]' + end + + it "parses syntax checked specification with error and reports it" do + src = <<-CODE + @(END:json) + ['foo', "bar"] + |- END + CODE + expect { parser.evaluate_string(scope, src)}.to raise_error(/Cannot parse invalid JSON string/) + end + + it "parses interpolated heredoc epression" do + src = <<-CODE + $name = 'Fjodor' + @("END") + Hello $name + |- END + CODE + parser.evaluate_string(scope, src).should == "Hello Fjodor" + end + + end + context "Detailed Error messages are reported" do it 'for illegal type references' do source = '1+1 { "title": }' # Error references position 5 at the opening '{' # Set file to nil to make it easier to match with line number (no file name in output) expect { parser.parse_string(source, nil) }.to raise_error(/Expression is not valid as a resource.*line 1:5/) end it 'for non r-value producing <| |>' do expect { parser.parse_string("$a = File <| |>", nil) }.to raise_error(/A Virtual Query does not produce a value at line 1:6/) end it 'for non r-value producing <<| |>>' do expect { parser.parse_string("$a = File <<| |>>", nil) }.to raise_error(/An Exported Query does not produce a value at line 1:6/) end it 'for non r-value producing define' do Puppet.expects(:err).with("Invalid use of expression. A 'define' expression does not produce a value at line 1:6") Puppet.expects(:err).with("Classes, definitions, and nodes may only appear at toplevel or inside other classes at line 1:6") expect { parser.parse_string("$a = define foo { }", nil) }.to raise_error(/2 errors/) end it 'for non r-value producing class' do Puppet.expects(:err).with("Invalid use of expression. A Host Class Definition does not produce a value at line 1:6") Puppet.expects(:err).with("Classes, definitions, and nodes may only appear at toplevel or inside other classes at line 1:6") expect { parser.parse_string("$a = class foo { }", nil) }.to raise_error(/2 errors/) end it 'for unclosed quote with indication of start position of string' do source = <<-SOURCE.gsub(/^ {6}/,'') $a = "xx yyy SOURCE # first char after opening " reported as being in error. expect { parser.parse_string(source) }.to raise_error(/Unclosed quote after '"' followed by 'xx\\nyy\.\.\.' at line 1:7/) end it 'for multiple errors with a summary exception' do Puppet.expects(:err).with("Invalid use of expression. A Node Definition does not produce a value at line 1:6") Puppet.expects(:err).with("Classes, definitions, and nodes may only appear at toplevel or inside other classes at line 1:6") expect { parser.parse_string("$a = node x { }",nil) }.to raise_error(/2 errors/) end it 'for a bad hostname' do expect { parser.parse_string("node 'macbook+owned+by+name' { }", nil) }.to raise_error(/The hostname 'macbook\+owned\+by\+name' contains illegal characters.*at line 1:6/) end it 'for a hostname with interpolation' do source = <<-SOURCE.gsub(/^ {6}/,'') $name = 'fred' node "macbook-owned-by$name" { } SOURCE expect { parser.parse_string(source, nil) }.to raise_error(/An interpolated expression is not allowed in a hostname of a node at line 2:23/) end end matcher :have_relationship do |expected| calc = Puppet::Pops::Types::TypeCalculator.new match do |compiler| op_name = {'->' => :relationship, '~>' => :subscription} compiler.relationships.any? do | relation | relation.source.type == expected[0] && relation.source.title == expected[1] && relation.type == op_name[expected[2]] && relation.target.type == expected[3] && relation.target.title == expected[4] end end failure_message_for_should do |actual| "Relationship #{expected[0]}[#{expected[1]}] #{expected[2]} #{expected[3]}[#{expected[4]}] but was unknown to compiler" end end end diff --git a/spec/unit/pops/parser/parse_heredoc_spec.rb b/spec/unit/pops/parser/parse_heredoc_spec.rb new file mode 100644 index 000000000..3616b9b2e --- /dev/null +++ b/spec/unit/pops/parser/parse_heredoc_spec.rb @@ -0,0 +1,73 @@ +require 'spec_helper' +require 'puppet/pops' + +# relative to this spec file (./) does not work as this file is loaded by rspec +require File.join(File.dirname(__FILE__), '/parser_rspec_helper') + +describe "egrammar parsing heredoc" do + include ParserRspecHelper + + it "parses plain heredoc" do + dump(parse("@(END)\nThis is\nheredoc text\nEND\n")).should == [ + "(@()", + " (sublocated 'This is\nheredoc text\n')", + ")" + ].join("\n") + end + + it "parses heredoc with margin" do + src = [ + "@(END)", + " This is", + " heredoc text", + " | END", + "" + ].join("\n") + dump(parse(src)).should == [ + "(@()", + " (sublocated 'This is\nheredoc text\n')", + ")" + ].join("\n") + end + + it "parses heredoc with margin and right newline trim" do + src = [ + "@(END)", + " This is", + " heredoc text", + " |- END", + "" + ].join("\n") + dump(parse(src)).should == [ + "(@()", + " (sublocated 'This is\nheredoc text')", + ")" + ].join("\n") + end + + it "parses syntax and escape specification" do + src = <<-CODE + @(END:syntax/t) + Tex\\tt\\n + |- END + CODE + dump(parse(src)).should == [ + "(@(syntax)", + " (sublocated 'Tex\tt\\n')", + ")" + ].join("\n") + end + + it "parses interpolated heredoc epression" do + src = <<-CODE + @("END") + Hello $name + |- END + CODE + dump(parse(src)).should == [ + "(@()", + " (sublocated (cat 'Hello ' (str $name) ''))", + ")" + ].join("\n") + end +end