diff --git a/benchmarks/evaluations/benchmarker.rb b/benchmarks/evaluations/benchmarker.rb index 3bf94d6dd..4ed397db7 100644 --- a/benchmarks/evaluations/benchmarker.rb +++ b/benchmarks/evaluations/benchmarker.rb @@ -1,140 +1,140 @@ require 'erb' require 'ostruct' require 'fileutils' require 'json' class Benchmarker include FileUtils def initialize(target, size) @target = target @size = size @micro_benchmarks = {} @parsecount = 100 @evalcount = 100 end def setup require 'puppet' require 'puppet/pops' config = File.join(@target, 'puppet.conf') Puppet.initialize_settings(['--config', config]) manifests = File.join('benchmarks', 'evaluations', 'manifests') Dir.foreach(manifests) do |f| if f =~ /^(.*)\.pp$/ @micro_benchmarks[$1] = File.read(File.join(manifests, f)) end end # Run / Evaluate the common puppet logic @env = Puppet.lookup(:environments).get('benchmarking') @node = Puppet::Node.new("testing", :environment => @env) - @parser = Puppet::Pops::Parser::EvaluatingParser::Transitional.new + @parser = Puppet::Pops::Parser::EvaluatingParser.new @compiler = Puppet::Parser::Compiler.new(@node) @scope = @compiler.topscope # Perform a portion of what a compile does (just enough to evaluate the site.pp logic) @compiler.catalog.environment_instance = @compiler.environment @compiler.send(:evaluate_main) # Then pretend we are running as part of a compilation Puppet.push_context(@compiler.context_overrides, "Benchmark masquerading as compiler configured context") end def run(args = {}) details = args[:detail] || 'all' measurements = [] @micro_benchmarks.each do |name, source| # skip if all but the wanted if a single benchmark is wanted next unless details == 'all' || match = details.match(/#{name}(?:[\._\s](parse|eval))?$/) # if name ends with .parse or .eval only do that part, else do both parts ending = match ? match[1] : nil # parse, eval or nil ending unless ending == 'eval' measurements << Benchmark.measure("#{name} parse") do 1..@parsecount.times { @parser.parse_string(source, name) } end end unless ending == 'parse' model = @parser.parse_string(source, name) measurements << Benchmark.measure("#{name} eval") do 1..@evalcount.times do begin # Run each in a local scope scope_memo = @scope.ephemeral_level @scope.new_ephemeral(true) @parser.evaluate(@scope, model) ensure # Toss the created local scope @scope.unset_ephemeral_var(scope_memo) end end end end end measurements end def generate environment = File.join(@target, 'environments', 'benchmarking') templates = File.join('benchmarks', 'evaluations') mkdir_p(File.join(environment, 'modules')) mkdir_p(File.join(environment, 'manifests')) render(File.join(templates, 'site.pp.erb'), File.join(environment, 'manifests', 'site.pp'),{}) render(File.join(templates, 'puppet.conf.erb'), File.join(@target, 'puppet.conf'), :location => @target) # Generate one module with a 3x function and a 4x function (namespaces) module_name = "module1" module_base = File.join(environment, 'modules', module_name) manifests = File.join(module_base, 'manifests') mkdir_p(manifests) functions_3x = File.join(module_base, 'lib', 'puppet', 'parser', 'functions') functions_4x = File.join(module_base, 'lib', 'puppet', 'functions') mkdir_p(functions_3x) mkdir_p(functions_4x) File.open(File.join(module_base, 'metadata.json'), 'w') do |f| JSON.dump({ "types" => [], "source" => "", "author" => "Evaluations Benchmark", "license" => "Apache 2.0", "version" => "1.0.0", "description" => "Evaluations Benchmark module 1", "summary" => "Module with supporting logic for evaluations benchmark", "dependencies" => [], }, f) end render(File.join(templates, 'module', 'init.pp.erb'), File.join(manifests, 'init.pp'), :name => module_name) render(File.join(templates, 'module', 'func3.rb.erb'), File.join(functions_3x, 'func3.rb'), :name => module_name) # namespaced function mkdir_p(File.join(functions_4x, module_name)) render(File.join(templates, 'module', 'module1_func4.rb.erb'), File.join(functions_4x, module_name, 'func4.rb'), :name => module_name) # non namespaced render(File.join(templates, 'module', 'func4.rb.erb'), File.join(functions_4x, 'func4.rb'), :name => module_name) end def render(erb_file, output_file, bindings) site = ERB.new(File.read(erb_file)) File.open(output_file, 'w') do |fh| fh.write(site.result(OpenStruct.new(bindings).instance_eval { binding })) end end end diff --git a/lib/puppet/defaults.rb b/lib/puppet/defaults.rb index 8be20a6cd..299b5a9c7 100644 --- a/lib/puppet/defaults.rb +++ b/lib/puppet/defaults.rb @@ -1,2037 +1,1993 @@ module Puppet def self.default_diffargs if (Facter.value(:kernel) == "AIX" && Facter.value(:kernelmajversion) == "5300") "" else "-u" end end ############################################################################################ # NOTE: For information about the available values for the ":type" property of settings, # see the docs for Settings.define_settings ############################################################################################ AS_DURATION = %q{This setting can be a time interval in seconds (30 or 30s), minutes (30m), hours (6h), days (2d), or years (5y).} STORECONFIGS_ONLY = %q{This setting is only used by the ActiveRecord storeconfigs and inventory backends, which are deprecated.} # This is defined first so that the facter implementation is replaced before other setting defaults are evaluated. define_settings(:main, :cfacter => { :default => false, :type => :boolean, :desc => 'Whether or not to use the native facter (cfacter) implementation instead of the Ruby one (facter). Defaults to false.', :hook => proc do |value| return unless value raise ArgumentError, 'facter has already evaluated facts.' if Facter.instance_variable_get(:@collection) require 'puppet/facts' raise ArgumentError, 'cfacter version 0.2.0 or later is not installed.' unless Puppet::Facts.replace_facter end } ) define_settings(:main, :confdir => { :default => nil, :type => :directory, :desc => "The main Puppet configuration directory. The default for this setting is calculated based on the user. If the process is running as root or the user that Puppet is supposed to run as, it defaults to a system directory, but if it's running as any other user, it defaults to being in the user's home directory.", }, :vardir => { :default => nil, :type => :directory, :owner => "service", :group => "service", :desc => "Where Puppet stores dynamic and growing data. The default for this setting is calculated specially, like `confdir`_.", }, ### NOTE: this setting is usually being set to a symbol value. We don't officially have a ### setting type for that yet, but we might want to consider creating one. :name => { :default => nil, :desc => "The name of the application, if we are running as one. The default is essentially $0 without the path or `.rb`.", } ) define_settings(:main, :logdir => { :default => nil, :type => :directory, :mode => 0750, :owner => "service", :group => "service", :desc => "The directory in which to store log files", }, :log_level => { :default => 'notice', :type => :enum, :values => ["debug","info","notice","warning","err","alert","emerg","crit"], :desc => "Default logging level for messages from Puppet. Allowed values are: * debug * info * notice * warning * err * alert * emerg * crit ", }, :disable_warnings => { :default => [], :type => :array, :desc => "A comma-separated list of warning types to suppress. If large numbers of warnings are making Puppet's logs too large or difficult to use, you can temporarily silence them with this setting. If you are preparing to upgrade Puppet to a new major version, you should re-enable all warnings for a while. Valid values for this setting are: * `deprecations` --- disables deprecation warnings.", :hook => proc do |value| values = munge(value) valid = %w[deprecations] invalid = values - (values & valid) if not invalid.empty? raise ArgumentError, "Cannot disable unrecognized warning types #{invalid.inspect}. Valid values are #{valid.inspect}." end end } ) define_settings(:main, :priority => { :default => nil, :type => :priority, :desc => "The scheduling priority of the process. Valid values are 'high', 'normal', 'low', or 'idle', which are mapped to platform-specific values. The priority can also be specified as an integer value and will be passed as is, e.g. -5. Puppet must be running as a privileged user in order to increase scheduling priority.", }, :trace => { :default => false, :type => :boolean, :desc => "Whether to print stack traces on some errors", }, :profile => { :default => false, :type => :boolean, :desc => "Whether to enable experimental performance profiling", }, :autoflush => { :default => true, :type => :boolean, :desc => "Whether log files should always flush to disk.", :hook => proc { |value| Log.autoflush = value } }, :syslogfacility => { :default => "daemon", :desc => "What syslog facility to use when logging to syslog. Syslog has a fixed list of valid facilities, and you must choose one of those; you cannot just make one up." }, :statedir => { :default => "$vardir/state", :type => :directory, :mode => 01755, :desc => "The directory where Puppet state is stored. Generally, this directory can be removed without causing harm (although it might result in spurious service restarts)." }, :rundir => { :default => nil, :type => :directory, :mode => 0755, :owner => "service", :group => "service", :desc => "Where Puppet PID files are kept." }, :genconfig => { :default => false, :type => :boolean, :desc => "When true, causes Puppet applications to print an example config file to stdout and exit. The example will include descriptions of each setting, and the current (or default) value of each setting, incorporating any settings overridden on the CLI (with the exception of `genconfig` itself). This setting only makes sense when specified on the command line as `--genconfig`.", }, :genmanifest => { :default => false, :type => :boolean, :desc => "Whether to just print a manifest to stdout and exit. Only makes sense when specified on the command line as `--genmanifest`. Takes into account arguments specified on the CLI.", }, :configprint => { :default => "", :desc => "Print the value of a specific configuration setting. If the name of a setting is provided for this, then the value is printed and puppet exits. Comma-separate multiple values. For a list of all values, specify 'all'.", }, :color => { :default => "ansi", :type => :string, :desc => "Whether to use colors when logging to the console. Valid values are `ansi` (equivalent to `true`), `html`, and `false`, which produces no color. Defaults to false on Windows, as its console does not support ansi colors.", }, :mkusers => { :default => false, :type => :boolean, :desc => "Whether to create the necessary user and group that puppet agent will run as.", }, :manage_internal_file_permissions => { :default => true, :type => :boolean, :desc => "Whether Puppet should manage the owner, group, and mode of files it uses internally", }, :onetime => { :default => false, :type => :boolean, :desc => "Perform one configuration run and exit, rather than spawning a long-running daemon. This is useful for interactively running puppet agent, or running puppet agent from cron.", :short => 'o', }, :path => { :default => "none", :desc => "The shell search path. Defaults to whatever is inherited from the parent process.", :call_hook => :on_define_and_write, :hook => proc do |value| ENV["PATH"] = "" if ENV["PATH"].nil? ENV["PATH"] = value unless value == "none" paths = ENV["PATH"].split(File::PATH_SEPARATOR) Puppet::Util::Platform.default_paths.each do |path| ENV["PATH"] += File::PATH_SEPARATOR + path unless paths.include?(path) end value end }, :libdir => { :type => :directory, :default => "$vardir/lib", :desc => "An extra search path for Puppet. This is only useful for those files that Puppet will load on demand, and is only guaranteed to work for those cases. In fact, the autoload mechanism is responsible for making sure this directory is in Ruby's search path\n", :call_hook => :on_initialize_and_write, :hook => proc do |value| $LOAD_PATH.delete(@oldlibdir) if defined?(@oldlibdir) and $LOAD_PATH.include?(@oldlibdir) @oldlibdir = value $LOAD_PATH << value end }, :ignoreimport => { :default => false, :type => :boolean, :desc => "If true, allows the parser to continue without requiring all files referenced with `import` statements to exist. This setting was primarily designed for use with commit hooks for parse-checking.", }, :environment => { :default => "production", :desc => "The environment Puppet is running in. For clients (e.g., `puppet agent`) this determines the environment itself, which is used to find modules and much more. For servers (i.e., `puppet master`) this provides the default environment for nodes we know nothing about." }, :environmentpath => { :default => "", :desc => "A search path for directory environments, as a list of directories separated by the system path separator character. (The POSIX path separator is ':', and the Windows path separator is ';'.) This setting must have a value set to enable **directory environments.** The recommended value is `$confdir/environments`. For more details, see http://docs.puppetlabs.com/puppet/latest/reference/environments.html", :type => :path, }, :diff_args => { :default => lambda { default_diffargs }, :desc => "Which arguments to pass to the diff command when printing differences between files. The command to use can be chosen with the `diff` setting.", }, :diff => { :default => (Puppet.features.microsoft_windows? ? "" : "diff"), :desc => "Which diff command to use when printing differences between files. This setting has no default value on Windows, as standard `diff` is not available, but Puppet can use many third-party diff tools.", }, :show_diff => { :type => :boolean, :default => false, :desc => "Whether to log and report a contextual diff when files are being replaced. This causes partial file contents to pass through Puppet's normal logging and reporting system, so this setting should be used with caution if you are sending Puppet's reports to an insecure destination. This feature currently requires the `diff/lcs` Ruby library.", }, :daemonize => { :type => :boolean, :default => (Puppet.features.microsoft_windows? ? false : true), :desc => "Whether to send the process into the background. This defaults to true on POSIX systems, and to false on Windows (where Puppet currently cannot daemonize).", :short => "D", :hook => proc do |value| if value and Puppet.features.microsoft_windows? raise "Cannot daemonize on Windows" end end }, :maximum_uid => { :default => 4294967290, :desc => "The maximum allowed UID. Some platforms use negative UIDs but then ship with tools that do not know how to handle signed ints, so the UIDs show up as huge numbers that can then not be fed back into the system. This is a hackish way to fail in a slightly more useful way when that happens.", }, :route_file => { :default => "$confdir/routes.yaml", :desc => "The YAML file containing indirector route configuration.", }, :node_terminus => { :type => :terminus, :default => "plain", :desc => "Where to find information about nodes.", }, :node_cache_terminus => { :type => :terminus, :default => nil, :desc => "How to store cached nodes. Valid values are (none), 'json', 'msgpack', 'yaml' or write only yaml ('write_only_yaml'). The master application defaults to 'write_only_yaml', all others to none.", }, :data_binding_terminus => { :type => :terminus, :default => "hiera", :desc => "Where to retrive information about data.", }, :hiera_config => { :default => "$confdir/hiera.yaml", :desc => "The hiera configuration file. Puppet only reads this file on startup, so you must restart the puppet master every time you edit it.", :type => :file, }, :binder => { :default => false, :desc => "Turns the binding system on or off. This includes bindings in modules. The binding system aggregates data from modules and other locations and makes them available for lookup. The binding system is experimental and any or all of it may change.", :type => :boolean, }, :binder_config => { :default => nil, :desc => "The binder configuration file. Puppet reads this file on each request to configure the bindings system. If set to nil (the default), a $confdir/binder_config.yaml is optionally loaded. If it does not exists, a default configuration is used. If the setting :binding_config is specified, it must reference a valid and existing yaml file.", :type => :file, }, :catalog_terminus => { :type => :terminus, :default => "compiler", :desc => "Where to get node catalogs. This is useful to change if, for instance, you'd like to pre-compile catalogs and store them in memcached or some other easily-accessed store.", }, :catalog_cache_terminus => { :type => :terminus, :default => nil, :desc => "How to store cached catalogs. Valid values are 'json', 'msgpack' and 'yaml'. The agent application defaults to 'json'." }, :facts_terminus => { :default => 'facter', :desc => "The node facts terminus.", :call_hook => :on_initialize_and_write, :hook => proc do |value| require 'puppet/node/facts' # Cache to YAML if we're uploading facts away if %w[rest inventory_service].include? value.to_s Puppet.info "configuring the YAML fact cache because a remote terminus is active" Puppet::Node::Facts.indirection.cache_class = :yaml end end }, :inventory_terminus => { :type => :terminus, :default => "$facts_terminus", :desc => "Should usually be the same as the facts terminus", }, :default_file_terminus => { :type => :terminus, :default => "rest", :desc => "The default source for files if no server is given in a uri, e.g. puppet:///file. The default of `rest` causes the file to be retrieved using the `server` setting. When running `apply` the default is `file_server`, causing requests to be filled locally." }, :httplog => { :default => "$logdir/http.log", :type => :file, :owner => "root", :mode => 0640, :desc => "Where the puppet agent web server logs.", }, :http_proxy_host => { :default => "none", :desc => "The HTTP proxy host to use for outgoing connections. Note: You may need to use a FQDN for the server hostname when using a proxy. Environment variable http_proxy or HTTP_PROXY will override this value", }, :http_proxy_port => { :default => 3128, :desc => "The HTTP proxy port to use for outgoing connections", }, :http_proxy_user => { :default => "none", :desc => "The user name for an authenticated HTTP proxy. Requires http_proxy_host.", }, :http_proxy_password =>{ :default => "none", :hook => proc do |value| if Puppet.settings[:http_proxy_password] =~ /[@!# \/]/ raise "Special characters in passwords must be URL compliant, we received #{value}" end end, :desc => "The password for the user of an authenticated HTTP proxy. Requires http_proxy_user. NOTE: Special characters must be escaped or encoded for URL compliance", }, :filetimeout => { :default => "15s", :type => :duration, :desc => "The minimum time to wait between checking for updates in configuration files. This timeout determines how quickly Puppet checks whether a file (such as manifests or templates) has changed on disk. #{AS_DURATION}", }, :environment_timeout => { :default => "3m", :type => :ttl, :desc => "The time to live for a cached environment. #{AS_DURATION} This setting can also be set to `unlimited`, which causes the environment to be cached until the master is restarted." }, :queue_type => { :default => "stomp", :desc => "Which type of queue to use for asynchronous processing.", }, :queue_type => { :default => "stomp", :desc => "Which type of queue to use for asynchronous processing.", }, :queue_source => { :default => "stomp://localhost:61613/", :desc => "Which type of queue to use for asynchronous processing. If your stomp server requires authentication, you can include it in the URI as long as your stomp client library is at least 1.1.1", }, :async_storeconfigs => { :default => false, :type => :boolean, :desc => "Whether to use a queueing system to provide asynchronous database integration. Requires that `puppet queue` be running.", :hook => proc do |value| if value # This reconfigures the termini for Node, Facts, and Catalog Puppet.settings.override_default(:storeconfigs, true) # But then we modify the configuration Puppet::Resource::Catalog.indirection.cache_class = :queue Puppet.settings.override_default(:catalog_cache_terminus, :queue) else raise "Cannot disable asynchronous storeconfigs in a running process" end end }, :thin_storeconfigs => { :default => false, :type => :boolean, :desc => "Boolean; whether Puppet should store only facts and exported resources in the storeconfigs database. This will improve the performance of exported resources with the older `active_record` backend, but will disable external tools that search the storeconfigs database. Thinning catalogs is generally unnecessary when using PuppetDB to store catalogs.", :hook => proc do |value| Puppet.settings.override_default(:storeconfigs, true) if value end }, :config_version => { :default => "", :desc => "How to determine the configuration version. By default, it will be the time that the configuration is parsed, but you can provide a shell script to override how the version is determined. The output of this script will be added to every log message in the reports, allowing you to correlate changes on your hosts to the source version on the server. Setting a global value for config_version in puppet.conf is deprecated. Please set a per-environment value in environment.conf instead. For more info, see http://docs.puppetlabs.com/puppet/latest/reference/environments.html", :deprecated => :allowed_on_commandline, }, :zlib => { :default => true, :type => :boolean, :desc => "Boolean; whether to use the zlib library", }, :prerun_command => { :default => "", :desc => "A command to run before every agent run. If this command returns a non-zero return code, the entire Puppet run will fail.", }, :postrun_command => { :default => "", :desc => "A command to run after every agent run. If this command returns a non-zero return code, the entire Puppet run will be considered to have failed, even though it might have performed work during the normal run.", }, :freeze_main => { :default => false, :type => :boolean, :desc => "Freezes the 'main' class, disallowing any code to be added to it. This essentially means that you can't have any code outside of a node, class, or definition other than in the site manifest.", }, :stringify_facts => { :default => true, :type => :boolean, :desc => "Flatten fact values to strings using #to_s. Means you can't have arrays or hashes as fact values.", }, :trusted_node_data => { :default => false, :type => :boolean, :desc => "Stores trusted node data in a hash called $trusted. When true also prevents $trusted from being overridden in any scope.", }, :immutable_node_data => { :default => '$trusted_node_data', :type => :boolean, :desc => "When true, also prevents $trusted and $facts from being overridden in any scope", } ) Puppet.define_settings(:module_tool, :module_repository => { :default => 'https://forgeapi.puppetlabs.com', :desc => "The module repository", }, :module_working_dir => { :default => '$vardir/puppet-module', :desc => "The directory into which module tool data is stored", }, :module_skeleton_dir => { :default => '$module_working_dir/skeleton', :desc => "The directory which the skeleton for module tool generate is stored.", } ) Puppet.define_settings( :main, # We have to downcase the fqdn, because the current ssl stuff (as oppsed to in master) doesn't have good facilities for # manipulating naming. :certname => { :default => lambda { Puppet::Settings.default_certname.downcase }, :desc => "The name to use when handling certificates. When a node requests a certificate from the CA puppet master, it uses the value of the `certname` setting as its requested Subject CN. This is the name used when managing a node's permissions in [auth.conf](http://docs.puppetlabs.com/puppet/latest/reference/config_file_auth.html). In most cases, it is also used as the node's name when matching [node definitions](http://docs.puppetlabs.com/puppet/latest/reference/lang_node_definitions.html) and requesting data from an ENC. (This can be changed with the `node_name_value` and `node_name_fact` settings, although you should only do so if you have a compelling reason.) A node's certname is available in Puppet manifests as `$trusted['certname']`. (See [Facts and Built-In Variables](http://docs.puppetlabs.com/puppet/latest/reference/lang_facts_and_builtin_vars.html) for more details.) * For best compatibility, you should limit the value of `certname` to only use letters, numbers, periods, underscores, and dashes. (That is, it should match `/\A[a-z0-9._-]+\Z/`.) * The special value `ca` is reserved, and can't be used as the certname for a normal node. Defaults to the node's fully qualified domain name.", :hook => proc { |value| raise(ArgumentError, "Certificate names must be lower case; see #1168") unless value == value.downcase }}, :certdnsnames => { :default => '', :hook => proc do |value| unless value.nil? or value == '' then Puppet.warning < < { :default => '', :desc => < { :default => "$confdir/csr_attributes.yaml", :type => :file, :desc => < { :default => "$ssldir/certs", :type => :directory, :mode => 0755, :owner => "service", :group => "service", :desc => "The certificate directory." }, :ssldir => { :default => "$confdir/ssl", :type => :directory, :mode => 0771, :owner => "service", :group => "service", :desc => "Where SSL certificates are kept." }, :publickeydir => { :default => "$ssldir/public_keys", :type => :directory, :mode => 0755, :owner => "service", :group => "service", :desc => "The public key directory." }, :requestdir => { :default => "$ssldir/certificate_requests", :type => :directory, :mode => 0755, :owner => "service", :group => "service", :desc => "Where host certificate requests are stored." }, :privatekeydir => { :default => "$ssldir/private_keys", :type => :directory, :mode => 0750, :owner => "service", :group => "service", :desc => "The private key directory." }, :privatedir => { :default => "$ssldir/private", :type => :directory, :mode => 0750, :owner => "service", :group => "service", :desc => "Where the client stores private certificate information." }, :passfile => { :default => "$privatedir/password", :type => :file, :mode => 0640, :owner => "service", :group => "service", :desc => "Where puppet agent stores the password for its private key. Generally unused." }, :hostcsr => { :default => "$ssldir/csr_$certname.pem", :type => :file, :mode => 0644, :owner => "service", :group => "service", :desc => "Where individual hosts store and look for their certificate requests." }, :hostcert => { :default => "$certdir/$certname.pem", :type => :file, :mode => 0644, :owner => "service", :group => "service", :desc => "Where individual hosts store and look for their certificates." }, :hostprivkey => { :default => "$privatekeydir/$certname.pem", :type => :file, :mode => 0640, :owner => "service", :group => "service", :desc => "Where individual hosts store and look for their private key." }, :hostpubkey => { :default => "$publickeydir/$certname.pem", :type => :file, :mode => 0644, :owner => "service", :group => "service", :desc => "Where individual hosts store and look for their public key." }, :localcacert => { :default => "$certdir/ca.pem", :type => :file, :mode => 0644, :owner => "service", :group => "service", :desc => "Where each client stores the CA certificate." }, :ssl_client_ca_auth => { :type => :file, :mode => 0644, :owner => "service", :group => "service", :desc => "Certificate authorities who issue server certificates. SSL servers will not be considered authentic unless they possess a certificate issued by an authority listed in this file. If this setting has no value then the Puppet master's CA certificate (localcacert) will be used." }, :ssl_server_ca_auth => { :type => :file, :mode => 0644, :owner => "service", :group => "service", :desc => "Certificate authorities who issue client certificates. SSL clients will not be considered authentic unless they possess a certificate issued by an authority listed in this file. If this setting has no value then the Puppet master's CA certificate (localcacert) will be used." }, :hostcrl => { :default => "$ssldir/crl.pem", :type => :file, :mode => 0644, :owner => "service", :group => "service", :desc => "Where the host's certificate revocation list can be found. This is distinct from the certificate authority's CRL." }, :certificate_revocation => { :default => true, :type => :boolean, :desc => "Whether certificate revocation should be supported by downloading a Certificate Revocation List (CRL) to all clients. If enabled, CA chaining will almost definitely not work.", }, :certificate_expire_warning => { :default => "60d", :type => :duration, :desc => "The window of time leading up to a certificate's expiration that a notification will be logged. This applies to CA, master, and agent certificates. #{AS_DURATION}" }, :digest_algorithm => { :default => 'md5', :type => :enum, :values => ["md5", "sha256"], :desc => 'Which digest algorithm to use for file resources and the filebucket. Valid values are md5, sha256. Default is md5.', } ) define_settings( :ca, :ca_name => { :default => "Puppet CA: $certname", :desc => "The name to use the Certificate Authority certificate.", }, :cadir => { :default => "$ssldir/ca", :type => :directory, :owner => "service", :group => "service", :mode => 0755, :desc => "The root directory for the certificate authority." }, :cacert => { :default => "$cadir/ca_crt.pem", :type => :file, :owner => "service", :group => "service", :mode => 0644, :desc => "The CA certificate." }, :cakey => { :default => "$cadir/ca_key.pem", :type => :file, :owner => "service", :group => "service", :mode => 0640, :desc => "The CA private key." }, :capub => { :default => "$cadir/ca_pub.pem", :type => :file, :owner => "service", :group => "service", :mode => 0644, :desc => "The CA public key." }, :cacrl => { :default => "$cadir/ca_crl.pem", :type => :file, :owner => "service", :group => "service", :mode => 0644, :desc => "The certificate revocation list (CRL) for the CA. Will be used if present but otherwise ignored.", }, :caprivatedir => { :default => "$cadir/private", :type => :directory, :owner => "service", :group => "service", :mode => 0750, :desc => "Where the CA stores private certificate information." }, :csrdir => { :default => "$cadir/requests", :type => :directory, :owner => "service", :group => "service", :mode => 0755, :desc => "Where the CA stores certificate requests" }, :signeddir => { :default => "$cadir/signed", :type => :directory, :owner => "service", :group => "service", :mode => 0755, :desc => "Where the CA stores signed certificates." }, :capass => { :default => "$caprivatedir/ca.pass", :type => :file, :owner => "service", :group => "service", :mode => 0640, :desc => "Where the CA stores the password for the private key." }, :serial => { :default => "$cadir/serial", :type => :file, :owner => "service", :group => "service", :mode => 0644, :desc => "Where the serial number for certificates is stored." }, :autosign => { :default => "$confdir/autosign.conf", :type => :autosign, :desc => "Whether (and how) to autosign certificate requests. This setting is only relevant on a puppet master acting as a certificate authority (CA). Valid values are true (autosigns all certificate requests; not recommended), false (disables autosigning certificates), or the absolute path to a file. The file specified in this setting may be either a **configuration file** or a **custom policy executable.** Puppet will automatically determine what it is: If the Puppet user (see the `user` setting) can execute the file, it will be treated as a policy executable; otherwise, it will be treated as a config file. If a custom policy executable is configured, the CA puppet master will run it every time it receives a CSR. The executable will be passed the subject CN of the request _as a command line argument,_ and the contents of the CSR in PEM format _on stdin._ It should exit with a status of 0 if the cert should be autosigned and non-zero if the cert should not be autosigned. If a certificate request is not autosigned, it will persist for review. An admin user can use the `puppet cert sign` command to manually sign it, or can delete the request. For info on autosign configuration files, see [the guide to Puppet's config files](http://docs.puppetlabs.com/guides/configuring.html).", }, :allow_duplicate_certs => { :default => false, :type => :boolean, :desc => "Whether to allow a new certificate request to overwrite an existing certificate.", }, :ca_ttl => { :default => "5y", :type => :duration, :desc => "The default TTL for new certificates. #{AS_DURATION}" }, :req_bits => { :default => 4096, :desc => "The bit length of the certificates.", }, :keylength => { :default => 4096, :desc => "The bit length of keys.", }, :cert_inventory => { :default => "$cadir/inventory.txt", :type => :file, :mode => 0644, :owner => "service", :group => "service", :desc => "The inventory file. This is a text file to which the CA writes a complete listing of all certificates." } ) # Define the config default. define_settings(:application, :config_file_name => { :type => :string, :default => Puppet::Settings.default_config_file_name, :desc => "The name of the puppet config file.", }, :config => { :type => :file, :default => "$confdir/${config_file_name}", :desc => "The configuration file for the current puppet application.", }, :pidfile => { :type => :file, :default => "$rundir/${run_mode}.pid", :desc => "The file containing the PID of a running process. This file is intended to be used by service management frameworks and monitoring systems to determine if a puppet process is still in the process table.", }, :bindaddress => { :default => "0.0.0.0", :desc => "The address a listening server should bind to.", } ) define_settings(:master, :user => { :default => "puppet", :desc => "The user puppet master should run as.", }, :group => { :default => "puppet", :desc => "The group puppet master should run as.", }, :manifestdir => { :default => "$confdir/manifests", :type => :directory, :desc => "Used to build the default value of the `manifest` setting. Has no other purpose. This setting is deprecated.", :deprecated => :completely, }, :manifest => { :default => "$manifestdir/site.pp", :type => :file_or_directory, :desc => "The entry-point manifest for puppet master. This can be one file or a directory of manifests to be evaluated in alphabetical order. Puppet manages this path as a directory if one exists or if the path ends with a / or \\. Setting a global value for `manifest` in puppet.conf is deprecated. Please use directory environments instead. If you need to use something other than the environment's `manifests` directory as the main manifest, you can set `manifest` in environment.conf. For more info, see http://docs.puppetlabs.com/puppet/latest/reference/environments.html", :deprecated => :allowed_on_commandline, }, :code => { :default => "", :desc => "Code to parse directly. This is essentially only used by `puppet`, and should only be set if you're writing your own Puppet executable.", }, :masterlog => { :default => "$logdir/puppetmaster.log", :type => :file, :owner => "service", :group => "service", :mode => 0660, :desc => "Where puppet master logs. This is generally not used, since syslog is the default log destination." }, :masterhttplog => { :default => "$logdir/masterhttp.log", :type => :file, :owner => "service", :group => "service", :mode => 0660, :create => true, :desc => "Where the puppet master web server logs." }, :masterport => { :default => 8140, :desc => "The port for puppet master traffic. For puppet master, this is the port to listen on; for puppet agent, this is the port to make requests on. Both applications use this setting to get the port.", }, :node_name => { :default => "cert", :desc => "How the puppet master determines the client's identity and sets the 'hostname', 'fqdn' and 'domain' facts for use in the manifest, in particular for determining which 'node' statement applies to the client. Possible values are 'cert' (use the subject's CN in the client's certificate) and 'facter' (use the hostname that the client reported in its facts)", }, :bucketdir => { :default => "$vardir/bucket", :type => :directory, :mode => 0750, :owner => "service", :group => "service", :desc => "Where FileBucket files are stored." }, :rest_authconfig => { :default => "$confdir/auth.conf", :type => :file, :desc => "The configuration file that defines the rights to the different rest indirections. This can be used as a fine-grained authorization system for `puppet master`.", }, :ca => { :default => true, :type => :boolean, :desc => "Whether the master should function as a certificate authority.", }, :basemodulepath => { :default => "$confdir/modules#{File::PATH_SEPARATOR}/usr/share/puppet/modules", :type => :path, :desc => "The search path for **global** modules. Should be specified as a list of directories separated by the system path separator character. (The POSIX path separator is ':', and the Windows path separator is ';'.) If you are using directory environments, these are the modules that will be used by _all_ environments. Note that the `modules` directory of the active environment will have priority over any global directories. For more info, see http://docs.puppetlabs.com/puppet/latest/reference/environments.html This setting also provides the default value for the deprecated `modulepath` setting, which is used when directory environments are disabled.", }, :modulepath => { :default => "$basemodulepath", :type => :path, :desc => "The search path for modules, as a list of directories separated by the system path separator character. (The POSIX path separator is ':', and the Windows path separator is ';'.) Setting a global value for `modulepath` in puppet.conf is deprecated. Please use directory environments instead. If you need to use something other than the default modulepath of `:$basemodulepath`, you can set `modulepath` in environment.conf. For more info, see http://docs.puppetlabs.com/puppet/latest/reference/environments.html", :deprecated => :allowed_on_commandline, }, :ssl_client_header => { :default => "HTTP_X_CLIENT_DN", :desc => "The header containing an authenticated client's SSL DN. This header must be set by the proxy to the authenticated client's SSL DN (e.g., `/CN=puppet.puppetlabs.com`). Puppet will parse out the Common Name (CN) from the Distinguished Name (DN) and use the value of the CN field for authorization. Note that the name of the HTTP header gets munged by the web server common gateway inteface: an `HTTP_` prefix is added, dashes are converted to underscores, and all letters are uppercased. Thus, to use the `X-Client-DN` header, this setting should be `HTTP_X_CLIENT_DN`.", }, :ssl_client_verify_header => { :default => "HTTP_X_CLIENT_VERIFY", :desc => "The header containing the status message of the client verification. This header must be set by the proxy to 'SUCCESS' if the client successfully authenticated, and anything else otherwise. Note that the name of the HTTP header gets munged by the web server common gateway inteface: an `HTTP_` prefix is added, dashes are converted to underscores, and all letters are uppercased. Thus, to use the `X-Client-Verify` header, this setting should be `HTTP_X_CLIENT_VERIFY`.", }, # To make sure this directory is created before we try to use it on the server, we need # it to be in the server section (#1138). :yamldir => { :default => "$vardir/yaml", :type => :directory, :owner => "service", :group => "service", :mode => "750", :desc => "The directory in which YAML data is stored, usually in a subdirectory."}, :server_datadir => { :default => "$vardir/server_data", :type => :directory, :owner => "service", :group => "service", :mode => "750", :desc => "The directory in which serialized data is stored, usually in a subdirectory."}, :reports => { :default => "store", :desc => "The list of report handlers to use. When using multiple report handlers, their names should be comma-separated, with whitespace allowed. (For example, `reports = http, tagmail`.) This setting is relevant to puppet master and puppet apply. The puppet master will call these report handlers with the reports it receives from agent nodes, and puppet apply will call them with its own report. (In all cases, the node applying the catalog must have `report = true`.) See the report reference for information on the built-in report handlers; custom report handlers can also be loaded from modules. (Report handlers are loaded from the lib directory, at `puppet/reports/NAME.rb`.)", }, :reportdir => { :default => "$vardir/reports", :type => :directory, :mode => 0750, :owner => "service", :group => "service", :desc => "The directory in which to store reports. Each node gets a separate subdirectory in this directory. This setting is only used when the `store` report processor is enabled (see the `reports` setting)."}, :reporturl => { :default => "http://localhost:3000/reports/upload", :desc => "The URL that reports should be forwarded to. This setting is only used when the `http` report processor is enabled (see the `reports` setting).", }, :fileserverconfig => { :default => "$confdir/fileserver.conf", :type => :file, :desc => "Where the fileserver configuration is stored.", }, :strict_hostname_checking => { :default => false, :desc => "Whether to only search for the complete hostname as it is in the certificate when searching for node information in the catalogs.", } ) define_settings(:metrics, :rrddir => { :type => :directory, :default => "$vardir/rrd", :mode => 0750, :owner => "service", :group => "service", :desc => "The directory where RRD database files are stored. Directories for each reporting host will be created under this directory." }, :rrdinterval => { :default => "$runinterval", :type => :duration, :desc => "How often RRD should expect data. This should match how often the hosts report back to the server. #{AS_DURATION}", } ) define_settings(:device, :devicedir => { :default => "$vardir/devices", :type => :directory, :mode => "750", :desc => "The root directory of devices' $vardir.", }, :deviceconfig => { :default => "$confdir/device.conf", :desc => "Path to the device config file for puppet device.", } ) define_settings(:agent, :node_name_value => { :default => "$certname", :desc => "The explicit value used for the node name for all requests the agent makes to the master. WARNING: This setting is mutually exclusive with node_name_fact. Changing this setting also requires changes to the default auth.conf configuration on the Puppet Master. Please see http://links.puppetlabs.com/node_name_value for more information." }, :node_name_fact => { :default => "", :desc => "The fact name used to determine the node name used for all requests the agent makes to the master. WARNING: This setting is mutually exclusive with node_name_value. Changing this setting also requires changes to the default auth.conf configuration on the Puppet Master. Please see http://links.puppetlabs.com/node_name_fact for more information.", :hook => proc do |value| if !value.empty? and Puppet[:node_name_value] != Puppet[:certname] raise "Cannot specify both the node_name_value and node_name_fact settings" end end }, :localconfig => { :default => "$statedir/localconfig", :type => :file, :owner => "root", :mode => 0660, :desc => "Where puppet agent caches the local configuration. An extension indicating the cache format is added automatically."}, :statefile => { :default => "$statedir/state.yaml", :type => :file, :mode => 0660, :desc => "Where puppet agent and puppet master store state associated with the running configuration. In the case of puppet master, this file reflects the state discovered through interacting with clients." }, :clientyamldir => { :default => "$vardir/client_yaml", :type => :directory, :mode => "750", :desc => "The directory in which client-side YAML data is stored." }, :client_datadir => { :default => "$vardir/client_data", :type => :directory, :mode => "750", :desc => "The directory in which serialized data is stored on the client." }, :classfile => { :default => "$statedir/classes.txt", :type => :file, :owner => "root", :mode => 0640, :desc => "The file in which puppet agent stores a list of the classes associated with the retrieved configuration. Can be loaded in the separate `puppet` executable using the `--loadclasses` option."}, :resourcefile => { :default => "$statedir/resources.txt", :type => :file, :owner => "root", :mode => 0640, :desc => "The file in which puppet agent stores a list of the resources associated with the retrieved configuration." }, :puppetdlog => { :default => "$logdir/puppetd.log", :type => :file, :owner => "root", :mode => 0640, :desc => "The log file for puppet agent. This is generally not used." }, :server => { :default => "puppet", :desc => "The puppet master server to which the puppet agent should connect." }, :use_srv_records => { :default => false, :type => :boolean, :desc => "Whether the server will search for SRV records in DNS for the current domain.", }, :srv_domain => { :default => lambda { Puppet::Settings.domain_fact }, :desc => "The domain which will be queried to find the SRV records of servers to use.", }, :ignoreschedules => { :default => false, :type => :boolean, :desc => "Boolean; whether puppet agent should ignore schedules. This is useful for initial puppet agent runs.", }, :default_schedules => { :default => true, :type => :boolean, :desc => "Boolean; whether to generate the default schedule resources. Setting this to false is useful for keeping external report processors clean of skipped schedule resources.", }, :puppetport => { :default => 8139, :desc => "Which port puppet agent listens on.", }, :noop => { :default => false, :type => :boolean, :desc => "Whether to apply catalogs in noop mode, which allows Puppet to partially simulate a normal run. This setting affects puppet agent and puppet apply. When running in noop mode, Puppet will check whether each resource is in sync, like it does when running normally. However, if a resource attribute is not in the desired state (as declared in the catalog), Puppet will take no action, and will instead report the changes it _would_ have made. These simulated changes will appear in the report sent to the puppet master, or be shown on the console if running puppet agent or puppet apply in the foreground. The simulated changes will not send refresh events to any subscribing or notified resources, although Puppet will log that a refresh event _would_ have been sent. **Important note:** [The `noop` metaparameter](http://docs.puppetlabs.com/references/latest/metaparameter.html#noop) allows you to apply individual resources in noop mode, and will override the global value of the `noop` setting. This means a resource with `noop => false` _will_ be changed if necessary, even when running puppet agent with `noop = true` or `--noop`. (Conversely, a resource with `noop => true` will only be simulated, even when noop mode is globally disabled.)", }, :runinterval => { :default => "30m", :type => :duration, :desc => "How often puppet agent applies the catalog. Note that a runinterval of 0 means \"run continuously\" rather than \"never run.\" If you want puppet agent to never run, you should start it with the `--no-client` option. #{AS_DURATION}", }, :listen => { :default => false, :type => :boolean, :desc => "Whether puppet agent should listen for connections. If this is true, then puppet agent will accept incoming REST API requests, subject to the default ACLs and the ACLs set in the `rest_authconfig` file. Puppet agent can respond usefully to requests on the `run`, `facts`, `certificate`, and `resource` endpoints.", }, :ca_server => { :default => "$server", :desc => "The server to use for certificate authority requests. It's a separate server because it cannot and does not need to horizontally scale.", }, :ca_port => { :default => "$masterport", :desc => "The port to use for the certificate authority.", }, :catalog_format => { :default => "", :desc => "(Deprecated for 'preferred_serialization_format') What format to use to dump the catalog. Only supports 'marshal' and 'yaml'. Only matters on the client, since it asks the server for a specific format.", :hook => proc { |value| if value Puppet.deprecation_warning "Setting 'catalog_format' is deprecated; use 'preferred_serialization_format' instead." Puppet.settings.override_default(:preferred_serialization_format, value) end } }, :preferred_serialization_format => { :default => "pson", :desc => "The preferred means of serializing ruby instances for passing over the wire. This won't guarantee that all instances will be serialized using this method, since not all classes can be guaranteed to support this format, but it will be used for all classes that support it.", }, :report_serialization_format => { :default => "pson", :type => :enum, :values => ["pson", "yaml"], :desc => "The serialization format to use when sending reports to the `report_server`. Possible values are `pson` and `yaml`. This setting affects puppet agent, but not puppet apply (which processes its own reports). This should almost always be set to `pson`. It can be temporarily set to `yaml` to let agents using this Puppet version connect to a puppet master running Puppet 3.0.0 through 3.2.x. Note that this is set to 'yaml' automatically if the agent detects an older master, so should never need to be set explicitly." }, :legacy_query_parameter_serialization => { :default => false, :type => :boolean, :desc => "The serialization format to use when sending file_metadata query parameters. Older versions of puppet master expect certain query parameters to be serialized as yaml, which is deprecated. This should almost always be false. It can be temporarily set to true to let agents using this Puppet version connect to a puppet master running Puppet 3.0.0 through 3.2.x. Note that this is set to true automatically if the agent detects an older master, so should never need to be set explicitly." }, :agent_catalog_run_lockfile => { :default => "$statedir/agent_catalog_run.lock", :type => :string, # (#2888) Ensure this file is not added to the settings catalog. :desc => "A lock file to indicate that a puppet agent catalog run is currently in progress. The file contains the pid of the process that holds the lock on the catalog run.", }, :agent_disabled_lockfile => { :default => "$statedir/agent_disabled.lock", :type => :file, :desc => "A lock file to indicate that puppet agent runs have been administratively disabled. File contains a JSON object with state information.", }, :usecacheonfailure => { :default => true, :type => :boolean, :desc => "Whether to use the cached configuration when the remote configuration will not compile. This option is useful for testing new configurations, where you want to fix the broken configuration rather than reverting to a known-good one.", }, :use_cached_catalog => { :default => false, :type => :boolean, :desc => "Whether to only use the cached catalog rather than compiling a new catalog on every run. Puppet can be run with this enabled by default and then selectively disabled when a recompile is desired.", }, :ignoremissingtypes => { :default => false, :type => :boolean, :desc => "Skip searching for classes and definitions that were missing during a prior compilation. The list of missing objects is maintained per-environment and persists until the environment is cleared or the master is restarted.", }, :ignorecache => { :default => false, :type => :boolean, :desc => "Ignore cache and always recompile the configuration. This is useful for testing new configurations, where the local cache may in fact be stale even if the timestamps are up to date - if the facts change or if the server changes.", }, :dynamicfacts => { :default => "memorysize,memoryfree,swapsize,swapfree", :desc => "(Deprecated) Facts that are dynamic; these facts will be ignored when deciding whether changed facts should result in a recompile. Multiple facts should be comma-separated.", :hook => proc { |value| if value Puppet.deprecation_warning "The dynamicfacts setting is deprecated and will be ignored." end } }, :splaylimit => { :default => "$runinterval", :type => :duration, :desc => "The maximum time to delay before runs. Defaults to being the same as the run interval. #{AS_DURATION}", }, :splay => { :default => false, :type => :boolean, :desc => "Whether to sleep for a pseudo-random (but consistent) amount of time before a run.", }, :clientbucketdir => { :default => "$vardir/clientbucket", :type => :directory, :mode => 0750, :desc => "Where FileBucket files are stored locally." }, :configtimeout => { :default => "2m", :type => :duration, :desc => "How long the client should wait for the configuration to be retrieved before considering it a failure. This can help reduce flapping if too many clients contact the server at one time. #{AS_DURATION}", }, :report_server => { :default => "$server", :desc => "The server to send transaction reports to.", }, :report_port => { :default => "$masterport", :desc => "The port to communicate with the report_server.", }, :inventory_server => { :default => "$server", :desc => "The server to send facts to.", }, :inventory_port => { :default => "$masterport", :desc => "The port to communicate with the inventory_server.", }, :report => { :default => true, :type => :boolean, :desc => "Whether to send reports after every transaction.", }, :lastrunfile => { :default => "$statedir/last_run_summary.yaml", :type => :file, :mode => 0644, :desc => "Where puppet agent stores the last run report summary in yaml format." }, :lastrunreport => { :default => "$statedir/last_run_report.yaml", :type => :file, :mode => 0640, :desc => "Where puppet agent stores the last run report in yaml format." }, :graph => { :default => false, :type => :boolean, :desc => "Whether to create dot graph files for the different configuration graphs. These dot files can be interpreted by tools like OmniGraffle or dot (which is part of ImageMagick).", }, :graphdir => { :default => "$statedir/graphs", :type => :directory, :desc => "Where to store dot-outputted graphs.", }, :http_compression => { :default => false, :type => :boolean, :desc => "Allow http compression in REST communication with the master. This setting might improve performance for agent -> master communications over slow WANs. Your puppet master needs to support compression (usually by activating some settings in a reverse-proxy in front of the puppet master, which rules out webrick). It is harmless to activate this settings if your master doesn't support compression, but if it supports it, this setting might reduce performance on high-speed LANs.", }, :waitforcert => { :default => "2m", :type => :duration, :desc => "How frequently puppet agent should ask for a signed certificate. When starting for the first time, puppet agent will submit a certificate signing request (CSR) to the server named in the `ca_server` setting (usually the puppet master); this may be autosigned, or may need to be approved by a human, depending on the CA server's configuration. Puppet agent cannot apply configurations until its approved certificate is available. Since the certificate may or may not be available immediately, puppet agent will repeatedly try to fetch it at this interval. You can turn off waiting for certificates by specifying a time of 0, in which case puppet agent will exit if it cannot get a cert. #{AS_DURATION}", }, :ordering => { :type => :enum, :values => ["manifest", "title-hash", "random"], :default => "title-hash", :desc => "How unrelated resources should be ordered when applying a catalog. Allowed values are `title-hash`, `manifest`, and `random`. This setting affects puppet agent and puppet apply, but not puppet master. * `title-hash` (the default) will order resources randomly, but will use the same order across runs and across nodes. * `manifest` will use the order in which the resources were declared in their manifest files. * `random` will order resources randomly and change their order with each run. This can work like a fuzzer for shaking out undeclared dependencies. Regardless of this setting's value, Puppet will always obey explicit dependencies set with the before/require/notify/subscribe metaparameters and the `->`/`~>` chaining arrows; this setting only affects the relative ordering of _unrelated_ resources." } ) define_settings(:inspect, :archive_files => { :type => :boolean, :default => false, :desc => "During an inspect run, whether to archive files whose contents are audited to a file bucket.", }, :archive_file_server => { :default => "$server", :desc => "During an inspect run, the file bucket server to archive files to if archive_files is set.", } ) # Plugin information. define_settings( :main, :plugindest => { :type => :directory, :default => "$libdir", :desc => "Where Puppet should store plugins that it pulls down from the central server.", }, :pluginsource => { :default => "puppet://$server/plugins", :desc => "From where to retrieve plugins. The standard Puppet `file` type is used for retrieval, so anything that is a valid file source can be used here.", }, :pluginfactdest => { :type => :directory, :default => "$vardir/facts.d", :desc => "Where Puppet should store external facts that are being handled by pluginsync", }, :pluginfactsource => { :default => "puppet://$server/pluginfacts", :desc => "Where to retrieve external facts for pluginsync", }, :pluginsync => { :default => true, :type => :boolean, :desc => "Whether plugins should be synced with the central server.", }, :pluginsignore => { :default => ".svn CVS .git", :desc => "What files to ignore when pulling down plugins.", } ) # Central fact information. define_settings( :main, :factpath => { :type => :path, :default => "$vardir/lib/facter#{File::PATH_SEPARATOR}$vardir/facts", :desc => "Where Puppet should look for facts. Multiple directories should be separated by the system path separator character. (The POSIX path separator is ':', and the Windows path separator is ';'.)", :call_hook => :on_initialize_and_write, # Call our hook with the default value, so we always get the value added to facter. :hook => proc do |value| paths = value.split(File::PATH_SEPARATOR) Facter.search(*paths) end } ) define_settings( :tagmail, :tagmap => { :default => "$confdir/tagmail.conf", :desc => "The mapping between reporting tags and email addresses.", }, :sendmail => { :default => which('sendmail') || '', :desc => "Where to find the sendmail binary with which to send email.", }, :reportfrom => { :default => lambda { "report@#{Puppet::Settings.default_certname.downcase}" }, :desc => "The 'from' email address for the reports.", }, :smtpserver => { :default => "none", :desc => "The server through which to send email reports.", }, :smtpport => { :default => 25, :desc => "The TCP port through which to send email reports.", }, :smtphelo => { :default => lambda { Facter.value 'fqdn' }, :desc => "The name by which we identify ourselves in SMTP HELO for reports. If you send to a smtpserver which does strict HELO checking (as with Postfix's `smtpd_helo_restrictions` access controls), you may need to ensure this resolves.", } ) define_settings( :rails, :dblocation => { :default => "$statedir/clientconfigs.sqlite3", :type => :file, :mode => 0660, :owner => "service", :group => "service", :desc => "The sqlite database file. #{STORECONFIGS_ONLY}" }, :dbadapter => { :default => "sqlite3", :desc => "The type of database to use. #{STORECONFIGS_ONLY}", }, :dbmigrate => { :default => false, :type => :boolean, :desc => "Whether to automatically migrate the database. #{STORECONFIGS_ONLY}", }, :dbname => { :default => "puppet", :desc => "The name of the database to use. #{STORECONFIGS_ONLY}", }, :dbserver => { :default => "localhost", :desc => "The database server for caching. Only used when networked databases are used.", }, :dbport => { :default => "", :desc => "The database password for caching. Only used when networked databases are used. #{STORECONFIGS_ONLY}", }, :dbuser => { :default => "puppet", :desc => "The database user for caching. Only used when networked databases are used. #{STORECONFIGS_ONLY}", }, :dbpassword => { :default => "puppet", :desc => "The database password for caching. Only used when networked databases are used. #{STORECONFIGS_ONLY}", }, :dbconnections => { :default => '', :desc => "The number of database connections for networked databases. Will be ignored unless the value is a positive integer. #{STORECONFIGS_ONLY}", }, :dbsocket => { :default => "", :desc => "The database socket location. Only used when networked databases are used. Will be ignored if the value is an empty string. #{STORECONFIGS_ONLY}", }, :railslog => { :default => "$logdir/rails.log", :type => :file, :mode => 0600, :owner => "service", :group => "service", :desc => "Where Rails-specific logs are sent. #{STORECONFIGS_ONLY}" }, :rails_loglevel => { :default => "info", :desc => "The log level for Rails connections. The value must be a valid log level within Rails. Production environments normally use `info` and other environments normally use `debug`. #{STORECONFIGS_ONLY}", } ) define_settings( :couchdb, :couchdb_url => { :default => "http://127.0.0.1:5984/puppet", :desc => "The url where the puppet couchdb database will be created. Only used when `facts_terminus` is set to `couch`.", } ) define_settings( :transaction, :tags => { :default => "", :desc => "Tags to use to find resources. If this is set, then only resources tagged with the specified tags will be applied. Values must be comma-separated.", }, :evaltrace => { :default => false, :type => :boolean, :desc => "Whether each resource should log when it is being evaluated. This allows you to interactively see exactly what is being done.", }, :summarize => { :default => false, :type => :boolean, :desc => "Whether to print a transaction summary.", } ) define_settings( :main, :external_nodes => { :default => "none", :desc => "An external command that can produce node information. The command's output must be a YAML dump of a hash, and that hash must have a `classes` key and/or a `parameters` key, where `classes` is an array or hash and `parameters` is a hash. For unknown nodes, the command should exit with a non-zero exit code. This command makes it straightforward to store your node mapping information in other data sources like databases.", } ) define_settings( :ldap, :ldapssl => { :default => false, :type => :boolean, :desc => "Whether SSL should be used when searching for nodes. Defaults to false because SSL usually requires certificates to be set up on the client side.", }, :ldaptls => { :default => false, :type => :boolean, :desc => "Whether TLS should be used when searching for nodes. Defaults to false because TLS usually requires certificates to be set up on the client side.", }, :ldapserver => { :default => "ldap", :desc => "The LDAP server. Only used if `node_terminus` is set to `ldap`.", }, :ldapport => { :default => 389, :desc => "The LDAP port. Only used if `node_terminus` is set to `ldap`.", }, :ldapstring => { :default => "(&(objectclass=puppetClient)(cn=%s))", :desc => "The search string used to find an LDAP node.", }, :ldapclassattrs => { :default => "puppetclass", :desc => "The LDAP attributes to use to define Puppet classes. Values should be comma-separated.", }, :ldapstackedattrs => { :default => "puppetvar", :desc => "The LDAP attributes that should be stacked to arrays by adding the values in all hierarchy elements of the tree. Values should be comma-separated.", }, :ldapattrs => { :default => "all", :desc => "The LDAP attributes to include when querying LDAP for nodes. All returned attributes are set as variables in the top-level scope. Multiple values should be comma-separated. The value 'all' returns all attributes.", }, :ldapparentattr => { :default => "parentnode", :desc => "The attribute to use to define the parent node.", }, :ldapuser => { :default => "", :desc => "The user to use to connect to LDAP. Must be specified as a full DN.", }, :ldappassword => { :default => "", :desc => "The password to use to connect to LDAP.", }, :ldapbase => { :default => "", :desc => "The search base for LDAP searches. It's impossible to provide a meaningful default here, although the LDAP libraries might have one already set. Generally, it should be the 'ou=Hosts' branch under your main directory.", } ) define_settings(:master, :storeconfigs => { :default => false, :type => :boolean, :desc => "Whether to store each client's configuration, including catalogs, facts, and related data. This also enables the import and export of resources in the Puppet language - a mechanism for exchange resources between nodes. By default this uses ActiveRecord and an SQL database to store and query the data; this, in turn, will depend on Rails being available. You can adjust the backend using the storeconfigs_backend setting.", # Call our hook with the default value, so we always get the libdir set. :call_hook => :on_initialize_and_write, :hook => proc do |value| require 'puppet/node' require 'puppet/node/facts' if value if not Puppet.settings[:async_storeconfigs] Puppet::Resource::Catalog.indirection.cache_class = :store_configs Puppet.settings.override_default(:catalog_cache_terminus, :store_configs) end Puppet::Node::Facts.indirection.cache_class = :store_configs Puppet::Resource.indirection.terminus_class = :store_configs end end }, :storeconfigs_backend => { :type => :terminus, :default => "active_record", :desc => "Configure the backend terminus used for StoreConfigs. By default, this uses the ActiveRecord store, which directly talks to the database from within the Puppet Master process." } ) define_settings(:parser, :templatedir => { :default => "$vardir/templates", :type => :directory, :desc => "Where Puppet looks for template files. Can be a list of colon-separated directories. This setting is deprecated. Please put your templates in modules instead.", :deprecated => :completely, }, :allow_variables_with_dashes => { :default => false, :desc => <<-'EOT' Permit hyphens (`-`) in variable names and issue deprecation warnings about them. This setting **should always be `false`;** setting it to `true` will cause subtle and wide-ranging bugs. It will be removed in a future version. Hyphenated variables caused major problems in the language, but were allowed between Puppet 2.7.3 and 2.7.14. If you used them during this window, we apologize for the inconvenience --- you can temporarily set this to `true` in order to upgrade, and can rename your variables at your leisure. Please revert it to `false` after you have renamed all affected variables. EOT }, :parser => { :default => "current", :desc => <<-'EOT' Selects the parser to use for parsing puppet manifests (in puppet DSL language/'.pp' files). Available choices are `current` (the default) and `future`. The `curent` parser means that the released version of the parser should be used. The `future` parser is a "time travel to the future" allowing early exposure to new language features. What these features are will vary from release to release and they may be invididually configurable. Available Since Puppet 3.2. EOT }, - :evaluator => { - :default => "future", - :hook => proc do |value| - if !['future', 'current'].include?(value) - raise "evaluator can only be set to 'future' or 'current', got '#{value}'" - end - end, - :desc => <<-'EOT' - Which evaluator to use when compiling Puppet manifests. Valid values - are `current` and `future` (the default). - - **Note:** This setting is only used when `parser = future`. It allows - testers to turn off the `future` evaluator when doing detailed tests and - comparisons of the new compilation system. - - Evaluation is the second stage of catalog compilation. After the parser - converts a manifest to a model of expressions, the evaluator processes - each expression. (For example, a resource declaration signals the - evaluator to add a resource to the catalog). - - The `future` parser and evaluator are slated to become default in Puppet - 4. Their purpose is to add new features and improve consistency - and reliability. - - Available Since Puppet 3.5. - EOT - }, - :biff => { - :default => false, - :type => :boolean, - :hook => proc do |value| - if Puppet.settings[:parser] != 'future' - Puppet.settings.override_default(:parser, 'future') - end - if Puppet.settings[:evaluator] != 'future' - Puppet.settings.override_default(:evaluator, 'future') - end - end, - :desc => <<-EOT - Turns on Biff the catalog builder, future parser, and future evaluator. - This is an experimental feature - and this setting may go away before - release of Pupet 3.6. - EOT - }, :max_errors => { :default => 10, :desc => <<-'EOT' Sets the max number of logged/displayed parser validation errors in case multiple errors have been detected. A value of 0 is the same as a value of 1; a minimum of one error is always raised. The count is per manifest. EOT }, :max_warnings => { :default => 10, :desc => <<-'EOT' Sets the max number of logged/displayed parser validation warnings in case multiple warnings have been detected. A value of 0 blocks logging of warnings. The count is per manifest. EOT }, :max_deprecations => { :default => 10, :desc => <<-'EOT' Sets the max number of logged/displayed parser validation deprecation warnings in case multiple deprecation warnings have been detected. A value of 0 blocks the logging of deprecation warnings. The count is per manifest. EOT }, :strict_variables => { :default => false, :type => :boolean, :desc => <<-'EOT' Makes the parser raise errors when referencing unknown variables. (This does not affect referencing variables that are explicitly set to undef). EOT } ) define_settings(:puppetdoc, :document_all => { :default => false, :type => :boolean, :desc => "Whether to document all resources when using `puppet doc` to generate manifest documentation.", } ) end diff --git a/lib/puppet/loaders.rb b/lib/puppet/loaders.rb index e85b5e3fc..3d150bdcf 100644 --- a/lib/puppet/loaders.rb +++ b/lib/puppet/loaders.rb @@ -1,20 +1,19 @@ module Puppet module Pops require 'puppet/pops/loaders' module Loader require 'puppet/pops/loader/loader' require 'puppet/pops/loader/base_loader' require 'puppet/pops/loader/gem_support' require 'puppet/pops/loader/module_loaders' require 'puppet/pops/loader/dependency_loader' require 'puppet/pops/loader/null_loader' require 'puppet/pops/loader/static_loader' require 'puppet/pops/loader/ruby_function_instantiator' - require 'puppet/pops/loader/ruby_legacy_function_instantiator' require 'puppet/pops/loader/loader_paths' require 'puppet/pops/loader/simple_environment_loader' end end end diff --git a/lib/puppet/parser/ast/pops_bridge.rb b/lib/puppet/parser/ast/pops_bridge.rb index cf3ab656a..63f36494d 100644 --- a/lib/puppet/parser/ast/pops_bridge.rb +++ b/lib/puppet/parser/ast/pops_bridge.rb @@ -1,246 +1,246 @@ require 'puppet/parser/ast/top_level_construct' require 'puppet/pops' # The AST::Bridge contains classes that bridges between the new Pops based model # and the 3.x AST. This is required to be able to reuse the Puppet::Resource::Type which is # fundamental for the rest of the logic. # class Puppet::Parser::AST::PopsBridge # Bridges to one Pops Model Expression # The @value is the expression # This is used to represent the body of a class, definition, or node, and for each parameter's default value # expression. # class Expression < Puppet::Parser::AST::Leaf def initialize args super - @@evaluator ||= Puppet::Pops::Parser::EvaluatingParser::Transitional.new() + @@evaluator ||= Puppet::Pops::Parser::EvaluatingParser.new() end def to_s Puppet::Pops::Model::ModelTreeDumper.new.dump(@value) end def evaluate(scope) @@evaluator.evaluate(scope, @value) end # Adapts to 3x where top level constructs needs to have each to iterate over children. Short circuit this # by yielding self. By adding this there is no need to wrap a pops expression inside an AST::BlockExpression # def each yield self end def sequence_with(other) if value.nil? # This happens when testing and not having a complete setup other else # When does this happen ? Ever ? raise "sequence_with called on Puppet::Parser::AST::PopsBridge::Expression - please report use case" # What should be done if the above happens (We don't want this to happen). # Puppet::Parser::AST::BlockExpression.new(:children => [self] + other.children) end end # The 3x requires code plugged in to an AST to have this in certain positions in the tree. The purpose # is to either print the content, or to look for things that needs to be defined. This implementation # cheats by always returning an empty array. (This allows simple files to not require a "Program" at the top. # def children [] end end class NilAsUndefExpression < Expression def evaluate(scope) result = super result.nil? ? :undef : result end end # Bridges the top level "Program" produced by the pops parser. # Its main purpose is to give one point where all definitions are instantiated (actually defined since the # Puppet 3x terminology is somewhat misleading - the definitions are instantiated, but instances of the created types # are not created, that happens when classes are included / required, nodes are matched and when resources are instantiated # by a resource expression (which is also used to instantiate a host class). # class Program < Puppet::Parser::AST::TopLevelConstruct attr_reader :program_model, :context def initialize(program_model, context = {}) @program_model = program_model @context = context @ast_transformer ||= Puppet::Pops::Model::AstTransformer.new(@context[:file]) - @@evaluator ||= Puppet::Pops::Parser::EvaluatingParser::Transitional.new() + @@evaluator ||= Puppet::Pops::Parser::EvaluatingParser.new() end # This is the 3x API, the 3x AST searches through all code to find the instructions that can be instantiated. # This Pops-model based instantiation relies on the parser to build this list while parsing (which is more # efficient as it avoids one full scan of all logic via recursive enumeration/yield) # def instantiate(modname) @program_model.definitions.collect do |d| case d when Puppet::Pops::Model::HostClassDefinition instantiate_HostClassDefinition(d, modname) when Puppet::Pops::Model::ResourceTypeDefinition instantiate_ResourceTypeDefinition(d, modname) when Puppet::Pops::Model::NodeDefinition instantiate_NodeDefinition(d, modname) else raise Puppet::ParseError, "Internal Error: Unknown type of definition - got '#{d.class}'" end end.flatten().compact() # flatten since node definition may have returned an array # Compact since functions are not understood by compiler end def evaluate(scope) @@evaluator.evaluate(scope, program_model) end # Adapts to 3x where top level constructs needs to have each to iterate over children. Short circuit this # by yielding self. This means that the HostClass container will call this bridge instance with `instantiate`. # def each yield self end private def instantiate_Parameter(o) # 3x needs parameters as an array of `[name]` or `[name, value_expr]` # One problem is that the parameter evaluation takes place in the wrong context in 3x (the caller's and # can thus reference all sorts of information. Here the value expression is wrapped in an AST Bridge to a Pops # expression since the Pops side can not control the evaluation if o.value [o.name, NilAsUndefExpression.new(:value => o.value)] else [o.name] end end def create_type_map(definition) result = {} # No need to do anything if there are no parameters return result unless definition.parameters.size > 0 # No need to do anything if there are no typed parameters typed_parameters = definition.parameters.select {|p| p.type_expr } return result if typed_parameters.empty? # If there are typed parameters, they need to be evaluated to produce the corresponding type # instances. This evaluation requires a scope. A scope is not available when doing deserialization # (there is also no initialized evaluator). When running apply and test however, the environment is # reused and we may reenter without a scope (which is fine). A debug message is then output in case # there is the need to track down the odd corner case. See {#obtain_scope}. # if scope = obtain_scope typed_parameters.each do |p| result[p.name] = @@evaluator.evaluate(scope, p.type_expr) end end result end # Obtains the scope or issues a warning if :global_scope is not bound def obtain_scope scope = Puppet.lookup(:global_scope) do # This occurs when testing and when applying a catalog (there is no scope available then), and # when running tests that run a partial setup. # This is bad if the logic is trying to compile, but a warning can not be issues since it is a normal # use case that there is no scope when requesting the type in order to just get the parameters. Puppet.debug("Instantiating Resource with type checked parameters - scope is missing, skipping type checking.") nil end scope end # Produces a hash with data for Definition and HostClass def args_from_definition(o, modname) args = { :arguments => o.parameters.collect {|p| instantiate_Parameter(p) }, :argument_types => create_type_map(o), :module_name => modname } unless is_nop?(o.body) args[:code] = Expression.new(:value => o.body) end @ast_transformer.merge_location(args, o) end def instantiate_HostClassDefinition(o, modname) args = args_from_definition(o, modname) args[:parent] = o.parent_class Puppet::Resource::Type.new(:hostclass, o.name, @context.merge(args)) end def instantiate_ResourceTypeDefinition(o, modname) Puppet::Resource::Type.new(:definition, o.name, @context.merge(args_from_definition(o, modname))) end def instantiate_NodeDefinition(o, modname) args = { :module_name => modname } unless is_nop?(o.body) args[:code] = Expression.new(:value => o.body) end unless is_nop?(o.parent) args[:parent] = @ast_transformer.hostname(o.parent) end host_matches = @ast_transformer.hostname(o.host_matches) @ast_transformer.merge_location(args, o) host_matches.collect do |name| Puppet::Resource::Type.new(:node, name, @context.merge(args)) end end # Propagates a found Function to the appropriate loader. # This is for 4x future-evaluator/loader # def instantiate_FunctionDefinition(function_definition, modname) loaders = (Puppet.lookup(:loaders) { nil }) unless loaders raise Puppet::ParseError, "Internal Error: Puppet Context ':loaders' missing - cannot define any functions" end loader = if modname.nil? || modname == "" # TODO : Later when functions can be private, a decision is needed regarding what that means. # A private environment loader could be used for logic outside of modules, then only that logic # would see the function. # # Use the private loader, this function may see the environment's dependencies (currently, all modules) loaders.private_environment_loader() else # TODO : Later check if function is private, and then add it to # private_loader_for_module # loaders.public_loader_for_module(modname) end unless loader raise Puppet::ParseError, "Internal Error: did not find public loader for module: '#{modname}'" end # Instantiate Function, and store it in the environment loader typed_name, f = Puppet::Pops::Loader::PuppetFunctionInstantiator.create_from_model(function_definition, loader) loader.set_entry(typed_name, f, Puppet::Pops::Adapters::SourcePosAdapter.adapt(function_definition).to_uri) nil # do not want the function to inadvertently leak into 3x end def code() Expression.new(:value => @value) end def is_nop?(o) @ast_transformer.is_nop?(o) end end end diff --git a/lib/puppet/parser/e4_parser_adapter.rb b/lib/puppet/parser/e4_parser_adapter.rb index 638e9524b..00911b5d5 100644 --- a/lib/puppet/parser/e4_parser_adapter.rb +++ b/lib/puppet/parser/e4_parser_adapter.rb @@ -1,81 +1,81 @@ require 'puppet/pops' module Puppet; module Parser; end; end; # Adapts an egrammar/eparser to respond to the public API of the classic parser # and makes use of the new evaluator. # class Puppet::Parser::E4ParserAdapter # Empty adapter fulfills watch_file contract without doing anything. # @api private class NullFileWatcher def watch_file(file) #nop end end # @param file_watcher [#watch_file] something that can watch a file def initialize(file_watcher = nil) @file_watcher = file_watcher || NullFileWatcher.new @file = '' @string = '' @use = :unspecified - @@evaluating_parser ||= Puppet::Pops::Parser::EvaluatingParser::Transitional.new() + @@evaluating_parser ||= Puppet::Pops::Parser::EvaluatingParser.new() end def file=(file) @file = file @use = :file # watch if possible, but only if the file is something worth watching @file_watcher.watch_file(file) if !file.nil? && file != '' end def parse(string = nil) self.string= string if string if @file =~ /\.rb$/ && @use != :string # Will throw an error parse_ruby_file end parse_result = if @use == :string # Parse with a source_file to set in created AST objects (it was either given, or it may be unknown # if caller did not set a file and the present a string. # @@evaluating_parser.parse_string(@string, @file || "unknown-source-location") else @@evaluating_parser.parse_file(@file) end # the parse_result may be # * empty / nil (no input) # * a Model::Program # * a Model::Expression # model = parse_result.nil? ? nil : parse_result.current args = {} Puppet::Pops::Model::AstTransformer.new(@file).merge_location(args, model) ast_code = if model.is_a? Puppet::Pops::Model::Program Puppet::Parser::AST::PopsBridge::Program.new(model, args) else args[:value] = model Puppet::Parser::AST::PopsBridge::Expression.new(args) end # Create the "main" class for the content - this content will get merged with all other "main" content Puppet::Parser::AST::Hostclass.new('', :code => ast_code) end def string=(string) @string = string @use = :string end def parse_ruby_file raise Puppet::ParseError, "Ruby DSL is no longer supported. Attempt to parse #{@file}" end end diff --git a/lib/puppet/parser/e_parser_adapter.rb b/lib/puppet/parser/e_parser_adapter.rb deleted file mode 100644 index fe0e28b55..000000000 --- a/lib/puppet/parser/e_parser_adapter.rb +++ /dev/null @@ -1,119 +0,0 @@ -require 'puppet/pops' - -module Puppet; module Parser; end; end; -# Adapts an egrammar/eparser to respond to the public API of the classic parser -# -class Puppet::Parser::EParserAdapter - - def initialize(classic_parser) - @classic_parser = classic_parser - @file = '' - @string = '' - @use = :undefined - end - - def file=(file) - @classic_parser.file = file - @file = file - @use = :file - end - - def parse(string = nil) - if @file =~ /\.rb$/ - return parse_ruby_file - else - self.string= string if string - parser = Puppet::Pops::Parser::Parser.new() - parse_result = if @use == :string - parser.parse_string(@string) - else - parser.parse_file(@file) - end - # Compute the source_file to set in created AST objects (it was either given, or it may be unknown - # if caller did not set a file and the present a string. - # - source_file = @file || "unknown-source-location" - - # Validate - validate(parse_result) - # Transform the result, but only if not nil - parse_result = Puppet::Pops::Model::AstTransformer.new(source_file, @classic_parser).transform(parse_result) if parse_result - if parse_result && !parse_result.is_a?(Puppet::Parser::AST::BlockExpression) - # Need to transform again, if result is not wrapped in something iterable when handed off to - # a new Hostclass as its code. - parse_result = Puppet::Parser::AST::BlockExpression.new(:children => [parse_result]) if parse_result - end - end - - Puppet::Parser::AST::Hostclass.new('', :code => parse_result) - end - - def validate(parse_result) - # TODO: This is too many hoops to jump through... ugly API - # could reference a ValidatorFactory.validator_3_1(acceptor) instead. - # and let the factory abstract the rest. - # - return unless parse_result - - acceptor = Puppet::Pops::Validation::Acceptor.new - validator = Puppet::Pops::Validation::ValidatorFactory_3_1.new().validator(acceptor) - validator.validate(parse_result) - - max_errors = Puppet[:max_errors] - max_warnings = Puppet[:max_warnings] + 1 - max_deprecations = Puppet[:max_deprecations] + 1 - - # If there are warnings output them - warnings = acceptor.warnings - if warnings.size > 0 - formatter = Puppet::Pops::Validation::DiagnosticFormatterPuppetStyle.new - emitted_w = 0 - emitted_dw = 0 - acceptor.warnings.each {|w| - if w.severity == :deprecation - # Do *not* call Puppet.deprecation_warning it is for internal deprecation, not - # deprecation of constructs in manifests! (It is not designed for that purpose even if - # used throughout the code base). - # - Puppet.warning(formatter.format(w)) if emitted_dw < max_deprecations - emitted_dw += 1 - else - Puppet.warning(formatter.format(w)) if emitted_w < max_warnings - emitted_w += 1 - end - break if emitted_w > max_warnings && emitted_dw > max_deprecations # but only then - } - end - - # If there were errors, report the first found. Use a puppet style formatter. - errors = acceptor.errors - if errors.size > 0 - formatter = Puppet::Pops::Validation::DiagnosticFormatterPuppetStyle.new - if errors.size == 1 || max_errors <= 1 - # raise immediately - raise Puppet::ParseError.new(formatter.format(errors[0])) - end - emitted = 0 - errors.each do |e| - Puppet.err(formatter.format(e)) - emitted += 1 - break if emitted >= max_errors - end - warnings_message = warnings.size > 0 ? ", and #{warnings.size} warnings" : "" - giving_up_message = "Found #{errors.size} errors#{warnings_message}. Giving up" - exception = Puppet::ParseError.new(giving_up_message) - exception.file = errors[0].file - raise exception - end - end - - def string=(string) - @classic_parser.string = string - @string = string - @use = :string - end - - def parse_ruby_file - @classic_parser.parse - end -end diff --git a/lib/puppet/parser/functions.rb b/lib/puppet/parser/functions.rb index 45e9a1f0f..be104d810 100644 --- a/lib/puppet/parser/functions.rb +++ b/lib/puppet/parser/functions.rb @@ -1,268 +1,262 @@ require 'puppet/util/autoload' require 'puppet/parser/scope' # A module for managing parser functions. Each specified function # is added to a central module that then gets included into the Scope # class. # # @api public module Puppet::Parser::Functions Environment = Puppet::Node::Environment class << self include Puppet::Util end # Reset the list of loaded functions. # # @api private def self.reset @modules = {} # Runs a newfunction to create a function for each of the log levels Puppet::Util::Log.levels.each do |level| newfunction(level, :environment => Puppet.lookup(:root_environment), :doc => "Log a message on the server at level #{level.to_s}.") do |vals| send(level, vals.join(" ")) end end end # Accessor for singleton autoloader # # @api private def self.autoloader @autoloader ||= Puppet::Util::Autoload.new( self, "puppet/parser/functions", :wrap => false ) end # Get the module that functions are mixed into corresponding to an # environment # # @api private def self.environment_module(env) @modules[env.name] ||= Module.new do @metadata = {} def self.all_function_info @metadata end def self.get_function_info(name) @metadata[name] end def self.add_function_info(name, info) @metadata[name] = info end end end # Create a new Puppet DSL function. # # **The {newfunction} method provides a public API.** # # This method is used both internally inside of Puppet to define parser # functions. For example, template() is defined in # {file:lib/puppet/parser/functions/template.rb template.rb} using the # {newfunction} method. Third party Puppet modules such as # [stdlib](https://forge.puppetlabs.com/puppetlabs/stdlib) use this method to # extend the behavior and functionality of Puppet. # # See also [Docs: Custom # Functions](http://docs.puppetlabs.com/guides/custom_functions.html) # # @example Define a new Puppet DSL Function # >> Puppet::Parser::Functions.newfunction(:double, :arity => 1, # :doc => "Doubles an object, typically a number or string.", # :type => :rvalue) {|i| i[0]*2 } # => {:arity=>1, :type=>:rvalue, # :name=>"function_double", # :doc=>"Doubles an object, typically a number or string."} # # @example Invoke the double function from irb as is done in RSpec examples: # >> require 'puppet_spec/scope' # >> scope = PuppetSpec::Scope.create_test_scope_for_node('example') # => Scope() # >> scope.function_double([2]) # => 4 # >> scope.function_double([4]) # => 8 # >> scope.function_double([]) # ArgumentError: double(): Wrong number of arguments given (0 for 1) # >> scope.function_double([4,8]) # ArgumentError: double(): Wrong number of arguments given (2 for 1) # >> scope.function_double(["hello"]) # => "hellohello" # # @param [Symbol] name the name of the function represented as a ruby Symbol. # The {newfunction} method will define a Ruby method based on this name on # the parser scope instance. # # @param [Proc] block the block provided to the {newfunction} method will be # executed when the Puppet DSL function is evaluated during catalog # compilation. The arguments to the function will be passed as an array to # the first argument of the block. The return value of the block will be # the return value of the Puppet DSL function for `:rvalue` functions. # # @option options [:rvalue, :statement] :type (:statement) the type of function. # Either `:rvalue` for functions that return a value, or `:statement` for # functions that do not return a value. # # @option options [String] :doc ('') the documentation for the function. # This string will be extracted by documentation generation tools. # # @option options [Integer] :arity (-1) the # [arity](http://en.wikipedia.org/wiki/Arity) of the function. When # specified as a positive integer the function is expected to receive # _exactly_ the specified number of arguments. When specified as a # negative number, the function is expected to receive _at least_ the # absolute value of the specified number of arguments incremented by one. # For example, a function with an arity of `-4` is expected to receive at # minimum 3 arguments. A function with the default arity of `-1` accepts # zero or more arguments. A function with an arity of 2 must be provided # with exactly two arguments, no more and no less. Added in Puppet 3.1.0. # # @option options [Puppet::Node::Environment] :environment (nil) can # explicitly pass the environment we wanted the function added to. Only used # to set logging functions in root environment # # @return [Hash] describing the function. # # @api public def self.newfunction(name, options = {}, &block) - # Short circuit this call when 4x "biff" is in effect to allow the new loader system to load - # and define the function a different way. - # - if Puppet[:biff] - return Puppet::Pops::Loader::RubyLegacyFunctionInstantiator.legacy_newfunction(name, options, &block) - end name = name.intern environment = options[:environment] || Puppet.lookup(:current_environment) Puppet.warning "Overwriting previous definition for function #{name}" if get_function(name, environment) arity = options[:arity] || -1 ftype = options[:type] || :statement unless ftype == :statement or ftype == :rvalue raise Puppet::DevError, "Invalid statement type #{ftype.inspect}" end # the block must be installed as a method because it may use "return", # which is not allowed from procs. real_fname = "real_function_#{name}" environment_module(environment).send(:define_method, real_fname, &block) fname = "function_#{name}" env_module = environment_module(environment) env_module.send(:define_method, fname) do |*args| Puppet::Util::Profiler.profile("Called #{name}", [:functions, name]) do if args[0].is_a? Array if arity >= 0 and args[0].size != arity raise ArgumentError, "#{name}(): Wrong number of arguments given (#{args[0].size} for #{arity})" elsif arity < 0 and args[0].size < (arity+1).abs raise ArgumentError, "#{name}(): Wrong number of arguments given (#{args[0].size} for minimum #{(arity+1).abs})" end self.send(real_fname, args[0]) else raise ArgumentError, "custom functions must be called with a single array that contains the arguments. For example, function_example([1]) instead of function_example(1)" end end end func = {:arity => arity, :type => ftype, :name => fname} func[:doc] = options[:doc] if options[:doc] env_module.add_function_info(name, func) func end # Determine if a function is defined # # @param [Symbol] name the function # @param [Puppet::Node::Environment] environment the environment to find the function in # # @return [Symbol, false] The name of the function if it's defined, # otherwise false. # # @api public def self.function(name, environment = Puppet.lookup(:current_environment)) name = name.intern func = nil unless func = get_function(name, environment) autoloader.load(name, environment) func = get_function(name, environment) end if func func[:name] else false end end def self.functiondocs(environment = Puppet.lookup(:current_environment)) autoloader.loadall ret = "" merged_functions(environment).sort { |a,b| a[0].to_s <=> b[0].to_s }.each do |name, hash| ret << "#{name}\n#{"-" * name.to_s.length}\n" if hash[:doc] ret << Puppet::Util::Docs.scrub(hash[:doc]) else ret << "Undocumented.\n" end ret << "\n\n- *Type*: #{hash[:type]}\n\n" end ret end # Determine whether a given function returns a value. # # @param [Symbol] name the function # @param [Puppet::Node::Environment] environment The environment to find the function in # @return [Boolean] whether it is an rvalue function # # @api public def self.rvalue?(name, environment = Puppet.lookup(:current_environment)) func = get_function(name, environment) func ? func[:type] == :rvalue : false end # Return the number of arguments a function expects. # # @param [Symbol] name the function # @param [Puppet::Node::Environment] environment The environment to find the function in # @return [Integer] The arity of the function. See {newfunction} for # the meaning of negative values. # # @api public def self.arity(name, environment = Puppet.lookup(:current_environment)) func = get_function(name, environment) func ? func[:arity] : -1 end class << self private def merged_functions(environment) root = environment_module(Puppet.lookup(:root_environment)) env = environment_module(environment) root.all_function_info.merge(env.all_function_info) end def get_function(name, environment) environment_module(environment).get_function_info(name.intern) || environment_module(Puppet.lookup(:root_environment)).get_function_info(name.intern) end end end diff --git a/lib/puppet/parser/parser_factory.rb b/lib/puppet/parser/parser_factory.rb index 576bc8755..a9d93cf85 100644 --- a/lib/puppet/parser/parser_factory.rb +++ b/lib/puppet/parser/parser_factory.rb @@ -1,88 +1,77 @@ module Puppet; end module Puppet::Parser # The ParserFactory makes selection of parser possible. # Currently, it is possible to switch between two different parsers: # * classic_parser, the parser in 3.1 # * eparser, the Expression Based Parser # class ParserFactory # Produces a parser instance for the given environment def self.parser(environment) - case Puppet[:parser] - when 'future' - if Puppet[:evaluator] == 'future' - evaluating_parser(environment) - else - eparser(environment) - end + if Puppet[:parser] == 'future' + evaluating_parser(environment) else classic_parser(environment) end end # Creates an instance of the classic parser. # def self.classic_parser(environment) - require 'puppet/parser' - + # avoid expensive require if already loaded + require 'puppet/parser' unless defined? Puppet::Parser::Parser Puppet::Parser::Parser.new(environment) end - # Returns an instance of an EvaluatingParser + # Creates an instance of an E4ParserAdapter that adapts an + # EvaluatingParser to the 3x way of parsing. + # def self.evaluating_parser(file_watcher) # Since RGen is optional, test that it is installed @@asserted ||= false - assert_rgen_installed() unless @@asserted + unless @@asserted + # avoid this expensive check if already done + assert_rgen_installed() + # avoid expensive requires + require 'puppet/parser/e4_parser_adapter' + require 'puppet/pops/parser/code_merger' + end @@asserted = true - require 'puppet/parser/e4_parser_adapter' - require 'puppet/pops/parser/code_merger' + E4ParserAdapter.new(file_watcher) end - # Creates an instance of the expression based parser 'eparser' + # Asserts that RGen >= 0.6.1 is installed by checking that certain behavior is available. + # Note that this assert is expensive as it also requires puppet/pops (if not already loaded). # - def self.eparser(environment) - # Since RGen is optional, test that it is installed - @@asserted ||= false - assert_rgen_installed() unless @@asserted - @@asserted = true - require 'puppet/parser' - require 'puppet/parser/e_parser_adapter' - EParserAdapter.new(Puppet::Parser::Parser.new(environment)) - end - - private - def self.assert_rgen_installed begin require 'rgen/metamodel_builder' rescue LoadError raise Puppet::DevError.new("The gem 'rgen' version >= 0.6.1 is required when using the setting '--parser future'. Please install 'rgen'.") end # Since RGen is optional, there is nothing specifying its version. # It is not installed in any controlled way, so not possible to use gems to check (it may be installed some other way). # Instead check that "eContainer, and eContainingFeature" has been installed. require 'puppet/pops' begin litstring = Puppet::Pops::Model::LiteralString.new(); container = Puppet::Pops::Model::ArithmeticExpression.new(); container.left_expr = litstring raise "no eContainer" if litstring.eContainer() != container raise "no eContainingFeature" if litstring.eContainingFeature() != :left_expr rescue raise Puppet::DevError.new("The gem 'rgen' version >= 0.6.1 is required when using '--parser future'. An older version is installed, please update.") end end def self.code_merger - if Puppet[:parser] == 'future' && Puppet[:evaluator] == 'future' + if Puppet[:parser] == 'future' Puppet::Pops::Parser::CodeMerger.new else Puppet::Parser::CodeMerger.new end end - end - end diff --git a/lib/puppet/parser/scope.rb b/lib/puppet/parser/scope.rb index 1e243d42a..4ef84c400 100644 --- a/lib/puppet/parser/scope.rb +++ b/lib/puppet/parser/scope.rb @@ -1,932 +1,933 @@ # The scope class, which handles storing and retrieving variables and types and # such. require 'forwardable' require 'puppet/parser' require 'puppet/parser/templatewrapper' require 'puppet/resource/type_collection_helper' require 'puppet/util/methodhelper' # This class is part of the internal parser/evaluator/compiler functionality of Puppet. # It is passed between the various classes that participate in evaluation. # None of its methods are API except those that are clearly marked as such. # # @api public class Puppet::Parser::Scope extend Forwardable include Puppet::Util::MethodHelper include Puppet::Resource::TypeCollectionHelper require 'puppet/parser/resource' AST = Puppet::Parser::AST Puppet::Util.logmethods(self) include Puppet::Util::Errors attr_accessor :source, :resource attr_accessor :compiler attr_accessor :parent attr_reader :namespaces # Hash of hashes of default values per type name attr_reader :defaults # Add some alias methods that forward to the compiler, since we reference # them frequently enough to justify the extra method call. def_delegators :compiler, :catalog, :environment # Abstract base class for LocalScope and MatchScope # class Ephemeral attr_reader :parent def initialize(parent = nil) @parent = parent end def is_local_scope? false end def [](name) if @parent @parent[name] end end def include?(name) (@parent and @parent.include?(name)) end def bound?(name) false end def add_entries_to(target = {}) @parent.add_entries_to(target) unless @parent.nil? # do not include match data ($0-$n) target end end class LocalScope < Ephemeral def initialize(parent=nil) super parent @symbols = {} end def [](name) if @symbols.include?(name) @symbols[name] else super end end def is_local_scope? true end def []=(name, value) @symbols[name] = value end def include?(name) bound?(name) || super end def delete(name) @symbols.delete(name) end def bound?(name) @symbols.include?(name) end def add_entries_to(target = {}) super @symbols.each do |k, v| if v == :undef || v.nil? target.delete(k) else target[ k ] = v end end target end end class MatchScope < Ephemeral attr_accessor :match_data def initialize(parent = nil, match_data = nil) super parent @match_data = match_data end def is_local_scope? false end def [](name) if bound?(name) @match_data[name.to_i] else super end end def include?(name) bound?(name) or super end def bound?(name) # A "match variables" scope reports all numeric variables to be bound if the scope has # match_data. Without match data the scope is transparent. # @match_data && name =~ /^\d+$/ end def []=(name, value) # TODO: Bad choice of exception raise Puppet::ParseError, "Numerical variables cannot be changed. Attempt to set $#{name}" end def delete(name) # TODO: Bad choice of exception raise Puppet::ParseError, "Numerical variables cannot be deleted: Attempt to delete: $#{name}" end def add_entries_to(target = {}) # do not include match data ($0-$n) super end end # Returns true if the variable of the given name has a non nil value. # TODO: This has vague semantics - does the variable exist or not? # use ['name'] to get nil or value, and if nil check with exist?('name') # this include? is only useful because of checking against the boolean value false. # def include?(name) ! self[name].nil? end # Returns true if the variable of the given name is set to any value (including nil) # def exist?(name) next_scope = inherited_scope || enclosing_scope effective_symtable(true).include?(name) || next_scope && next_scope.exist?(name) end # Returns true if the given name is bound in the current (most nested) scope for assignments. # def bound?(name) # Do not look in ephemeral (match scope), the semantics is to answer if an assignable variable is bound effective_symtable(false).bound?(name) end # Is the value true? This allows us to control the definition of truth # in one place. def self.true?(value) case value when '' false when :undef false else !!value end end # Coerce value to a number, or return `nil` if it isn't one. def self.number?(value) case value when Numeric value when /^-?\d+(:?\.\d+|(:?\.\d+)?e\d+)$/ value.to_f when /^0x[0-9a-f]+$/i value.to_i(16) when /^0[0-7]+$/ value.to_i(8) when /^-?\d+$/ value.to_i else nil end end # Add to our list of namespaces. def add_namespace(ns) return false if @namespaces.include?(ns) if @namespaces == [""] @namespaces = [ns] else @namespaces << ns end end def find_hostclass(name, options = {}) known_resource_types.find_hostclass(namespaces, name, options) end def find_definition(name) known_resource_types.find_definition(namespaces, name) end def find_global_scope() # walk upwards until first found node_scope or top_scope if is_nodescope? || is_topscope? self else next_scope = inherited_scope || enclosing_scope if next_scope.nil? # this happens when testing, and there is only a single test scope and no link to any # other scopes self else next_scope.find_global_scope() end end end # This just delegates directly. def_delegator :compiler, :findresource # Initialize our new scope. Defaults to having no parent. def initialize(compiler, options = {}) if compiler.is_a? Puppet::Parser::Compiler self.compiler = compiler else raise Puppet::DevError, "you must pass a compiler instance to a new scope object" end if n = options.delete(:namespace) @namespaces = [n] else @namespaces = [""] end raise Puppet::DevError, "compiler passed in options" if options.include? :compiler set_options(options) extend_with_functions_module # The symbol table for this scope. This is where we store variables. # @symtable = Ephemeral.new(nil, true) @symtable = LocalScope.new(nil) @ephemeral = [ MatchScope.new(@symtable, nil) ] # All of the defaults set for types. It's a hash of hashes, # with the first key being the type, then the second key being # the parameter. @defaults = Hash.new { |dhash,type| dhash[type] = {} } # The table for storing class singletons. This will only actually # be used by top scopes and node scopes. @class_scopes = {} @enable_immutable_data = Puppet[:immutable_node_data] end # Store the fact that we've evaluated a class, and store a reference to # the scope in which it was evaluated, so that we can look it up later. def class_set(name, scope) if parent parent.class_set(name, scope) else @class_scopes[name] = scope end end # Return the scope associated with a class. This is just here so # that subclasses can set their parent scopes to be the scope of # their parent class, and it's also used when looking up qualified # variables. def class_scope(klass) # They might pass in either the class or class name k = klass.respond_to?(:name) ? klass.name : klass @class_scopes[k] || (parent && parent.class_scope(k)) end # Collect all of the defaults set at any higher scopes. # This is a different type of lookup because it's additive -- # it collects all of the defaults, with defaults in closer scopes # overriding those in later scopes. def lookupdefaults(type) if Puppet[:parser] == 'future' lookupdefaults_4x(type) else lookupdefaults_3x(type) end end # The implemetation for lookupdefaults for 3x where the order is: # inherited, contained (recursive), self # def lookupdefaults_3x(type) values = {} # first collect the values from the parents if parent parent.lookupdefaults(type).each { |var,value| values[var] = value } end # then override them with any current values # this should probably be done differently if @defaults.include?(type) @defaults[type].each { |var,value| values[var] = value } end values end # The implementation for lookupdefaults for 4x where the order is: # inherited-scope, closure-scope, self # def lookupdefaults_4x(type) # This is an optimized version that avoids method calls and garbage creation # Build array with scopes from most significant to least significant influencing_scopes = [self] is = inherited_scope while is do influencing_scopes << is is = is.inherited_scope end es = enclosing_scope while es do influencing_scopes << es es = es.enclosing_scope end # apply from least significant, to most significant influencing_scopes.reverse.reduce({}) do | values, scope | scope_defaults = scope.defaults if scope_defaults.include?(type) values.merge!(scope_defaults[type]) end values end end # Look up a defined type. def lookuptype(name) find_definition(name) || find_hostclass(name) end def undef_as(x,v) if v.nil? or v == :undef x else v end end # Lookup a variable within this scope using the Puppet language's # scoping rules. Variables can be qualified using just as in a # manifest. # # @param [String] name the variable name to lookup # # @return Object the value of the variable, or nil if it's not found # # @api public def lookupvar(name, options = {}) unless name.is_a? String raise Puppet::ParseError, "Scope variable name #{name.inspect} is a #{name.class}, not a string" end table = @ephemeral.last if name =~ /^(.*)::(.+)$/ class_name = $1 variable_name = $2 lookup_qualified_variable(class_name, variable_name, options) # TODO: optimize with an assoc instead, this searches through scopes twice for a hit elsif table.include?(name) table[name] else next_scope = inherited_scope || enclosing_scope if next_scope next_scope.lookupvar(name, options) else variable_not_found(name) end end end def variable_not_found(name, reason=nil) if Puppet[:strict_variables] - if Puppet[:evaluator] == 'future' && Puppet[:parser] == 'future' + if Puppet[:parser] == 'future' throw :undefined_variable else reason_msg = reason.nil? ? '' : "; #{reason}" raise Puppet::ParseError, "Undefined variable #{name.inspect}#{reason_msg}" end else nil end end + # Retrieves the variable value assigned to the name given as an argument. The name must be a String, # and namespace can be qualified with '::'. The value is looked up in this scope, its parent scopes, # or in a specific visible named scope. # # @param varname [String] the name of the variable (may be a qualified name using `(ns'::')*varname` # @param options [Hash] Additional options, not part of api. # @return [Object] the value assigned to the given varname # @see #[]= # @api public # def [](varname, options={}) lookupvar(varname, options) end # The scope of the inherited thing of this scope's resource. This could # either be a node that was inherited or the class. # # @return [Puppet::Parser::Scope] The scope or nil if there is not an inherited scope def inherited_scope if has_inherited_class? qualified_scope(resource.resource_type.parent) else nil end end # The enclosing scope (topscope or nodescope) of this scope. # The enclosing scopes are produced when a class or define is included at # some point. The parent scope of the included class or define becomes the # scope in which it was included. The chain of parent scopes is followed # until a node scope or the topscope is found # # @return [Puppet::Parser::Scope] The scope or nil if there is no enclosing scope def enclosing_scope if has_enclosing_scope? if parent.is_topscope? or parent.is_nodescope? parent else parent.enclosing_scope end else nil end end def is_classscope? resource and resource.type == "Class" end def is_nodescope? resource and resource.type == "Node" end def is_topscope? compiler and self == compiler.topscope end def lookup_qualified_variable(class_name, variable_name, position) begin if lookup_as_local_name?(class_name, variable_name) self[variable_name] else qualified_scope(class_name).lookupvar(variable_name, position) end rescue RuntimeError => e unless Puppet[:strict_variables] # Do not issue warning if strict variables are on, as an error will be raised by variable_not_found location = if position[:lineproc] " at #{position[:lineproc].call}" elsif position[:file] && position[:line] " at #{position[:file]}:#{position[:line]}" else "" end warning "Could not look up qualified variable '#{class_name}::#{variable_name}'; #{e.message}#{location}" end variable_not_found("#{class_name}::#{variable_name}", e.message) end end # Handles the special case of looking up fully qualified variable in not yet evaluated top scope # This is ok if the lookup request originated in topscope (this happens when evaluating # bindings; using the top scope to provide the values for facts. # @param class_name [String] the classname part of a variable name, may be special "" # @param variable_name [String] the variable name without the absolute leading '::' # @return [Boolean] true if the given variable name should be looked up directly in this scope # def lookup_as_local_name?(class_name, variable_name) # not a local if name has more than one segment return nil if variable_name =~ /::/ # partial only if the class for "" cannot be found return nil unless class_name == "" && klass = find_hostclass(class_name) && class_scope(klass).nil? is_topscope? end def has_inherited_class? is_classscope? and resource.resource_type.parent end private :has_inherited_class? def has_enclosing_scope? not parent.nil? end private :has_enclosing_scope? def qualified_scope(classname) raise "class #{classname} could not be found" unless klass = find_hostclass(classname) raise "class #{classname} has not been evaluated" unless kscope = class_scope(klass) kscope end private :qualified_scope # Returns a Hash containing all variables and their values, optionally (and # by default) including the values defined in parent. Local values # shadow parent values. Ephemeral scopes for match results ($0 - $n) are not included. # # This is currently a wrapper for to_hash_legacy or to_hash_future. # # @see to_hash_future # # @see to_hash_legacy def to_hash(recursive = true) @parser ||= Puppet[:parser] if @parser == 'future' to_hash_future(recursive) else to_hash_legacy(recursive) end end # Fixed version of to_hash that implements scoping correctly (i.e., with # dynamic scoping disabled #28200 / PUP-1220 # # @see to_hash def to_hash_future(recursive) if recursive and has_enclosing_scope? target = enclosing_scope.to_hash_future(recursive) if !(inherited = inherited_scope).nil? target.merge!(inherited.to_hash_future(recursive)) end else target = Hash.new end # add all local scopes @ephemeral.last.add_entries_to(target) target end # The old broken implementation of to_hash that retains the dynamic scoping # semantics # # @see to_hash def to_hash_legacy(recursive = true) if recursive and parent target = parent.to_hash_legacy(recursive) else target = Hash.new end # add all local scopes @ephemeral.last.add_entries_to(target) target end def namespaces @namespaces.dup end # Create a new scope and set these options. def newscope(options = {}) compiler.newscope(self, options) end def parent_module_name return nil unless @parent return nil unless @parent.source @parent.source.module_name end # Set defaults for a type. The typename should already be downcased, # so that the syntax is isolated. We don't do any kind of type-checking # here; instead we let the resource do it when the defaults are used. def define_settings(type, params) table = @defaults[type] # if we got a single param, it'll be in its own array params = [params] unless params.is_a?(Array) params.each { |param| if table.include?(param.name) raise Puppet::ParseError.new("Default already defined for #{type} { #{param.name} }; cannot redefine", param.line, param.file) end table[param.name] = param } end RESERVED_VARIABLE_NAMES = ['trusted', 'facts'].freeze # Set a variable in the current scope. This will override settings # in scopes above, but will not allow variables in the current scope # to be reassigned. # It's preferred that you use self[]= instead of this; only use this # when you need to set options. def setvar(name, value, options = {}) if name =~ /^[0-9]+$/ raise Puppet::ParseError.new("Cannot assign to a numeric match result variable '$#{name}'") # unless options[:ephemeral] end unless name.is_a? String raise Puppet::ParseError, "Scope variable name #{name.inspect} is a #{name.class}, not a string" end # Check for reserved variable names if @enable_immutable_data && !options[:privileged] && RESERVED_VARIABLE_NAMES.include?(name) raise Puppet::ParseError, "Attempt to assign to a reserved variable name: '#{name}'" end table = effective_symtable options[:ephemeral] if table.bound?(name) if options[:append] error = Puppet::ParseError.new("Cannot append, variable #{name} is defined in this scope") else error = Puppet::ParseError.new("Cannot reassign variable #{name}") end error.file = options[:file] if options[:file] error.line = options[:line] if options[:line] raise error end if options[:append] table[name] = append_value(undef_as('', self[name]), value) else table[name] = value end table[name] end def set_trusted(hash) setvar('trusted', deep_freeze(hash), :privileged => true) end def set_facts(hash) setvar('facts', deep_freeze(hash), :privileged => true) end # Deeply freezes the given object. The object and its content must be of the types: # Array, Hash, Numeric, Boolean, Symbol, Regexp, NilClass, or String. All other types raises an Error. # (i.e. if they are assignable to Puppet::Pops::Types::Data type). # def deep_freeze(object) case object when Array object.each {|v| deep_freeze(v) } object.freeze when Hash object.each {|k, v| deep_freeze(k); deep_freeze(v) } object.freeze when NilClass, Numeric, TrueClass, FalseClass # do nothing when String object.freeze else raise Puppet::Error, "Unsupported data type: '#{object.class}'" end object end private :deep_freeze # Return the effective "table" for setting variables. # This method returns the first ephemeral "table" that acts as a local scope, or this # scope's symtable. If the parameter `use_ephemeral` is true, the "top most" ephemeral "table" # will be returned (irrespective of it being a match scope or a local scope). # # @param use_ephemeral [Boolean] whether the top most ephemeral (of any kind) should be used or not def effective_symtable use_ephemeral s = @ephemeral.last return s || @symtable if use_ephemeral # Why check if ephemeral is a Hash ??? Not needed, a hash cannot be a parent scope ??? while s && !(s.is_a?(Hash) || s.is_local_scope?()) s = s.parent end s ? s : @symtable end # Sets the variable value of the name given as an argument to the given value. The value is # set in the current scope and may shadow a variable with the same name in a visible outer scope. # It is illegal to re-assign a variable in the same scope. It is illegal to set a variable in some other # scope/namespace than the scope passed to a method. # # @param varname [String] The variable name to which the value is assigned. Must not contain `::` # @param value [String] The value to assign to the given variable name. # @param options [Hash] Additional options, not part of api. # # @api public # def []=(varname, value, options = {}) setvar(varname, value, options = {}) end def append_value(bound_value, new_value) case new_value when Array bound_value + new_value when Hash bound_value.merge(new_value) else if bound_value.is_a?(Hash) raise ArgumentError, "Trying to append to a hash with something which is not a hash is unsupported" end bound_value + new_value end end private :append_value # Return the tags associated with this scope. def_delegator :resource, :tags # Used mainly for logging def to_s "Scope(#{@resource})" end # remove ephemeral scope up to level # TODO: Who uses :all ? Remove ?? # def unset_ephemeral_var(level=:all) if level == :all @ephemeral = [ MatchScope.new(@symtable, nil)] else @ephemeral.pop(@ephemeral.size - level) end end def ephemeral_level @ephemeral.size end # TODO: Who calls this? def new_ephemeral(local_scope = false) if local_scope @ephemeral.push(LocalScope.new(@ephemeral.last)) else @ephemeral.push(MatchScope.new(@ephemeral.last, nil)) end end # Sets match data in the most nested scope (which always is a MatchScope), it clobbers match data already set there # def set_match_data(match_data) @ephemeral.last.match_data = match_data end # Nests a match data scope def new_match_scope(match_data) @ephemeral.push(MatchScope.new(@ephemeral.last, match_data)) end def ephemeral_from(match, file = nil, line = nil) case match when Hash # Create local scope ephemeral and set all values from hash new_ephemeral(true) match.each {|k,v| setvar(k, v, :file => file, :line => line, :ephemeral => true) } # Must always have an inner match data scope (that starts out as transparent) # In 3x slightly wasteful, since a new nested scope is created for a match # (TODO: Fix that problem) new_ephemeral(false) else raise(ArgumentError,"Invalid regex match data. Got a #{match.class}") unless match.is_a?(MatchData) # Create a match ephemeral and set values from match data new_match_scope(match) end end def find_resource_type(type) # It still works fine without the type == 'class' short-cut, but it is a lot slower. return nil if ["class", "node"].include? type.to_s.downcase find_builtin_resource_type(type) || find_defined_resource_type(type) end def find_builtin_resource_type(type) Puppet::Type.type(type.to_s.downcase.to_sym) end def find_defined_resource_type(type) known_resource_types.find_definition(namespaces, type.to_s.downcase) end def method_missing(method, *args, &block) method.to_s =~ /^function_(.*)$/ name = $1 super unless name super unless Puppet::Parser::Functions.function(name) # In odd circumstances, this might not end up defined by the previous # method, so we might as well be certain. if respond_to? method send(method, *args) else raise Puppet::DevError, "Function #{name} not defined despite being loaded!" end end def resolve_type_and_titles(type, titles) raise ArgumentError, "titles must be an array" unless titles.is_a?(Array) case type.downcase when "class" # resolve the titles titles = titles.collect do |a_title| hostclass = find_hostclass(a_title) hostclass ? hostclass.name : a_title end when "node" # no-op else # resolve the type resource_type = find_resource_type(type) type = resource_type.name if resource_type end return [type, titles] end # Transforms references to classes to the form suitable for # lookup in the compiler. # # Makes names passed in the names array absolute if they are relative # Names are now made absolute if Puppet[:parser] == 'future', this will # be the default behavior in Puppet 4.0 # # Transforms Class[] and Resource[] type referenes to class name # or raises an error if a Class[] is unspecific, if a Resource is not # a 'class' resource, or if unspecific (no title). # # TODO: Change this for 4.0 to always make names absolute # # @param names [Array] names to (optionally) make absolute # @return [Array] names after transformation # def transform_and_assert_classnames(names) if Puppet[:parser] == 'future' names.map do |name| case name when String name.sub(/^([^:]{1,2})/, '::\1') when Puppet::Resource assert_class_and_title(name.type, name.title) name.title.sub(/^([^:]{1,2})/, '::\1') when Puppet::Pops::Types::PHostClassType raise ArgumentError, "Cannot use an unspecific Class[] Type" unless name.class_name name.class_name.sub(/^([^:]{1,2})/, '::\1') when Puppet::Pops::Types::PResourceType assert_class_and_title(name.type_name, name.title) name.title.sub(/^([^:]{1,2})/, '::\1') end end else names end end private def assert_class_and_title(type_name, title) if type_name.nil? || type_name == '' raise ArgumentError, "Cannot use an unspecific Resource[] where a Resource['class', name] is expected" end unless type_name =~ /^[Cc]lass$/ raise ArgumentError, "Cannot use a Resource[#{type_name}] where a Resource['class', name] is expected" end if title.nil? raise ArgumentError, "Cannot use an unspecific Resource['class'] where a Resource['class', name] is expected" end end def extend_with_functions_module root = Puppet.lookup(:root_environment) extend Puppet::Parser::Functions.environment_module(root) extend Puppet::Parser::Functions.environment_module(environment) if environment != root end end diff --git a/lib/puppet/pops.rb b/lib/puppet/pops.rb index b4688000b..49cc08c17 100644 --- a/lib/puppet/pops.rb +++ b/lib/puppet/pops.rb @@ -1,121 +1,119 @@ module Puppet # The Pops language system. This includes the parser, evaluator, AST model, and # Binder. # # @todo Explain how a user should use this to parse and evaluate the puppet # language. # # @note Warning: Pops is still considered experimental, as such the API may # change at any time. # # @api public module Pops require 'puppet/pops/patterns' require 'puppet/pops/utils' require 'puppet/pops/adaptable' require 'puppet/pops/adapters' require 'puppet/pops/visitable' require 'puppet/pops/visitor' require 'puppet/pops/containment' require 'puppet/pops/issues' require 'puppet/pops/semantic_error' require 'puppet/pops/label_provider' require 'puppet/pops/validation' require 'puppet/pops/issue_reporter' require 'puppet/pops/model/model' module Types require 'puppet/pops/types/types' require 'puppet/pops/types/type_calculator' require 'puppet/pops/types/type_factory' require 'puppet/pops/types/type_parser' require 'puppet/pops/types/class_loader' require 'puppet/pops/types/enumeration' end module Model require 'puppet/pops/model/tree_dumper' require 'puppet/pops/model/ast_transformer' require 'puppet/pops/model/ast_tree_dumper' require 'puppet/pops/model/factory' require 'puppet/pops/model/model_tree_dumper' require 'puppet/pops/model/model_label_provider' end module Binder module SchemeHandler # the handlers are auto loaded via bindings end module Producers require 'puppet/pops/binder/producers' end require 'puppet/pops/binder/binder' require 'puppet/pops/binder/bindings_model' require 'puppet/pops/binder/binder_issues' require 'puppet/pops/binder/bindings_checker' require 'puppet/pops/binder/bindings_factory' require 'puppet/pops/binder/bindings_label_provider' require 'puppet/pops/binder/bindings_validator_factory' require 'puppet/pops/binder/injector_entry' require 'puppet/pops/binder/key_factory' require 'puppet/pops/binder/injector' require 'puppet/pops/binder/bindings_composer' require 'puppet/pops/binder/bindings_model_dumper' require 'puppet/pops/binder/system_bindings' require 'puppet/pops/binder/bindings_loader' require 'puppet/pops/binder/lookup' module Config require 'puppet/pops/binder/config/binder_config' require 'puppet/pops/binder/config/binder_config_checker' require 'puppet/pops/binder/config/issues' require 'puppet/pops/binder/config/diagnostic_producer' end end module Parser require 'puppet/pops/parser/eparser' require 'puppet/pops/parser/parser_support' require 'puppet/pops/parser/locator' require 'puppet/pops/parser/locatable' require 'puppet/pops/parser/lexer' require 'puppet/pops/parser/lexer2' require 'puppet/pops/parser/evaluating_parser' require 'puppet/pops/parser/epp_parser' end module Validation - require 'puppet/pops/validation/checker3_1' - require 'puppet/pops/validation/validator_factory_3_1' require 'puppet/pops/validation/checker4_0' require 'puppet/pops/validation/validator_factory_4_0' end module Evaluator require 'puppet/pops/evaluator/callable_signature' require 'puppet/pops/evaluator/runtime3_support' require 'puppet/pops/evaluator/evaluator_impl' require 'puppet/pops/evaluator/epp_evaluator' require 'puppet/pops/evaluator/callable_mismatch_describer' end # Subsystem for puppet functions defined in ruby. # # @api public module Functions require 'puppet/pops/functions/function' require 'puppet/pops/functions/dispatch' require 'puppet/pops/functions/dispatcher' end end require 'puppet/parser/ast/pops_bridge' require 'puppet/bindings' require 'puppet/functions' end diff --git a/lib/puppet/pops/binder/bindings_checker.rb b/lib/puppet/pops/binder/bindings_checker.rb index a69ad0732..9d568be02 100644 --- a/lib/puppet/pops/binder/bindings_checker.rb +++ b/lib/puppet/pops/binder/bindings_checker.rb @@ -1,197 +1,197 @@ # A validator/checker of a bindings model # @api public # class Puppet::Pops::Binder::BindingsChecker Bindings = Puppet::Pops::Binder::Bindings Issues = Puppet::Pops::Binder::BinderIssues Types = Puppet::Pops::Types attr_reader :type_calculator attr_reader :acceptor # @api public def initialize(diagnostics_producer) @@check_visitor ||= Puppet::Pops::Visitor.new(nil, "check", 0, 0) @type_calculator = Puppet::Pops::Types::TypeCalculator.new() - @expression_validator = Puppet::Pops::Validation::ValidatorFactory_3_1.new().checker(diagnostics_producer) + @expression_validator = Puppet::Pops::Validation::ValidatorFactory_4_0.new().checker(diagnostics_producer) @acceptor = diagnostics_producer end # Validates the entire model by visiting each model element and calling `check`. # The result is collected (or acted on immediately) by the configured diagnostic provider/acceptor # given when creating this Checker. # # @api public # def validate(b) check(b) b.eAllContents.each {|c| check(c) } end # Performs binding validity check # @api private def check(b) @@check_visitor.visit_this(self, b) end # Checks that a binding has a producer and a type # @api private def check_Binding(b) # Must have a type acceptor.accept(Issues::MISSING_TYPE, b) unless b.type.is_a?(Types::PAnyType) # Must have a producer acceptor.accept(Issues::MISSING_PRODUCER, b) unless b.producer.is_a?(Bindings::ProducerDescriptor) end # Checks that the producer is a Multibind producer and that the type is a PCollectionType # @api private def check_Multibinding(b) # id is optional (empty id blocks contributions) # A multibinding must have PCollectionType acceptor.accept(Issues::MULTIBIND_TYPE_ERROR, b, {:actual_type => b.type}) unless b.type.is_a?(Types::PCollectionType) # if the producer is nil, a suitable producer will be picked automatically unless b.producer.nil? || b.producer.is_a?(Bindings::MultibindProducerDescriptor) acceptor.accept(Issues::MULTIBIND_NOT_COLLECTION_PRODUCER, b, {:actual_producer => b.producer}) end end # Checks that the bindings object contains at least one binding. Then checks each binding in turn # @api private def check_Bindings(b) acceptor.accept(Issues::MISSING_BINDINGS, b) unless has_entries?(b.bindings) end # Checks that a name has been associated with the bindings # @api private def check_NamedBindings(b) acceptor.accept(Issues::MISSING_BINDINGS_NAME, b) unless has_chars?(b.name) check_Bindings(b) end # Check layer has a name # @api private def check_NamedLayer(l) acceptor.accept(Issues::MISSING_LAYER_NAME, binding_parent(l)) unless has_chars?(l.name) end # Checks that the binding has layers and that each layer has a name and at least one binding # @api private def check_LayeredBindings(b) acceptor.accept(Issues::MISSING_LAYERS, b) unless has_entries?(b.layers) end # Checks that the non caching producer has a producer to delegate to # @api private def check_NonCachingProducerDescriptor(p) acceptor.accept(Issues::PRODUCER_MISSING_PRODUCER, p) unless p.producer.is_a?(Bindings::ProducerDescriptor) end # Checks that a constant value has been declared in the producer and that the type # of the value is compatible with the type declared in the binding # @api private def check_ConstantProducerDescriptor(p) # the product must be of compatible type # TODO: Likely to change when value becomes a typed Puppet Object b = binding_parent(p) if p.value.nil? acceptor.accept(Issues::MISSING_VALUE, p, {:binding => b}) else infered = type_calculator.infer(p.value) unless type_calculator.assignable?(b.type, infered) acceptor.accept(Issues::INCOMPATIBLE_TYPE, p, {:binding => b, :expected_type => b.type, :actual_type => infered}) end end end # Checks that an expression has been declared in the producer # @api private def check_EvaluatingProducerDescriptor(p) unless p.expression.is_a?(Puppet::Pops::Model::Expression) acceptor.accept(Issues::MISSING_EXPRESSION, p, {:binding => binding_parent(p)}) end end # Checks that a class name has been declared in the producer # @api private def check_InstanceProducerDescriptor(p) acceptor.accept(Issues::MISSING_CLASS_NAME, p, {:binding => binding_parent(p)}) unless has_chars?(p.class_name) end # Checks that a type and a name has been declared. The type must be assignable to the type # declared in the binding. The name can be an empty string to denote 'no name' # @api private def check_LookupProducerDescriptor(p) b = binding_parent(p) unless type_calculator.assignable(b.type, p.type) acceptor.accept(Issues::INCOMPATIBLE_TYPE, p, {:binding => b, :expected_type => b.type, :actual_type => p.type }) end acceptor.accept(Issues::MISSING_NAME, p, {:binding => b}) if p.name.nil? # empty string is OK end # Checks that a key has been declared, then calls producer_LookupProducerDescriptor to perform # checks associated with the super class # @api private def check_HashLookupProducerDescriptor(p) acceptor.accept(Issues::MISSING_KEY, p, {:binding => binding_parent(p)}) unless has_chars?(p.key) check_LookupProducerDescriptor(p) end # Checks that the type declared in the binder is a PArrayType # @api private def check_ArrayMultibindProducerDescriptor(p) b = binding_parent(p) acceptor.accept(Issues::MULTIBIND_INCOMPATIBLE_TYPE, p, {:binding => b, :actual_type => b.type}) unless b.type.is_a?(Types::PArrayType) end # Checks that the type declared in the binder is a PHashType # @api private def check_HashMultibindProducerDescriptor(p) b = binding_parent(p) acceptor.accept(Issues::MULTIBIND_INCOMPATIBLE_TYPE, p, {:binding => b, :actual_type => b.type}) unless b.type.is_a?(Types::PHashType) end # Checks that the producer that this producer delegates to is declared # @api private def check_ProducerProducerDescriptor(p) unless p.producer.is_a?(Bindings::ProducerDescriptor) acceptor.accept(Issues::PRODUCER_MISSING_PRODUCER, p, {:binding => binding_parent(p)}) end end # @api private def check_Expression(t) @expression_validator.validate(t) end # @api private def check_PAnyType(t) # Do nothing end # Returns true if the argument is a non empty string # @api private def has_chars?(s) s.is_a?(String) && !s.empty? end # @api private def has_entries?(s) !(s.nil? || s.empty?) end # @api private def binding_parent(p) begin x = p.eContainer if x.nil? acceptor.accept(Issues::MODEL_OBJECT_IS_UNBOUND, p) return nil end p = x end while !p.is_a?(Bindings::AbstractBinding) p end end diff --git a/lib/puppet/pops/binder/producers.rb b/lib/puppet/pops/binder/producers.rb index ad6fe2503..8302ebf4c 100644 --- a/lib/puppet/pops/binder/producers.rb +++ b/lib/puppet/pops/binder/producers.rb @@ -1,828 +1,826 @@ # This module contains the various producers used by Puppet Bindings. # The main (abstract) class is {Puppet::Pops::Binder::Producers::Producer} which documents the # Producer API and serves as a base class for all other producers. # It is required that custom producers inherit from this producer (directly or indirectly). # # The selection of a Producer is typically performed by the Innjector when it configures itself # from a Bindings model where a {Puppet::Pops::Binder::Bindings::ProducerDescriptor} describes # which producer to use. The configuration uses this to create the concrete producer. # It is possible to describe that a particular producer class is to be used, and also to describe that # a custom producer (derived from Producer) should be used. This is available for both regular # bindings as well as multi-bindings. # # # @api public # module Puppet::Pops::Binder::Producers # Producer is an abstract base class representing the base contract for a bound producer. # Typically, when a lookup is performed it is the value that is returned (via a producer), but # it is also possible to lookup the producer, and ask it to produce the value (the producer may # return a series of values, which makes this especially useful). # # When looking up a producer, it is of importance to only use the API of the Producer class # unless it is known that a particular custom producer class has been bound. # # Custom Producers # ---------------- # The intent is that this class is derived for custom producers that require additional # options/arguments when producing an instance. Such a custom producer may raise an error if called # with too few arguments, or may implement specific `produce` methods and always raise an # error on #produce indicating that this producer requires custom calls and that it can not # be used as an implicit producer. # # Features of Producer # -------------------- # The Producer class is abstract, but offers the ability to transform the produced result # by passing the option `:transformer` which should be a Puppet Lambda Expression taking one argument # and producing the transformed (wanted) result. # # @abstract # @api public # class Producer # A Puppet 3 AST Lambda Expression # @api public # attr_reader :transformer # Creates a Producer. # Derived classes should call this constructor to get support for transformer lambda. # # @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates # @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer # @param scope [Puppet::Parser::Scope] The scope to use for evaluation # @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value # @api public # def initialize(injector, binding, scope, options) if transformer_lambda = options[:transformer] if transformer_lambda.is_a?(Proc) raise ArgumentError, "Transformer Proc must take two arguments; scope, value." unless transformer_lambda.arity == 2 @transformer = transformer_lambda else raise ArgumentError, "Transformer must be a LambdaExpression" unless transformer_lambda.is_a?(Puppet::Pops::Model::LambdaExpression) raise ArgumentError, "Transformer lambda must take one argument; value." unless transformer_lambda.parameters.size() == 1 - # NOTE: This depends on Puppet 3 AST Lambda - @transformer = Puppet::Pops::Model::AstTransformer.new().transform(transformer_lambda) + @transformer = Puppet::Pops::Parser::EvaluatingParser.new.closure(transformer_lambda, scope) end end end # Produces an instance. # @param scope [Puppet::Parser:Scope] the scope to use for evaluation # @param args [Object] arguments to custom producers, always empty for implicit productions # @return [Object] the produced instance (should never be nil). # @api public # def produce(scope, *args) do_transformation(scope, internal_produce(scope)) end # Returns the producer after possibly having recreated an internal/wrapped producer. # This implementation returns `self`. A derived class may want to override this method # to perform initialization/refresh of its internal state. This method is called when # a producer is requested. # @see Puppet::Pops::Binder::ProducerProducer for an example of implementation. # @param scope [Puppet::Parser:Scope] the scope to use for evaluation # @return [Puppet::Pops::Binder::Producer] the producer to use # @api public # def producer(scope) self end protected # Derived classes should implement this method to do the production of a value # @param scope [Puppet::Parser::Scope] the scope to use when performing lookup and evaluation # @raise [NotImplementedError] this implementation always raises an error # @abstract # @api private # def internal_produce(scope) raise NotImplementedError, "Producer-class '#{self.class.name}' should implement #internal_produce(scope)" end # Transforms the produced value if a transformer has been defined. # @param scope [Puppet::Parser::Scope] the scope used for evaluation # @param produced_value [Object, nil] the produced value (possibly nil) # @return [Object] the transformed value if a transformer is defined, else the given `produced_value` # @api private # def do_transformation(scope, produced_value) return produced_value unless transformer transformer.call(scope, produced_value) end end # Abstract Producer holding a value # @abstract # @api public # class AbstractValueProducer < Producer # @api public attr_reader :value # @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates # @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer # @param scope [Puppet::Parser::Scope] The scope to use for evaluation # @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value # @option options [Puppet::Pops::Model::LambdaExpression, nil] :value (nil) the value to produce # @api public # def initialize(injector, binding, scope, options) super # nil is ok here, as an abstract value producer may be used to signal "not found" @value = options[:value] end end # Produces the same/singleton value on each production # @api public # class SingletonProducer < AbstractValueProducer protected # @api private def internal_produce(scope) value() end end # Produces a deep clone of its value on each production. # @api public # class DeepCloningProducer < AbstractValueProducer protected # @api private def internal_produce(scope) case value when Integer, Float, TrueClass, FalseClass, Symbol # These are immutable return value when String # ok if frozen, else fall through to default return value() if value.frozen? end # The default: serialize/deserialize to get a deep copy Marshal.load(Marshal.dump(value())) end end # This abstract producer class remembers the injector and binding. # @abstract # @api public # class AbstractArgumentedProducer < Producer # @api public attr_reader :injector # @api public attr_reader :binding # @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates # @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer # @param scope [Puppet::Parser::Scope] The scope to use for evaluation # @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value # @api public # def initialize(injector, binding, scope, options) super @injector = injector @binding = binding end end # @api public class InstantiatingProducer < AbstractArgumentedProducer # @api public attr_reader :the_class # @api public attr_reader :init_args # @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates # @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer # @param scope [Puppet::Parser::Scope] The scope to use for evaluation # @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value # @option options [String] :class_name The name of the class to create instance of # @option options [Array] :init_args ([]) Optional arguments to class constructor # @api public # def initialize(injector, binding, scope, options) # Better do this, even if a transformation of a created instance is kind of an odd thing to do, one can imagine # sending it to a function for further detailing. # super class_name = options[:class_name] raise ArgumentError, "Option 'class_name' must be given for an InstantiatingProducer" unless class_name # get class by name @the_class = Puppet::Pops::Types::ClassLoader.provide(class_name) @init_args = options[:init_args] || [] raise ArgumentError, "Can not load the class #{class_name} specified in binding named: '#{binding.name}'" unless @the_class end protected # Performs initialization the same way as Assisted Inject does (but handle arguments to # constructor) # @api private # def internal_produce(scope) result = nil # A class :inject method wins over an instance :initialize if it is present, unless a more specific # constructor exists. (i.e do not pick :inject from superclass if class has a constructor). # if the_class.respond_to?(:inject) inject_method = the_class.method(:inject) initialize_method = the_class.instance_method(:initialize) if inject_method.owner <= initialize_method.owner result = the_class.inject(injector, scope, binding, *init_args) end end if result.nil? result = the_class.new(*init_args) end result end end # @api public class FirstFoundProducer < Producer # @api public attr_reader :producers # @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates # @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer # @param scope [Puppet::Parser::Scope] The scope to use for evaluation # @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value # @option options [Array] :producers list of producers to consult. Required. # @api public # def initialize(injector, binding, scope, options) super @producers = options[:producers] raise ArgumentError, "Option :producers' must be set to a list of producers." if @producers.nil? raise ArgumentError, "Given 'producers' option is not an Array" unless @producers.is_a?(Array) end protected # @api private def internal_produce(scope) # return the first produced value that is non-nil (unfortunately there is no such enumerable method) producers.reduce(nil) {|memo, p| break memo unless memo.nil?; p.produce(scope)} end end # Evaluates a Puppet Expression and returns the result. # This is typically used for strings with interpolated expressions. # @api public # class EvaluatingProducer < Producer # A Puppet 3 AST Expression # @api public # attr_reader :expression # @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates # @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer # @param scope [Puppet::Parser::Scope] The scope to use for evaluation # @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value # @option options [Array] :expression The expression to evaluate # @api public # def initialize(injector, binding, scope, options) super - expr = options[:expression] - raise ArgumentError, "Option 'expression' must be given to an EvaluatingProducer." unless expr - @expression = Puppet::Pops::Model::AstTransformer.new().transform(expr) + @expression = options[:expression] + raise ArgumentError, "Option 'expression' must be given to an EvaluatingProducer." unless @expression end # @api private def internal_produce(scope) - expression.evaluate(scope) + Puppet::Pops::Parser::EvaluatingParser.new.evaluate(scope, expression) end end # @api public class LookupProducer < AbstractArgumentedProducer # @api public attr_reader :type # @api public attr_reader :name # @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates # @param binder [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer # @param scope [Puppet::Parser::Scope] The scope to use for evaluation # @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value # @option options [Puppet::Pops::Types::PAnyType] :type The type to lookup # @option options [String] :name ('') The name to lookup # @api public # def initialize(injector, binder, scope, options) super @type = options[:type] @name = options[:name] || '' raise ArgumentError, "Option 'type' must be given in a LookupProducer." unless @type end protected # @api private def internal_produce(scope) injector.lookup_type(scope, type, name) end end # @api public class LookupKeyProducer < LookupProducer # @api public attr_reader :key # @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates # @param binder [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer # @param scope [Puppet::Parser::Scope] The scope to use for evaluation # @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value # @option options [Puppet::Pops::Types::PAnyType] :type The type to lookup # @option options [String] :name ('') The name to lookup # @option options [Puppet::Pops::Types::PAnyType] :key The key to lookup in the hash # @api public # def initialize(injector, binder, scope, options) super @key = options[:key] raise ArgumentError, "Option 'key' must be given in a LookupKeyProducer." if key.nil? end protected # @api private def internal_produce(scope) result = super result.is_a?(Hash) ? result[key] : nil end end # Produces the given producer, then uses that producer. # @see ProducerProducer for the non singleton version # @api public # class SingletonProducerProducer < Producer # @api public attr_reader :value_producer # @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates # @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer # @param scope [Puppet::Parser::Scope] The scope to use for evaluation # @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value # @option options [Puppet::Pops::Model::LambdaExpression] :producer_producer a producer of a value producer (required) # @api public # def initialize(injector, binding, scope, options) super p = options[:producer_producer] raise ArgumentError, "Option :producer_producer must be given in a SingletonProducerProducer" unless p @value_producer = p.produce(scope) end protected # @api private def internal_produce(scope) value_producer.produce(scope) end end # A ProducerProducer creates a producer via another producer, and then uses this created producer # to produce values. This is useful for custom production of series of values. # On each request for a producer, this producer will reset its internal producer (i.e. restarting # the series). # # @param producer_producer [#produce(scope)] the producer of the producer # # @api public # class ProducerProducer < Producer # @api public attr_reader :producer_producer # @api public attr_reader :value_producer # Creates new ProducerProducer given a producer. # # @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates # @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer # @param scope [Puppet::Parser::Scope] The scope to use for evaluation # @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value # @option options [Puppet::Pops::Binder::Producer] :producer_producer a producer of a value producer (required) # # @api public # def initialize(injector, binding, scope, options) super unless producer_producer = options[:producer_producer] raise ArgumentError, "The option :producer_producer must be set in a ProducerProducer" end raise ArgumentError, "Argument must be a Producer" unless producer_producer.is_a?(Producer) @producer_producer = producer_producer @value_producer = nil end # Updates the internal state to use a new instance of the wrapped producer. # @api public # def producer(scope) @value_producer = @producer_producer.produce(scope) self end protected # Produces a value after having created an instance of the wrapped producer (if not already created). # @api private # def internal_produce(scope, *args) producer() unless value_producer value_producer.produce(scope) end end # This type of producer should only be created by the Injector. # # @api private # class AssistedInjectProducer < Producer # An Assisted Inject Producer is created when a lookup is made of a type that is # not bound. It does not support a transformer lambda. # @note This initializer has a different signature than all others. Do not use in regular logic. # @api private # def initialize(injector, clazz) raise ArgumentError, "class must be given" unless clazz.is_a?(Class) @injector = injector @clazz = clazz @inst = nil end def produce(scope, *args) producer(scope, *args) unless @inst @inst end # @api private def producer(scope, *args) @inst = nil # A class :inject method wins over an instance :initialize if it is present, unless a more specific zero args # constructor exists. (i.e do not pick :inject from superclass if class has a zero args constructor). # if @clazz.respond_to?(:inject) inject_method = @clazz.method(:inject) initialize_method = @clazz.instance_method(:initialize) if inject_method.owner <= initialize_method.owner || initialize_method.arity != 0 @inst = @clazz.inject(@injector, scope, nil, *args) end end if @inst.nil? unless args.empty? raise ArgumentError, "Assisted Inject can not pass arguments to no-args constructor when there is no class inject method." end @inst = @clazz.new() end self end end # Abstract base class for multibind producers. # Is suitable as base class for custom implementations of multibind producers. # @abstract # @api public # class MultibindProducer < AbstractArgumentedProducer attr_reader :contributions_key # @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates # @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer # @param scope [Puppet::Parser::Scope] The scope to use for evaluation # @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value # # @api public # def initialize(injector, binding, scope, options) super @contributions_key = injector.key_factory.multibind_contributions(binding.id) end # @param expected [Array, Puppet::Pops::Types::PAnyType] expected type or types # @param actual [Object, Puppet::Pops::Types::PAnyType> the actual value (or its type) # @return [String] a formatted string for inclusion as detail in an error message # @api private # def type_error_detail(expected, actual) tc = injector.type_calculator expected = [expected] unless expected.is_a?(Array) actual_t = tc.is_ptype?(actual) ? actual : tc.infer(actual) expstrs = expected.collect {|t| tc.string(t) } "expected: #{expstrs.join(', or ')}, got: #{tc.string(actual_t)}" end end # A configurable multibind producer for Array type multibindings. # # This implementation collects all contributions to the multibind and then combines them using the following rules: # # - all *unnamed* entries are added unless the option `:priority_on_unnamed` is set to true, in which case the unnamed # contribution with the highest priority is added, and the rest are ignored (unless they have the same priority in which # case an error is raised). # - all *named* entries are handled the same way as *unnamed* but the option `:priority_on_named` controls their handling. # - the option `:uniq` post processes the result to only contain unique entries # - the option `:flatten` post processes the result by flattening all nested arrays. # - If both `:flatten` and `:uniq` are true, flattening is done first. # # @note # Collection accepts elements that comply with the array's element type, or the entire type (i.e. Array[element_type]). # If the type is restrictive - e.g. Array[String] and an Array[String] is contributed, the result will not be type # compliant without also using the `:flatten` option, and a type error will be raised. For an array with relaxed typing # i.e. Array[Data], it is valid to produce a result such as `['a', ['b', 'c'], 'd']` and no flattening is required # and no error is raised (but using the array needs to be aware of potential array, non-array entries. # The use of the option `:flatten` controls how the result is flattened. # # @api public # class ArrayMultibindProducer < MultibindProducer # @return [Boolean] whether the result should be made contain unique (non-equal) entries or not # @api public attr_reader :uniq # @return [Boolean, Integer] If result should be flattened (true), or not (false), or flattened to given level (0 = none, -1 = all) # @api public attr_reader :flatten # @return [Boolean] whether priority should be considered for named contributions # @api public attr_reader :priority_on_named # @return [Boolean] whether priority should be considered for unnamed contributions # @api public attr_reader :priority_on_unnamed # @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates # @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer # @param scope [Puppet::Parser::Scope] The scope to use for evaluation # @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value # @option options [Boolean] :uniq (false) if collected result should be post-processed to contain only unique entries # @option options [Boolean, Integer] :flatten (false) if collected result should be post-processed so all contained arrays # are flattened. May be set to an Integer value to indicate the level of recursion (-1 is endless, 0 is none). # @option options [Boolean] :priority_on_named (true) if highest precedented named element should win or if all should be included # @option options [Boolean] :priority_on_unnamed (false) if highest precedented unnamed element should win or if all should be included # @api public # def initialize(injector, binding, scope, options) super @uniq = !!options[:uniq] @flatten = options[:flatten] @priority_on_named = options[:priority_on_named].nil? ? true : options[:priority_on_name] @priority_on_unnamed = !!options[:priority_on_unnamed] case @flatten when Integer when true @flatten = -1 when false @flatten = nil when NilClass @flatten = nil else raise ArgumentError, "Option :flatten must be nil, Boolean, or an integer value" unless @flatten.is_a?(Integer) end end protected # @api private def internal_produce(scope) seen = {} included_keys = [] injector.get_contributions(scope, contributions_key).each do |element| key = element[0] entry = element[1] name = entry.binding.name existing = seen[name] empty_name = name.nil? || name.empty? if existing if empty_name && priority_on_unnamed if (seen[name] <=> entry) >= 0 raise ArgumentError, "Duplicate key (same priority) contributed to Array Multibinding '#{binding.name}' with unnamed entry." end next elsif !empty_name && priority_on_named if (seen[name] <=> entry) >= 0 raise ArgumentError, "Duplicate key (same priority) contributed to Array Multibinding '#{binding.name}', key: '#{name}'." end next end else seen[name] = entry end included_keys << key end result = included_keys.collect do |k| x = injector.lookup_key(scope, k) assert_type(binding(), injector.type_calculator(), x) x end result.flatten!(flatten) if flatten result.uniq! if uniq result end # @api private def assert_type(binding, tc, value) infered = tc.infer(value) unless tc.assignable?(binding.type.element_type, infered) || tc.assignable?(binding.type, infered) raise ArgumentError, ["Type Error: contribution to '#{binding.name}' does not match type of multibind, ", "#{type_error_detail([binding.type.element_type, binding.type], value)}"].join() end end end # @api public class HashMultibindProducer < MultibindProducer # @return [Symbol] One of `:error`, `:merge`, `:append`, `:priority`, `:ignore` # @api public attr_reader :conflict_resolution # @return [Boolean] # @api public attr_reader :uniq # @return [Boolean, Integer] Flatten all if true, or none if false, or to given level (0 = none, -1 = all) # @api public attr_reader :flatten # The hash multibind producer provides options to control conflict resolution. # By default, the hash is produced using `:priority` resolution - the highest entry is selected, the rest are # ignored unless they have the same priority which is an error. # # @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates # @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer # @param scope [Puppet::Parser::Scope] The scope to use for evaluation # @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value # @option options [Symbol, String] :conflict_resolution (:priority) One of `:error`, `:merge`, `:append`, `:priority`, `:ignore` #
  • `ignore` the first found highest priority contribution is used, the rest are ignored
  • #
  • `error` any duplicate key is an error
  • #
  • `append` element type must be compatible with Array, makes elements be arrays and appends all found
  • #
  • `merge` element type must be compatible with hash, merges hashes with retention of highest priority hash content
  • #
  • `priority` the first found highest priority contribution is used, duplicates with same priority raises and error, the rest are # ignored.
# @option options [Boolean, Integer] :flatten (false) If appended should be flattened. Also see {#flatten}. # @option options [Boolean] :uniq (false) If appended result should be made unique. # # @api public # def initialize(injector, binding, scope, options) super @conflict_resolution = options[:conflict_resolution].nil? ? :priority : options[:conflict_resolution] @uniq = !!options[:uniq] @flatten = options[:flatten] unless [:error, :merge, :append, :priority, :ignore].include?(@conflict_resolution) raise ArgumentError, "Unknown conflict_resolution for Multibind Hash: '#{@conflict_resolution}." end case @flatten when Integer when true @flatten = -1 when false @flatten = nil when NilClass @flatten = nil else raise ArgumentError, "Option :flatten must be nil, Boolean, or an integer value" unless @flatten.is_a?(Integer) end if uniq || flatten || conflict_resolution.to_s == 'append' etype = binding.type.element_type unless etype.class == Puppet::Pops::Types::PDataType || etype.is_a?(Puppet::Pops::Types::PArrayType) detail = [] detail << ":uniq" if uniq detail << ":flatten" if flatten detail << ":conflict_resolution => :append" if conflict_resolution.to_s == 'append' raise ArgumentError, ["Options #{detail.join(', and ')} cannot be used with a Multibind ", "of type #{injector.type_calculator.string(binding.type)}"].join() end end end protected # @api private def internal_produce(scope) seen = {} included_entries = [] injector.get_contributions(scope, contributions_key).each do |element| key = element[0] entry = element[1] name = entry.binding.name raise ArgumentError, "A Hash Multibind contribution to '#{binding.name}' must have a name." if name.nil? || name.empty? existing = seen[name] if existing case conflict_resolution.to_s when 'priority' # skip if duplicate has lower prio if (comparison = (seen[name] <=> entry)) <= 0 raise ArgumentError, "Internal Error: contributions not given in decreasing precedence order" unless comparison == 0 raise ArgumentError, "Duplicate key (same priority) contributed to Hash Multibinding '#{binding.name}', key: '#{name}'." end next when 'ignore' # skip, ignore conflict if prio is the same next when 'error' raise ArgumentError, "Duplicate key contributed to Hash Multibinding '#{binding.name}', key: '#{name}'." end else seen[name] = entry end included_entries << [key, entry] end result = {} included_entries.each do |element| k = element[ 0 ] entry = element[ 1 ] x = injector.lookup_key(scope, k) name = entry.binding.name assert_type(binding(), injector.type_calculator(), name, x) if result[ name ] merge(result, name, result[ name ], x) else result[ name ] = conflict_resolution().to_s == 'append' ? [x] : x end end result end # @api private def merge(result, name, higher, lower) case conflict_resolution.to_s when 'append' unless higher.is_a?(Array) higher = [higher] end tmp = higher + [lower] tmp.flatten!(flatten) if flatten tmp.uniq! if uniq result[name] = tmp when 'merge' result[name] = lower.merge(higher) end end # @api private def assert_type(binding, tc, key, value) unless tc.instance?(binding.type.key_type, key) raise ArgumentError, ["Type Error: key contribution to #{binding.name}['#{key}'] ", "is incompatible with key type: #{tc.label(binding.type)}, ", type_error_detail(binding.type.key_type, key)].join() end if key.nil? || !key.is_a?(String) || key.empty? raise ArgumentError, "Entry contributing to multibind hash with id '#{binding.id}' must have a name." end unless tc.instance?(binding.type.element_type, value) raise ArgumentError, ["Type Error: value contribution to #{binding.name}['#{key}'] ", "is incompatible, ", type_error_detail(binding.type.element_type, value)].join() end end end end diff --git a/lib/puppet/pops/evaluator/runtime3_support.rb b/lib/puppet/pops/evaluator/runtime3_support.rb index ca06e632a..2232e14bc 100644 --- a/lib/puppet/pops/evaluator/runtime3_support.rb +++ b/lib/puppet/pops/evaluator/runtime3_support.rb @@ -1,544 +1,543 @@ # A module with bindings between the new evaluator and the 3x runtime. # The intention is to separate all calls into scope, compiler, resource, etc. in this module # to make it easier to later refactor the evaluator for better implementations of the 3x classes. # # @api private module Puppet::Pops::Evaluator::Runtime3Support # Fails the evaluation of _semantic_ with a given issue. # # @param issue [Puppet::Pops::Issue] the issue to report # @param semantic [Puppet::Pops::ModelPopsObject] the object for which evaluation failed in some way. Used to determine origin. # @param options [Hash] hash of optional named data elements for the given issue # @return [!] this method does not return # @raise [Puppet::ParseError] an evaluation error initialized from the arguments (TODO: Change to EvaluationError?) # def fail(issue, semantic, options={}, except=nil) optionally_fail(issue, semantic, options, except) # an error should have been raised since fail always fails raise ArgumentError, "Internal Error: Configuration of runtime error handling wrong: should have raised exception" end # Optionally (based on severity) Fails the evaluation of _semantic_ with a given issue # If the given issue is configured to be of severity < :error it is only reported, and the function returns. # # @param issue [Puppet::Pops::Issue] the issue to report # @param semantic [Puppet::Pops::ModelPopsObject] the object for which evaluation failed in some way. Used to determine origin. # @param options [Hash] hash of optional named data elements for the given issue # @return [!] this method does not return # @raise [Puppet::ParseError] an evaluation error initialized from the arguments (TODO: Change to EvaluationError?) # def optionally_fail(issue, semantic, options={}, except=nil) if except.nil? # Want a stacktrace, and it must be passed as an exception begin raise EvaluationError.new() rescue EvaluationError => e except = e end end diagnostic_producer.accept(issue, semantic, options, except) end # Binds the given variable name to the given value in the given scope. # The reference object `o` is intended to be used for origin information - the 3x scope implementation # only makes use of location when there is an error. This is now handled by other mechanisms; first a check # is made if a variable exists and an error is raised if attempting to change an immutable value. Errors # in name, numeric variable assignment etc. have also been validated prior to this call. In the event the # scope.setvar still raises an error, the general exception handling for evaluation of the assignment # expression knows about its location. Because of this, there is no need to extract the location for each # setting (extraction is somewhat expensive since 3x requires line instead of offset). # def set_variable(name, value, o, scope) # Scope also checks this but requires that location information are passed as options. # Those are expensive to calculate and a test is instead made here to enable failing with better information. # The error is not specific enough to allow catching it - need to check the actual message text. # TODO: Improve the messy implementation in Scope. # if scope.bound?(name) if Puppet::Parser::Scope::RESERVED_VARIABLE_NAMES.include?(name) fail(Puppet::Pops::Issues::ILLEGAL_RESERVED_ASSIGNMENT, o, {:name => name} ) else fail(Puppet::Pops::Issues::ILLEGAL_REASSIGNMENT, o, {:name => name} ) end end scope.setvar(name, value) end # Returns the value of the variable (nil is returned if variable has no value, or if variable does not exist) # def get_variable_value(name, o, scope) # Puppet 3x stores all variables as strings (then converts them back to numeric with a regexp... to see if it is a match variable) # Not ideal, scope should support numeric lookup directly instead. # TODO: consider fixing scope catch(:undefined_variable) { return scope.lookupvar(name.to_s) } # It is always ok to reference numeric variables even if they are not assigned. They are always undef # if not set by a match expression. # unless name =~ Puppet::Pops::Patterns::NUMERIC_VAR_NAME fail(Puppet::Pops::Issues::UNKNOWN_VARIABLE, o, {:name => name}) end end # Returns true if the variable of the given name is set in the given most nested scope. True is returned even if # variable is bound to nil. # def variable_bound?(name, scope) scope.bound?(name.to_s) end # Returns true if the variable is bound to a value or nil, in the scope or it's parent scopes. # def variable_exists?(name, scope) scope.exist?(name.to_s) end def set_match_data(match_data, scope) # See set_variable for rationale for not passing file and line to ephemeral_from. # NOTE: The 3x scope adds one ephemeral(match) to its internal stack per match that succeeds ! It never # clears anything. Thus a context that performs many matches will get very deep (there simply is no way to # clear the match variables without rolling back the ephemeral stack.) # This implementation does not attempt to fix this, it behaves the same bad way. unless match_data.nil? scope.ephemeral_from(match_data) end end # Creates a local scope with vairalbes set from a hash of variable name to value # def create_local_scope_from(hash, scope) # two dummy values are needed since the scope tries to give an error message (can not happen in this # case - it is just wrong, the error should be reported by the caller who knows in more detail where it # is in the source. # raise ArgumentError, "Internal error - attempt to create a local scope without a hash" unless hash.is_a?(Hash) scope.ephemeral_from(hash) end # Creates a nested match scope def create_match_scope_from(scope) # Create a transparent match scope (for future matches) scope.new_match_scope(nil) end def get_scope_nesting_level(scope) scope.ephemeral_level end def set_scope_nesting_level(scope, level) # Yup, 3x uses this method to reset the level, it also supports passing :all to destroy all # ephemeral/local scopes - which is a sure way to create havoc. # scope.unset_ephemeral_var(level) end # Adds a relationship between the given `source` and `target` of the given `relationship_type` # @param source [Puppet:Pops::Types::PCatalogEntryType] the source end of the relationship (from) # @param target [Puppet:Pops::Types::PCatalogEntryType] the target end of the relationship (to) # @param relationship_type [:relationship, :subscription] the type of the relationship # def add_relationship(source, target, relationship_type, scope) # The 3x way is to record a Puppet::Parser::Relationship that is evaluated at the end of the compilation. # This means it is not possible to detect any duplicates at this point (and signal where an attempt is made to # add a duplicate. There is also no location information to signal the original place in the logic. The user will have # to go fish. # The 3.x implementation is based on Strings :-o, so the source and target must be transformed. The resolution is # done by Catalog#resource(type, title). To do that, it creates a Puppet::Resource since it is responsible for # translating the name/type/title and create index-keys used by the catalog. The Puppet::Resource has bizarre parsing of # the type and title (scan for [] that is interpreted as type/title (but it gets it wrong). # Moreover if the type is "" or "component", the type is Class, and if the type is :main, it is :main, all other cases # undergo capitalization of name-segments (foo::bar becomes Foo::Bar). (This was earlier done in the reverse by the parser). # Further, the title undergoes the same munging !!! # # That bug infested nest of messy logic needs serious Exorcism! # # Unfortunately it is not easy to simply call more intelligent methods at a lower level as the compiler evaluates the recorded # Relationship object at a much later point, and it is responsible for invoking all the messy logic. # # TODO: Revisit the below logic when there is a sane implementation of the catalog, compiler and resource. For now # concentrate on transforming the type references to what is expected by the wacky logic. # # HOWEVER, the Compiler only records the Relationships, and the only method it calls is @relationships.each{|x| x.evaluate(catalog) } # Which means a smarter Relationship class could do this right. Instead of obtaining the resource from the catalog using # the borked resource(type, title) which creates a resource for the purpose of looking it up, it needs to instead # scan the catalog's resources # # GAAAH, it is even worse! # It starts in the parser, which parses "File['foo']" into an AST::ResourceReference with type = File, and title = foo # This AST is evaluated by looking up the type/title in the scope - causing it to be loaded if it exists, and if not, the given # type name/title is used. It does not search for resource instances, only classes and types. It returns symbolic information # [type, [title, title]]. From this, instances of Puppet::Resource are created and returned. These only have type/title information # filled out. One or an array of resources are returned. # This set of evaluated (empty reference) Resource instances are then passed to the relationship operator. It creates a # Puppet::Parser::Relationship giving it a source and a target that are (empty reference) Resource instances. These are then remembered # until the relationship is evaluated by the compiler (at the end). When evaluation takes place, the (empty reference) Resource instances # are converted to String (!?! WTF) on the simple format "#{type}[#{title}]", and the catalog is told to find a resource, by giving # it this string. If it cannot find the resource it fails, else the before/notify parameter is appended with the target. # The search for the resource begin with (you guessed it) again creating an (empty reference) resource from type and title (WTF?!?!). # The catalog now uses the reference resource to compute a key [r.type, r.title.to_s] and also gets a uniqueness key from the # resource (This is only a reference type created from title and type). If it cannot find it with the first key, it uses the # uniqueness key to lookup. # # This is probably done to allow a resource type to munge/translate the title in some way (but it is quite unclear from the long # and convoluted path of evaluation. # In order to do this in a way that is similar to 3.x two resources are created to be used as keys. # # And if that is not enough, a source/target may be a Collector (a baked query that will be evaluated by the # compiler - it is simply passed through here for processing by the compiler at the right time). # if source.is_a?(Puppet::Parser::Collector) # use verbatim - behavior defined by 3x source_resource = source else # transform into the wonderful String representation in 3x type, title = catalog_type_to_split_type_title(source) source_resource = Puppet::Resource.new(type, title) end if target.is_a?(Puppet::Parser::Collector) # use verbatim - behavior defined by 3x target_resource = target else # transform into the wonderful String representation in 3x type, title = catalog_type_to_split_type_title(target) target_resource = Puppet::Resource.new(type, title) end # Add the relationship to the compiler for later evaluation. scope.compiler.add_relationship(Puppet::Parser::Relationship.new(source_resource, target_resource, relationship_type)) end # Coerce value `v` to numeric or fails. # The given value `v` is coerced to Numeric, and if that fails the operation # calls {#fail}. # @param v [Object] the value to convert # @param o [Object] originating instruction # @param scope [Object] the (runtime specific) scope where evaluation of o takes place # @return [Numeric] value `v` converted to Numeric. # def coerce_numeric(v, o, scope) unless n = Puppet::Pops::Utils.to_n(v) fail(Puppet::Pops::Issues::NOT_NUMERIC, o, {:value => v}) end n end def call_function(name, args, o, scope) # Call via 4x API if the function exists there loaders = scope.compiler.loaders # find the loader that loaded the code, or use the private_environment_loader (sees env + all modules) adapter = Puppet::Pops::Utils.find_adapter(o, Puppet::Pops::Adapters::LoaderAdapter) loader = adapter.nil? ? loaders.private_environment_loader : adapter.loader if loader && func = loader.load(:function, name) return func.call(scope, *args) end # Call via 3x API if function exists there fail(Puppet::Pops::Issues::UNKNOWN_FUNCTION, o, {:name => name}) unless Puppet::Parser::Functions.function(name) - # TODO: if Puppet[:biff] == true, then 3x functions should be called via loaders above # Arguments must be mapped since functions are unaware of the new and magical creatures in 4x. # NOTE: Passing an empty string last converts nil/:undef to empty string mapped_args = args.map {|a| convert(a, scope, '') } result = scope.send("function_#{name}", mapped_args) # Prevent non r-value functions from leaking their result (they are not written to care about this) Puppet::Parser::Functions.rvalue?(name) ? result : nil end # The o is used for source reference def create_resource_parameter(o, scope, name, value, operator) file, line = extract_file_line(o) Puppet::Parser::Resource::Param.new( :name => name, # Here we must convert nil values to :undef for the 3x logic to work :value => convert(value, scope, :undef), # converted to 3x since 4x supports additional objects / types :source => scope.source, :line => line, :file => file, :add => operator == :'+>' ) end def create_resources(o, scope, virtual, exported, type_name, resource_titles, evaluated_parameters) # TODO: Unknown resource causes creation of Resource to fail with ArgumentError, should give # a proper Issue. Now the result is "Error while evaluating a Resource Statement" with the message # from the raised exception. (It may be good enough). # resolve in scope. fully_qualified_type, resource_titles = scope.resolve_type_and_titles(type_name, resource_titles) # Not 100% accurate as this is the resource expression location and each title is processed separately # The titles are however the result of evaluation and they have no location at this point (an array # of positions for the source expressions are required for this to work). # TODO: Revisit and possible improve the accuracy. # file, line = extract_file_line(o) # Build a resource for each title resource_titles.map do |resource_title| resource = Puppet::Parser::Resource.new( fully_qualified_type, resource_title, :parameters => evaluated_parameters, :file => file, :line => line, :exported => exported, :virtual => virtual, # WTF is this? Which source is this? The file? The name of the context ? :source => scope.source, :scope => scope, :strict => true ) if resource.resource_type.is_a? Puppet::Resource::Type resource.resource_type.instantiate_resource(scope, resource) end scope.compiler.add_resource(scope, resource) scope.compiler.evaluate_classes([resource_title], scope, false, true) if fully_qualified_type == 'class' # Turn the resource into a PType (a reference to a resource type) # weed out nil's resource_to_ptype(resource) end end # Defines default parameters for a type with the given name. # def create_resource_defaults(o, scope, type_name, evaluated_parameters) # Note that name must be capitalized in this 3x call # The 3x impl creates a Resource instance with a bogus title and then asks the created resource # for the type of the name. # Note, locations are available per parameter. # scope.define_settings(capitalize_qualified_name(type_name), evaluated_parameters) end # Capitalizes each segment of a qualified name # def capitalize_qualified_name(name) name.split(/::/).map(&:capitalize).join('::') end # Creates resource overrides for all resource type objects in evaluated_resources. The same set of # evaluated parameters are applied to all. # def create_resource_overrides(o, scope, evaluated_resources, evaluated_parameters) # Not 100% accurate as this is the resource expression location and each title is processed separately # The titles are however the result of evaluation and they have no location at this point (an array # of positions for the source expressions are required for this to work. # TODO: Revisit and possible improve the accuracy. # file, line = extract_file_line(o) evaluated_resources.each do |r| resource = Puppet::Parser::Resource.new( r.type_name, r.title, :parameters => evaluated_parameters, :file => file, :line => line, # WTF is this? Which source is this? The file? The name of the context ? :source => scope.source, :scope => scope ) scope.compiler.add_override(resource) end end # Finds a resource given a type and a title. # def find_resource(scope, type_name, title) scope.compiler.findresource(type_name, title) end # Returns the value of a resource's parameter by first looking up the parameter in the resource # and then in the defaults for the resource. Since the resource exists (it must in order to look up its # parameters, any overrides have already been applied). Defaults are not applied to a resource until it # has been finished (which typically has not taken place when this is evaluated; hence the dual lookup). # def get_resource_parameter_value(scope, resource, parameter_name) # This gets the parameter value, or nil (for both valid parameters and parameters that do not exist). val = resource[parameter_name] # The defaults must be looked up in the scope where the resource was created (not in the given # scope where the lookup takes place. resource_scope = resource.scope if val.nil? && resource_scope && defaults = resource_scope.lookupdefaults(resource.type) # NOTE: 3x resource keeps defaults as hash using symbol for name as key to Parameter which (again) holds # name and value. # NOTE: meta parameters that are unset ends up here, and there are no defaults for those encoded # in the defaults, they may receive hardcoded defaults later (e.g. 'tag'). param = defaults[parameter_name.to_sym] # Some parameters (meta parameters like 'tag') does not return a param from which the value can be obtained # at all times. Instead, they return a nil param until a value has been set. val = param.nil? ? nil : param.value end val end # Returns true, if the given name is the name of a resource parameter. # def is_parameter_of_resource?(scope, resource, name) resource.valid_parameter?(name) end def resource_to_ptype(resource) nil if resource.nil? type_calculator.infer(resource) end # This is the same type of "truth" as used in the current Puppet DSL. # def is_true? o # Is the value true? This allows us to control the definition of truth # in one place. case o # Support :undef since it may come from a 3x structure when :undef false else !!o end end # Utility method for TrueClass || FalseClass # @param x [Object] the object to test if it is instance of TrueClass or FalseClass def is_boolean? x x.is_a?(TrueClass) || x.is_a?(FalseClass) end def initialize @@convert_visitor ||= Puppet::Pops::Visitor.new(self, "convert", 2, 2) end # Converts 4x supported values to 3x values. This is required because # resources and other objects do not know about the new type system, and does not support # regular expressions. Unfortunately this has to be done for array and hash as well. # A complication is that catalog types needs to be resolved against the scope. # def convert(o, scope, undef_value) @@convert_visitor.visit_this_2(self, o, scope, undef_value) end def convert_NilClass(o, scope, undef_value) undef_value end def convert_Object(o, scope, undef_value) o end def convert_Array(o, scope, undef_value) o.map {|x| convert(x, scope, undef_value) } end def convert_Hash(o, scope, undef_value) result = {} o.each {|k,v| result[convert(k, scope, undef_value)] = convert(v, scope, undef_value) } result end def convert_Regexp(o, scope, undef_value) # Puppet 3x cannot handle parameter values that are reqular expressions. Turn into regexp string in # source form o.inspect end def convert_Symbol(o, scope, undef_value) case o # Support :undef since it may come from a 3x structure when :undef undef_value # 3x wants undef as either empty string or :undef else o # :default, and all others are verbatim since they are new in future evaluator end end def convert_PAbstractType(o, scope, undef_value) o end def convert_PCatalogEntryType(o, scope, undef_value) # Since 4x does not support dynamic scoping, all names are absolute and can be # used as is (with some check/transformation/mangling between absolute/relative form # due to Puppet::Resource's idiosyncratic behavior where some references must be # absolute and others cannot be. # Thus there is no need to call scope.resolve_type_and_titles to do dynamic lookup. Puppet::Resource.new(*catalog_type_to_split_type_title(o)) end private # Produces an array with [type, title] from a PCatalogEntryType # This method is used to produce the arguments for creation of reference resource instances # (used when 3x is operating on a resource). # Ensures that resources are *not* absolute. # def catalog_type_to_split_type_title(catalog_type) split_type = catalog_type.is_a?(Puppet::Pops::Types::PType) ? catalog_type.type : catalog_type case split_type when Puppet::Pops::Types::PHostClassType class_name = split_type.class_name ['class', class_name.nil? ? nil : class_name.sub(/^::/, '')] when Puppet::Pops::Types::PResourceType type_name = split_type.type_name title = split_type.title if type_name =~ /^(::)?[Cc]lass/ ['class', title.nil? ? nil : title.sub(/^::/, '')] else # Ensure that title is '' if nil # Resources with absolute name always results in error because tagging does not support leading :: [type_name.nil? ? nil : type_name.sub(/^::/, ''), title.nil? ? '' : title] end else raise ArgumentError, "Cannot split the type #{catalog_type.class}, it represents neither a PHostClassType, nor a PResourceType." end end def extract_file_line(o) source_pos = Puppet::Pops::Utils.find_closest_positioned(o) return [nil, -1] unless source_pos [source_pos.locator.file, source_pos.line] end def find_closest_positioned(o) return nil if o.nil? || o.is_a?(Puppet::Pops::Model::Program) o.offset.nil? ? find_closest_positioned(o.eContainer) : Puppet::Pops::Adapters::SourcePosAdapter.adapt(o) end # Creates a diagnostic producer def diagnostic_producer Puppet::Pops::Validation::DiagnosticProducer.new( ExceptionRaisingAcceptor.new(), # Raises exception on all issues SeverityProducer.new(), # All issues are errors Puppet::Pops::Model::ModelLabelProvider.new()) end # Configure the severity of failures class SeverityProducer < Puppet::Pops::Validation::SeverityProducer Issues = Puppet::Pops::Issues def initialize super p = self # Issues triggering warning only if --debug is on if Puppet[:debug] p[Issues::EMPTY_RESOURCE_SPECIALIZATION] = :warning else p[Issues::EMPTY_RESOURCE_SPECIALIZATION] = :ignore end end end # An acceptor of diagnostics that immediately raises an exception. class ExceptionRaisingAcceptor < Puppet::Pops::Validation::Acceptor def accept(diagnostic) super Puppet::Pops::IssueReporter.assert_and_report(self, {:message => "Evaluation Error:", :emit_warnings => true }) if errors? raise ArgumentError, "Internal Error: Configuration of runtime error handling wrong: should have raised exception" end end end class EvaluationError < StandardError end end diff --git a/lib/puppet/pops/loader/loader_paths.rb b/lib/puppet/pops/loader/loader_paths.rb index a431a4801..09bb7e5b0 100644 --- a/lib/puppet/pops/loader/loader_paths.rb +++ b/lib/puppet/pops/loader/loader_paths.rb @@ -1,137 +1,118 @@ # LoaderPaths # === # The central loader knowledge about paths, what they represent and how to instantiate from them. # Contains helpers (*smart paths*) to deal with lazy resolution of paths. # # TODO: Currently only supports loading of functions (3 kinds) # module Puppet::Pops::Loader::LoaderPaths # Returns an array of SmartPath, each instantiated with a reference to the given loader (for root path resolution # and existence checks). The smart paths in the array appear in precedence order. The returned array may be # mutated. # - def self.relative_paths_for_type(type, loader) #, start_index_in_name) + def self.relative_paths_for_type(type, loader) result = - case type # typed_name.type + case type when :function - if Puppet[:biff] == true - [FunctionPath4x.new(loader), FunctionPath3x.new(loader)] - else [FunctionPath4x.new(loader)] - end - - # when :xxx # TODO: Add all other types - else # unknown types, simply produce an empty result; no paths to check, nothing to find... move along... [] end result end # # DO NOT REMOVE YET. needed later? when there is the need to decamel a classname # def de_camel(fq_name) # fq_name.to_s.gsub(/::/, '/'). # gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2'). # gsub(/([a-z\d])([A-Z])/,'\1_\2'). # tr("-", "_"). # downcase # end class SmartPath # Generic path, in the sense of "if there are any entities of this kind to load, where are they?" attr_reader :generic_path # Creates SmartPath for the given loader (loader knows how to check for existence etc.) def initialize(loader) @loader = loader end def generic_path() return @generic_path unless @generic_path.nil? root_path = @loader.path @generic_path = (root_path.nil? ? relative_path : File.join(root_path, relative_path)) end # Effective path is the generic path + the name part(s) + extension. # def effective_path(typed_name, start_index_in_name) "#{File.join(generic_path, typed_name.name_parts)}#{extension}" end def relative_path() raise NotImplementedError.new end def instantiator() raise NotImplementedError.new end end class RubySmartPath < SmartPath def extension ".rb" end # Duplication of extension information, but avoids one call def effective_path(typed_name, start_index_in_name) "#{File.join(generic_path, typed_name.name_parts)}.rb" end end class FunctionPath4x < RubySmartPath FUNCTION_PATH_4X = File.join('lib', 'puppet', 'functions') def relative_path FUNCTION_PATH_4X end def instantiator() Puppet::Pops::Loader::RubyFunctionInstantiator end end - class FunctionPath3x < RubySmartPath - FUNCTION_PATH_3X = File.join('lib', 'puppet', 'parser', 'functions') - - def relative_path - FUNCTION_PATH_3X - end - - def instantiator() - Puppet::Pops::Loader::RubyLegacyFunctionInstantiator - end - end - # SmartPaths # === # Holds effective SmartPath instances per type # class SmartPaths def initialize(path_based_loader) @loader = path_based_loader @smart_paths = {} end # Ensures that the paths for the type have been probed and pruned to what is existing relative to # the given root. # # @param type [Symbol] the entity type to load # @return [Array] array of effective paths for type (may be empty) # def effective_paths(type) smart_paths = @smart_paths loader = @loader unless effective_paths = smart_paths[type] # type not yet processed, does the various directories for the type exist ? # Get the relative dirs for the type paths_for_type = Puppet::Pops::Loader::LoaderPaths.relative_paths_for_type(type, loader) # Check which directories exist in the loader's content/index effective_paths = smart_paths[type] = paths_for_type.select { |sp| loader.meaningful_to_search?(sp) } end effective_paths end end end diff --git a/lib/puppet/pops/loader/ruby_legacy_function_instantiator.rb b/lib/puppet/pops/loader/ruby_legacy_function_instantiator.rb deleted file mode 100644 index ed98b6f77..000000000 --- a/lib/puppet/pops/loader/ruby_legacy_function_instantiator.rb +++ /dev/null @@ -1,108 +0,0 @@ -# The RubyLegacyFunctionInstantiator loads a 3x function and turns it into a 4x function -# that is called with 3x semantics (values are transformed to be 3x compliant). -# -# The code is loaded from a string obtained by reading the 3x function ruby code into a string -# and then passing it to the loaders class method `create`. When Puppet[:biff] == true, the -# 3x Puppet::Parser::Function.newfunction method relays back to this function loader's -# class method legacy_newfunction which creates a Puppet::Functions class wrapping the -# 3x function's block into a method in a function class derived from Puppet::Function. -# This class is then returned, and the Legacy loader continues the same way as it does -# for a 4x function. -# -# TODO: Wrapping of Scope -# The 3x function expects itself to be Scope. It passes itself as scope to other parts of the runtime, -# it expects to find all sorts of information in itself, get/set variables, get compiler, get environment -# etc. -# TODO: Transformation of arguments to 3x compliant objects -# -class Puppet::Pops::Loader::RubyLegacyFunctionInstantiator - - # Produces an instance of the Function class with the given typed_name, or fails with an error if the - # given ruby source does not produce this instance when evaluated. - # - # @param loader [Puppet::Pops::Loader::Loader] The loader the function is associated with - # @param typed_name [Puppet::Pops::Loader::TypedName] the type / name of the function to load - # @param source_ref [URI, String] a reference to the source / origin of the ruby code to evaluate - # @param ruby_code_string [String] ruby code in a string - # - # @return [Puppet::Pops::Functions.Function] - an instantiated function with global scope closure associated with the given loader - # - def self.create(loader, typed_name, source_ref, ruby_code_string) - # Old Ruby API supports calling a method via :: - # this must also be checked as well as call with '.' - # - unless ruby_code_string.is_a?(String) && ruby_code_string =~ /Puppet\:\:Parser\:\:Functions(?:\.|\:\:)newfunction/ - raise ArgumentError, "The code loaded from #{source_ref} does not seem to be a Puppet 3x API function - no newfunction call." - end - - # The evaluation of the 3x function creation source should result in a call to the legacy_newfunction - # - created = eval(ruby_code_string, nil, source_ref, 1) - unless created.is_a?(Class) - raise ArgumentError, "The code loaded from #{source_ref} did not produce a Function class when evaluated. Got '#{created.class}'" - end - unless created.name.to_s == typed_name.name() - raise ArgumentError, "The code loaded from #{source_ref} produced mis-matched name, expected '#{typed_name.name}', got #{created.name}" - end - # create the function instance - it needs closure (scope), and loader (i.e. where it should start searching for things - # when calling functions etc. - # It should be bound to global scope - - # TODO: Cheating wrt. scope - assuming it is found in the context - closure_scope = Puppet.lookup(:global_scope) { {} } - created.new(closure_scope, loader) - end - - # This is a new implementation of the method that is used in 3x to create a function. - # The arguments are the same as those passed to Puppet::Parser::Functions.newfunction, hence its - # deviation from regular method naming practice. - # - def self.legacy_newfunction(name, options, &block) - - # 3x api allows arity to be specified, if unspecified it is 0 or more arguments - # arity >= 0, is an exact count - # airty < 0 is the number of required arguments -1 (i.e. -1 is 0 or more) - # (there is no upper cap, there is no support for optional values, or defaults) - # - arity = options[:arity] || -1 - if arity >= 0 - min_arg_count = arity - max_arg_count = arity - else - min_arg_count = (arity + 1).abs - # infinity - max_arg_count = :default - end - - # Create a 4x function wrapper around the 3x Function - created_function_class = Puppet::Functions.create_function(name) do - # define a method on the new Function class with the same name as the function, but - # padded with __ because the function may represent a ruby method with the same name that - # expects to have inherited from Kernel, and then Object. - # (This can otherwise lead to infinite recursion, or that an ArgumentError is raised). - # - __name__ = :"__#{name}__" - define_method(__name__, &block) - - # Define the method that is called from dispatch - this method just changes a call - # with multiple unknown arguments to passing all in an array (since this is expected in the 3x API). - # We want the call to be checked for type and number of arguments so cannot call the function - # defined by the block directly since it is defined to take a single argument. - # - define_method(:__relay__call__) do |*args| - # dup the args since the function may destroy them - # TODO: Should convert arguments to 3x, now nil values are sent to the function - send(__name__, args.dup) - end - - # Define a dispatch that performs argument type/count checking - # - dispatch :__relay__call__ do - param 'Any', 'args' - # Specify arg count (transformed from 3x function arity specification). - arg_count(min_arg_count, max_arg_count) - end - end - created_function_class - end -end diff --git a/lib/puppet/pops/parser/evaluating_parser.rb b/lib/puppet/pops/parser/evaluating_parser.rb index 572de0bcc..596f549bc 100644 --- a/lib/puppet/pops/parser/evaluating_parser.rb +++ b/lib/puppet/pops/parser/evaluating_parser.rb @@ -1,156 +1,140 @@ # Does not support "import" and parsing ruby files # class Puppet::Pops::Parser::EvaluatingParser attr_reader :parser def initialize() @parser = Puppet::Pops::Parser::Parser.new() end def parse_string(s, file_source = 'unknown') @file_source = file_source clear() # Handling of syntax error can be much improved (in general), now it bails out of the parser # and does not have as rich information (when parsing a string), need to update it with the file source # (ideally, a syntax error should be entered as an issue, and not just thrown - but that is a general problem # and an improvement that can be made in the eparser (rather than here). # Also a possible improvement (if the YAML parser returns positions) is to provide correct output of position. # begin assert_and_report(parser.parse_string(s)) rescue Puppet::ParseError => e # TODO: This is not quite right, why does not the exception have the correct file? e.file = @file_source unless e.file.is_a?(String) && !e.file.empty? raise e end end def parse_file(file) @file_source = file clear() assert_and_report(parser.parse_file(file)) end def evaluate_string(scope, s, file_source='unknown') evaluate(scope, parse_string(s, file_source)) end def evaluate_file(file) evaluate(parse_file(file)) end def clear() @acceptor = nil end + # Create a closure that can be called in the given scope + def closure(model, scope) + Puppet::Pops::Evaluator::Closure.new(evaluator, model, scope) + end + def evaluate(scope, model) return nil unless model - ast = Puppet::Pops::Model::AstTransformer.new(@file_source, nil).transform(model) - return nil unless ast - ast.safeevaluate(scope) + evaluator.evaluate(model, scope) + end + + def evaluator + @@evaluator ||= Puppet::Pops::Evaluator::EvaluatorImpl.new() + @@evaluator end def validate(parse_result) resulting_acceptor = acceptor() validator(resulting_acceptor).validate(parse_result) resulting_acceptor end def acceptor() Puppet::Pops::Validation::Acceptor.new end def validator(acceptor) - Puppet::Pops::Validation::ValidatorFactory_3_1.new().validator(acceptor) + Puppet::Pops::Validation::ValidatorFactory_4_0.new().validator(acceptor) end def assert_and_report(parse_result) return nil unless parse_result if parse_result.source_ref.nil? or parse_result.source_ref == '' parse_result.source_ref = @file_source end validation_result = validate(parse_result) Puppet::Pops::IssueReporter.assert_and_report(validation_result, :emit_warnings => true) parse_result end def quote(x) self.class.quote(x) end # Translates an already parsed string that contains control characters, quotes # and backslashes into a quoted string where all such constructs have been escaped. # Parsing the return value of this method using the puppet parser should yield # exactly the same string as the argument passed to this method # # The method makes an exception for the two character sequences \$ and \s. They # will not be escaped since they have a special meaning in puppet syntax. # # TODO: Handle \uXXXX characters ?? # # @param x [String] The string to quote and "unparse" # @return [String] The quoted string # def self.quote(x) escaped = '"' p = nil x.each_char do |c| case p when nil # do nothing when "\t" escaped << '\\t' when "\n" escaped << '\\n' when "\f" escaped << '\\f' # TODO: \cx is a range of characters - skip for now # when "\c" # escaped << '\\c' when '"' escaped << '\\"' when '\\' escaped << if c == '$' || c == 's'; p; else '\\\\'; end # don't escape \ when followed by s or $ else escaped << p end p = c end escaped << p unless p.nil? escaped << '"' end - # This is a temporary solution to making it possible to use the new evaluator. The main class - # will eventually have this behavior instead of using transformation to Puppet 3.x AST - class Transitional < Puppet::Pops::Parser::EvaluatingParser - - def evaluator - @@evaluator ||= Puppet::Pops::Evaluator::EvaluatorImpl.new() - @@evaluator - end - - def evaluate(scope, model) - return nil unless model - evaluator.evaluate(model, scope) - end - - def validator(acceptor) - Puppet::Pops::Validation::ValidatorFactory_4_0.new().validator(acceptor) - end - - # Create a closure that can be called in the given scope - def closure(model, scope) - Puppet::Pops::Evaluator::Closure.new(evaluator, model, scope) - end - end - - class EvaluatingEppParser < Transitional + class EvaluatingEppParser < Puppet::Pops::Parser::EvaluatingParser def initialize() @parser = Puppet::Pops::Parser::EppParser.new() end end end diff --git a/lib/puppet/pops/validation/checker3_1.rb b/lib/puppet/pops/validation/checker3_1.rb deleted file mode 100644 index 77f643fb1..000000000 --- a/lib/puppet/pops/validation/checker3_1.rb +++ /dev/null @@ -1,558 +0,0 @@ -# A Validator validates a model. -# -# Validation is performed on each model element in isolation. Each method should validate the model element's state -# but not validate its referenced/contained elements except to check their validity in their respective role. -# The intent is to drive the validation with a tree iterator that visits all elements in a model. -# -# -# TODO: Add validation of multiplicities - this is a general validation that can be checked for all -# Model objects via their metamodel. (I.e an extra call to multiplicity check in polymorph check). -# This is however mostly valuable when validating model to model transformations, and is therefore T.B.D -# -class Puppet::Pops::Validation::Checker3_1 - Issues = Puppet::Pops::Issues - Model = Puppet::Pops::Model - - attr_reader :acceptor - # Initializes the validator with a diagnostics producer. This object must respond to - # `:will_accept?` and `:accept`. - # - def initialize(diagnostics_producer) - @@check_visitor ||= Puppet::Pops::Visitor.new(nil, "check", 0, 0) - @@rvalue_visitor ||= Puppet::Pops::Visitor.new(nil, "rvalue", 0, 0) - @@hostname_visitor ||= Puppet::Pops::Visitor.new(nil, "hostname", 1, 2) - @@assignment_visitor ||= Puppet::Pops::Visitor.new(nil, "assign", 0, 1) - @@query_visitor ||= Puppet::Pops::Visitor.new(nil, "query", 0, 0) - @@top_visitor ||= Puppet::Pops::Visitor.new(nil, "top", 1, 1) - @@relation_visitor ||= Puppet::Pops::Visitor.new(nil, "relation", 1, 1) - - @acceptor = diagnostics_producer - end - - # Validates the entire model by visiting each model element and calling `check`. - # The result is collected (or acted on immediately) by the configured diagnostic provider/acceptor - # given when creating this Checker. - # - def validate(model) - # tree iterate the model, and call check for each element - check(model) - model.eAllContents.each {|m| check(m) } - end - - # Performs regular validity check - def check(o) - @@check_visitor.visit_this_0(self, o) - end - - # Performs check if this is a vaid hostname expression - # @param single_feature_name [String, nil] the name of a single valued hostname feature of the value's container. e.g. 'parent' - def hostname(o, semantic, single_feature_name = nil) - @@hostname_visitor.visit_this_2(self, o, semantic, single_feature_name) - end - - # Performs check if this is valid as a query - def query(o) - @@query_visitor.visit_this_0(self, o) - end - - # Performs check if this is valid as a relationship side - def relation(o, container) - @@relation_visitor.visit_this_1(self, o, container) - end - - # Performs check if this is valid as a rvalue - def rvalue(o) - @@rvalue_visitor.visit_this_0(self, o) - end - - # Performs check if this is valid as a container of a definition (class, define, node) - def top(o, definition) - @@top_visitor.visit_this_1(self, o, definition) - end - - # Checks the LHS of an assignment (is it assignable?). - # If args[0] is true, assignment via index is checked. - # - def assign(o, via_index = false) - @@assignment_visitor.visit_this_1(self, o, via_index) - end - - #---ASSIGNMENT CHECKS - - def assign_VariableExpression(o, via_index) - varname_string = varname_to_s(o.expr) - if varname_string =~ /^[0-9]+$/ - acceptor.accept(Issues::ILLEGAL_NUMERIC_ASSIGNMENT, o, :varname => varname_string) - end - # Can not assign to something in another namespace (i.e. a '::' in the name is not legal) - if acceptor.will_accept? Issues::CROSS_SCOPE_ASSIGNMENT - if varname_string =~ /::/ - acceptor.accept(Issues::CROSS_SCOPE_ASSIGNMENT, o, :name => varname_string) - end - end - # TODO: Could scan for reassignment of the same variable if done earlier in the same container - # Or if assigning to a parameter (more work). - # TODO: Investigate if there are invalid cases for += assignment - end - - def assign_AccessExpression(o, via_index) - # Are indexed assignments allowed at all ? $x[x] = '...' - if acceptor.will_accept? Issues::ILLEGAL_INDEXED_ASSIGNMENT - acceptor.accept(Issues::ILLEGAL_INDEXED_ASSIGNMENT, o) - else - # Then the left expression must be assignable-via-index - assign(o.left_expr, true) - end - end - - def assign_Object(o, via_index) - # Can not assign to anything else (differentiate if this is via index or not) - # i.e. 10 = 'hello' vs. 10['x'] = 'hello' (the root is reported as being in error in both cases) - # - acceptor.accept(via_index ? Issues::ILLEGAL_ASSIGNMENT_VIA_INDEX : Issues::ILLEGAL_ASSIGNMENT, o) - end - - #---CHECKS - - def check_Object(o) - end - - def check_Factory(o) - check(o.current) - end - - def check_AccessExpression(o) - # Check multiplicity of keys - case o.left_expr - when Model::QualifiedName - # allows many keys, but the name should really be a QualifiedReference - acceptor.accept(Issues::DEPRECATED_NAME_AS_TYPE, o, :name => o.left_expr.value) - when Model::QualifiedReference - # ok, allows many - this is a resource reference - - else - # i.e. for any other expression that may produce an array or hash - if o.keys.size > 1 - acceptor.accept(Issues::UNSUPPORTED_RANGE, o, :count => o.keys.size) - end - if o.keys.size < 1 - acceptor.accept(Issues::MISSING_INDEX, o) - end - end - end - - def check_AssignmentExpression(o) - acceptor.accept(Issues::UNSUPPORTED_OPERATOR, o, {:operator => o.operator}) unless [:'=', :'+='].include? o.operator - assign(o.left_expr) - rvalue(o.right_expr) - end - - # Checks that operation with :+> is contained in a ResourceOverride or Collector. - # - # Parent of an AttributeOperation can be one of: - # * CollectExpression - # * ResourceOverride - # * ResourceBody (ILLEGAL this is a regular resource expression) - # * ResourceDefaults (ILLEGAL) - # - def check_AttributeOperation(o) - if o.operator == :'+>' - # Append operator use is constrained - parent = o.eContainer - unless parent.is_a?(Model::CollectExpression) || parent.is_a?(Model::ResourceOverrideExpression) - acceptor.accept(Issues::ILLEGAL_ATTRIBUTE_APPEND, o, {:name=>o.attribute_name, :parent=>parent}) - end - end - rvalue(o.value_expr) - end - - def check_BinaryExpression(o) - rvalue(o.left_expr) - rvalue(o.right_expr) - end - - def check_CallNamedFunctionExpression(o) - unless o.functor_expr.is_a? Model::QualifiedName - acceptor.accept(Issues::ILLEGAL_EXPRESSION, o.functor_expr, :feature => 'function name', :container => o) - end - end - - def check_MethodCallExpression(o) - unless o.functor_expr.is_a? Model::QualifiedName - acceptor.accept(Issues::ILLEGAL_EXPRESSION, o.functor_expr, :feature => 'function name', :container => o) - end - end - - def check_CaseExpression(o) - # There should only be one LiteralDefault case option value - # TODO: Implement this check - end - - def check_CollectExpression(o) - unless o.type_expr.is_a? Model::QualifiedReference - acceptor.accept(Issues::ILLEGAL_EXPRESSION, o.type_expr, :feature=> 'type name', :container => o) - end - - # If a collect expression tries to collect exported resources and storeconfigs is not on - # then it will not work... This was checked in the parser previously. This is a runtime checking - # thing as opposed to a language thing. - if acceptor.will_accept?(Issues::RT_NO_STORECONFIGS) && o.query.is_a?(Model::ExportedQuery) - acceptor.accept(Issues::RT_NO_STORECONFIGS, o) - end - end - - # Only used for function names, grammar should not be able to produce something faulty, but - # check anyway if model is created programatically (it will fail in transformation to AST for sure). - def check_NamedAccessExpression(o) - name = o.right_expr - unless name.is_a? Model::QualifiedName - acceptor.accept(Issues::ILLEGAL_EXPRESSION, name, :feature=> 'function name', :container => o.eContainer) - end - end - - # for 'class' and 'define' - def check_NamedDefinition(o) - top(o.eContainer, o) - if (acceptor.will_accept? Issues::NAME_WITH_HYPHEN) && o.name.include?('-') - acceptor.accept(Issues::NAME_WITH_HYPHEN, o, {:name => o.name}) - end - end - - def check_ImportExpression(o) - o.files.each do |f| - unless f.is_a? Model::LiteralString - acceptor.accept(Issues::ILLEGAL_EXPRESSION, f, :feature => 'file name', :container => o) - end - end - end - - def check_InstanceReference(o) - # TODO: Original warning is : - # Puppet.warning addcontext("Deprecation notice: Resource references should now be capitalized") - # This model element is not used in the egrammar. - # Either implement checks or deprecate the use of InstanceReference (the same is acheived by - # transformation of AccessExpression when used where an Instance/Resource reference is allowed. - # - end - - # Restrictions on hash key are because of the strange key comparisons/and merge rules in the AST evaluation - # (Even the allowed ones are handled in a strange way). - # - def transform_KeyedEntry(o) - case o.key - when Model::QualifiedName - when Model::LiteralString - when Model::LiteralNumber - when Model::ConcatenatedString - else - acceptor.accept(Issues::ILLEGAL_EXPRESSION, o.key, :feature => 'hash key', :container => o.eContainer) - end - end - - # A Lambda is a Definition, but it may appear in other scopes that top scope (Which check_Definition asserts). - # - def check_LambdaExpression(o) - end - - def check_NodeDefinition(o) - # Check that hostnames are valid hostnames (or regular expressons) - hostname(o.host_matches, o) - hostname(o.parent, o, 'parent') unless o.parent.nil? - top(o.eContainer, o) - end - - # No checking takes place - all expressions using a QualifiedName need to check. This because the - # rules are slightly different depending on the container (A variable allows a numeric start, but not - # other names). This means that (if the lexer/parser so chooses) a QualifiedName - # can be anything when it represents a Bare Word and evaluates to a String. - # - def check_QualifiedName(o) - end - - # Checks that the value is a valid UpperCaseWord (a CLASSREF), and optionally if it contains a hypen. - # DOH: QualifiedReferences are created with LOWER CASE NAMES at parse time - def check_QualifiedReference(o) - # Is this a valid qualified name? - if o.value !~ Puppet::Pops::Patterns::CLASSREF - acceptor.accept(Issues::ILLEGAL_CLASSREF, o, {:name=>o.value}) - elsif (acceptor.will_accept? Issues::NAME_WITH_HYPHEN) && o.value.include?('-') - acceptor.accept(Issues::NAME_WITH_HYPHEN, o, {:name => o.value}) - end - end - - def check_QueryExpression(o) - query(o.expr) if o.expr # is optional - end - - def relation_Object(o, rel_expr) - acceptor.accept(Issues::ILLEGAL_EXPRESSION, o, {:feature => o.eContainingFeature, :container => rel_expr}) - end - - def relation_AccessExpression(o, rel_expr); end - - def relation_CollectExpression(o, rel_expr); end - - def relation_VariableExpression(o, rel_expr); end - - def relation_LiteralString(o, rel_expr); end - - def relation_ConcatenatedStringExpression(o, rel_expr); end - - def relation_SelectorExpression(o, rel_expr); end - - def relation_CaseExpression(o, rel_expr); end - - def relation_ResourceExpression(o, rel_expr); end - - def relation_RelationshipExpression(o, rel_expr); end - - def check_Parameter(o) - if o.name =~ /^[0-9]+$/ - acceptor.accept(Issues::ILLEGAL_NUMERIC_PARAMETER, o, :name => o.name) - end - end - - #relationship_side: resource - # | resourceref - # | collection - # | variable - # | quotedtext - # | selector - # | casestatement - # | hasharrayaccesses - - def check_RelationshipExpression(o) - relation(o.left_expr, o) - relation(o.right_expr, o) - end - - def check_ResourceExpression(o) - # A resource expression must have a lower case NAME as its type e.g. 'file { ... }' - unless o.type_name.is_a? Model::QualifiedName - acceptor.accept(Issues::ILLEGAL_EXPRESSION, o.type_name, :feature => 'resource type', :container => o) - end - - # This is a runtime check - the model is valid, but will have runtime issues when evaluated - # and storeconfigs is not set. - if acceptor.will_accept?(Issues::RT_NO_STORECONFIGS) && o.exported - acceptor.accept(Issues::RT_NO_STORECONFIGS_EXPORT, o) - end - end - - def check_ResourceDefaultsExpression(o) - if o.form && o.form != :regular - acceptor.accept(Issues::NOT_VIRTUALIZEABLE, o) - end - end - - # Transformation of SelectorExpression is limited to certain types of expressions. - # This is probably due to constraints in the old grammar rather than any real concerns. - def select_SelectorExpression(o) - case o.left_expr - when Model::CallNamedFunctionExpression - when Model::AccessExpression - when Model::VariableExpression - when Model::ConcatenatedString - else - acceptor.accept(Issues::ILLEGAL_EXPRESSION, o.left_expr, :feature => 'left operand', :container => o) - end - end - - def check_UnaryExpression(o) - rvalue(o.expr) - end - - def check_UnlessExpression(o) - # TODO: Unless may not have an elsif - # TODO: 3.x unless may not have an else - end - - def check_VariableExpression(o) - # The expression must be a qualified name - if !o.expr.is_a? Model::QualifiedName - acceptor.accept(Issues::ILLEGAL_EXPRESSION, o, :feature => 'name', :container => o) - else - # Note, that if it later becomes illegal with hyphen in any name, this special check - # can be skipped in favor of the check in QualifiedName, which is now not done if contained in - # a VariableExpression - name = o.expr.value - if (acceptor.will_accept? Issues::VAR_WITH_HYPHEN) && name.include?('-') - acceptor.accept(Issues::VAR_WITH_HYPHEN, o, {:name => name}) - end - end - end - - #--- HOSTNAME CHECKS - - # Transforms Array of host matching expressions into a (Ruby) array of AST::HostName - def hostname_Array(o, semantic, single_feature_name) - if single_feature_name - acceptor.accept(Issues::ILLEGAL_EXPRESSION, o, {:feature=>single_feature_name, :container=>semantic}) - end - o.each {|x| hostname(x, semantic, false) } - end - - def hostname_String(o, semantic, single_feature_name) - # The 3.x checker only checks for illegal characters - if matching /[^-\w.]/ the name is invalid, - # but this allows pathological names like "a..b......c", "----" - # TODO: Investigate if more illegal hostnames should be flagged. - # - if o =~ Puppet::Pops::Patterns::ILLEGAL_HOSTNAME_CHARS - acceptor.accept(Issues::ILLEGAL_HOSTNAME_CHARS, semantic, :hostname => o) - end - end - - def hostname_LiteralValue(o, semantic, single_feature_name) - hostname_String(o.value.to_s, o, single_feature_name) - end - - def hostname_ConcatenatedString(o, semantic, single_feature_name) - # Puppet 3.1. only accepts a concatenated string without interpolated expressions - if the_expr = o.segments.index {|s| s.is_a?(Model::TextExpression) } - acceptor.accept(Issues::ILLEGAL_HOSTNAME_INTERPOLATION, o.segments[the_expr].expr) - elsif o.segments.size() != 1 - # corner case, bad model, concatenation of several plain strings - acceptor.accept(Issues::ILLEGAL_HOSTNAME_INTERPOLATION, o) - else - # corner case, may be ok, but lexer may have replaced with plain string, this is - # here if it does not - hostname_String(o.segments[0], o.segments[0], false) - end - end - - def hostname_QualifiedName(o, semantic, single_feature_name) - hostname_String(o.value.to_s, o, single_feature_name) - end - - def hostname_QualifiedReference(o, semantic, single_feature_name) - hostname_String(o.value.to_s, o, single_feature_name) - end - - def hostname_LiteralNumber(o, semantic, single_feature_name) - # always ok - end - - def hostname_LiteralDefault(o, semantic, single_feature_name) - # always ok - end - - def hostname_LiteralRegularExpression(o, semantic, single_feature_name) - # always ok - end - - def hostname_Object(o, semantic, single_feature_name) - acceptor.accept(Issues::ILLEGAL_EXPRESSION, o, {:feature=> single_feature_name || 'hostname', :container=>semantic}) - end - - #---QUERY CHECKS - - # Anything not explicitly allowed is flagged as error. - def query_Object(o) - acceptor.accept(Issues::ILLEGAL_QUERY_EXPRESSION, o) - end - - # Puppet AST only allows == and != - # - def query_ComparisonExpression(o) - acceptor.accept(Issues::ILLEGAL_QUERY_EXPRESSION, o) unless [:'==', :'!='].include? o.operator - end - - # Allows AND, OR, and checks if left/right are allowed in query. - def query_BooleanExpression(o) - query o.left_expr - query o.right_expr - end - - def query_ParenthesizedExpression(o) - query(o.expr) - end - - def query_VariableExpression(o); end - - def query_QualifiedName(o); end - - def query_LiteralNumber(o); end - - def query_LiteralString(o); end - - def query_LiteralBoolean(o); end - - #---RVALUE CHECKS - - # By default, all expressions are reported as being rvalues - # Implement specific rvalue checks for those that are not. - # - def rvalue_Expression(o); end - - def rvalue_ImportExpression(o) ; acceptor.accept(Issues::NOT_RVALUE, o) ; end - - def rvalue_BlockExpression(o) ; acceptor.accept(Issues::NOT_RVALUE, o) ; end - - def rvalue_CaseExpression(o) ; acceptor.accept(Issues::NOT_RVALUE, o) ; end - - def rvalue_IfExpression(o) ; acceptor.accept(Issues::NOT_RVALUE, o) ; end - - def rvalue_UnlessExpression(o) ; acceptor.accept(Issues::NOT_RVALUE, o) ; end - - def rvalue_ResourceExpression(o) ; acceptor.accept(Issues::NOT_RVALUE, o) ; end - - def rvalue_ResourceDefaultsExpression(o); acceptor.accept(Issues::NOT_RVALUE, o) ; end - - def rvalue_ResourceOverrideExpression(o); acceptor.accept(Issues::NOT_RVALUE, o) ; end - - def rvalue_CollectExpression(o) ; acceptor.accept(Issues::NOT_RVALUE, o) ; end - - def rvalue_Definition(o) ; acceptor.accept(Issues::NOT_RVALUE, o) ; end - - def rvalue_NodeDefinition(o) ; acceptor.accept(Issues::NOT_RVALUE, o) ; end - - def rvalue_UnaryExpression(o) ; rvalue o.expr ; end - - #---TOP CHECK - - def top_NilClass(o, definition) - # ok, reached the top, no more parents - end - - def top_Object(o, definition) - # fail, reached a container that is not top level - acceptor.accept(Issues::NOT_TOP_LEVEL, definition) - end - - def top_BlockExpression(o, definition) - # ok, if this is a block representing the body of a class, or is top level - top o.eContainer, definition - end - - def top_HostClassDefinition(o, definition) - # ok, stop scanning parents - end - - def top_Program(o, definition) - # ok - end - - # A LambdaExpression is a BlockExpression, and this method is needed to prevent the polymorph method for BlockExpression - # to accept a lambda. - # A lambda can not iteratively create classes, nodes or defines as the lambda does not have a closure. - # - def top_LambdaExpression(o, definition) - # fail, stop scanning parents - acceptor.accept(Issues::NOT_TOP_LEVEL, definition) - end - - #--- NON POLYMORPH, NON CHECKING CODE - - # Produces string part of something named, or nil if not a QualifiedName or QualifiedReference - # - def varname_to_s(o) - case o - when Model::QualifiedName - o.value - when Model::QualifiedReference - o.value - else - nil - end - end -end diff --git a/lib/puppet/pops/validation/validator_factory_3_1.rb b/lib/puppet/pops/validation/validator_factory_3_1.rb deleted file mode 100644 index 738eac404..000000000 --- a/lib/puppet/pops/validation/validator_factory_3_1.rb +++ /dev/null @@ -1,31 +0,0 @@ -# Configures validation suitable for 3.1 + iteration -# -class Puppet::Pops::Validation::ValidatorFactory_3_1 < Puppet::Pops::Validation::Factory - Issues = Puppet::Pops::Issues - - # Produces the checker to use - def checker diagnostic_producer - Puppet::Pops::Validation::Checker3_1.new(diagnostic_producer) - end - - # Produces the label provider to use - def label_provider - Puppet::Pops::Model::ModelLabelProvider.new() - end - - # Produces the severity producer to use - def severity_producer - p = super - - # Configure each issue that should **not** be an error - # - # Validate as per the current runtime configuration - p[Issues::RT_NO_STORECONFIGS_EXPORT] = Puppet[:storeconfigs] ? :ignore : :warning - p[Issues::RT_NO_STORECONFIGS] = Puppet[:storeconfigs] ? :ignore : :warning - - p[Issues::NAME_WITH_HYPHEN] = :deprecation - p[Issues::DEPRECATED_NAME_AS_TYPE] = :deprecation - - p - end -end diff --git a/spec/integration/node/environment_spec.rb b/spec/integration/node/environment_spec.rb index 493b0d622..797e4105c 100755 --- a/spec/integration/node/environment_spec.rb +++ b/spec/integration/node/environment_spec.rb @@ -1,142 +1,125 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet_spec/files' require 'puppet_spec/scope' require 'matchers/resource' describe Puppet::Node::Environment do include PuppetSpec::Files include Matchers::Resource def a_module_in(name, dir) Dir.mkdir(dir) moddir = File.join(dir, name) Dir.mkdir(moddir) moddir end it "should be able to return each module from its environment with the environment, name, and path set correctly" do base = tmpfile("env_modules") Dir.mkdir(base) dirs = [] mods = {} %w{1 2}.each do |num| dir = File.join(base, "dir#{num}") dirs << dir mods["mod#{num}"] = a_module_in("mod#{num}", dir) end environment = Puppet::Node::Environment.create(:foo, dirs) environment.modules.each do |mod| mod.environment.should == environment mod.path.should == mods[mod.name] end end it "should not yield the same module from different module paths" do base = tmpfile("env_modules") Dir.mkdir(base) dirs = [] %w{1 2}.each do |num| dir = File.join(base, "dir#{num}") dirs << dir a_module_in("mod", dir) end environment = Puppet::Node::Environment.create(:foo, dirs) mods = environment.modules mods.length.should == 1 mods[0].path.should == File.join(base, "dir1", "mod") end shared_examples_for "the environment's initial import" do |settings| it "a manifest referring to a directory invokes parsing of all its files in sorted order" do settings.each do |name, value| Puppet[name] = value end # fixture has three files 00_a.pp, 01_b.pp, and 02_c.pp. The 'b' file # depends on 'a' being evaluated first. The 'c' file is empty (to ensure # empty things do not break the directory import). # dirname = my_fixture('sitedir') # Set the manifest to the directory to make it parse and combine them when compiling node = Puppet::Node.new('testnode', :environment => Puppet::Node::Environment.create(:testing, [], dirname)) catalog = Puppet::Parser::Compiler.compile(node) expect(catalog).to have_resource('Class[A]') expect(catalog).to have_resource('Class[B]') expect(catalog).to have_resource('Notify[variables]').with_parameter(:message, "a: 10, b: 10") end end shared_examples_for "the environment's initial import in the future" do |settings| it "a manifest referring to a directory invokes recursive parsing of all its files in sorted order" do settings.each do |name, value| Puppet[name] = value end # fixture has three files 00_a.pp, 01_b.pp, and 02_c.pp. The 'b' file # depends on 'a' being evaluated first. The 'c' file is empty (to ensure # empty things do not break the directory import). # dirname = my_fixture('sitedir2') # Set the manifest to the directory to make it parse and combine them when compiling node = Puppet::Node.new('testnode', :environment => Puppet::Node::Environment.create(:testing, [], dirname)) catalog = Puppet::Parser::Compiler.compile(node) expect(catalog).to have_resource('Class[A]') expect(catalog).to have_resource('Class[B]') expect(catalog).to have_resource('Notify[variables]').with_parameter(:message, "a: 10, b: 10 c: 20") end end describe 'using classic parser' do it_behaves_like "the environment's initial import", :parser => 'current', # fixture uses variables that are set in a particular order (this ensures # that files are parsed and combined in the right order or an error will # be raised if 'b' is evaluated before 'a'). :strict_variables => true end describe 'using future parser' do it_behaves_like "the environment's initial import", :parser => 'future', - :evaluator => 'future', # Turned off because currently future parser turns on the binder which # causes lookup of facts that are uninitialized and it will fail with # errors for 'osfamily' etc. This can be turned back on when the binder # is taken out of the equation. :strict_variables => false - - it_behaves_like "the environment's initial import in the future", - :parser => 'future', - :evaluator => 'future', - # Turned off because currently future parser turns on the binder which - # causes lookup of facts that are uninitialized and it will fail with - # errors for 'osfamily' etc. This can be turned back on when the binder - # is taken out of the equation. - :strict_variables => false - - context 'and evaluator current' do - it_behaves_like "the environment's initial import", - :parser => 'future', - :evaluator => 'current', - :strict_variables => false - end end end diff --git a/spec/unit/functions4_spec.rb b/spec/unit/functions4_spec.rb index 5c067f43d..6c31b75c2 100644 --- a/spec/unit/functions4_spec.rb +++ b/spec/unit/functions4_spec.rb @@ -1,671 +1,670 @@ require 'spec_helper' require 'puppet/pops' require 'puppet/loaders' require 'puppet_spec/pops' require 'puppet_spec/scope' module FunctionAPISpecModule class TestDuck end class TestFunctionLoader < Puppet::Pops::Loader::StaticLoader def initialize @functions = {} end def add_function(name, function) typed_name = Puppet::Pops::Loader::Loader::TypedName.new(:function, name) entry = Puppet::Pops::Loader::Loader::NamedEntry.new(typed_name, function, __FILE__) @functions[typed_name] = entry end # override StaticLoader def load_constant(typed_name) @functions[typed_name] end end end describe 'the 4x function api' do include FunctionAPISpecModule include PuppetSpec::Pops include PuppetSpec::Scope let(:loader) { FunctionAPISpecModule::TestFunctionLoader.new } it 'allows a simple function to be created without dispatch declaration' do f = Puppet::Functions.create_function('min') do def min(x,y) x <= y ? x : y end end # the produced result is a Class inheriting from Function expect(f.class).to be(Class) expect(f.superclass).to be(Puppet::Functions::Function) # and this class had the given name (not a real Ruby class name) expect(f.name).to eql('min') end it 'refuses to create functions that are not based on the Function class' do expect do Puppet::Functions.create_function('testing', Object) {} end.to raise_error(ArgumentError, 'Functions must be based on Puppet::Pops::Functions::Function. Got Object') end it 'a function without arguments can be defined and called without dispatch declaration' do f = create_noargs_function_class() func = f.new(:closure_scope, :loader) expect(func.call({})).to eql(10) end it 'an error is raised when calling a no arguments function with arguments' do f = create_noargs_function_class() func = f.new(:closure_scope, :loader) expect{func.call({}, 'surprise')}.to raise_error(ArgumentError, "function 'test' called with mis-matched arguments expected: test() - arg count {0} actual: test(String) - arg count {1}") end it 'a simple function can be called' do f = create_min_function_class() # TODO: Bogus parameters, not yet used func = f.new(:closure_scope, :loader) expect(func.is_a?(Puppet::Functions::Function)).to be_true expect(func.call({}, 10,20)).to eql(10) end it 'an error is raised if called with too few arguments' do f = create_min_function_class() # TODO: Bogus parameters, not yet used func = f.new(:closure_scope, :loader) expect(func.is_a?(Puppet::Functions::Function)).to be_true signature = if RUBY_VERSION =~ /^1\.8/ 'Any{2}' else 'Any x, Any y' end expect do func.call({}, 10) end.to raise_error(ArgumentError, "function 'min' called with mis-matched arguments expected: min(#{signature}) - arg count {2} actual: min(Integer) - arg count {1}") end it 'an error is raised if called with too many arguments' do f = create_min_function_class() # TODO: Bogus parameters, not yet used func = f.new(:closure_scope, :loader) expect(func.is_a?(Puppet::Functions::Function)).to be_true signature = if RUBY_VERSION =~ /^1\.8/ 'Any{2}' else 'Any x, Any y' end expect do func.call({}, 10, 10, 10) end.to raise_error(ArgumentError, Regexp.new(Regexp.escape( "function 'min' called with mis-matched arguments expected: min(#{signature}) - arg count {2} actual: min(Integer, Integer, Integer) - arg count {3}"))) end it 'an error is raised if simple function-name and method are not matched' do expect do f = create_badly_named_method_function_class() end.to raise_error(ArgumentError, /Function Creation Error, cannot create a default dispatcher for function 'mix', no method with this name found/) end it 'the implementation separates dispatchers for different functions' do # this tests that meta programming / construction puts class attributes in the correct class f1 = create_min_function_class() f2 = create_max_function_class() d1 = f1.dispatcher d2 = f2.dispatcher expect(d1).to_not eql(d2) expect(d1.dispatchers[0]).to_not eql(d2.dispatchers[0]) end context 'when using regular dispatch' do it 'a function can be created using dispatch and called' do f = create_min_function_class_using_dispatch() func = f.new(:closure_scope, :loader) expect(func.call({}, 3,4)).to eql(3) end it 'an error is raised with reference to given parameter names when called with mis-matched arguments' do f = create_min_function_class_using_dispatch() # TODO: Bogus parameters, not yet used func = f.new(:closure_scope, :loader) expect(func.is_a?(Puppet::Functions::Function)).to be_true expect do func.call({}, 10, 10, 10) end.to raise_error(ArgumentError, Regexp.new(Regexp.escape( "function 'min' called with mis-matched arguments expected: min(Numeric a, Numeric b) - arg count {2} actual: min(Integer, Integer, Integer) - arg count {3}"))) end it 'an error includes optional indicators and count for last element' do f = create_function_with_optionals_and_varargs() # TODO: Bogus parameters, not yet used func = f.new(:closure_scope, :loader) expect(func.is_a?(Puppet::Functions::Function)).to be_true signature = if RUBY_VERSION =~ /^1\.8/ 'Any{2,}' else 'Any x, Any y, Any a?, Any b?, Any c{0,}' end expect do func.call({}, 10) end.to raise_error(ArgumentError, "function 'min' called with mis-matched arguments expected: min(#{signature}) - arg count {2,} actual: min(Integer) - arg count {1}") end it 'an error includes optional indicators and count for last element when defined via dispatch' do f = create_function_with_optionals_and_varargs_via_dispatch() # TODO: Bogus parameters, not yet used func = f.new(:closure_scope, :loader) expect(func.is_a?(Puppet::Functions::Function)).to be_true expect do func.call({}, 10) end.to raise_error(ArgumentError, "function 'min' called with mis-matched arguments expected: min(Numeric x, Numeric y, Numeric a?, Numeric b?, Numeric c{0,}) - arg count {2,} actual: min(Integer) - arg count {1}") end it 'a function can be created using dispatch and called' do f = create_min_function_class_disptaching_to_two_methods() func = f.new(:closure_scope, :loader) expect(func.call({}, 3,4)).to eql(3) expect(func.call({}, 'Apple', 'Banana')).to eql('Apple') end it 'an error is raised with reference to multiple methods when called with mis-matched arguments' do f = create_min_function_class_disptaching_to_two_methods() # TODO: Bogus parameters, not yet used func = f.new(:closure_scope, :loader) expect(func.is_a?(Puppet::Functions::Function)).to be_true expect do func.call({}, 10, 10, 10) end.to raise_error(ArgumentError, "function 'min' called with mis-matched arguments expected one of: min(Numeric a, Numeric b) - arg count {2} min(String s1, String s2) - arg count {2} actual: min(Integer, Integer, Integer) - arg count {3}") end context 'can use injection' do before :all do injector = Puppet::Pops::Binder::Injector.create('test') do bind.name('a_string').to('evoe') bind.name('an_int').to(42) end Puppet.push_context({:injector => injector}, "injector for testing function API") end after :all do Puppet.pop_context() end it 'attributes can be injected' do f1 = create_function_with_class_injection() f = f1.new(:closure_scope, :loader) expect(f.test_attr2()).to eql("evoe") expect(f.serial().produce(nil)).to eql(42) expect(f.test_attr().class.name).to eql("FunctionAPISpecModule::TestDuck") end it 'parameters can be injected and woven with regular dispatch' do f1 = create_function_with_param_injection_regular() f = f1.new(:closure_scope, :loader) expect(f.call(nil, 10, 20)).to eql("evoe! 10, and 20 < 42 = true") expect(f.call(nil, 50, 20)).to eql("evoe! 50, and 20 < 42 = false") end end context 'when requesting a type' do it 'responds with a Callable for a single signature' do tf = Puppet::Pops::Types::TypeFactory fc = create_min_function_class_using_dispatch() t = fc.dispatcher.to_type expect(t.class).to be(Puppet::Pops::Types::PCallableType) expect(t.param_types.class).to be(Puppet::Pops::Types::PTupleType) expect(t.param_types.types).to eql([tf.numeric(), tf.numeric()]) expect(t.block_type).to be_nil end it 'responds with a Variant[Callable...] for multiple signatures' do tf = Puppet::Pops::Types::TypeFactory fc = create_min_function_class_disptaching_to_two_methods() t = fc.dispatcher.to_type expect(t.class).to be(Puppet::Pops::Types::PVariantType) expect(t.types.size).to eql(2) t1 = t.types[0] expect(t1.param_types.class).to be(Puppet::Pops::Types::PTupleType) expect(t1.param_types.types).to eql([tf.numeric(), tf.numeric()]) expect(t1.block_type).to be_nil t2 = t.types[1] expect(t2.param_types.class).to be(Puppet::Pops::Types::PTupleType) expect(t2.param_types.types).to eql([tf.string(), tf.string()]) expect(t2.block_type).to be_nil end end context 'supports lambdas' do it 'such that, a required block can be defined and given as an argument' do # use a Function as callable the_callable = create_min_function_class().new(:closure_scope, :loader) the_function = create_function_with_required_block_all_defaults().new(:closure_scope, :loader) result = the_function.call({}, 10, the_callable) expect(result).to be(the_callable) end it 'such that, a missing required block when called raises an error' do # use a Function as callable the_function = create_function_with_required_block_all_defaults().new(:closure_scope, :loader) expect do the_function.call({}, 10) end.to raise_error(ArgumentError, "function 'test' called with mis-matched arguments expected: test(Integer x, Callable block) - arg count {2} actual: test(Integer) - arg count {1}") end it 'such that, an optional block can be defined and given as an argument' do # use a Function as callable the_callable = create_min_function_class().new(:closure_scope, :loader) the_function = create_function_with_optional_block_all_defaults().new(:closure_scope, :loader) result = the_function.call({}, 10, the_callable) expect(result).to be(the_callable) end it 'such that, an optional block can be omitted when called and gets the value nil' do # use a Function as callable the_function = create_function_with_optional_block_all_defaults().new(:closure_scope, :loader) expect(the_function.call({}, 10)).to be_nil end end context 'provides signature information' do it 'about capture rest (varargs)' do fc = create_function_with_optionals_and_varargs signatures = fc.signatures expect(signatures.size).to eql(1) signature = signatures[0] expect(signature.last_captures_rest?).to be_true end it 'about optional and required parameters' do fc = create_function_with_optionals_and_varargs signature = fc.signatures[0] expect(signature.args_range).to eql( [2, Puppet::Pops::Types::INFINITY ] ) expect(signature.infinity?(signature.args_range[1])).to be_true end it 'about block not being allowed' do fc = create_function_with_optionals_and_varargs signature = fc.signatures[0] expect(signature.block_range).to eql( [ 0, 0 ] ) expect(signature.block_type).to be_nil end it 'about required block' do fc = create_function_with_required_block_all_defaults signature = fc.signatures[0] expect(signature.block_range).to eql( [ 1, 1 ] ) expect(signature.block_type).to_not be_nil end it 'about optional block' do fc = create_function_with_optional_block_all_defaults signature = fc.signatures[0] expect(signature.block_range).to eql( [ 0, 1 ] ) expect(signature.block_type).to_not be_nil end it 'about the type' do fc = create_function_with_optional_block_all_defaults signature = fc.signatures[0] expect(signature.type.class).to be(Puppet::Pops::Types::PCallableType) end # conditional on Ruby 1.8.7 which does not do parameter introspection if Method.method_defined?(:parameters) it 'about parameter names obtained from ruby introspection' do fc = create_min_function_class signature = fc.signatures[0] expect(signature.parameter_names).to eql(['x', 'y']) end end it 'about parameter names specified with dispatch' do fc = create_min_function_class_using_dispatch signature = fc.signatures[0] expect(signature.parameter_names).to eql(['a', 'b']) end it 'about block_name when it is *not* given in the definition' do # neither type, nor name fc = create_function_with_required_block_all_defaults signature = fc.signatures[0] expect(signature.block_name).to eql('block') # no name given, only type fc = create_function_with_required_block_given_type signature = fc.signatures[0] expect(signature.block_name).to eql('block') end it 'about block_name when it *is* given in the definition' do # neither type, nor name fc = create_function_with_required_block_default_type signature = fc.signatures[0] expect(signature.block_name).to eql('the_block') # no name given, only type fc = create_function_with_required_block_fully_specified signature = fc.signatures[0] expect(signature.block_name).to eql('the_block') end end context 'supports calling other functions' do before(:all) do Puppet.push_context( {:loaders => Puppet::Pops::Loaders.new(Puppet::Node::Environment.create(:testing, []))}) end after(:all) do Puppet.pop_context() end it 'such that, other functions are callable by name' do fc = Puppet::Functions.create_function(:test) do def test() # Call a function available in the puppet system call_function('assert_type', 'Integer', 10) end end # initiate the function the same way the loader initiates it f = fc.new(:closure_scope, Puppet.lookup(:loaders).puppet_system_loader) expect(f.call({})).to eql(10) end it 'such that, calling a non existing function raises an error' do fc = Puppet::Functions.create_function(:test) do def test() # Call a function not available in the puppet system call_function('no_such_function', 'Integer', 'hello') end end # initiate the function the same way the loader initiates it f = fc.new(:closure_scope, Puppet.lookup(:loaders).puppet_system_loader) expect{f.call({})}.to raise_error(ArgumentError, "Function test(): cannot call function 'no_such_function' - not found") end end context 'supports calling ruby functions with lambda from puppet' do before(:all) do Puppet.push_context( {:loaders => Puppet::Pops::Loaders.new(Puppet::Node::Environment.create(:testing, []))}) end after(:all) do Puppet.pop_context() end 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.new } let(:node) { 'node.example.com' } let(:scope) { s = create_test_scope_for_node(node); s } it 'function with required block can be called' do # construct ruby function to call fc = Puppet::Functions.create_function('testing::test') do dispatch :test do param 'Integer', 'x' # block called 'the_block', and using "all_callables" required_block_param #(all_callables(), 'the_block') end def test(x, block) # call the block with x block.call(closure_scope, x) end end # add the function to the loader (as if it had been loaded from somewhere) the_loader = loader() f = fc.new({}, the_loader) loader.add_function('testing::test', f) # evaluate a puppet call source = "testing::test(10) |$x| { $x+1 }" program = parser.parse_string(source, __FILE__) Puppet::Pops::Adapters::LoaderAdapter.adapt(program.model).loader = the_loader expect(parser.evaluate(scope, program)).to eql(11) end end end def create_noargs_function_class f = Puppet::Functions.create_function('test') do def test() 10 end end end def create_min_function_class f = Puppet::Functions.create_function('min') do def min(x,y) x <= y ? x : y end end end def create_max_function_class f = Puppet::Functions.create_function('max') do def max(x,y) x >= y ? x : y end end end def create_badly_named_method_function_class f = Puppet::Functions.create_function('mix') do def mix_up(x,y) x <= y ? x : y end end end def create_min_function_class_using_dispatch f = Puppet::Functions.create_function('min') do dispatch :min do param 'Numeric', 'a' param 'Numeric', 'b' end def min(x,y) x <= y ? x : y end end end def create_min_function_class_disptaching_to_two_methods f = Puppet::Functions.create_function('min') do dispatch :min do param 'Numeric', 'a' param 'Numeric', 'b' end dispatch :min_s do param 'String', 's1' param 'String', 's2' end def min(x,y) x <= y ? x : y end def min_s(x,y) cmp = (x.downcase <=> y.downcase) cmp <= 0 ? x : y end end end def create_function_with_optionals_and_varargs f = Puppet::Functions.create_function('min') do def min(x,y,a=1, b=1, *c) x <= y ? x : y end end end def create_function_with_optionals_and_varargs_via_dispatch f = Puppet::Functions.create_function('min') do dispatch :min do param 'Numeric', 'x' param 'Numeric', 'y' param 'Numeric', 'a' param 'Numeric', 'b' param 'Numeric', 'c' arg_count 2, :default end def min(x,y,a=1, b=1, *c) x <= y ? x : y end end end def create_function_with_class_injection f = Puppet::Functions.create_function('test', Puppet::Functions::InternalFunction) do attr_injected Puppet::Pops::Types::TypeFactory.type_of(FunctionAPISpecModule::TestDuck), :test_attr attr_injected Puppet::Pops::Types::TypeFactory.string(), :test_attr2, "a_string" attr_injected_producer Puppet::Pops::Types::TypeFactory.integer(), :serial, "an_int" def test(x,y,a=1, b=1, *c) x <= y ? x : y end end end def create_function_with_param_injection_regular f = Puppet::Functions.create_function('test', Puppet::Functions::InternalFunction) do attr_injected Puppet::Pops::Types::TypeFactory.type_of(FunctionAPISpecModule::TestDuck), :test_attr attr_injected Puppet::Pops::Types::TypeFactory.string(), :test_attr2, "a_string" attr_injected_producer Puppet::Pops::Types::TypeFactory.integer(), :serial, "an_int" dispatch :test do injected_param Puppet::Pops::Types::TypeFactory.string, 'x', 'a_string' injected_producer_param Puppet::Pops::Types::TypeFactory.integer, 'y', 'an_int' param 'Scalar', 'a' param 'Scalar', 'b' end def test(x,y,a,b) y_produced = y.produce(nil) "#{x}! #{a}, and #{b} < #{y_produced} = #{ !!(a < y_produced && b < y_produced)}" end end end def create_function_with_required_block_all_defaults f = Puppet::Functions.create_function('test') do dispatch :test do param 'Integer', 'x' # use defaults, any callable, name is 'block' required_block_param end def test(x, block) # returns the block to make it easy to test what it got when called block end end end def create_function_with_required_block_default_type f = Puppet::Functions.create_function('test') do dispatch :test do param 'Integer', 'x' # use defaults, any callable, name is 'block' required_block_param 'the_block' end def test(x, block) # returns the block to make it easy to test what it got when called block end end end def create_function_with_required_block_given_type f = Puppet::Functions.create_function('test') do dispatch :test do param 'Integer', 'x' required_block_param end def test(x, block) # returns the block to make it easy to test what it got when called block end end end def create_function_with_required_block_fully_specified f = Puppet::Functions.create_function('test') do dispatch :test do param 'Integer', 'x' # use defaults, any callable, name is 'block' required_block_param('Callable', 'the_block') end def test(x, block) # returns the block to make it easy to test what it got when called block end end end def create_function_with_optional_block_all_defaults f = Puppet::Functions.create_function('test') do dispatch :test do param 'Integer', 'x' # use defaults, any callable, name is 'block' optional_block_param end def test(x, block=nil) # returns the block to make it easy to test what it got when called # a default of nil must be used or the call will fail with a missing parameter block end end end end diff --git a/spec/unit/parser/eparser_adapter_spec.rb b/spec/unit/parser/eparser_adapter_spec.rb deleted file mode 100644 index 173cfb783..000000000 --- a/spec/unit/parser/eparser_adapter_spec.rb +++ /dev/null @@ -1,407 +0,0 @@ -#! /usr/bin/env ruby -require 'spec_helper' -require 'puppet/parser/e_parser_adapter' - -describe Puppet::Parser do - - Puppet::Parser::AST - - before :each do - @known_resource_types = Puppet::Resource::TypeCollection.new("development") - @classic_parser = Puppet::Parser::Parser.new "development" - @parser = Puppet::Parser::EParserAdapter.new(@classic_parser) - @classic_parser.stubs(:known_resource_types).returns @known_resource_types - @true_ast = Puppet::Parser::AST::Boolean.new :value => true - end - - it "should require an environment at initialization" do - expect { - Puppet::Parser::EParserAdapter.new - }.to raise_error(ArgumentError, /wrong number of arguments/) - end - - describe "when parsing append operator" do - - it "should not raise syntax errors" do - expect { @parser.parse("$var += something") }.to_not raise_error - end - - it "should raise syntax error on incomplete syntax " do - expect { - @parser.parse("$var += ") - }.to raise_error(Puppet::ParseError, /Syntax error at end of file/) - end - - it "should create ast::VarDef with append=true" do - vardef = @parser.parse("$var += 2").code[0] - vardef.should be_a(Puppet::Parser::AST::VarDef) - vardef.append.should == true - end - - it "should work with arrays too" do - vardef = @parser.parse("$var += ['test']").code[0] - vardef.should be_a(Puppet::Parser::AST::VarDef) - vardef.append.should == true - end - - end - - describe "when parsing selector" do - it "should support hash access on the left hand side" do - expect { @parser.parse("$h = { 'a' => 'b' } $a = $h['a'] ? { 'b' => 'd', default => undef }") }.to_not raise_error - end - end - - describe "parsing 'unless'" do - it "should create the correct ast objects" do - Puppet::Parser::AST::Not.expects(:new).with { |h| h[:value].is_a?(Puppet::Parser::AST::Boolean) } - @parser.parse("unless false { $var = 1 }") - end - - it "should not raise an error with empty statements" do - expect { @parser.parse("unless false { }") }.to_not raise_error - end - - #test for bug #13296 - it "should not override 'unless' as a parameter inside resources" do - lambda { @parser.parse("exec {'/bin/echo foo': unless => '/usr/bin/false',}") }.should_not raise_error - end - end - - describe "when parsing parameter names" do - Puppet::Parser::Lexer::KEYWORDS.sort_tokens.each do |keyword| - it "should allow #{keyword} as a keyword" do - lambda { @parser.parse("exec {'/bin/echo foo': #{keyword} => '/usr/bin/false',}") }.should_not raise_error - end - end - end - - describe "when parsing 'if'" do - it "not, it should create the correct ast objects" do - Puppet::Parser::AST::Not.expects(:new).with { |h| h[:value].is_a?(Puppet::Parser::AST::Boolean) } - @parser.parse("if ! true { $var = 1 }") - end - - it "boolean operation, it should create the correct ast objects" do - Puppet::Parser::AST::BooleanOperator.expects(:new).with { - |h| h[:rval].is_a?(Puppet::Parser::AST::Boolean) and h[:lval].is_a?(Puppet::Parser::AST::Boolean) and h[:operator]=="or" - } - @parser.parse("if true or true { $var = 1 }") - - end - - it "comparison operation, it should create the correct ast objects" do - Puppet::Parser::AST::ComparisonOperator.expects(:new).with { - |h| h[:lval].is_a?(Puppet::Parser::AST::Name) and h[:rval].is_a?(Puppet::Parser::AST::Name) and h[:operator]=="<" - } - @parser.parse("if 1 < 2 { $var = 1 }") - - end - - end - - describe "when parsing if complex expressions" do - it "should create a correct ast tree" do - aststub = stub_everything 'ast' - Puppet::Parser::AST::ComparisonOperator.expects(:new).with { - |h| h[:rval].is_a?(Puppet::Parser::AST::Name) and h[:lval].is_a?(Puppet::Parser::AST::Name) and h[:operator]==">" - }.returns(aststub) - Puppet::Parser::AST::ComparisonOperator.expects(:new).with { - |h| h[:rval].is_a?(Puppet::Parser::AST::Name) and h[:lval].is_a?(Puppet::Parser::AST::Name) and h[:operator]=="==" - }.returns(aststub) - Puppet::Parser::AST::BooleanOperator.expects(:new).with { - |h| h[:rval]==aststub and h[:lval]==aststub and h[:operator]=="and" - } - @parser.parse("if (1 > 2) and (1 == 2) { $var = 1 }") - end - - it "should raise an error on incorrect expression" do - expect { - @parser.parse("if (1 > 2 > ) or (1 == 2) { $var = 1 }") - }.to raise_error(Puppet::ParseError, /Syntax error at '\)'/) - end - - end - - describe "when parsing resource references" do - - it "should not raise syntax errors" do - expect { @parser.parse('exec { test: param => File["a"] }') }.to_not raise_error - end - - it "should not raise syntax errors with multiple references" do - expect { @parser.parse('exec { test: param => File["a","b"] }') }.to_not raise_error - end - - it "should create an ast::ResourceReference" do - # NOTE: In egrammar, type and name are unified immediately to lower case whereas the regular grammar - # keeps the UC name in some contexts - it gets downcased later as the name of the type is in lower case. - # - Puppet::Parser::AST::ResourceReference.expects(:new).with { |arg| - arg[:line]==1 and arg[:pos] ==25 and arg[:type]=="file" and arg[:title].is_a?(Puppet::Parser::AST::ASTArray) - } - @parser.parse('exec { test: command => File["a","b"] }') - end - end - - describe "when parsing resource overrides" do - - it "should not raise syntax errors" do - expect { @parser.parse('Resource["title"] { param => value }') }.to_not raise_error - end - - it "should not raise syntax errors with multiple overrides" do - expect { @parser.parse('Resource["title1","title2"] { param => value }') }.to_not raise_error - end - - it "should create an ast::ResourceOverride" do - ro = @parser.parse('Resource["title1","title2"] { param => value }').code[0] - ro.should be_a(Puppet::Parser::AST::ResourceOverride) - ro.line.should == 1 - ro.object.should be_a(Puppet::Parser::AST::ResourceReference) - ro.parameters[0].should be_a(Puppet::Parser::AST::ResourceParam) - end - - end - - describe "when parsing if statements" do - - it "should not raise errors with empty if" do - expect { @parser.parse("if true { }") }.to_not raise_error - end - - it "should not raise errors with empty else" do - expect { @parser.parse("if false { notice('if') } else { }") }.to_not raise_error - end - - it "should not raise errors with empty if and else" do - expect { @parser.parse("if false { } else { }") }.to_not raise_error - end - - it "should create a nop node for empty branch" do - Puppet::Parser::AST::Nop.expects(:new).twice - @parser.parse("if true { }") - end - - it "should create a nop node for empty else branch" do - Puppet::Parser::AST::Nop.expects(:new) - @parser.parse("if true { notice('test') } else { }") - end - - it "should build a chain of 'ifs' if there's an 'elsif'" do - expect { @parser.parse(<<-PP) }.to_not raise_error - if true { notice('test') } elsif true {} else { } - PP - end - - end - - describe "when parsing function calls" do - it "should not raise errors with no arguments" do - expect { @parser.parse("tag()") }.to_not raise_error - end - - it "should not raise errors with rvalue function with no args" do - expect { @parser.parse("$a = template()") }.to_not raise_error - end - - it "should not raise errors with arguments" do - expect { @parser.parse("notice(1)") }.to_not raise_error - end - - it "should not raise errors with multiple arguments" do - expect { @parser.parse("notice(1,2)") }.to_not raise_error - end - - it "should not raise errors with multiple arguments and a trailing comma" do - expect { @parser.parse("notice(1,2,)") }.to_not raise_error - end - - end - - describe "when parsing arrays" do - it "should parse an array" do - expect { @parser.parse("$a = [1,2]") }.to_not raise_error - end - - it "should not raise errors with a trailing comma" do - expect { @parser.parse("$a = [1,2,]") }.to_not raise_error - end - - it "should accept an empty array" do - expect { @parser.parse("$var = []\n") }.to_not raise_error - end - end - - describe "when parsing classes" do - before :each do - @krt = Puppet::Resource::TypeCollection.new("development") - @classic_parser = Puppet::Parser::Parser.new "development" - @parser = Puppet::Parser::EParserAdapter.new(@classic_parser) - @classic_parser.stubs(:known_resource_types).returns @krt - end - - it "should not create new classes" do - @parser.parse("class foobar {}").code[0].should be_a(Puppet::Parser::AST::Hostclass) - @krt.hostclass("foobar").should be_nil - end - - it "should correctly set the parent class when one is provided" do - @parser.parse("class foobar inherits yayness {}").code[0].instantiate('')[0].parent.should == "yayness" - end - - it "should correctly set the parent class for multiple classes at a time" do - statements = @parser.parse("class foobar inherits yayness {}\nclass boo inherits bar {}").code - statements[0].instantiate('')[0].parent.should == "yayness" - statements[1].instantiate('')[0].parent.should == "bar" - end - - it "should define the code when some is provided" do - @parser.parse("class foobar { $var = val }").code[0].code.should_not be_nil - end - - it "should accept parameters with trailing comma" do - @parser.parse("file { '/example': ensure => file, }").should be - end - - it "should accept parametrized classes with trailing comma" do - @parser.parse("class foobar ($var1 = 0,) { $var = val }").code[0].code.should_not be_nil - end - - it "should define parameters when provided" do - foobar = @parser.parse("class foobar($biz,$baz) {}").code[0].instantiate('')[0] - foobar.arguments.should == {"biz" => nil, "baz" => nil} - end - end - - describe "when parsing resources" do - before :each do - @krt = Puppet::Resource::TypeCollection.new("development") - @classic_parser = Puppet::Parser::Parser.new "development" - @parser = Puppet::Parser::EParserAdapter.new(@classic_parser) - @classic_parser.stubs(:known_resource_types).returns @krt - end - - it "should be able to parse class resources" do - @krt.add(Puppet::Resource::Type.new(:hostclass, "foobar", :arguments => {"biz" => nil})) - expect { @parser.parse("class { foobar: biz => stuff }") }.to_not raise_error - end - - it "should correctly mark exported resources as exported" do - @parser.parse("@@file { '/file': }").code[0].exported.should be_true - end - - it "should correctly mark virtual resources as virtual" do - @parser.parse("@file { '/file': }").code[0].virtual.should be_true - end - end - - describe "when parsing nodes" do - it "should be able to parse a node with a single name" do - node = @parser.parse("node foo { }").code[0] - node.should be_a Puppet::Parser::AST::Node - node.names.length.should == 1 - node.names[0].value.should == "foo" - end - - it "should be able to parse a node with two names" do - node = @parser.parse("node foo, bar { }").code[0] - node.should be_a Puppet::Parser::AST::Node - node.names.length.should == 2 - node.names[0].value.should == "foo" - node.names[1].value.should == "bar" - end - - it "should be able to parse a node with three names" do - node = @parser.parse("node foo, bar, baz { }").code[0] - node.should be_a Puppet::Parser::AST::Node - node.names.length.should == 3 - node.names[0].value.should == "foo" - node.names[1].value.should == "bar" - node.names[2].value.should == "baz" - end - end - - it "should fail if trying to collect defaults" do - expect { - @parser.parse("@Port { protocols => tcp }") - }.to raise_error(Puppet::ParseError, /Defaults are not virtualizable/) - end - - context "when parsing collections" do - it "should parse basic collections" do - @parser.parse("Port <| |>").code. - should be_all {|x| x.is_a? Puppet::Parser::AST::Collection } - end - - it "should parse fully qualified collections" do - @parser.parse("Port::Range <| |>").code. - should be_all {|x| x.is_a? Puppet::Parser::AST::Collection } - end - end - - it "should not assign to a fully qualified variable" do - expect { - @parser.parse("$one::two = yay") - }.to raise_error(Puppet::ParseError, /Cannot assign to variables in other namespaces/) - end - - it "should parse assignment of undef" do - tree = @parser.parse("$var = undef") - tree.code.children[0].should be_an_instance_of Puppet::Parser::AST::VarDef - tree.code.children[0].value.should be_an_instance_of Puppet::Parser::AST::Undef - end - - it "should treat classes as case insensitive" do - @classic_parser.known_resource_types.import_ast(@parser.parse("class yayness {}"), '') - @classic_parser.known_resource_types.hostclass('yayness'). - should == @classic_parser.find_hostclass("", "YayNess") - end - - it "should treat defines as case insensitive" do - @classic_parser.known_resource_types.import_ast(@parser.parse("define funtest {}"), '') - @classic_parser.known_resource_types.hostclass('funtest'). - should == @classic_parser.find_hostclass("", "fUntEst") - end - context "when parsing method calls" do - it "should parse method call with one param lambda" do - expect { @parser.parse("$a.each |$a|{ debug $a }") }.to_not raise_error - end - it "should parse method call with two param lambda" do - expect { @parser.parse("$a.each |$a,$b|{ debug $a }") }.to_not raise_error - end - it "should parse method call with two param lambda and default value" do - expect { @parser.parse("$a.each |$a,$b=1|{ debug $a }") }.to_not raise_error - end - it "should parse method call without lambda (statement)" do - expect { @parser.parse("$a.each") }.to_not raise_error - end - it "should parse method call without lambda (expression)" do - expect { @parser.parse("$x = $a.each + 1") }.to_not raise_error - end - context "a receiver expression of type" do - it "variable should be allowed" do - expect { @parser.parse("$a.each") }.to_not raise_error - end - it "hasharrayaccess should be allowed" do - expect { @parser.parse("$a[0][1].each") }.to_not raise_error - end - it "quoted text should be allowed" do - expect { @parser.parse("\"monkey\".each") }.to_not raise_error - expect { @parser.parse("'monkey'.each") }.to_not raise_error - end - it "selector text should be allowed" do - expect { @parser.parse("$a ? { 'banana'=>[1,2,3]}.each") }.to_not raise_error - end - it "function call should be allowed" do - expect { @parser.parse("duh(1,2,3).each") }.to_not raise_error - end - it "method call should be allowed" do - expect { @parser.parse("$a.foo.bar") }.to_not raise_error - end - it "chained method calls with lambda should be allowed" do - expect { @parser.parse("$a.foo||{}.bar||{}") }.to_not raise_error - end - end - end -end diff --git a/spec/unit/parser/type_loader_spec.rb b/spec/unit/parser/type_loader_spec.rb index 659ffa942..5454528a7 100755 --- a/spec/unit/parser/type_loader_spec.rb +++ b/spec/unit/parser/type_loader_spec.rb @@ -1,225 +1,224 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/parser/type_loader' require 'puppet/parser/parser_factory' -require 'puppet/parser/e_parser_adapter' require 'puppet_spec/modules' require 'puppet_spec/files' describe Puppet::Parser::TypeLoader do include PuppetSpec::Modules include PuppetSpec::Files let(:empty_hostclass) { Puppet::Parser::AST::Hostclass.new('') } let(:loader) { Puppet::Parser::TypeLoader.new(:myenv) } it "should support an environment" do loader = Puppet::Parser::TypeLoader.new(:myenv) loader.environment.name.should == :myenv end it "should delegate its known resource types to its environment" do loader.known_resource_types.should be_instance_of(Puppet::Resource::TypeCollection) end describe "when loading names from namespaces" do it "should do nothing if the name to import is an empty string" do loader.try_load_fqname(:hostclass, "").should be_nil end it "should attempt to import each generated name" do loader.expects(:import_from_modules).with("foo/bar").returns([]) loader.expects(:import_from_modules).with("foo").returns([]) loader.try_load_fqname(:hostclass, "foo::bar") end it "should attempt to load each possible name going from most to least specific" do path_order = sequence('path') ['foo/bar/baz', 'foo/bar', 'foo'].each do |path| Puppet::Parser::Files.expects(:find_manifests_in_modules).with(path, anything).returns([nil, []]).in_sequence(path_order) end loader.try_load_fqname(:hostclass, 'foo::bar::baz') end end describe "when importing" do let(:stub_parser) { stub 'Parser', :file= => nil, :parse => empty_hostclass } before(:each) do Puppet::Parser::ParserFactory.stubs(:parser).with(anything).returns(stub_parser) end it "should return immediately when imports are being ignored" do Puppet::Parser::Files.expects(:find_manifests_in_modules).never Puppet[:ignoreimport] = true loader.import("foo", "/path").should be_nil end it "should find all manifests matching the file or pattern" do Puppet::Parser::Files.expects(:find_manifests_in_modules).with("myfile", anything).returns ["modname", %w{one}] loader.import("myfile", "/path") end it "should pass the environment when looking for files" do Puppet::Parser::Files.expects(:find_manifests_in_modules).with(anything, loader.environment).returns ["modname", %w{one}] loader.import("myfile", "/path") end it "should fail if no files are found" do Puppet::Parser::Files.expects(:find_manifests_in_modules).returns [nil, []] lambda { loader.import("myfile", "/path") }.should raise_error(Puppet::ImportError) end it "should parse each found file" do Puppet::Parser::Files.expects(:find_manifests_in_modules).returns ["modname", [make_absolute("/one")]] loader.expects(:parse_file).with(make_absolute("/one")).returns(Puppet::Parser::AST::Hostclass.new('')) loader.import("myfile", "/path") end it "should not attempt to import files that have already been imported" do loader = Puppet::Parser::TypeLoader.new(:myenv) Puppet::Parser::Files.expects(:find_manifests_in_modules).twice.returns ["modname", %w{/one}] loader.import("myfile", "/path").should_not be_empty loader.import("myfile", "/path").should be_empty end end describe "when importing all" do before do @base = tmpdir("base") # Create two module path directories @modulebase1 = File.join(@base, "first") FileUtils.mkdir_p(@modulebase1) @modulebase2 = File.join(@base, "second") FileUtils.mkdir_p(@modulebase2) Puppet[:modulepath] = "#{@modulebase1}#{File::PATH_SEPARATOR}#{@modulebase2}" end def mk_module(basedir, name) PuppetSpec::Modules.create(name, basedir) end # We have to pass the base path so that we can # write to modules that are in the second search path def mk_manifests(base, mod, type, files) exts = {"ruby" => ".rb", "puppet" => ".pp"} files.collect do |file| name = mod.name + "::" + file.gsub("/", "::") path = File.join(base, mod.name, "manifests", file + exts[type]) FileUtils.mkdir_p(File.split(path)[0]) # write out the class if type == "ruby" File.open(path, "w") { |f| f.print "hostclass '#{name}' do\nend" } else File.open(path, "w") { |f| f.print "class #{name} {}" } end name end end it "should load all puppet manifests from all modules in the specified environment" do @module1 = mk_module(@modulebase1, "one") @module2 = mk_module(@modulebase2, "two") mk_manifests(@modulebase1, @module1, "puppet", %w{a b}) mk_manifests(@modulebase2, @module2, "puppet", %w{c d}) loader.import_all loader.environment.known_resource_types.hostclass("one::a").should be_instance_of(Puppet::Resource::Type) loader.environment.known_resource_types.hostclass("one::b").should be_instance_of(Puppet::Resource::Type) loader.environment.known_resource_types.hostclass("two::c").should be_instance_of(Puppet::Resource::Type) loader.environment.known_resource_types.hostclass("two::d").should be_instance_of(Puppet::Resource::Type) end it "should load all ruby manifests from all modules in the specified environment" do Puppet.expects(:deprecation_warning).at_least(1) @module1 = mk_module(@modulebase1, "one") @module2 = mk_module(@modulebase2, "two") mk_manifests(@modulebase1, @module1, "ruby", %w{a b}) mk_manifests(@modulebase2, @module2, "ruby", %w{c d}) loader.import_all loader.environment.known_resource_types.hostclass("one::a").should be_instance_of(Puppet::Resource::Type) loader.environment.known_resource_types.hostclass("one::b").should be_instance_of(Puppet::Resource::Type) loader.environment.known_resource_types.hostclass("two::c").should be_instance_of(Puppet::Resource::Type) loader.environment.known_resource_types.hostclass("two::d").should be_instance_of(Puppet::Resource::Type) end it "should not load manifests from duplicate modules later in the module path" do @module1 = mk_module(@modulebase1, "one") # duplicate @module2 = mk_module(@modulebase2, "one") mk_manifests(@modulebase1, @module1, "puppet", %w{a}) mk_manifests(@modulebase2, @module2, "puppet", %w{c}) loader.import_all loader.environment.known_resource_types.hostclass("one::c").should be_nil end it "should load manifests from subdirectories" do @module1 = mk_module(@modulebase1, "one") mk_manifests(@modulebase1, @module1, "puppet", %w{a a/b a/b/c}) loader.import_all loader.environment.known_resource_types.hostclass("one::a::b").should be_instance_of(Puppet::Resource::Type) loader.environment.known_resource_types.hostclass("one::a::b::c").should be_instance_of(Puppet::Resource::Type) end it "should skip modules that don't have manifests" do @module1 = mk_module(@modulebase1, "one") @module2 = mk_module(@modulebase2, "two") mk_manifests(@modulebase2, @module2, "ruby", %w{c d}) loader.import_all loader.environment.known_resource_types.hostclass("one::a").should be_nil loader.environment.known_resource_types.hostclass("two::c").should be_instance_of(Puppet::Resource::Type) loader.environment.known_resource_types.hostclass("two::d").should be_instance_of(Puppet::Resource::Type) end end describe "when parsing a file" do it "should create a new parser instance for each file using the current environment" do parser = stub 'Parser', :file= => nil, :parse => empty_hostclass Puppet::Parser::ParserFactory.expects(:parser).twice.with(loader.environment).returns(parser) loader.parse_file("/my/file") loader.parse_file("/my/other_file") end it "should assign the parser its file and parse" do parser = mock 'parser' Puppet::Parser::ParserFactory.expects(:parser).with(loader.environment).returns(parser) parser.expects(:file=).with("/my/file") parser.expects(:parse).returns(empty_hostclass) loader.parse_file("/my/file") end end it "should be able to add classes to the current resource type collection" do file = tmpfile("simple_file.pp") File.open(file, "w") { |f| f.puts "class foo {}" } loader.import(File.basename(file), File.dirname(file)) loader.known_resource_types.hostclass("foo").should be_instance_of(Puppet::Resource::Type) end end diff --git a/spec/unit/pops/benchmark_spec.rb b/spec/unit/pops/benchmark_spec.rb index 03c2e743d..462c03947 100644 --- a/spec/unit/pops/benchmark_spec.rb +++ b/spec/unit/pops/benchmark_spec.rb @@ -1,142 +1,142 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/pops' require 'puppet_spec/pops' require 'puppet_spec/scope' require 'rgen/environment' require 'rgen/metamodel_builder' require 'rgen/serializer/json_serializer' require 'rgen/instantiator/json_instantiator' describe "Benchmark", :benchmark => true do include PuppetSpec::Pops include PuppetSpec::Scope def code 'if true { $a = 10 + 10 } else { $a = "interpolate ${foo} and stuff" } ' end class StringWriter < String alias write concat end class MyJSonSerializer < RGen::Serializer::JsonSerializer def attributeValue(value, a) x = super puts "#{a.eType} value: <<#{value}>> serialize: <<#{x}>>" x end end def json_dump(model) output = StringWriter.new ser = MyJSonSerializer.new(output) ser.serialize(model) output end def json_load(string) env = RGen::Environment.new inst = RGen::Instantiator::JsonInstantiator.new(env, Puppet::Pops::Model) inst.instantiate(string) end it "transformer", :profile => true do parser = Puppet::Pops::Parser::Parser.new() model = parser.parse_string(code).current transformer = Puppet::Pops::Model::AstTransformer.new() m = Benchmark.measure { 10000.times { transformer.transform(model) }} puts "Transformer: #{m}" end it "validator", :profile => true do parser = Puppet::Pops::Parser::EvaluatingParser.new() model = parser.parse_string(code) m = Benchmark.measure { 100000.times { parser.assert_and_report(model) }} puts "Validator: #{m}" end it "parse transform", :profile => true do parser = Puppet::Pops::Parser::Parser.new() transformer = Puppet::Pops::Model::AstTransformer.new() m = Benchmark.measure { 10000.times { transformer.transform(parser.parse_string(code).current) }} puts "Parse and transform: #{m}" end it "parser0", :profile => true do parser = Puppet::Parser::Parser.new('test') m = Benchmark.measure { 10000.times { parser.parse(code) }} puts "Parser 0: #{m}" end it "parser1", :profile => true do parser = Puppet::Pops::Parser::EvaluatingParser.new() m = Benchmark.measure { 10000.times { parser.parse_string(code) }} puts "Parser1: #{m}" end it "marshal1", :profile => true do parser = Puppet::Pops::Parser::EvaluatingParser.new() model = parser.parse_string(code).current dumped = Marshal.dump(model) m = Benchmark.measure { 10000.times { Marshal.load(dumped) }} puts "Marshal1: #{m}" end it "rgenjson", :profile => true do parser = Puppet::Pops::Parser::EvaluatingParser.new() model = parser.parse_string(code).current dumped = json_dump(model) m = Benchmark.measure { 10000.times { json_load(dumped) }} puts "RGen Json: #{m}" end it "lexer2", :profile => true do lexer = Puppet::Pops::Parser::Lexer2.new m = Benchmark.measure {10000.times {lexer.string = code; lexer.fullscan }} puts "Lexer2: #{m}" end it "lexer1", :profile => true do lexer = Puppet::Pops::Parser::Lexer.new m = Benchmark.measure {10000.times {lexer.string = code; lexer.fullscan }} puts "Pops Lexer: #{m}" end it "lexer0", :profile => true do lexer = Puppet::Parser::Lexer.new m = Benchmark.measure {10000.times {lexer.string = code; lexer.fullscan }} puts "Original Lexer: #{m}" end context "Measure Evaluator" do - let(:parser) { Puppet::Pops::Parser::EvaluatingParser::Transitional.new } + let(:parser) { Puppet::Pops::Parser::EvaluatingParser.new } let(:node) { 'node.example.com' } let(:scope) { s = create_test_scope_for_node(node); s } it "evaluator", :profile => true do # Do the loop in puppet code since it otherwise drowns in setup puppet_loop = 'Integer[0, 1000].each |$i| { if true { $a = 10 + 10 } else { $a = "interpolate ${foo} and stuff" }} ' # parse once, only measure the evaluation model = parser.parse_string(puppet_loop, __FILE__) m = Benchmark.measure { parser.evaluate(create_test_scope_for_node(node), model) } puts("Evaluator: #{m}") end end end diff --git a/spec/unit/pops/evaluator/evaluating_parser_spec.rb b/spec/unit/pops/evaluator/evaluating_parser_spec.rb index 54c85eb8e..2c2d5a001 100644 --- a/spec/unit/pops/evaluator/evaluating_parser_spec.rb +++ b/spec/unit/pops/evaluator/evaluating_parser_spec.rb @@ -1,1304 +1,1303 @@ require 'spec_helper' require 'puppet/pops' require 'puppet/pops/evaluator/evaluator_impl' require 'puppet/loaders' 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 3x logic switches some behaviors on these even if the tests explicitly # use the 4x 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' # Tests needs a known configuration of node/scope/compiler since it parses and evaluates # snippets as the compiler will evaluate them, butwithout the overhead of compiling a complete # catalog for each tested expression. # - @parser = Puppet::Pops::Parser::EvaluatingParser::Transitional.new + @parser = Puppet::Pops::Parser::EvaluatingParser.new @node = Puppet::Node.new('node.example.com') @node.environment = Puppet::Node::Environment.create(:testing, []) @compiler = Puppet::Parser::Compiler.new(@node) @scope = Puppet::Parser::Scope.new(@compiler) @scope.source = Puppet::Resource::Type.new(:node, 'node.example.com') @scope.parent = @compiler.topscope end let(:parser) { @parser } let(:scope) { @scope } 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 < 'a'" => 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.*']" => false, # String is not subtype of Regexp. PUP-957 "$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 "String[1] in bananas" => false, # Philosophically true though :-) "'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 { "if /(ana)/ in bananas {$1}" => 'ana', "if /(xyz)/ in bananas {$1} else {$1}" => nil, "$a = bananas =~ /(ana)/; $b = /(xyz)/ in bananas; $1" => 'ana', "$a = xyz =~ /(xyz)/; $b = /(ana)/ in bananas; $1" => 'ana', "if /p/ in [pineapple, bananas] {$0}" => 'p', "if /b/ in [pineapple, bananas] {$0}" => 'b', }.each do |source, result| it "sets match variables for a regexp search using in such that '#{source}' produces '#{result}'" do parser.evaluate_string(scope, source, __FILE__).should == result end end { 'Any' => ['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'" => 0, "'- 2' + '2'" => 0, '"-\t 2" + "2"' => 0, "'+2' + '2'" => 4, "'+ 2' + '2'" => 4, "'2.2' + '2.2'" => 4.4, "'-2.2' + '2.2'" => 0.0, "'0xF7' + '010'" => 0xFF, "'0xF7' + '0x8'" => 0xFF, "'0367' + '010'" => 0xFF, "'012.3' + '010'" => 20.3, "'-0x2' + '0x4'" => 2, "'+0x2' + '0x4'" => 6, "'-02' + '04'" => 2, "'+02' + '04'" => 6, }.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, '"-\n 2" + "2"' => :error, '"-\v 2" + "2"' => :error, '"-2\n" + "2"' => :error, '"-2\n " + "2"' => :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', # supports unfold "case ringo { *[paul, john, ringo, george] : { 'beatle' } }" => 'beatle', }.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, default => yes }" => 'yes', "3 ? { 1 => no, default => yes, 3 => no }" => 'no', "3 ? { 1 => no, 3 => no, default => yes }" => 'no', "4 ? { 1 => no, default => yes, 3 => no }" => 'yes', "4 ? { 1 => no, 3 => no, default => yes }" => 'yes', "'banana' ? { /.*(ana).*/ => $1 }" => 'ana', "[2] ? { Array[String] => yes, Array => yes}" => 'yes', "ringo ? *[paul, john, ringo, george] => 'beatle'" => 'beatle', }.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 it 'fails if a selector does not match' do expect{parser.evaluate_string(scope, "2 ? 3 => 4")}.to raise_error(/No matching entry for selector parameter with value '2'/) end end context "When evaluator evaluated unfold" do { "*[1,2,3]" => [1,2,3], "*1" => [1], "*'a'" => ['a'] }.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 it "should parse and evaluate the expression '*{a=>10, b=>20} to [['a',10],['b',20]]" do result = parser.evaluate_string(scope, '*{a=>10, b=>20}', __FILE__) expect(result).to include(['a', 10]) expect(result).to include(['b', 20]) 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]" => [], "[1,*[2,3],4]" => [1,2,3,4], "[1,*[2,3],4][1]" => 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, 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) Puppet.override({:global_scope => scope}, "test") do scope.known_resource_types.import_ast(ast, '') ast.code.safeevaluate(scope).should == 'chocolate' end end # Resource default and override expressions and resource parameter access with [] { # Properties "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", # Parameters "notify { id: withpath=>explicit} Notify[id][withpath]" => "explicit", "Notify { withpath=>by_default } notify { foo: } Notify[foo][withpath]" => "by_default", "notify {foo:} Notify[foo]{withpath=>by_override} Notify[foo][withpath]" => "by_override", # Metaparameters "notify { foo: tag => evoe} Notify[foo][tag]" => "evoe", # Does not produce the defaults for tag parameter (title, type or names of scopes) "notify { foo: } Notify[foo][tag]" => nil, # But a default may be specified on the type "Notify { tag=>by_default } notify { foo: } Notify[foo][tag]" => "by_default", "Notify { tag=>by_default } notify { foo: } Notify[foo]{ tag=>by_override } Notify[foo][tag]" => "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 # Virtual and realized resource default and overridden resource parameter access with [] { # Properties "@notify { id: message=>explicit } Notify[id][message]" => "explicit", "@notify { id: message=>explicit } realize Notify[id] Notify[id][message]" => "explicit", "Notify { message=>by_default } @notify { id: } Notify[id][message]" => "by_default", "Notify { message=>by_default } @notify { id: tag=>thisone } Notify <| tag == thisone |>; Notify[id][message]" => "by_default", "@notify { id: } Notify[id]{message=>by_override} Notify[id][message]" => "by_override", # Parameters "@notify { id: withpath=>explicit } Notify[id][withpath]" => "explicit", "Notify { withpath=>by_default } @notify { id: } Notify[id][withpath]" => "by_default", "@notify { id: } realize Notify[id] Notify[id]{withpath=>by_override} Notify[id][withpath]" => "by_override", # Metaparameters "@notify { id: tag=>explicit } Notify[id][tag]" => "explicit", }.each do |source, result| it "parses and evaluates virtual and realized resources in the expression '#{source}' to #{result}" do expect(parser.evaluate_string(scope, source, __FILE__)).to eq(result) end end # Exported resource attributes { "@@notify { id: message=>explicit } Notify[id][message]" => "explicit", "@@notify { id: message=>explicit, tag=>thisone } Notify <<| tag == thisone |>> Notify[id][message]" => "explicit", }.each do |source, result| it "parses and evaluates exported resources in the expression '#{source}' to #{result}" do expect(parser.evaluate_string(scope, source, __FILE__)).to eq(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'/, # NOTE: these meta-esque parameters are not recognized as such "notify { id: message=>explicit} Notify[id][title]" => /does not have a parameter called 'title'/, "notify { id: message=>explicit} Notify[id]['type']" => /does not have a parameter called 'type'/, "notify { id: message=>explicit } Notify[id]{message=>override}" => /'message' is already set on Notify\[id\]/ }.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, "! ''" => false, "! 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", # unfolds '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 it "provides location information on error in unparenthesized call logic" do expect{parser.evaluate_string(scope, "include non_existing_class", __FILE__)}.to raise_error(Puppet::ParseError, /line 1\:1/) end it 'defaults can be given in a lambda and used only when arg is missing' do env_loader = @compiler.loaders.public_environment_loader fc = Puppet::Functions.create_function(:test) do dispatch :test do param 'Integer', 'count' required_block_param end def test(count, block) block.call({}, *[].fill(10, 0, count)) end end the_func = fc.new({}, env_loader) env_loader.add_entry(:function, 'test', the_func, __FILE__) expect(parser.evaluate_string(scope, "test(1) |$x, $y=20| { $x + $y}")).to eql(30) expect(parser.evaluate_string(scope, "test(2) |$x, $y=20| { $x + $y}")).to eql(20) end it 'a given undef does not select the default value' do env_loader = @compiler.loaders.public_environment_loader fc = Puppet::Functions.create_function(:test) do dispatch :test do param 'Any', 'lambda_arg' required_block_param end def test(lambda_arg, block) block.call({}, lambda_arg) end end the_func = fc.new({}, env_loader) env_loader.add_entry(:function, 'test', the_func, __FILE__) expect(parser.evaluate_string(scope, "test(undef) |$x=20| { $x == undef}")).to eql(true) end it 'a given undef is given as nil' do env_loader = @compiler.loaders.public_environment_loader fc = Puppet::Functions.create_function(:assert_no_undef) do dispatch :assert_no_undef do param 'Any', 'x' end def assert_no_undef(x) case x when Array return unless x.include?(:undef) when Hash return unless x.keys.include?(:undef) || x.values.include?(:undef) else return unless x == :undef end raise "contains :undef" end end the_func = fc.new({}, env_loader) env_loader.add_entry(:function, 'assert_no_undef', the_func, __FILE__) expect{parser.evaluate_string(scope, "assert_no_undef(undef)")}.to_not raise_error() expect{parser.evaluate_string(scope, "assert_no_undef([undef])")}.to_not raise_error() expect{parser.evaluate_string(scope, "assert_no_undef({undef => 1})")}.to_not raise_error() expect{parser.evaluate_string(scope, "assert_no_undef({1 => undef})")}.to_not raise_error() 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 it 'should accept a variable with leading underscore when used directly' do source = '$_x = 10; "$_x"' expect(parser.evaluate_string(scope, source, __FILE__)).to eql('10') end it 'should accept a variable with leading underscore when used as an expression' do source = '$_x = 10; "${_x}"' expect(parser.evaluate_string(scope, source, __FILE__)).to eql('10') 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 expression" do src = <<-CODE $name = 'Fjodor' @("END") Hello $name |- END CODE parser.evaluate_string(scope, src).should == "Hello Fjodor" end end context "Handles Deprecations and Discontinuations" do it 'of import statements' do source = "\nimport foo" # 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.evaluate_string(scope, source) }.to raise_error(/'import' has been discontinued.*line 2:1/) 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 context 'does not leak variables' do it 'local variables are gone when lambda ends' do source = <<-SOURCE [1,2,3].each |$x| { $y = $x} $a = $y SOURCE expect do parser.evaluate_string(scope, source) end.to raise_error(/Unknown variable: 'y'/) end it 'lambda parameters are gone when lambda ends' do source = <<-SOURCE [1,2,3].each |$x| { $y = $x} $a = $x SOURCE expect do parser.evaluate_string(scope, source) end.to raise_error(/Unknown variable: 'x'/) end it 'does not leak match variables' do source = <<-SOURCE if 'xyz' =~ /(x)(y)(z)/ { notice $2 } case 'abc' { /(a)(b)(c)/ : { $x = $2 } } "-$x-$2-" SOURCE expect(parser.evaluate_string(scope, source)).to eq('-b--') 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/loaders/loader_paths_spec.rb b/spec/unit/pops/loaders/loader_paths_spec.rb index 2929fe7f8..2cd565a8c 100644 --- a/spec/unit/pops/loaders/loader_paths_spec.rb +++ b/spec/unit/pops/loaders/loader_paths_spec.rb @@ -1,66 +1,55 @@ require 'spec_helper' require 'puppet_spec/files' require 'puppet/pops' require 'puppet/loaders' describe 'loader paths' do include PuppetSpec::Files - before(:each) { Puppet[:biff] = true } let(:static_loader) { Puppet::Pops::Loader::StaticLoader.new() } let(:unused_loaders) { nil } describe 'the relative_path_for_types method' do it 'produces paths to load in precendence order' do module_dir = dir_containing('testmodule', { 'functions' => {}, 'lib' => { 'puppet' => { 'functions' => {}, - 'parser' => { - 'functions' => {}, - } }}}) module_loader = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(static_loader, unused_loaders, 'testmodule', module_dir, 'test1') effective_paths = Puppet::Pops::Loader::LoaderPaths.relative_paths_for_type(:function, module_loader) expect(effective_paths.collect(&:generic_path)).to eq([ - File.join(module_dir, 'lib', 'puppet', 'functions'), # 4x functions - File.join(module_dir, 'lib', 'puppet','parser', 'functions') # 3x functions + File.join(module_dir, 'lib', 'puppet', 'functions') ]) end it 'module loader has smart-paths that prunes unavailable paths' do module_dir = dir_containing('testmodule', {'lib' => {'puppet' => {'functions' => {'foo.rb' => 'Puppet::Functions.create_function("testmodule::foo") { def foo; end; }' }}}}) module_loader = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(static_loader, unused_loaders, 'testmodule', module_dir, 'test1') effective_paths = module_loader.smart_paths.effective_paths(:function) expect(effective_paths.size).to be_eql(1) expect(effective_paths[0].generic_path).to be_eql(File.join(module_dir, 'lib', 'puppet', 'functions')) - expect(module_loader.path_index.size).to be_eql(1) - expect(module_loader.path_index.include?(File.join(module_dir, 'lib', 'puppet', 'functions', 'foo.rb'))).to be(true) end it 'all function smart-paths produces entries if they exist' do module_dir = dir_containing('testmodule', { 'lib' => { 'puppet' => { 'functions' => {'foo4x.rb' => 'ignored in this test'}, - 'parser' => { - 'functions' => {'foo3x.rb' => 'ignored in this test'}, - } }}}) module_loader = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(static_loader, unused_loaders, 'testmodule', module_dir, 'test1') effective_paths = module_loader.smart_paths.effective_paths(:function) - expect(effective_paths.size).to eq(2) - expect(module_loader.path_index.size).to eq(2) + expect(effective_paths.size).to eq(1) + expect(module_loader.path_index.size).to eq(1) path_index = module_loader.path_index - expect(path_index.include?(File.join(module_dir, 'lib', 'puppet', 'functions', 'foo4x.rb'))).to eq(true) - expect(path_index.include?(File.join(module_dir, 'lib', 'puppet', 'parser', 'functions', 'foo3x.rb'))).to eq(true) + expect(path_index).to include(File.join(module_dir, 'lib', 'puppet', 'functions', 'foo4x.rb')) end end end diff --git a/spec/unit/pops/loaders/loaders_spec.rb b/spec/unit/pops/loaders/loaders_spec.rb index a9156ecd9..831236698 100644 --- a/spec/unit/pops/loaders/loaders_spec.rb +++ b/spec/unit/pops/loaders/loaders_spec.rb @@ -1,151 +1,125 @@ require 'spec_helper' require 'puppet_spec/files' require 'puppet/pops' require 'puppet/loaders' describe 'loader helper classes' do it 'NamedEntry holds values and is frozen' do ne = Puppet::Pops::Loader::Loader::NamedEntry.new('name', 'value', 'origin') expect(ne.frozen?).to be_true expect(ne.typed_name).to eql('name') expect(ne.origin).to eq('origin') expect(ne.value).to eq('value') end it 'TypedName holds values and is frozen' do tn = Puppet::Pops::Loader::Loader::TypedName.new(:function, '::foo::bar') expect(tn.frozen?).to be_true expect(tn.type).to eq(:function) expect(tn.name_parts).to eq(['foo', 'bar']) expect(tn.name).to eq('foo::bar') expect(tn.qualified).to be_true end end describe 'loaders' do include PuppetSpec::Files let(:module_without_metadata) { File.join(config_dir('wo_metadata_module'), 'modules') } let(:module_with_metadata) { File.join(config_dir('single_module'), 'modules') } let(:dependent_modules_with_metadata) { config_dir('dependent_modules_with_metadata') } let(:empty_test_env) { environment_for() } # Loaders caches the puppet_system_loader, must reset between tests before(:each) { Puppet::Pops::Loaders.clear() } it 'creates a puppet_system loader' do loaders = Puppet::Pops::Loaders.new(empty_test_env) expect(loaders.puppet_system_loader()).to be_a(Puppet::Pops::Loader::ModuleLoaders::FileBased) end it 'creates an environment loader' do loaders = Puppet::Pops::Loaders.new(empty_test_env) expect(loaders.public_environment_loader()).to be_a(Puppet::Pops::Loader::SimpleEnvironmentLoader) expect(loaders.public_environment_loader().to_s).to eql("(SimpleEnvironmentLoader 'environment:*test*')") expect(loaders.private_environment_loader()).to be_a(Puppet::Pops::Loader::DependencyLoader) expect(loaders.private_environment_loader().to_s).to eql("(DependencyLoader 'environment' [])") end - it 'can load 3x system functions' do - Puppet[:biff] = true - loaders = Puppet::Pops::Loaders.new(empty_test_env) - puppet_loader = loaders.puppet_system_loader() - - function = puppet_loader.load_typed(typed_name(:function, 'sprintf')).value - - expect(function.class.name).to eq('sprintf') - expect(function).to be_a(Puppet::Functions::Function) - end - - it 'can load 3x system functions more than once' do - Puppet[:biff] = true - loaders = Puppet::Pops::Loaders.new(empty_test_env) - puppet_loader = loaders.puppet_system_loader() - - function = puppet_loader.load_typed(typed_name(:function, 'sprintf')).value - - expect(function.class.name).to eq('sprintf') - expect(function).to be_a(Puppet::Functions::Function) - - function = puppet_loader.load_typed(typed_name(:function, 'sprintf')).value - expect(function.class.name).to eq('sprintf') - expect(function).to be_a(Puppet::Functions::Function) - end - it 'can load a function using a qualified or unqualified name from a module with metadata' do loaders = Puppet::Pops::Loaders.new(environment_for(module_with_metadata)) modulea_loader = loaders.public_loader_for_module('modulea') unqualified_function = modulea_loader.load_typed(typed_name(:function, 'rb_func_a')).value qualified_function = modulea_loader.load_typed(typed_name(:function, 'modulea::rb_func_a')).value expect(unqualified_function).to be_a(Puppet::Functions::Function) expect(qualified_function).to be_a(Puppet::Functions::Function) expect(unqualified_function.class.name).to eq('rb_func_a') expect(qualified_function.class.name).to eq('modulea::rb_func_a') end it 'can load a function with a qualified name from module without metadata' do loaders = Puppet::Pops::Loaders.new(environment_for(module_without_metadata)) moduleb_loader = loaders.public_loader_for_module('moduleb') function = moduleb_loader.load_typed(typed_name(:function, 'moduleb::rb_func_b')).value expect(function).to be_a(Puppet::Functions::Function) expect(function.class.name).to eq('moduleb::rb_func_b') end it 'cannot load an unqualified function from a module without metadata' do loaders = Puppet::Pops::Loaders.new(environment_for(module_without_metadata)) moduleb_loader = loaders.public_loader_for_module('moduleb') expect(moduleb_loader.load_typed(typed_name(:function, 'rb_func_b'))).to be_nil end it 'makes all other modules visible to a module without metadata' do env = environment_for(module_with_metadata, module_without_metadata) loaders = Puppet::Pops::Loaders.new(env) moduleb_loader = loaders.private_loader_for_module('moduleb') function = moduleb_loader.load_typed(typed_name(:function, 'moduleb::rb_func_b')).value expect(function.call({})).to eql("I am modulea::rb_func_a() + I am moduleb::rb_func_b()") end it 'makes dependent modules visible to a module with metadata' do env = environment_for(dependent_modules_with_metadata) loaders = Puppet::Pops::Loaders.new(env) moduleb_loader = loaders.private_loader_for_module('user') function = moduleb_loader.load_typed(typed_name(:function, 'user::caller')).value expect(function.call({})).to eql("usee::callee() was told 'passed value' + I am user::caller()") end it 'can load a function more than once from modules' do env = environment_for(dependent_modules_with_metadata) loaders = Puppet::Pops::Loaders.new(env) moduleb_loader = loaders.private_loader_for_module('user') function = moduleb_loader.load_typed(typed_name(:function, 'user::caller')).value expect(function.call({})).to eql("usee::callee() was told 'passed value' + I am user::caller()") function = moduleb_loader.load_typed(typed_name(:function, 'user::caller')).value expect(function.call({})).to eql("usee::callee() was told 'passed value' + I am user::caller()") end def environment_for(*module_paths) Puppet::Node::Environment.create(:'*test*', module_paths, '') end def typed_name(type, name) Puppet::Pops::Loader::Loader::TypedName.new(type, name) end def config_dir(config_name) my_fixture(config_name) end end diff --git a/spec/unit/pops/loaders/module_loaders_spec.rb b/spec/unit/pops/loaders/module_loaders_spec.rb index 9d0c1a86d..058e765de 100644 --- a/spec/unit/pops/loaders/module_loaders_spec.rb +++ b/spec/unit/pops/loaders/module_loaders_spec.rb @@ -1,119 +1,90 @@ require 'spec_helper' require 'puppet_spec/files' require 'puppet/pops' require 'puppet/loaders' describe 'FileBased module loader' do include PuppetSpec::Files let(:static_loader) { Puppet::Pops::Loader::StaticLoader.new() } let(:loaders) { Puppet::Pops::Loaders.new(Puppet::Node::Environment.create(:testing, [])) } it 'can load a 4x function API ruby function in global name space' do module_dir = dir_containing('testmodule', { 'lib' => { 'puppet' => { 'functions' => { 'foo4x.rb' => <<-CODE Puppet::Functions.create_function(:foo4x) do def foo4x() 'yay' end end CODE } } } }) module_loader = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(static_loader, loaders, 'testmodule', module_dir, 'test1') function = module_loader.load_typed(typed_name(:function, 'foo4x')).value expect(function.class.name).to eq('foo4x') expect(function.is_a?(Puppet::Functions::Function)).to eq(true) end it 'can load a 4x function API ruby function in qualified name space' do module_dir = dir_containing('testmodule', { 'lib' => { 'puppet' => { 'functions' => { 'testmodule' => { 'foo4x.rb' => <<-CODE Puppet::Functions.create_function('testmodule::foo4x') do def foo4x() 'yay' end end CODE } } } }}) module_loader = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(static_loader, loaders, 'testmodule', module_dir, 'test1') function = module_loader.load_typed(typed_name(:function, 'testmodule::foo4x')).value expect(function.class.name).to eq('testmodule::foo4x') expect(function.is_a?(Puppet::Functions::Function)).to eq(true) end it 'makes parent loader win over entries in child' do module_dir = dir_containing('testmodule', { 'lib' => { 'puppet' => { 'functions' => { 'testmodule' => { 'foo.rb' => <<-CODE Puppet::Functions.create_function('testmodule::foo') do def foo() 'yay' end end CODE }}}}}) module_loader = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(static_loader, loaders, 'testmodule', module_dir, 'test1') module_dir2 = dir_containing('testmodule2', { 'lib' => { 'puppet' => { 'functions' => { 'testmodule2' => { 'foo.rb' => <<-CODE raise "should not get here" CODE }}}}}) module_loader2 = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(module_loader, loaders, 'testmodule2', module_dir2, 'test2') function = module_loader2.load_typed(typed_name(:function, 'testmodule::foo')).value expect(function.class.name).to eq('testmodule::foo') expect(function.is_a?(Puppet::Functions::Function)).to eq(true) end - context 'when delegating 3x to 4x' do - before(:each) { Puppet[:biff] = true } - - it 'can load a 3x function API ruby function in global name space' do - module_dir = dir_containing('testmodule', { - 'lib' => { - 'puppet' => { - 'parser' => { - 'functions' => { - 'foo3x.rb' => <<-CODE - Puppet::Parser::Functions::newfunction( - :foo3x, :type => :rvalue, - :arity => 1 - ) do |args| - args[0] - end - CODE - } - } - } - }}) - - module_loader = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(static_loader, loaders, 'testmodule', module_dir, 'test1') - function = module_loader.load_typed(typed_name(:function, 'foo3x')).value - expect(function.class.name).to eq('foo3x') - expect(function.is_a?(Puppet::Functions::Function)).to eq(true) - end - end - def typed_name(type, name) Puppet::Pops::Loader::Loader::TypedName.new(type, name) end end diff --git a/spec/unit/pops/parser/evaluating_parser_spec.rb b/spec/unit/pops/parser/evaluating_parser_spec.rb index 8448a5af3..7645b9bc2 100644 --- a/spec/unit/pops/parser/evaluating_parser_spec.rb +++ b/spec/unit/pops/parser/evaluating_parser_spec.rb @@ -1,90 +1,89 @@ require 'spec_helper' require 'puppet/pops' require 'puppet_spec/pops' require 'puppet_spec/scope' describe 'The Evaluating Parser' do include PuppetSpec::Pops include PuppetSpec::Scope let(:acceptor) { Puppet::Pops::Validation::Acceptor.new() } - let(:diag) { Puppet::Pops::Binder::Hiera2::DiagnosticProducer.new(acceptor) } let(:scope) { s = create_test_scope_for_node(node); s } let(:node) { 'node.example.com' } def quote(x) Puppet::Pops::Parser::EvaluatingParser.quote(x) end def evaluator() Puppet::Pops::Parser::EvaluatingParser.new() end def evaluate(s) evaluator.evaluate(scope, quote(s)) end def test(x) evaluator.evaluate_string(scope, quote(x)).should == x end def test_interpolate(x, y) scope['a'] = 'expansion' evaluator.evaluate_string(scope, quote(x)).should == y end context 'when evaluating' do it 'should produce an empty string with no change' do test('') end it 'should produce a normal string with no change' do test('A normal string') end it 'should produce a string with newlines with no change' do test("A\nnormal\nstring") end it 'should produce a string with escaped newlines with no change' do test("A\\nnormal\\nstring") end it 'should produce a string containing quotes without change' do test('This " should remain untouched') end it 'should produce a string containing escaped quotes without change' do test('This \" should remain untouched') end it 'should expand ${a} variables' do test_interpolate('This ${a} was expanded', 'This expansion was expanded') end it 'should expand quoted ${a} variables' do test_interpolate('This "${a}" was expanded', 'This "expansion" was expanded') end it 'should not expand escaped ${a}' do test_interpolate('This \${a} was not expanded', 'This ${a} was not expanded') end it 'should expand $a variables' do test_interpolate('This $a was expanded', 'This expansion was expanded') end it 'should expand quoted $a variables' do test_interpolate('This "$a" was expanded', 'This "expansion" was expanded') end it 'should not expand escaped $a' do test_interpolate('This \$a was not expanded', 'This $a was not expanded') end it 'should produce an single space from a \s' do test_interpolate("\\s", ' ') end end end diff --git a/spec/unit/pops/parser/parsing_typed_parameters_spec.rb b/spec/unit/pops/parser/parsing_typed_parameters_spec.rb index 88a6c302d..678f6523c 100644 --- a/spec/unit/pops/parser/parsing_typed_parameters_spec.rb +++ b/spec/unit/pops/parser/parsing_typed_parameters_spec.rb @@ -1,73 +1,72 @@ 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 # 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' end - let(:parser) { Puppet::Pops::Parser::EvaluatingParser::Transitional.new } + let(:parser) { Puppet::Pops::Parser::EvaluatingParser.new } context "captures-rest parameter" do it 'is allowed in lambda when placed last' do source = <<-CODE foo() |$a, *$b| { $a + $b[0] } CODE expect do parser.parse_string(source, __FILE__) end.to_not raise_error() end it 'allows a type annotation' do source = <<-CODE foo() |$a, Integer *$b| { $a + $b[0] } CODE expect do parser.parse_string(source, __FILE__) end.to_not raise_error() end it 'is not allowed in lambda except last' do source = <<-CODE foo() |*$a, $b| { $a + $b[0] } CODE expect do parser.parse_string(source, __FILE__) end.to raise_error(Puppet::ParseError, /Parameter \$a is not last, and has 'captures rest'/) end it 'is not allowed in define' do source = <<-CODE define foo(*$a) { } CODE expect do parser.parse_string(source, __FILE__) end.to raise_error(Puppet::ParseError, /Parameter \$a has 'captures rest' - not supported in a 'define'/) end it 'is not allowed in class' do source = <<-CODE class foo(*$a) { } CODE expect do parser.parse_string(source, __FILE__) end.to raise_error(Puppet::ParseError, /Parameter \$a has 'captures rest' - not supported in a Host Class Definition/) end end end diff --git a/spec/unit/pops/validator/validator_spec.rb b/spec/unit/pops/validator/validator_spec.rb index 46fe644c6..ab4f3ebc7 100644 --- a/spec/unit/pops/validator/validator_spec.rb +++ b/spec/unit/pops/validator/validator_spec.rb @@ -1,156 +1,126 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/pops' require 'puppet_spec/pops' # relative to this spec file (./) does not work as this file is loaded by rspec require File.join(File.dirname(__FILE__), '../parser/parser_rspec_helper') -describe "validating 3x" do - include ParserRspecHelper - include PuppetSpec::Pops - - let(:acceptor) { Puppet::Pops::Validation::Acceptor.new() } - let(:validator) { Puppet::Pops::Validation::ValidatorFactory_3_1.new().validator(acceptor) } - - def validate(model) - validator.validate(model) - acceptor - end - - it 'should raise error for illegal names' do - pending "validation was too strict, now too relaxed - validation missing" - expect(validate(fqn('Aaa'))).to have_issue(Puppet::Pops::Issues::ILLEGAL_NAME) - expect(validate(fqn('AAA'))).to have_issue(Puppet::Pops::Issues::ILLEGAL_NAME) - end - - it 'should raise error for illegal variable names' do - pending "validation was too strict, now too relaxed - validation missing" - expect(validate(fqn('Aaa').var())).to have_issue(Puppet::Pops::Issues::ILLEGAL_NAME) - expect(validate(fqn('AAA').var())).to have_issue(Puppet::Pops::Issues::ILLEGAL_NAME) - end - - it 'should raise error for -= assignment' do - expect(validate(fqn('aaa').minus_set(2))).to have_issue(Puppet::Pops::Issues::UNSUPPORTED_OPERATOR) - end - -end - describe "validating 4x" do include ParserRspecHelper include PuppetSpec::Pops let(:acceptor) { Puppet::Pops::Validation::Acceptor.new() } let(:validator) { Puppet::Pops::Validation::ValidatorFactory_4_0.new().validator(acceptor) } def validate(model) validator.validate(model) acceptor end it 'should raise error for illegal names' do pending "validation was too strict, now too relaxed - validation missing" expect(validate(fqn('Aaa'))).to have_issue(Puppet::Pops::Issues::ILLEGAL_NAME) expect(validate(fqn('AAA'))).to have_issue(Puppet::Pops::Issues::ILLEGAL_NAME) end it 'should raise error for illegal variable names' do expect(validate(fqn('Aaa').var())).to have_issue(Puppet::Pops::Issues::ILLEGAL_VAR_NAME) expect(validate(fqn('AAA').var())).to have_issue(Puppet::Pops::Issues::ILLEGAL_VAR_NAME) expect(validate(fqn('aaa::_aaa').var())).to have_issue(Puppet::Pops::Issues::ILLEGAL_VAR_NAME) end it 'should not raise error for variable name with underscore first in first name segment' do expect(validate(fqn('_aa').var())).to_not have_issue(Puppet::Pops::Issues::ILLEGAL_VAR_NAME) expect(validate(fqn('::_aa').var())).to_not have_issue(Puppet::Pops::Issues::ILLEGAL_VAR_NAME) end context 'for non productive expressions' do [ '1', '3.14', "'a'", '"a"', '"${$a=10}"', # interpolation with side effect 'false', 'true', 'default', 'undef', '[1,2,3]', '{a=>10}', 'if 1 {2}', 'if 1 {2} else {3}', 'if 1 {2} elsif 3 {4}', 'unless 1 {2}', 'unless 1 {2} else {3}', '1 ? 2 => 3', '1 ? { 2 => 3}', '-1', '-foo()', # unary minus on productive '1+2', '1<2', '(1<2)', '!true', '!foo()', # not on productive '$a', '$a[1]', 'name', 'Type', 'Type[foo]' ].each do |expr| it "produces error for non productive: #{expr}" do source = "#{expr}; $a = 10" expect(validate(parse(source))).to have_issue(Puppet::Pops::Issues::IDEM_EXPRESSION_NOT_LAST) end it "does not produce error when last for non productive: #{expr}" do source = " $a = 10; #{expr}" expect(validate(parse(source))).to_not have_issue(Puppet::Pops::Issues::IDEM_EXPRESSION_NOT_LAST) end end [ 'if 1 {$a = 1}', 'if 1 {2} else {$a=1}', 'if 1 {2} elsif 3 {$a=1}', 'unless 1 {$a=1}', 'unless 1 {2} else {$a=1}', '$a = 1 ? 2 => 3', '$a = 1 ? { 2 => 3}', 'Foo[a] -> Foo[b]', '($a=1)', 'foo()', '$a.foo()' ].each do |expr| it "does not produce error when for productive: #{expr}" do source = "#{expr}; $x = 1" expect(validate(parse(source))).to_not have_issue(Puppet::Pops::Issues::IDEM_EXPRESSION_NOT_LAST) end end ['class', 'define', 'node'].each do |type| it "flags non productive expression last in #{type}" do source = <<-SOURCE #{type} nope { 1 } end SOURCE expect(validate(parse(source))).to have_issue(Puppet::Pops::Issues::IDEM_NOT_ALLOWED_LAST) end end end context 'for reserved words' do ['function', 'private', 'type', 'attr'].each do |word| it "produces an error for the word '#{word}'" do source = "$a = #{word}" expect(validate(parse(source))).to have_issue(Puppet::Pops::Issues::RESERVED_WORD) end end end def parse(source) Puppet::Pops::Parser::Parser.new().parse_string(source) end end