diff --git a/CHANGELOG b/CHANGELOG index d615ed843..1df2e53c4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,934 +1,938 @@ + Exec resources must now have unique names. This is easily accomplished + by just specifying a unique name with whatever (unique or otherwise) + command you need. + Fixed #989 -- missing CRL files are correctly ignored, and the value should be set to 'false' to explicitly not look for these files. Fixed #1017 -- environment-specific modulepath is no longer ignored. Fixing #794 -- consolidating the gentoo configuration files. Fixing #976 -- both the full name of qualified classes and the class parts are now added as tags. I've also created a Tagging module that we should push throughout the rest of the system that uses tags. Fixing #995 -- puppetd no longer dies at startup if the server is not running. Fixing #977 -- the rundir is again set to 1777. Fixed #971 -- classes can once again be included multiple times. Added builtin support for Nagios types using Naginator to parse and generate the files. 0.24.1 Updated vim filetype detection. (#900 and #963) Default resources like schedules no longer conflict with managed resources. (#965) Removing the ability to disable http keep-alive, since it didn't really work anyway and it should no longer be necessary. Refactored http keep-alive so it actually works again. This should be sufficient enough that we no longer need the ability to disable keep-alive. There is now a central module responsible for managing HTTP instances, along with all certificates in those instances. Fixed a backward compatibility issue when running 0.23.x clients against 0.24.0 servers -- relationships would consistently not work. (#967) Closing existing http connections when opening a new one, and closing all connections after each run. (#961) Removed warning about deprecated explicit plugins mounts. 0.24.0 (misspiggy) Modifying the behaviour of the certdnsnames setting. It now defaults to an empty string, and will only be used if it is set to something else. If it is set, then the host's FQDN will also be added as an alias. The default behaviour is now to add 'puppet' and 'puppet.$domain' as DNS aliases when the name for the cert being signed is equal to the signing machine's name, which will only be the case for CA servers. This should result in servers always having the alias set up and no one else, but you can still override the aliases if you want. External node support now requires that you set the 'node_terminus' setting to 'exec'. See the IndirectionReference on the wiki for more information. http_enable_post_connection_check added as a configuration option for puppetd. This defaults to true, which validates the server SSL certificate against the requested host name in new versions of ruby. See #896 for more information. Mounts no longer remount swap filesystems. Slightly modifying how services manage their list of paths (and adding documention for it). Services now default to the paths specified by the provider classes. Removed 'type' as a valid attribute for services, since it's been deprecated since the creation of providers. Removed 'running' as a valid attribute for services, since it's been deprecated since February 2006. Added modified patch by Matt Palmer which adds a 'plugins' mount, fixing #891. See PluginsInModules on the wiki for information on usage. Empty dbserver and dbpassword settings will now be ignored when initializing Rails connections (patch by womble). Configuration settings can now be blank (patch by womble). Added calls to endpwent/endgrent when searching for user and group IDs, which fixes #791. Obviated 'target' in interfaces, as all file paths were automatically calculated anyway. The parameter is still there, but it's not used and just generates a warning. Fixing some of the problems with interface management on Red Hat. Puppet now uses the :netmask property and does not try to set the bootproto (#762). You now must specify an environment and you are required to specify the valid environments for your site. (#911) Certificates now always specify a subjectAltName, but it defaults to '*', meaning that it doesn't require DNS names to match. You can override that behaviour by specifying a value for 'certdnsnames', which will then require that hostname as a match (#896). Relationship metaparams (:notify, :require, :subscribe, and :before) now stack when they are collecting metaparam values from their containers (#446). For instance, if a resource inside a definition has a value set for 'require', and you call the definition with 'require', the resource gets both requires, where before it would only retain its initial value. Changed the behavior of --debug to include Mongrel client debugging information. Mongrel output will be written to the terminal only, not to the puppet debug log. This should help anyone working with reverse HTTP SSL proxies. (#905) Fixed #800 -- invalid configurations are no longer cached. This was done partially by adding a relationship validation step once the entire configuration is created, but it also required the previously-mentioned changes to how the configuration retrieval process works. Removed some functionality from the Master client, since the local functionality has been replaced with the Indirector already, and rearranging how configuration retrieval is done to fix ordering and caching bugs. The node scope is now above all other scopes besides the 'main' scope, which should help make its variables visible to other classes, assuming those classes were not included in the node's parent. Replaced GRATR::Digraph with Puppet::SimpleGraph as the base class for Puppet's graphing. Functionality should be equivalent but with dramatically better performance. The --use-nodes and --no-nodes options are now obsolete. Puppet automatically detects when nodes are defined, and if they are defined it will require that a node be found, else it will not look for a node nor will it fail if it fails to find one. Fixed #832. Added the '--no-daemonize' option to puppetd and puppetmasterd. NOTE: The default behavior of 'verbose' and 'debug' no longer cause puppetd and puppetmasterd to not daemonize. Added k5login type. (#759) Fixed CA race condition. (#693) Added shortname support to config.rb and refactored addargs 0.23.2 Fixed the problem in cron jobs where environment settings tended to multiple. (#749) Collection of resources now correctly only collects exported resources again. This was broken in 0.23.0. (#731) 'gen_config' now generates a configuration with all parameters under a heading that matches the process name, rather than keeping section headings. Refactored how the parser and interpreter relate, so parsing is now effectively an atomic process (thus fixing #314 and #729). This makes the interpreter less prone to error and less prone to show the error to the clients. Note that this means that if a configuration fails to parse, then the previous, parseable configuration will be used instead, so the client will not know that the configuration failed to parse. Added support for managing interfaces, thanks to work by Paul Rose. Fixed #652, thanks to a patch by emerose; --fqdn again works with puppetd. Added an extra check to the Mongrel support so that Apache can be used with optional cert checking, instead of mandatory, thus allowing Mongrel to function as the CA. This is thanks to work done by Marcin Owsiany. 0.23.1 (beaker) You can now specify relationships to classes, which work exactly like relationships to defined types: require => Class[myclass] This works with qualified classes, too. You can now do simple queries in a collection of exported resources. You still cannot do multi-condition queries, though. (#703) puppetca now exits with a non-zero code if it cannot find any host certificates to clean. (Patch by Dean Wilson.) Fully-qualified resources can now have defaults. (#589) Resource references can now be fully-qualified names, meaning you can list definitions with a namespace as dependencies. (#468) Files modified using a FileType instance, as ParsedFile does, will now automatically get backed up to the filebucket named "puppet". Added a 'maillist' type for managing mailing lists. Added a 'mailalias' type for managing mail aliases. Added patch by Valentin Vidic that adds the '+>' syntax to resources, so parameter values can be added to. The configuration client now pulls libraries down to $libdir, and all autoloading is done from there with full support for any reloadable file, such as types and providers. (#621) Note that this is not backward compatible -- if you're using pluginsync right now, you'll need to disable it on your clients until you can upgrade them. The Rails log level can now be set via (shockingly!) the 'rails_loglevel' parameter (#710). Note that this isn't exactly the feature asked for, but I could not find a way to directly copy ActiveRecord's concept of an environment. External node sources can now return undefined classes (#687). Puppet clients now have http proxy support (#701). The parser now throws an error when a resource reference is created for an unknown type. Also, resource references look up defined types and translate their type accordingly. (#706) Hostnames can now be double quoted. Adding module autoloading (#596) -- you can now 'include' classes from modules without ever needing to specifically load them. Class names and node names now conflict (#620). 0.23.0 Modified the fileserver to cache file information, so that each file isn't being read on every connection. Also, added londo's patch from #678 to avoid reading entire files into memory. Fixed environment handling in the crontab provider (#669). Added patch by trombik in #572, supporting old-style freebsd init scripts with '.sh' endings. Added fink package provider (#642), as provided by 'do'. Marked the dpkg package provider as versionable (#647). Applied patches by trombik to fix FreeBSD ports (#624 and #628). Fixed the CA server so that it refuses to send back a certificate whose public key doesn't match the CSR. Instead, it tells the user to run 'puppetca --clean'. Invalid certificates are no longer written to disk (#578). Added a package provider (appdmg) able to install .app packages on .dmg files on OS X (#641). Applied the patch from #667 to hopefully kill the client hanging problems (permanently, this time). Fixed functions so that they accept most other rvalues as valid values (#548). COMPATIBILITY ALERT: Significantly reworked external node support, in a way that's NOT backward-compatible: Only ONE node source can be used -- you can use LDAP, code, or an external node program, but not more than one. LDAP node support has two changes: First, the "ldapattrs" attribute is now used for setting the attributes to retrieve from the server (in addition to required attriutes), and second, all retrieved attributes are set as variables in the top scope. This means you can set attributes on your LDAP nodes and they will automatically appear as variables in your configurations. External node support has been completely rewritten. These programs must now generate a YAML dump of a hash, with "classes" and "parameters" keys. The classes should be an array, and the parameters should be a hash. The external node program has no support for parent nodes -- the script must handle that on its own. Reworked the database schema used to store configurations with the storeconfigs option. Replaced the obsolete RRD ruby library with the maintained RubyRRDtool library (which requires rrdtool2) (#659). The Portage package provider now calls eix-update automatically when eix's database is absent or out of sync (#666). Mounts now correctly handle existing fstabs with no pass or dump values (#550). Mounts now default to 0 for pass and dump (#112). Added urpmi support (#592). Finishing up the type => provider interface work. Basically, package providers now return lists of provider instances. In the proces, I rewrote the interface between package types and providers, and also enabled prefetching on all packages. This should significantly speed up most package operations. Hopefully fixing the file descriptor/open port problems, with patches from Valentin Vidic. Significantly reworked the type => provider interface with respect to listing existing provider instances. The class method on both class heirarchies has been renamed to 'instances', to start. Providers are now expected to return provider instances, instead of creating resources, and the resource's 'instances' method is expected to find the matching resource, if any, and set the resource's provider appropriately. This *significantly* reduces the reliance on effectively global state (resource references in the resource classes). This global state will go away soon. Along with this change, the 'prefetch' class method on providers now accepts the list of resources for prefetching. This again reduces reliance on global state, and makes the execution path much easier to follow. Fixed #532 -- reparsing config files now longer throws an exception. Added some warnings and logs to the service type so users will be encouraged to specify either "ensure" or "enabled" and added debugging to indicate why restarting is skipped when it is. Changed the location of the classes.txt to the state directory. Added better error reporting on unmatched brackets. Moved puppetd and puppetmasterd to sbin in svn and fixed install.rb to copy them into sbin on the local system appropriately. (#323) Added a splay option (#501). It's disabled when running under --test in puppetd. The value is random but cached. It defaults to the runinterval but can be tuned with --splaylimit Changing the notify type so that it always uses the loglevel. Fixing #568 - nodes can inherit from quoted node names. Tags (and thus definitions and classes) can now be a single character. (#566) Added an 'undef' keyword (#629), which will evaluate to "" within strings but when used as a resource parameter value will cause that parameter to be evaluated as undefined. Changed the topological sort algorithm (#507) so it will always fail on cycles. Added a 'dynamicfacts' configuration option; any facts in that comma-separated list will be ignored when comparing facts to see if they have changed and thus whether a recompile is necessary. Renamed some poorly named internal variables: @models in providers are now either @resource or @resource_type (#605). @children is no longer used except by components (#606). @parent is now @resource within parameters (#607). The old variables are still set for backward compatibility. Significantly reworking configuration parsing. Executables all now look for 'puppet.conf' (#206), although they will parse the old-style configuration files if they are present, although they throw a deprecation warning. Also, file parameters (owner, mode, group) are now set on the same line as the parameter, in brackets. (#422) Added transaction summaries (available with the --summarize option), useful for getting a quick idea of what happened in a transaction. Currently only useful on the client or with the puppet interpreter. Changed the interal workings for retrieve and removed the :is attribute from Property. The retrieve methods now return the current value of the property for the system. Removed acts_as_taggable from the rails models. 0.22.4 Execs now autorequire the user they run as, as long as the user is specified by name. (#430) Files on the local machine but not on the remote server during a source copy are now purged if purge => true. (#594) Providers can now specify that some commands are optional (#585). Also, the 'command' method returns nil on missing commands, rather than throwing an error, so the presence of commands be tested. The 'useradd' provider for Users can now manage passwords. No other providers can, at this point. Parameters can now declare a dependency on specific features, and parameters that require missing features will not be instantiated. This is most useful for properties. FileParsing classes can now use instance_eval to add many methods at once to a record type. Modules no longer return directories in the list of found manifests (#588). The crontab provider now defaults to root when there is no USER set in the environment. Puppetd once again correctly responds to HUP. Added a syntax for referring to variables defined in other classes (e.g., $puppet::server). STDIN, STDOUT, STDERR are now redirected to /dev/null in service providers descending from base. Certificates are now valid starting one day before they are created, to help handle small amounts of clock skew. Files are no longer considered out of sync if some properties are out of sync but they have no properties that can create the file. 0.22.3 Fixed backward compatibility for logs and metrics from older clients. Fixed the location of the authconfig parameters so there aren't loading order issues. Enabling attribute validation on the providers that subclass 'nameservice', so we can verify that an integer is passed to UID and GID. Added a stand-alone filebucket client, named 'filebucket'. Fixed the new nested paths for filebuckets; the entire md5 sum was not being stored. Fixing #553; -M is no longer added when home directories are being managed on Red Hat. 0.22.2 (grover) Users can now manage their home directories, using the managehome parameter, partially using patches provided by Tim Stoop and Matt Palmer. (#432) Added 'ralsh' (formerly x2puppet) to the svn tree. When possible it should be added to the packages. The 'notify' type now defaults to its message being the same as its name. Reopening $stdin to read from /dev/null during execution, in hopes that init scripts will stop hanging. Changed the 'servername' fact set on the server to use the server's fqdn, instead of the short-name. Changing the location of the configuration cache. It now defaults to being in the state directory, rather than in the configuration directory. All parameter instances are stored in a single @parameters instance variable hash within resource type instances. We used to use separate hashes for each parameter type. Added the concept of provider features. Eventually these should be able to express the full range of provider functionality, but for now they can test a provider to see what methods it has set and determine what features it provides as a result. These features are integrated into the doc generation system so that you get feature documentation automatically. Switched apt/aptitide to using "apt-cache policy" instead of "apt-cache showpkg" for determining the latest available version. (#487) FileBuckets now use a deeply nested structure for storing files, so you do not end up with hundreds or thousands of files in the same directory. (#447) Facts are now cached in the state file, and when they change the configuration is always recompiled. (#519) Added 'ignoreimport' setting for use in commit hooks. This causes the parser to ignore import statements so a single file can be parse-checked. (#544) Import statements can now specify multiple comma-separated arguments. Definitions now support both 'name' and 'title', just like any other resource type. (#539) Added a generate() command, which sets values to the result of an external command. (#541) Added a file() command to read in files with no interpolation. The first found file has its content returned. puppetd now exits if no cert is present in onetime mode. (#533) The client configuration cache can be safely removed and the client will correctly realize the client is not in sync. Resources can now be freely deleted, thus fixing many problems introduced when deletion of required resources was forbidden when purging was introduced. Only resources being purged will not be deleted. Facts and plugins now download even in noop mode (#540). Resources in noop mode now log when they would have responded to an event (#542). Refactored cron support entirely. Cron now uses providers, and there is a single 'crontab' provider that handles user crontabs. While this refactor does not include providers for /etc/crontab or cron.d, it should now be straightforward to write those providers. Changed the parameter sorting so that the provider parameter comes right after name, so the provider is available when the other parameters and properties are being created. Redid some of the internals of the ParsedFile provider base class. It now passes a FileRecord around instead of a hash. Fixing a bug related to link recursion that caused link directories to always be considered out of sync. The bind address for puppetmasterd can now be specified with --bindaddress. Added (probably experimental) mongrel support. At this point you're still responsible for starting each individual process, and you have to set up a proxy in front of it. Redesigned the 'network' tree to support multiple web servers, including refactoring most of the structural code so it's much clearer and more reusable now. Set up the CA client to default to ca_server and ca_port, so you can easily run a separate CA. Supporting hosts with no domain name, thanks to a patch from Dennis Jacobfeuerborn. Added an 'ignorecache' option to tell puppetd to force a recompile, thanks to a patch by Chris McEniry. Made up2date the default for RHEL < 4 and yum the default for the rest. The yum provider now supports versions. Case statements correctly match when multiple values are provided, thanks to a patch by David Schmitt. Functions can now be called with no arguments. String escapes parse correctly in all cases now, thanks to a patch by cstorey. Subclasses again search parent classes for defaults. You can now purge apt and dpkg packages. When doing file recursion, 'ensure' only affects the top-level directory. States have been renamed to Properties. 0.22.1 (kermit) -- Mostly a bugfix release Compile times now persist between restarts of puppetd. Timeouts have been added to many parts of Puppet, reducing the likelihood if it hanging forever on broken scripts or servers. All of the documentation and recipes have been moved to the wiki by Peter Abrahamsen and Ben Kite has moved the FAQ to the wiki. Explicit relationships now override automatic relationships, allowing you to manually specify deletion order when removing resources. Resources with dependencies can now be deleted as long as all of their dependencies are also being deleted. Namespaces for both classes and definitions now work much more consistently. You should now be able to specify a class or definition with a namespace everywhere you would normally expect to be able to specify one without. Downcasing of facts can be selectively disabled. Cyclic dependency graphs are now checked for and forbidden. The netinfo mounts provider was commented out, because it really doesn't work at all. Stupid NetInfo stores mount information with the device as the key, which doesn't work with my current NetInfo code. Otherwise, lots and lots of bugfixes. Check the tickets associated with the 'kermit' milestone. 0.22.0 Integrated the GRATR graph library into Puppet, for handling resource relationships. Lots of bug-fixes (see bugs tickets associated with the 'minor' milestone). Added new 'resources' metatype, which currently only includes the ability to purge unmanaged resources. Added better ability to generate new resource objects during transactions (using 'generate' and 'eval_generate' methods). Rewrote all Rails support with a much better database design. Export/collect now works, although the database is incompatible with previous versions. Removed downcasing of facts and made most of the language case-insensitive. Added support for printing the graphs built during transactions. Reworked how paths are built for logging. Switched all providers to directly executing commands instead of going through a subshell, which removes the need to quote or escape arguments. 0.20.1 Mostly a bug-fix release, with the most important fix being the multiple-definition error. Completely rewrote the ParsedFile system; each provider is now much shorter and much more maintainable. However, fundamental problems were found with the 'port' type, so it was disabled. Also, added a NetInfo provider for 'host' and an experimental NetInfo provider for 'mount'. Made the RRDGraph report *much* better and added reference generation for reports and functions. 0.20.0 Significantly refactored the parser. Resource overrides now consistently work anywhere in a class hierarchy. The language was also modified somewhat. The previous export/collect syntax is now used for handling virtual objects, and export/collect (which is still experimental) now uses double sigils (@@ and <<| |>>). Resource references (e.g., File["/etc/passwd"]) now have to be capitalized, in fitting in with capitalizing type operations. As usual, lots of other smaller fixes, but most of the work was in the language. 0.19.3 Fixing a bug in server/master.rb that causes the hostname not to be available in locally-executed manifests. 0.19.2 Fixing a few smaller bugs, notably in the reports system. Refreshed objects now generate an event, which can result in further refreshes of other objects. 0.19.1 Fixing two critical bugs: User management works again and cron jobs are no longer added to all user accounts. 0.19.0 Added provider support. Added support for %h, %H, and %d expansion in fileserver.conf. Added Certificate Revocation support. Made dynamic loading pervasive -- nearly every aspect of Puppet will now automatically load new instances (e.g., types, providers, and reports). Added support for automatic distribution of facts and plugins (custom types). 0.18.4 Another bug-fix release. The most import bug fixed is that cronjobs again work even with initially empty crontabs. 0.18.3 Mostly a bug-fix release; fixed small bugs in the functionality added in 0.18.2. 0.18.2 Added templating support. Added reporting. Added gem and blastwave packaging support. 0.18.1 Added signal handlers for HUP, so both client and server deal correctly with it. Added signal handler for USR1, which triggers a run on the client. As usual, fixed many bugs. Significant fixes to puppetrun -- it should behave much more correctly now. Added "fail" function which throws a syntax error if it's encountered. Added plugin downloading from the central server to the client. It must be enabled with --pluginsync. Added support for FreeBSD's special "@daily" cron schedules. Correctly handling spaces in file sources. Moved documentation into svn tree. 0.18.0 Added support for a "default" node. When multiple nodes are specified, they must now be comma-separated (this introduces a language incompatibility). Failed dependencies cause dependent objects within the same transaction not to run. Many updates to puppetrun Many bug fixes Function names are no longer reserved words. Links can now replace files. 0.17.2 Added "puppetrun" application and associated runner server and client classes. Fixed cron support so it better supports valid values and environment settings. 0.17.1 Fixing a bug requiring rails on all Debian boxes Fixing a couple of other small bugs 0.17.0 Adding ActiveRecord integration on the server Adding export/collect functionality Fixing many bugs 0.16.5 Fixing a critical bug in importing classes from other files Fixing nodename handling to actually allow dashes 0.16.4 Fixing a critical bug in puppetd when acquiring a certificate for the first time 0.16.3 Some significant bug fixes Modified puppetd so that it can now function as an agent independent of a puppetmasterd process, e.g., using the PuppetShow web application. 0.16.2 Modified some of the AST classes so that class names, definition names, and node names are all set within the code being evaluated, so 'tagged(name)' returns true while evaluating 'name', for instance. Added '--clean' argument to puppetca to remove all traces of a given client. 0.16.1 Added 'tagged' and 'defined' functions. Moved all functions to a general framework that makes it very easy to add new functions. 0.16.0 Added 'tag' keyword/function. Added FreeBSD Ports support Added 'pelement' server for sending or receiving Puppet objects, although none of the executables use it yet. 0.15.3 Fixed many bugs in :exec, including adding support for arrays of checks Added autoloading for types and service variants (e.g., you can now just create a new type in the appropriate location and use it in Puppet, without modifying the core Puppet libs). 0.15.2 Added darwinport, Apple .pkg, and freebsd package types Added 'mount type Host facts are now set at the top scope (Bug #103) Added -e (inline exection) flag to 'puppet' executable Many small bug fixes 0.15.1 Fixed 'yum' installs so that they successfully upgrade packages. Fixed puppetmasterd.conf file so group settings take. 0.15.0 Upped the minor release because the File server is incompatible with 0.14, because it now handles links. The 'symlink' type is deprecated (but still present), in favor of using files with the 'target' parameter. Unset variables no longer throw an error, they just return an empty string You can now specify tags to restrict which objects run during a given run. You can also specify to skip running against the cached copy when there's a failure, which is useful for testing new configurations. RPMs and Sun packages can now install, as long as they specify a package location, and they'll automatically upgrade if you point them to a new file with an upgrade. Multiple bug fixes. 0.14.1 Fixed a couple of small logging bugs Fixed a bug with handling group ownership of links 0.14.0 Added some ability to selectively manage symlinks when doing file management Many bug fixes Variables can now be used as the test values in case statements and selectors Bumping a minor release number because 0.13.4 introduced a protocol incompatibility and should have had a minor rev bump 0.13.6 Many, many small bug fixes FreeBSD user/group support has been added The configuration system has been rewritten so that daemons can now generate and repair the files and directories they need. (Fixed bug #68.) Fixed the element override issues; now only subclasses can override values. 0.13.5 Fixed packages so types can be specified Added 'enable' state to services, although it does not work everywhere yet 0.13.4 A few important bug fixes, mostly in the parser. 0.13.3 Changed transactions to be one-stage instead of two Changed all types to use self[:name] instead of self.name, to support the symbolic naming implemented in 0.13.1 0.13.2 Changed package[answerfile] to package[adminfile], and added package[responsefile] Fixed a bunch of internal functions to behave more consistently and usefully 0.13.1 Fixed RPM spec files to create puppet user and group (lutter) Fixed crontab reading and writing (luke) Added symbolic naming in the language (luke) 0.13.0 Added support for configuration files. Even more bug fixes, including the infamous 'frozen object' bug, which was a problem with 'waitforcert'. David Lutterkort got RPM into good shape. 0.12.0 Added Scheduling, and many bug fixes, of course. 0.11.2 Fixed bugs related to specifying arrays of requirements Fixed a key bug in retrieving checksums Fixed lots of usability bugs Added 'fail' methods that automatically add file and line info when possible, and converted many errors to use that method 0.11.1 Fixed bug with recursive copying with 'ignore' set. Added OpenBSD package support. 0.11.0 Added 'ensure' state to many elements. Modified puppetdoc to correctly handle indentation and such. Significantly rewrote much of the builtin documentation to take advantage of the new features in puppetdoc, including many examples. 0.10.2 Added SMF support Added autorequire functionality, with specific support for exec and file Exec elements autorequire any mentioned files, including the scripts, along with their CWDs. Files autorequire any parent directories. Added 'alias' metaparam. Fixed dependencies so they don't depend on file order. 0.10.1 Added Solaris package support and changed puppetmasterd to run as a non-root user. 0.10.0 Significant refactoring of how types, states, and parameters work, including breaking out parameters into a separate class. This refactoring did not introduce much new functionality, but made extension of Puppet significantly easier Also, fixed the bug with 'waitforcert' in puppetd. 0.9.4 Small fix to wrap the StatusServer class in the checks for required classes. 0.9.3 Fixed some significant bugs in cron job management. 0.9.2 Second Public Beta 0.9.0 First Public Beta diff --git a/lib/puppet/node/catalog.rb b/lib/puppet/node/catalog.rb index 9601309d8..03187cc94 100644 --- a/lib/puppet/node/catalog.rb +++ b/lib/puppet/node/catalog.rb @@ -1,494 +1,518 @@ require 'puppet/indirector' # This class models a node catalog. It is the thing # meant to be passed from server to client, and it contains all # of the information in the catalog, including the resources # and the relationships between them. class Puppet::Node::Catalog < Puppet::PGraph extend Puppet::Indirector indirects :catalog, :terminus_class => :compiler # The host name this is a catalog for. attr_accessor :name # The catalog version. Used for testing whether a catalog # is up to date. attr_accessor :version # How long this catalog took to retrieve. Used for reporting stats. attr_accessor :retrieval_duration # How we should extract the catalog for sending to the client. attr_reader :extraction_format # We need the ability to set this externally, so we can yaml-dump the # catalog. attr_accessor :edgelist_class # Whether this is a host catalog, which behaves very differently. # In particular, reports are sent, graphs are made, and state is # stored in the state database. If this is set incorrectly, then you often # end up in infinite loops, because catalogs are used to make things # that the host catalog needs. attr_accessor :host_config # Whether this graph is another catalog's relationship graph. # We don't want to accidentally create a relationship graph for another # relationship graph. attr_accessor :is_relationship_graph # Whether this catalog was retrieved from the cache, which affects # whether it is written back out again. attr_accessor :from_cache # Add classes to our class list. def add_class(*classes) classes.each do |klass| @classes << klass end # Add the class names as tags, too. tag(*classes) end # Add one or more resources to our graph and to our resource table. def add_resource(*resources) resources.each do |resource| unless resource.respond_to?(:ref) raise ArgumentError, "Can only add objects that respond to :ref" end ref = resource.ref if @resource_table.include?(ref) raise ArgumentError, "Resource %s is already defined" % ref else @resource_table[ref] = resource end - resource.catalog = self unless is_relationship_graph + resource.catalog = self if resource.respond_to?(:catalog=) and ! is_relationship_graph add_vertex!(resource) end end # Create an alias for a resource. def alias(resource, name) resource.ref =~ /^(.+)\[/ newref = "%s[%s]" % [$1 || resource.class.name, name] raise(ArgumentError, "Cannot alias %s to %s; resource %s already exists" % [resource.ref, name, newref]) if @resource_table[newref] @resource_table[newref] = resource @aliases[resource.ref] << newref end # Apply our catalog to the local host. Valid options # are: # :tags - set the tags that restrict what resources run # during the transaction # :ignoreschedules - tell the transaction to ignore schedules # when determining the resources to run def apply(options = {}) @applying = true Puppet::Util::Storage.load if host_config? transaction = Puppet::Transaction.new(self) transaction.tags = options[:tags] if options[:tags] transaction.ignoreschedules = true if options[:ignoreschedules] transaction.addtimes :config_retrieval => @retrieval_duration begin transaction.evaluate rescue Puppet::Error => detail Puppet.err "Could not apply complete catalog: %s" % detail rescue => detail puts detail.backtrace if Puppet[:trace] Puppet.err "Got an uncaught exception of type %s: %s" % [detail.class, detail] ensure # Don't try to store state unless we're a host config # too recursive. Puppet::Util::Storage.store if host_config? end yield transaction if block_given? transaction.send_report if host_config and (Puppet[:report] or Puppet[:summarize]) return transaction ensure @applying = false cleanup() transaction.cleanup if defined? transaction and transaction end # Are we in the middle of applying the catalog? def applying? @applying end def clear(remove_resources = true) super() # We have to do this so that the resources clean themselves up. @resource_table.values.each { |resource| resource.remove } if remove_resources @resource_table.clear if defined?(@relationship_graph) and @relationship_graph @relationship_graph.clear(false) @relationship_graph = nil end end def classes @classes.dup end # Create an implicit resource, meaning that it will lose out # to any explicitly defined resources. This method often returns # nil. # The quirk of this method is that it's not possible to create # an implicit resource before an explicit resource of the same name, # because all explicit resources are created before any generate() # methods are called on the individual resources. Thus, this # method can safely just check if an explicit resource already exists # and toss this implicit resource if so. def create_implicit_resource(type, options) unless options.include?(:implicit) options[:implicit] = true end # This will return nil if an equivalent explicit resource already exists. # When resource classes no longer retain references to resource instances, # this will need to be modified to catch that conflict and discard # implicit resources. if resource = create_resource(type, options) resource.implicit = true return resource else return nil end end # Create a new resource and register it in the catalog. def create_resource(type, options) unless klass = Puppet::Type.type(type) raise ArgumentError, "Unknown resource type %s" % type end return unless resource = klass.create(options) @transient_resources << resource if applying? add_resource(resource) if @relationship_graph @relationship_graph.add_resource(resource) unless @relationship_graph.resource(resource.ref) end resource end # Make sure we support the requested extraction format. def extraction_format=(value) unless respond_to?("extract_to_%s" % value) raise ArgumentError, "Invalid extraction format %s" % value end @extraction_format = value end # Turn our catalog graph into whatever the client is expecting. def extract send("extract_to_%s" % extraction_format) end # Create the traditional TransBuckets and TransObjects from our catalog # graph. This will hopefully be deprecated soon. def extract_to_transportable top = nil current = nil buckets = {} unless main = vertices.find { |res| res.type == "Class" and res.title == :main } raise Puppet::DevError, "Could not find 'main' class; cannot generate catalog" end # Create a proc for examining edges, which we'll use to build our tree # of TransBuckets and TransObjects. bucket = nil walk(main, :out) do |source, target| # The sources are always non-builtins. unless tmp = buckets[source.to_s] if tmp = buckets[source.to_s] = source.to_trans bucket = tmp else # This is because virtual resources return nil. If a virtual # container resource contains realized resources, we still need to get # to them. So, we keep a reference to the last valid bucket # we returned and use that if the container resource is virtual. end end bucket = tmp || bucket if child = target.to_trans unless bucket raise "No bucket created for %s" % source end bucket.push child # It's important that we keep a reference to any TransBuckets we've created, so # we don't create multiple buckets for children. unless target.builtin? buckets[target.to_s] = child end end end # Retrieve the bucket for the top-level scope and set the appropriate metadata. unless result = buckets[main.to_s] # This only happens when the catalog is entirely empty. result = buckets[main.to_s] = main.to_trans end result.classes = classes # Clear the cache to encourage the GC buckets.clear return result end # Make sure all of our resources are "finished". def finalize make_default_resources @resource_table.values.each { |resource| resource.finish } write_graph(:resources) end def host_config? host_config || false end def initialize(name = nil) super() @name = name if name @extraction_format ||= :transportable @tags = [] @classes = [] @resource_table = {} @transient_resources = [] @applying = false @relationship_graph = nil @aliases = Hash.new { |hash, key| hash[key] = [] } if block_given? yield(self) finalize() end end # Make the default objects necessary for function. def make_default_resources # We have to add the resources to the catalog, or else they won't get cleaned up after # the transaction. # First create the default scheduling objects Puppet::Type.type(:schedule).mkdefaultschedules.each { |res| add_resource(res) unless resource(res.ref) } # And filebuckets if bucket = Puppet::Type.type(:filebucket).mkdefaultbucket add_resource(bucket) end end # Create a graph of all of the relationships in our catalog. def relationship_graph raise(Puppet::DevError, "Tried get a relationship graph for a relationship graph") if self.is_relationship_graph unless defined? @relationship_graph and @relationship_graph # It's important that we assign the graph immediately, because # the debug messages below use the relationships in the # relationship graph to determine the path to the resources # spitting out the messages. If this is not set, # then we get into an infinite loop. @relationship_graph = Puppet::Node::Catalog.new @relationship_graph.host_config = host_config? @relationship_graph.is_relationship_graph = true # First create the dependency graph self.vertices.each do |vertex| @relationship_graph.add_vertex! vertex vertex.builddepends.each do |edge| @relationship_graph.add_edge!(edge) end end # Lastly, add in any autorequires @relationship_graph.vertices.each do |vertex| vertex.autorequire.each do |edge| unless @relationship_graph.edge?(edge.source, edge.target) # don't let automatic relationships conflict with manual ones. unless @relationship_graph.edge?(edge.target, edge.source) vertex.debug "Autorequiring %s" % [edge.source] @relationship_graph.add_edge!(edge) else vertex.debug "Skipping automatic relationship with %s" % (edge.source == vertex ? edge.target : edge.source) end end end end @relationship_graph.write_graph(:relationships) # Then splice in the container information @relationship_graph.splice!(self, Puppet::Type::Component) @relationship_graph.write_graph(:expanded_relationships) end @relationship_graph end # Remove the resource from our catalog. Notice that we also call # 'remove' on the resource, at least until resource classes no longer maintain # references to the resource instances. def remove_resource(*resources) resources.each do |resource| @resource_table.delete(resource.ref) @aliases[resource.ref].each { |res_alias| @resource_table.delete(res_alias) } @aliases[resource.ref].clear remove_vertex!(resource) if vertex?(resource) @relationship_graph.remove_vertex!(resource) if @relationship_graph and @relationship_graph.vertex?(resource) resource.remove end end # Look a resource up by its reference (e.g., File[/etc/passwd]). def resource(type, title = nil) # Always create a resource reference, so that it always canonizes how we # are referring to them. if title ref = Puppet::ResourceReference.new(type, title).to_s else # If they didn't provide a title, then we expect the first # argument to be of the form 'Class[name]', which our # Reference class canonizes for us. ref = Puppet::ResourceReference.new(nil, type).to_s end if resource = @resource_table[ref] return resource elsif defined?(@relationship_graph) and @relationship_graph @relationship_graph.resource(ref) end end # Return an array of all resources. def resources @resource_table.keys end # Add a tag. def tag(*names) names.each do |name| name = name.to_s @tags << name unless @tags.include?(name) if name.include?("::") name.split("::").each do |sub| @tags << sub unless @tags.include?(sub) end end end nil end # Return the list of tags. def tags @tags.dup end # Convert our catalog into a RAL catalog. def to_ral to_catalog :to_type end # Turn our parser catalog into a transportable catalog. def to_transportable to_catalog :to_transobject end # Produce the graph files if requested. def write_graph(name) # We only want to graph the main host catalog. return unless host_config? return unless Puppet[:graph] Puppet.settings.use(:graphing) file = File.join(Puppet[:graphdir], "%s.dot" % name.to_s) File.open(file, "w") { |f| f.puts to_dot("name" => name.to_s.capitalize) } end # LAK:NOTE We cannot yaml-dump the class in the edgelist_class, because classes cannot be # dumped by default, nor does yaml-dumping # the edge-labels work at this point (I don't # know why). # Neither of these matters right now, but I suppose it could at some point. # We also have to have the vertex_dict dumped after the resource table, because yaml can't # seem to handle the output of yaml-dumping the vertex_dict. def to_yaml_properties props = instance_variables.reject { |v| %w{@edgelist_class @edge_labels @vertex_dict}.include?(v) } props << "@vertex_dict" props end private def cleanup unless @transient_resources.empty? remove_resource(*@transient_resources) @transient_resources.clear @relationship_graph = nil end end # An abstracted method for converting one catalog into another type of catalog. # This pretty much just converts all of the resources from one class to another, using # a conversion method. def to_catalog(convert) result = self.class.new(self.name) map = {} vertices.each do |resource| next if resource.respond_to?(:virtual?) and resource.virtual? newres = resource.send(convert) # We can't guarantee that resources don't munge their names # (like files do with trailing slashes), so we have to keep track # of what a resource got converted to. map[resource.ref] = newres result.add_resource newres end message = convert.to_s.gsub "_", " " edges.each do |edge| # Skip edges between virtual resources. next if edge.source.respond_to?(:virtual?) and edge.source.virtual? next if edge.target.respond_to?(:virtual?) and edge.target.virtual? unless source = map[edge.source.ref] raise Puppet::DevError, "Could not find resource %s when converting %s resources" % [edge.source.ref, message] end unless target = map[edge.target.ref] raise Puppet::DevError, "Could not find resource %s when converting %s resources" % [edge.target.ref, message] end result.add_edge!(source, target, edge.label) end map.clear result.add_class *self.classes result.tag(*self.tags) return result end + + # Verify that the given resource isn't defined elsewhere. + def verify_resource_uniqueness(resource) + # Short-curcuit the common case, + unless existing_resource = @resource_table[resource.ref] + return true + end + + # Either it's a defined type, which are never + # isomorphic, or it's a non-isomorphic type, so + # we should throw an exception. + msg = "Duplicate definition: %s is already defined" % resource.ref + + if existing_resource.file and existing_resource.line + msg << " in file %s at line %s" % + [existing_resource.file, existing_resource.line] + end + + if resource.line or resource.file + msg << "; cannot redefine" + end + + raise Puppet::ParseError.new(msg) + end end diff --git a/lib/puppet/parser/compile.rb b/lib/puppet/parser/compile.rb index 42d229867..46ce1cb9b 100644 --- a/lib/puppet/parser/compile.rb +++ b/lib/puppet/parser/compile.rb @@ -1,522 +1,477 @@ # Created by Luke A. Kanies on 2007-08-13. # Copyright (c) 2007. All rights reserved. require 'puppet/node' require 'puppet/node/catalog' require 'puppet/util/errors' # Maintain a graph of scopes, along with a bunch of data # about the individual catalog we're compiling. class Puppet::Parser::Compile include Puppet::Util include Puppet::Util::Errors attr_reader :parser, :node, :facts, :collections, :catalog, :node_scope # Add a collection to the global list. def add_collection(coll) @collections << coll end # Do we use nodes found in the code, vs. the external node sources? def ast_nodes? parser.nodes.length > 0 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 existing = @class_scopes[name] if existing.nodescope? or scope.nodescope? raise Puppet::ParseError, "Cannot have classes, nodes, or definitions with the same name" else raise Puppet::DevError, "Somehow evaluated the same class twice" end end @class_scopes[name] = scope @catalog.add_class(name) unless name == "" 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 if klass.respond_to?(:classname) @class_scopes[klass.classname] else @class_scopes[klass] end end # Return a list of all of the defined classes. def classlist return @catalog.classes end # Compile our catalog. This mostly revolves around finding and evaluating classes. # This is the main entry into our catalog. def compile # Set the client's parameters into the top scope. set_node_parameters() evaluate_main() evaluate_ast_node() evaluate_node_classes() evaluate_generators() finish() fail_on_unevaluated() if Puppet[:storeconfigs] store() end return @catalog end # LAK:FIXME There are no tests for this. def delete_collection(coll) @collections.delete(coll) if @collections.include?(coll) end - # LAK:FIXME There are no tests for this. - def delete_resource(resource) - @resource_table.delete(resource.ref) if @resource_table.include?(resource.ref) - end - # Return the node's environment. def environment unless defined? @environment if node.environment and node.environment != "" @environment = node.environment else @environment = nil end end @environment end # Evaluate all of the classes specified by the node. def evaluate_node_classes evaluate_classes(@node.classes, topscope) end # Evaluate each specified class in turn. If there are any classes we can't # find, just tag the catalog and move on. This method really just # creates resource objects that point back to the classes, and then the # resources are themselves evaluated later in the process. def evaluate_classes(classes, scope, lazy_evaluate = true) unless scope.source raise Puppet::DevError, "No source for scope passed to evaluate_classes" end found = [] classes.each do |name| # If we can find the class, then make a resource that will evaluate it. if klass = scope.findclass(name) found << name and next if class_scope(klass) # Create a resource to model this class, and then add it to the list # of resources. resource = Puppet::Parser::Resource.new(:type => "class", :title => klass.classname, :scope => scope, :source => scope.source) store_resource(scope, resource) # If they've disabled lazy evaluation (which the :include function does), # then evaluate our resource immediately. resource.evaluate unless lazy_evaluate @catalog.tag(klass.classname) found << name else Puppet.info "Could not find class %s for %s" % [name, node.name] @catalog.tag(name) end end found end # Return a resource by either its ref or its type and title. - def findresource(string, name = nil) - string = "%s[%s]" % [string.capitalize, name] if name - - @resource_table[string] + def findresource(*args) + @catalog.resource(*args) end # Set up our compile. We require a parser # and a node object; the parser is so we can look up classes # and AST nodes, and the node has all of the client's info, # like facts and environment. def initialize(node, parser, options = {}) @node = node @parser = parser options.each do |param, value| begin send(param.to_s + "=", value) rescue NoMethodError raise ArgumentError, "Compile objects do not accept %s" % param end end initvars() init_main() end # Create a new scope, with either a specified parent scope or # using the top scope. Adds an edge between the scope and # its parent to the graph. def newscope(parent, options = {}) parent ||= topscope options[:compile] = self options[:parser] ||= self.parser scope = Puppet::Parser::Scope.new(options) @scope_graph.add_edge!(parent, scope) scope end # Find the parent of a given scope. Assumes scopes only ever have # one in edge, which will always be true. def parent(scope) if ary = @scope_graph.adjacent(scope, :direction => :in) and ary.length > 0 ary[0] else nil end end # Return any overrides for the given resource. def resource_overrides(resource) @resource_overrides[resource.ref] end # Return a list of all resources. def resources - @resource_table.values + @catalog.vertices end # Store a resource override. def store_override(override) # If possible, merge the override in immediately. - if resource = @resource_table[override.ref] + if resource = @catalog.resource(override.ref) resource.merge(override) else # Otherwise, store the override for later; these # get evaluated in Resource#finish. @resource_overrides[override.ref] << override end end # Store a resource in our resource table. def store_resource(scope, resource) - # This might throw an exception - verify_uniqueness(resource) - - # Store it in the global table. - @resource_table[resource.ref] = resource + @catalog.add_resource(resource) # And in the resource graph. At some point, this might supercede # the global resource table, but the table is a lot faster # so it makes sense to maintain for now. @catalog.add_edge!(scope.resource, resource) end # The top scope is usually the top-level scope, but if we're using AST nodes, # then it is instead the node's scope. def topscope node_scope || @topscope end private # If ast nodes are enabled, then see if we can find and evaluate one. def evaluate_ast_node return unless ast_nodes? # Now see if we can find the node. astnode = nil @node.names.each do |name| break if astnode = @parser.nodes[name.to_s.downcase] end unless (astnode ||= @parser.nodes["default"]) raise Puppet::ParseError, "Could not find default node or by name with '%s'" % node.names.join(", ") end # Create a resource to model this node, and then add it to the list # of resources. resource = Puppet::Parser::Resource.new(:type => "node", :title => astnode.classname, :scope => topscope, :source => topscope.source) store_resource(topscope, resource) @catalog.tag(astnode.classname) resource.evaluate # Now set the node scope appropriately, so that :topscope can # behave differently. @node_scope = class_scope(astnode) end # Evaluate our collections and return true if anything returned an object. # The 'true' is used to continue a loop, so it's important. def evaluate_collections return false if @collections.empty? found_something = false exceptwrap do # We have to iterate over a dup of the array because # collections can delete themselves from the list, which # changes its length and causes some collections to get missed. @collections.dup.each do |collection| found_something = true if collection.evaluate end end return found_something end # Make sure all of our resources have been evaluated into native resources. # We return true if any resources have, so that we know to continue the # evaluate_generators loop. def evaluate_definitions exceptwrap do if ary = unevaluated_resources ary.each do |resource| resource.evaluate end # If we evaluated, let the loop know. return true else return false end end end # Iterate over collections and resources until we're sure that the whole # compile is evaluated. This is necessary because both collections # and defined resources can generate new resources, which themselves could # be defined resources. def evaluate_generators count = 0 loop do done = true # Call collections first, then definitions. done = false if evaluate_collections done = false if evaluate_definitions break if done count += 1 if count > 1000 raise Puppet::ParseError, "Somehow looped more than 1000 times while evaluating host catalog" end end end # Find and evaluate our main object, if possible. def evaluate_main @main = @parser.findclass("", "") || @parser.newclass("") @topscope.source = @main @main_resource = Puppet::Parser::Resource.new(:type => "class", :title => :main, :scope => @topscope, :source => @main) @topscope.resource = @main_resource - @catalog.add_vertex!(@main_resource) - - @resource_table["Class[main]"] = @main_resource + @catalog.add_resource(@main_resource) @main_resource.evaluate end # Make sure the entire catalog is evaluated. def fail_on_unevaluated fail_on_unevaluated_overrides fail_on_unevaluated_resource_collections end # If there are any resource overrides remaining, then we could # not find the resource they were supposed to override, so we # want to throw an exception. def fail_on_unevaluated_overrides remaining = [] @resource_overrides.each do |name, overrides| remaining += overrides end unless remaining.empty? fail Puppet::ParseError, "Could not find object(s) %s" % remaining.collect { |o| o.ref }.join(", ") end end # Make sure we don't have any remaining collections that specifically # look for resources, because we want to consider those to be # parse errors. def fail_on_unevaluated_resource_collections remaining = [] @collections.each do |coll| # We're only interested in the 'resource' collections, # which result from direct calls of 'realize'. Anything # else is allowed not to return resources. # Collect all of them, so we have a useful error. if r = coll.resources if r.is_a?(Array) remaining += r else remaining << r end end end unless remaining.empty? raise Puppet::ParseError, "Failed to realize virtual resources %s" % remaining.join(', ') end end # Make sure all of our resources and such have done any last work # necessary. def finish - @resource_table.each do |name, resource| + @catalog.resources.each do |name| + resource = @catalog.resource(name) + # Add in any resource overrides. if overrides = resource_overrides(resource) overrides.each do |over| resource.merge(over) end # Remove the overrides, so that the configuration knows there # are none left. overrides.clear end resource.finish if resource.respond_to?(:finish) end end # Initialize the top-level scope, class, and resource. def init_main # Create our initial scope and a resource that will evaluate main. @topscope = Puppet::Parser::Scope.new(:compile => self, :parser => self.parser) @scope_graph.add_vertex!(@topscope) end # Set up all of our internal variables. def initvars # The table for storing class singletons. This will only actually # be used by top scopes and node scopes. @class_scopes = {} - # The table for all defined resources. - @resource_table = {} - # The list of objects that will available for export. @exported_resources = {} # The list of overrides. This is used to cache overrides on objects # that don't exist yet. We store an array of each override. @resource_overrides = Hash.new do |overs, ref| overs[ref] = [] end # The list of collections that have been created. This is a global list, # but they each refer back to the scope that created them. @collections = [] # A list of tags we've generated; most class names. @tags = [] # A graph for maintaining scope relationships. @scope_graph = Puppet::SimpleGraph.new # For maintaining the relationship between scopes and their resources. @catalog = Puppet::Node::Catalog.new(@node.name) @catalog.version = @parser.version end # Set the node's parameters into the top-scope as variables. def set_node_parameters node.parameters.each do |param, value| @topscope.setvar(param, value) end end # Store the catalog into the database. def store unless Puppet.features.rails? raise Puppet::Error, "storeconfigs is enabled but rails is unavailable" end unless ActiveRecord::Base.connected? Puppet::Rails.connect end # We used to have hooks here for forking and saving, but I don't # think it's worth retaining at this point. - store_to_active_record(@node, @resource_table.values) + store_to_active_record(@node, @catalog.vertices) end # Do the actual storage. def store_to_active_record(node, resources) begin # We store all of the objects, even the collectable ones benchmark(:info, "Stored catalog for #{node.name}") do Puppet::Rails::Host.transaction do Puppet::Rails::Host.store(node, resources) end end rescue => detail if Puppet[:trace] puts detail.backtrace end Puppet.err "Could not store configs: %s" % detail.to_s end end # Return an array of all of the unevaluated resources. These will be definitions, # which need to get evaluated into native resources. def unevaluated_resources - ary = @resource_table.find_all do |name, object| - ! object.builtin? and ! object.evaluated? - end.collect { |name, object| object } + ary = @catalog.vertices.reject { |resource| resource.builtin? or resource.evaluated? } if ary.empty? return nil else return ary end end - - # Verify that the given resource isn't defined elsewhere. - def verify_uniqueness(resource) - # Short-curcuit the common case, - unless existing_resource = @resource_table[resource.ref] - return true - end - - if typeclass = Puppet::Type.type(resource.type) and ! typeclass.isomorphic? - Puppet.info "Allowing duplicate %s" % typeclass.name - return true - end - - # Either it's a defined type, which are never - # isomorphic, or it's a non-isomorphic type, so - # we should throw an exception. - msg = "Duplicate definition: %s is already defined" % resource.ref - - if existing_resource.file and existing_resource.line - msg << " in file %s at line %s" % - [existing_resource.file, existing_resource.line] - end - - if resource.line or resource.file - msg << "; cannot redefine" - end - - raise Puppet::ParseError.new(msg) - end end diff --git a/lib/puppet/parser/resource.rb b/lib/puppet/parser/resource.rb index 67428d5f3..fb0799011 100644 --- a/lib/puppet/parser/resource.rb +++ b/lib/puppet/parser/resource.rb @@ -1,435 +1,434 @@ # A resource that we're managing. This handles making sure that only subclasses # can set parameters. class Puppet::Parser::Resource require 'puppet/parser/resource/param' require 'puppet/parser/resource/reference' require 'puppet/util/tagging' include Puppet::Util include Puppet::Util::MethodHelper include Puppet::Util::Errors include Puppet::Util::Logging include Puppet::Util::Tagging attr_accessor :source, :line, :file, :scope, :rails_id attr_accessor :virtual, :override, :translated attr_reader :exported, :evaluated, :params # Determine whether the provided parameter name is a relationship parameter. def self.relationship_parameter?(name) unless defined?(@relationship_names) @relationship_names = Puppet::Type.relationship_params.collect { |p| p.name } end @relationship_names.include?(name) end # Proxy a few methods to our @ref object. [:builtin?, :type, :title].each do |method| define_method(method) do @ref.send(method) end end # Set up some boolean test methods [:exported, :translated, :override, :virtual, :evaluated].each do |method| newmeth = (method.to_s + "?").intern define_method(newmeth) do self.send(method) end end def [](param) param = symbolize(param) if param == :title return self.title end if @params.has_key?(param) @params[param].value else nil end end def builtin=(bool) @ref.builtin = bool end # Retrieve the associated definition and evaluate it. def evaluate if klass = @ref.definedtype finish() - scope.compile.delete_resource(self) return klass.evaluate_code(self) elsif builtin? devfail "Cannot evaluate a builtin type" else self.fail "Cannot find definition %s" % self.type end ensure @evaluated = true end # Mark this resource as both exported and virtual, # or remove the exported mark. def exported=(value) if value @virtual = true @exported = value else @exported = value end end # Do any finishing work on this object, called before evaluation or # before storage/translation. def finish add_defaults() add_metaparams() add_scope_tags() validate() end def initialize(options) # Set all of the options we can. options.each do |option, value| if respond_to?(option.to_s + "=") send(option.to_s + "=", value) options.delete(option) end end unless self.scope raise ArgumentError, "Resources require a scope" end @source ||= scope.source options = symbolize_options(options) # Set up our reference. if type = options[:type] and title = options[:title] options.delete(:type) options.delete(:title) else raise ArgumentError, "Resources require a type and title" end @ref = Reference.new(:type => type, :title => title, :scope => self.scope) @params = {} # Define all of the parameters if params = options[:params] options.delete(:params) params.each do |param| set_parameter(param) end end # Throw an exception if we've got any arguments left to set. unless options.empty? raise ArgumentError, "Resources do not accept %s" % options.keys.collect { |k| k.to_s }.join(", ") end tag(@ref.type) tag(@ref.title) if valid_tag?(@ref.title.to_s) end # Merge an override resource in. This will throw exceptions if # any overrides aren't allowed. def merge(resource) # Test the resource scope, to make sure the resource is even allowed # to override. unless self.source.object_id == resource.source.object_id || resource.source.child_of?(self.source) raise Puppet::ParseError.new("Only subclasses can override parameters", resource.line, resource.file) end # Some of these might fail, but they'll fail in the way we want. resource.params.each do |name, param| override_parameter(param) end end # Modify this resource in the Rails database. Poor design, yo. def modify_rails(db_resource) args = rails_args args.each do |param, value| db_resource[param] = value unless db_resource[param] == value end # Handle file specially if (self.file and (!db_resource.file or db_resource.file != self.file)) db_resource.file = self.file end updated_params = @params.inject({}) do |hash, ary| hash[ary[0].to_s] = ary[1] hash end db_resource.ar_hash_merge(db_resource.get_params_hash(db_resource.param_values), updated_params, :create => Proc.new { |name, parameter| parameter.to_rails(db_resource) }, :delete => Proc.new { |values| values.each { |value| db_resource.param_values.delete(value) } }, :modify => Proc.new { |db, mem| mem.modify_rails_values(db) }) updated_tags = tags.inject({}) { |hash, tag| hash[tag] = tag hash } db_resource.ar_hash_merge(db_resource.get_tag_hash(), updated_tags, :create => Proc.new { |name, tag| db_resource.add_resource_tag(name) }, :delete => Proc.new { |tag| db_resource.resource_tags.delete(tag) }, :modify => Proc.new { |db, mem| # nothing here }) end # Return the resource name, or the title if no name # was specified. def name unless defined? @name @name = self[:name] || self.title end @name end # This *significantly* reduces the number of calls to Puppet.[]. def paramcheck? unless defined? @@paramcheck @@paramcheck = Puppet[:paramcheck] end @@paramcheck end # A temporary occasion, until I get paths in the scopes figured out. def path to_s end # Return the short version of our name. def ref @ref.to_s end def to_hash @params.inject({}) do |hash, ary| param = ary[1] # Skip "undef" values. if param.value != :undef hash[param.name] = param.value end hash end end # Turn our parser resource into a Rails resource. def to_rails(host) args = rails_args db_resource = host.resources.build(args) # Handle file specially db_resource.file = self.file db_resource.save @params.each { |name, param| param.to_rails(db_resource) } tags.each { |tag| db_resource.add_resource_tag(tag) } return db_resource end def to_s self.ref end # Translate our object to a transportable object. def to_trans return nil if virtual? if builtin? to_transobject else to_transbucket end end def to_transbucket bucket = Puppet::TransBucket.new([]) bucket.type = self.type bucket.name = self.title # TransBuckets don't support parameters, which is why they're being deprecated. return bucket end def to_transobject # Now convert to a transobject obj = Puppet::TransObject.new(@ref.title, @ref.type) to_hash.each do |p, v| if v.is_a?(Reference) v = v.to_ref elsif v.is_a?(Array) v = v.collect { |av| if av.is_a?(Reference) av = av.to_ref end av } end # If the value is an array with only one value, then # convert it to a single value. This is largely so that # the database interaction doesn't have to worry about # whether it returns an array or a string. obj[p.to_s] = if v.is_a?(Array) and v.length == 1 v[0] else v end end obj.file = self.file obj.line = self.line obj.tags = self.tags return obj end private # Add default values from our definition. def add_defaults scope.lookupdefaults(self.type).each do |name, param| unless @params.include?(name) self.debug "Adding default for %s" % name @params[name] = param end end end # Add any metaparams defined in our scope. This actually adds any metaparams # from any parent scope, and there's currently no way to turn that off. def add_metaparams Puppet::Type.eachmetaparam do |name| # Skip metaparams that we already have defined, unless they're relationship metaparams. # LAK:NOTE Relationship metaparams get treated specially -- we stack them, instead of # overriding. next if @params[name] and not self.class.relationship_parameter?(name) # Skip metaparams for which we get no value. next unless val = scope.lookupvar(name.to_s, false) and val != :undefined # The default case: just set the value return set_parameter(name, val) unless @params[name] # For relationship params, though, join the values (a la #446). @params[name].value = [@params[name].value, val].flatten end end def add_scope_tags if scope_resource = scope.resource tag(*scope_resource.tags) end end # Accept a parameter from an override. def override_parameter(param) # This can happen if the override is defining a new parameter, rather # than replacing an existing one. (@params[param.name] = param and return) unless current = @params[param.name] # The parameter is already set. Fail if they're not allowed to override it. unless param.source.child_of?(current.source) if Puppet[:trace] puts caller end msg = "Parameter '%s' is already set on %s" % [param.name, self.to_s] if current.source.to_s != "" msg += " by %s" % current.source end if current.file or current.line fields = [] fields << current.file if current.file fields << current.line.to_s if current.line msg += " at %s" % fields.join(":") end msg += "; cannot redefine" raise Puppet::ParseError.new(msg, param.line, param.file) end # If we've gotten this far, we're allowed to override. # Merge with previous value, if the parameter was generated with the +> syntax. # It's important that we use the new param instance here, not the old one, # so that the source is registered correctly for later overrides. param.value = [current.value, param.value].flatten if param.add @params[param.name] = param end # Verify that all passed parameters are valid. This throws an error if # there's a problem, so we don't have to worry about the return value. def paramcheck(param) param = param.to_s # Now make sure it's a valid argument to our class. These checks # are organized in order of commonhood -- most types, it's a valid # argument and paramcheck is enabled. if @ref.typeclass.validattr?(param) true elsif %w{name title}.include?(param) # always allow these true elsif paramcheck? self.fail Puppet::ParseError, "Invalid parameter '%s' for type '%s'" % [param, @ref.type] end end def rails_args return [:type, :title, :line, :exported].inject({}) do |hash, param| # 'type' isn't a valid column name, so we have to use another name. to = (param == :type) ? :restype : param if value = self.send(param) hash[to] = value end hash end end # Define a parameter in our resource. def set_parameter(param, value = nil) if value param = Puppet::Parser::Resource::Param.new( :name => param, :value => value, :source => self.source ) elsif ! param.is_a?(Puppet::Parser::Resource::Param) raise ArgumentError, "Must pass a parameter or all necessary values" end # And store it in our parameter hash. @params[param.name] = param end # Make sure the resource's parameters are all valid for the type. def validate @params.each do |name, param| # Make sure it's a valid parameter. paramcheck(name) end end end diff --git a/spec/unit/parser/compile.rb b/spec/unit/parser/compile.rb index a72713360..d495ac343 100755 --- a/spec/unit/parser/compile.rb +++ b/spec/unit/parser/compile.rb @@ -1,583 +1,566 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' module CompileTesting def setup @node = Puppet::Node.new "testnode" @parser = Puppet::Parser::Parser.new :environment => "development" - @scope = stub 'scope', :resource => stub("scope_resource"), :source => mock("source") + @scope_resource = stub 'scope_resource', :builtin? => true + @scope = stub 'scope', :resource => @scope_resource, :source => mock("source") @compile = Puppet::Parser::Compile.new(@node, @parser) end end describe Puppet::Parser::Compile, " when compiling" do include CompileTesting def compile_methods [:set_node_parameters, :evaluate_main, :evaluate_ast_node, :evaluate_node_classes, :evaluate_generators, :fail_on_unevaluated, :finish, :store, :extract] end # Stub all of the main compile methods except the ones we're specifically interested in. def compile_stub(*except) (compile_methods - except).each { |m| @compile.stubs(m) } end it "should set node parameters as variables in the top scope" do params = {"a" => "b", "c" => "d"} @node.stubs(:parameters).returns(params) compile_stub(:set_node_parameters) @compile.compile @compile.topscope.lookupvar("a").should == "b" @compile.topscope.lookupvar("c").should == "d" end it "should evaluate any existing classes named in the node" do classes = %w{one two three four} main = stub 'main' one = stub 'one', :classname => "one" three = stub 'three', :classname => "three" @node.stubs(:name).returns("whatever") @node.stubs(:classes).returns(classes) @compile.expects(:evaluate_classes).with(classes, @compile.topscope) @compile.class.publicize_methods(:evaluate_node_classes) { @compile.evaluate_node_classes } end it "should enable ast_nodes if the parser has any nodes" do @parser.expects(:nodes).returns(:one => :yay) @compile.ast_nodes?.should be_true end it "should disable ast_nodes if the parser has no nodes" do @parser.expects(:nodes).returns({}) @compile.ast_nodes?.should be_false end it "should evaluate the main class if it exists" do compile_stub(:evaluate_main) main_class = mock 'main_class' main_class.expects(:evaluate_code).with { |r| r.is_a?(Puppet::Parser::Resource) } @compile.topscope.expects(:source=).with(main_class) @parser.stubs(:findclass).with("", "").returns(main_class) @compile.compile end it "should evaluate any node classes" do @node.stubs(:classes).returns(%w{one two three four}) @compile.expects(:evaluate_classes).with(%w{one two three four}, @compile.topscope) @compile.send(:evaluate_node_classes) end it "should evaluate all added collections" do colls = [] # And when the collections fail to evaluate. colls << mock("coll1-false") colls << mock("coll2-false") colls.each { |c| c.expects(:evaluate).returns(false) } @compile.add_collection(colls[0]) @compile.add_collection(colls[1]) compile_stub(:evaluate_generators) @compile.compile end it "should ignore builtin resources" do resource = stub 'builtin', :ref => "File[testing]", :builtin? => true - scope = stub 'scope', :resource => stub("scope_resource") - @compile.store_resource(scope, resource) + @compile.store_resource(@scope, resource) resource.expects(:evaluate).never @compile.compile end it "should evaluate unevaluated resources" do resource = stub 'notevaluated', :ref => "File[testing]", :builtin? => false, :evaluated? => false - scope = stub 'scope', :resource => stub("scope_resource") - @compile.store_resource(scope, resource) + @compile.store_resource(@scope, resource) # We have to now mark the resource as evaluated resource.expects(:evaluate).with { |*whatever| resource.stubs(:evaluated?).returns true } @compile.compile end it "should not evaluate already-evaluated resources" do resource = stub 'already_evaluated', :ref => "File[testing]", :builtin? => false, :evaluated? => true - scope = stub 'scope', :resource => stub("scope_resource") - @compile.store_resource(scope, resource) + @compile.store_resource(@scope, resource) resource.expects(:evaluate).never @compile.compile end it "should evaluate unevaluated resources created by evaluating other resources" do resource = stub 'notevaluated', :ref => "File[testing]", :builtin? => false, :evaluated? => false - scope = stub 'scope', :resource => stub("scope_resource") - @compile.store_resource(scope, resource) + @compile.store_resource(@scope, resource) resource2 = stub 'created', :ref => "File[other]", :builtin? => false, :evaluated? => false # We have to now mark the resource as evaluated - resource.expects(:evaluate).with { |*whatever| resource.stubs(:evaluated?).returns(true); @compile.store_resource(scope, resource2) } + resource.expects(:evaluate).with { |*whatever| resource.stubs(:evaluated?).returns(true); @compile.store_resource(@scope, resource2) } resource2.expects(:evaluate).with { |*whatever| resource2.stubs(:evaluated?).returns(true) } @compile.compile end it "should call finish() on all resources" do # Add a resource that does respond to :finish - yep = stub "finisher", :ref => "File[finish]" - yep.expects(:respond_to?).with(:finish).returns(true) - yep.expects(:finish) + resource = Puppet::Parser::Resource.new :scope => @scope, :type => "file", :title => "finish" + resource.expects(:finish) - @compile.store_resource(@scope, yep) + @compile.store_resource(@scope, resource) # And one that does not dnf = stub "dnf", :ref => "File[dnf]" - dnf.expects(:respond_to?).with(:finish).returns(false) @compile.store_resource(@scope, dnf) @compile.send(:finish) end it "should add resources that do not conflict with existing resources" do resource = stub "noconflict", :ref => "File[yay]" @compile.store_resource(@scope, resource) @compile.catalog.should be_vertex(resource) end - it "should not fail when conflicting resources are non-isormphic" do - type = stub 'faketype', :isomorphic? => false, :name => "mytype" - Puppet::Type.stubs(:type).with("mytype").returns(type) - - resource1 = stub "iso1conflict", :ref => "Mytype[yay]", :type => "mytype" - resource2 = stub "iso2conflict", :ref => "Mytype[yay]", :type => "mytype" - - @compile.store_resource(@scope, resource1) - lambda { @compile.store_resource(@scope, resource2) }.should_not raise_error - end - it "should fail to add resources that conflict with existing resources" do type = stub 'faketype', :isomorphic? => true, :name => "mytype" Puppet::Type.stubs(:type).with("mytype").returns(type) resource1 = stub "iso1conflict", :ref => "Mytype[yay]", :type => "mytype", :file => "eh", :line => 0 resource2 = stub "iso2conflict", :ref => "Mytype[yay]", :type => "mytype", :file => "eh", :line => 0 @compile.store_resource(@scope, resource1) - lambda { @compile.store_resource(@scope, resource2) }.should raise_error(Puppet::ParseError) + lambda { @compile.store_resource(@scope, resource2) }.should raise_error(ArgumentError) end it "should have a method for looking up resources" do resource = stub 'resource', :ref => "Yay[foo]" @compile.store_resource(@scope, resource) @compile.findresource("Yay[foo]").should equal(resource) end it "should be able to look resources up by type and title" do resource = stub 'resource', :ref => "Yay[foo]" @compile.store_resource(@scope, resource) @compile.findresource("Yay", "foo").should equal(resource) end end describe Puppet::Parser::Compile, " when evaluating classes" do include CompileTesting it "should fail if there's no source listed for the scope" do scope = stub 'scope', :source => nil proc { @compile.evaluate_classes(%w{one two}, scope) }.should raise_error(Puppet::DevError) end it "should tag the catalog with the name of each not-found class" do @compile.catalog.expects(:tag).with("notfound") @scope.expects(:findclass).with("notfound").returns(nil) @compile.evaluate_classes(%w{notfound}, @scope) end end describe Puppet::Parser::Compile, " when evaluating collections" do include CompileTesting it "should evaluate each collection" do 2.times { |i| coll = mock 'coll%s' % i @compile.add_collection(coll) # This is the hard part -- we have to emulate the fact that # collections delete themselves if they are done evaluating. coll.expects(:evaluate).with do @compile.delete_collection(coll) end } @compile.class.publicize_methods(:evaluate_collections) { @compile.evaluate_collections } end it "should not fail when there are unevaluated resource collections that do not refer to specific resources" do coll = stub 'coll', :evaluate => false coll.expects(:resources).returns(nil) @compile.add_collection(coll) lambda { @compile.compile }.should_not raise_error end it "should fail when there are unevaluated resource collections that refer to a specific resource" do coll = stub 'coll', :evaluate => false coll.expects(:resources).returns(:something) @compile.add_collection(coll) lambda { @compile.compile }.should raise_error(Puppet::ParseError) end it "should fail when there are unevaluated resource collections that refer to multiple specific resources" do coll = stub 'coll', :evaluate => false coll.expects(:resources).returns([:one, :two]) @compile.add_collection(coll) lambda { @compile.compile }.should raise_error(Puppet::ParseError) end end describe Puppet::Parser::Compile, " when evaluating found classes" do include CompileTesting before do @class = stub 'class', :classname => "my::class" @scope.stubs(:findclass).with("myclass").returns(@class) @resource = stub 'resource', :ref => 'Class[myclass]' end it "should create a resource for each found class" do @compile.catalog.stubs(:tag) @compile.stubs :store_resource Puppet::Parser::Resource.expects(:new).with(:scope => @scope, :source => @scope.source, :title => "my::class", :type => "class").returns(@resource) @compile.evaluate_classes(%w{myclass}, @scope) end it "should store each created resource in the compile" do @compile.catalog.stubs(:tag) @compile.expects(:store_resource).with(@scope, @resource) Puppet::Parser::Resource.stubs(:new).returns(@resource) @compile.evaluate_classes(%w{myclass}, @scope) end it "should tag the catalog with the fully-qualified name of each found class" do @compile.catalog.expects(:tag).with("my::class") @compile.stubs(:store_resource) Puppet::Parser::Resource.stubs(:new).returns(@resource) @compile.evaluate_classes(%w{myclass}, @scope) end it "should not evaluate the resources created for found classes unless asked" do @compile.catalog.stubs(:tag) @compile.stubs(:store_resource) @resource.expects(:evaluate).never Puppet::Parser::Resource.stubs(:new).returns(@resource) @compile.evaluate_classes(%w{myclass}, @scope) end it "should immediately evaluate the resources created for found classes when asked" do @compile.catalog.stubs(:tag) @compile.stubs(:store_resource) @resource.expects(:evaluate) Puppet::Parser::Resource.stubs(:new).returns(@resource) @compile.evaluate_classes(%w{myclass}, @scope, false) end it "should skip classes that have already been evaluated" do @compile.catalog.stubs(:tag) @compile.expects(:class_scope).with(@class).returns("something") @compile.expects(:store_resource).never @resource.expects(:evaluate).never Puppet::Parser::Resource.expects(:new).never @compile.evaluate_classes(%w{myclass}, @scope, false) end it "should return the list of found classes" do @compile.catalog.stubs(:tag) @compile.stubs(:store_resource) @scope.stubs(:findclass).with("notfound").returns(nil) Puppet::Parser::Resource.stubs(:new).returns(@resource) @compile.evaluate_classes(%w{myclass notfound}, @scope).should == %w{myclass} end end describe Puppet::Parser::Compile, " when evaluating AST nodes with no AST nodes present" do include CompileTesting it "should do nothing" do @compile.expects(:ast_nodes?).returns(false) @compile.parser.expects(:nodes).never Puppet::Parser::Resource.expects(:new).never @compile.send(:evaluate_ast_node) end end describe Puppet::Parser::Compile, " when evaluating AST nodes with AST nodes present" do include CompileTesting before do @nodes = mock 'node_hash' @compile.stubs(:ast_nodes?).returns(true) @compile.parser.stubs(:nodes).returns(@nodes) # Set some names for our test @node.stubs(:names).returns(%w{a b c}) @nodes.stubs(:[]).with("a").returns(nil) @nodes.stubs(:[]).with("b").returns(nil) @nodes.stubs(:[]).with("c").returns(nil) # It should check this last, of course. @nodes.stubs(:[]).with("default").returns(nil) end it "should fail if the named node cannot be found" do proc { @compile.send(:evaluate_ast_node) }.should raise_error(Puppet::ParseError) end it "should create a resource for the first node class matching the node name" do node_class = stub 'node', :classname => "c" @nodes.stubs(:[]).with("c").returns(node_class) node_resource = stub 'node resource', :ref => "Node[c]", :evaluate => nil Puppet::Parser::Resource.expects(:new).with { |args| args[:title] == "c" and args[:type] == "node" }.returns(node_resource) @compile.send(:evaluate_ast_node) end it "should match the default node if no matching node can be found" do node_class = stub 'node', :classname => "default" @nodes.stubs(:[]).with("default").returns(node_class) node_resource = stub 'node resource', :ref => "Node[default]", :evaluate => nil Puppet::Parser::Resource.expects(:new).with { |args| args[:title] == "default" and args[:type] == "node" }.returns(node_resource) @compile.send(:evaluate_ast_node) end it "should tag the catalog with the found node name" do node_class = stub 'node', :classname => "c" @nodes.stubs(:[]).with("c").returns(node_class) node_resource = stub 'node resource', :ref => "Node[c]", :evaluate => nil Puppet::Parser::Resource.stubs(:new).returns(node_resource) @compile.catalog.expects(:tag).with("c") @compile.send(:evaluate_ast_node) end it "should evaluate the node resource immediately rather than using lazy evaluation" do node_class = stub 'node', :classname => "c" @nodes.stubs(:[]).with("c").returns(node_class) node_resource = stub 'node resource', :ref => "Node[c]" Puppet::Parser::Resource.stubs(:new).returns(node_resource) node_resource.expects(:evaluate) @compile.send(:evaluate_ast_node) end it "should set the node's scope as the top scope" do node_class = stub 'node', :classname => "c" @nodes.stubs(:[]).with("c").returns(node_class) node_resource = stub 'node resource', :ref => "Node[c]" Puppet::Parser::Resource.stubs(:new).returns(node_resource) # The #evaluate method normally does this. @compile.class_set(node_class.classname, :my_node_scope) node_resource.stubs(:evaluate) @compile.send(:evaluate_ast_node) @compile.topscope.should == :my_node_scope end end describe Puppet::Parser::Compile, " when initializing" do include CompileTesting it "should set its node attribute" do @compile.node.should equal(@node) end it "should set its parser attribute" do @compile.parser.should equal(@parser) end it "should detect when ast nodes are absent" do @compile.ast_nodes?.should be_false end it "should detect when ast nodes are present" do @parser.nodes["testing"] = "yay" @compile.ast_nodes?.should be_true end end describe Puppet::Parser::Compile do include CompileTesting it "should be able to store references to class scopes" do lambda { @compile.class_set "myname", "myscope" }.should_not raise_error end it "should be able to retrieve class scopes by name" do @compile.class_set "myname", "myscope" @compile.class_scope("myname").should == "myscope" end it "should be able to retrieve class scopes by object" do klass = mock 'ast_class' klass.expects(:classname).returns("myname") @compile.class_set "myname", "myscope" @compile.class_scope(klass).should == "myscope" end it "should be able to return a class list containing all set classes" do @compile.class_set "", "empty" @compile.class_set "one", "yep" @compile.class_set "two", "nope" @compile.classlist.sort.should == %w{one two}.sort end end describe Puppet::Parser::Compile, "when managing scopes" do include CompileTesting it "should create a top scope" do @compile.topscope.should be_instance_of(Puppet::Parser::Scope) end it "should be able to create new scopes" do @compile.newscope(@compile.topscope).should be_instance_of(Puppet::Parser::Scope) end it "should correctly set the level of newly created scopes" do @compile.newscope(@compile.topscope, :level => 5).level.should == 5 end it "should set the parent scope of the new scope to be the passed-in parent" do scope = mock 'scope' newscope = @compile.newscope(scope) @compile.parent(newscope).should equal(scope) end end describe Puppet::Parser::Compile, "when storing compiled resources" do include CompileTesting it "should store the resources" do Puppet.features.expects(:rails?).returns(true) Puppet::Rails.expects(:connect) - resource_table = mock 'resources' - resource_table.expects(:values).returns(:resources) - @compile.instance_variable_set("@resource_table", resource_table) + @compile.catalog.expects(:vertices).returns(:resources) + @compile.expects(:store_to_active_record).with(@node, :resources) @compile.send(:store) end it "should store to active_record" do @node.expects(:name).returns("myname") Puppet::Rails::Host.stubs(:transaction).yields Puppet::Rails::Host.expects(:store).with(@node, :resources) @compile.send(:store_to_active_record, @node, :resources) end end describe Puppet::Parser::Compile, "when managing resource overrides" do include CompileTesting before do @override = stub 'override', :ref => "My[ref]" @resource = stub 'resource', :ref => "My[ref]", :builtin? => true end it "should be able to store overrides" do lambda { @compile.store_override(@override) }.should_not raise_error end it "should apply overrides to the appropriate resources" do @compile.store_resource(@scope, @resource) @resource.expects(:merge).with(@override) @compile.store_override(@override) @compile.compile end it "should accept overrides before the related resource has been created" do @resource.expects(:merge).with(@override) # First store the override @compile.store_override(@override) # Then the resource @compile.store_resource(@scope, @resource) # And compile, so they get resolved @compile.compile end it "should fail if the compile is finished and resource overrides have not been applied" do @compile.store_override(@override) lambda { @compile.compile }.should raise_error(Puppet::ParseError) end end # #620 - Nodes and classes should conflict, else classes don't get evaluated describe Puppet::Parser::Compile, "when evaluating nodes and classes with the same name (#620)" do include CompileTesting before do @node = stub :nodescope? => true @class = stub :nodescope? => false end it "should fail if a node already exists with the same name as the class being evaluated" do @compile.class_set("one", @node) lambda { @compile.class_set("one", @class) }.should raise_error(Puppet::ParseError) end it "should fail if a class already exists with the same name as the node being evaluated" do @compile.class_set("one", @class) lambda { @compile.class_set("one", @node) }.should raise_error(Puppet::ParseError) end end diff --git a/test/language/ast/resource.rb b/test/language/ast/resource.rb index ef7800066..aff1dba16 100755 --- a/test/language/ast/resource.rb +++ b/test/language/ast/resource.rb @@ -1,59 +1,58 @@ #!/usr/bin/env ruby # # Created by Luke A. Kanies on 2007-07-8. # Copyright (c) 2007. All rights reserved. require File.dirname(__FILE__) + '/../../lib/puppettest' require 'puppettest' require 'puppettest/parsertesting' class TestASTResource< Test::Unit::TestCase include PuppetTest include PuppetTest::ParserTesting AST = Puppet::Parser::AST def setup super @scope = mkscope @parser = @scope.compile.parser - @scope.compile.send(:evaluate_main) end def newdef(type, title, params = nil) params ||= AST::ASTArray.new(:children => []) AST::Resource.new(:type => type, :title => AST::String.new(:value => title), :params => params) end # Related to #806, make sure resources always look up the full path to the resource. def test_scoped_types @parser.newdefine "one" @parser.newdefine "one::two" @parser.newdefine "three" twoscope = @scope.newscope(:namespace => "one") twoscope.resource = @scope.resource assert(twoscope.finddefine("two"), "Could not find 'two' definition") title = "title" # First try a qualified type assert_equal("One::Two", newdef("two", title).evaluate(twoscope)[0].type, "Defined type was not made fully qualified") # Then try a type that does not need to be qualified assert_equal("One", newdef("one", title).evaluate(twoscope)[0].type, "Unqualified defined type was not handled correctly") # Then an unqualified type from within the one namespace assert_equal("Three", newdef("three", title).evaluate(twoscope)[0].type, "Defined type was not made fully qualified") # Then a builtin type assert_equal("File", newdef("file", title).evaluate(twoscope)[0].type, "Builtin type was not handled correctly") # Now try a type that does not exist, which should throw an error. assert_raise(Puppet::ParseError, "Did not fail on a missing type in a resource reference") do newdef("nosuchtype", title).evaluate(twoscope) end end end diff --git a/test/language/compile.rb b/test/language/compile.rb deleted file mode 100755 index 082b37a1c..000000000 --- a/test/language/compile.rb +++ /dev/null @@ -1,569 +0,0 @@ -#!/usr/bin/env ruby - -require File.dirname(__FILE__) + '/../lib/puppettest' - -require 'mocha' -require 'puppettest' -require 'puppettest/parsertesting' -require 'puppet/parser/compile' - -# Test our compile object. -class TestCompile < Test::Unit::TestCase - include PuppetTest - include PuppetTest::ParserTesting - - Compile = Puppet::Parser::Compile - Scope = Puppet::Parser::Scope - Node = Puppet::Network::Handler.handler(:node) - SimpleNode = Puppet::Node - - def mknode(name = "foo") - @node = SimpleNode.new(name) - end - - def mkparser - # This should mock an interpreter - @parser = stub 'parser', :version => "1.0", :nodes => {} - end - - def mkcompile(options = {}) - if node = options[:node] - options.delete(:node) - else - node = mknode - end - @compile = Compile.new(node, mkparser, options) - end - - def test_initialize - compile = nil - node = stub 'node', :name => "foo" - parser = stub 'parser', :version => "1.0", :nodes => {} - assert_nothing_raised("Could not init compile with all required options") do - compile = Compile.new(node, parser) - end - - assert_equal(node, compile.node, "Did not set node correctly") - assert_equal(parser, compile.parser, "Did not set parser correctly") - - # We're not testing here whether we call initvars, because it's too difficult to - # mock. - - # Now try it with some options - assert_nothing_raised("Could not init compile with extra options") do - compile = Compile.new(node, parser) - end - - assert_equal(false, compile.ast_nodes?, "Did not set ast_nodes? correctly") - end - - def test_initvars - compile = mkcompile - [:class_scopes, :resource_table, :exported_resources, :resource_overrides].each do |table| - assert_instance_of(Hash, compile.send(:instance_variable_get, "@#{table}"), "Did not set %s table correctly" % table) - end - assert_instance_of(Scope, compile.topscope, "Did not create a topscope") - graph = compile.instance_variable_get("@scope_graph") - assert_instance_of(Puppet::SimpleGraph, graph, "Did not create scope graph") - assert(graph.vertex?(compile.topscope), "Did not add top scope as a vertex in the graph") - end - - # Make sure we store and can retrieve references to classes and their scopes. - def test_class_set_and_class_scope - klass = mock 'ast_class' - klass.expects(:classname).returns("myname") - - compile = mkcompile - compile.catalog.expects(:tag).with("myname") - - assert_nothing_raised("Could not set class") do - compile.class_set "myname", "myscope" - end - # First try to retrieve it by name. - assert_equal("myscope", compile.class_scope("myname"), "Could not retrieve class scope by name") - - # Then by object - assert_equal("myscope", compile.class_scope(klass), "Could not retrieve class scope by object") - end - - def test_classlist - compile = mkcompile - - compile.class_set "", "empty" - compile.class_set "one", "yep" - compile.class_set "two", "nope" - - # Make sure our class list is correct - assert_equal(%w{one two}.sort, compile.classlist.sort, "Did not get correct class list") - end - - # Make sure collections get added to our internal array - def test_add_collection - compile = mkcompile - assert_nothing_raised("Could not add collection") do - compile.add_collection "nope" - end - assert_equal(%w{nope}, compile.instance_variable_get("@collections"), "Did not add collection") - end - - # Make sure we create a graph of scopes. - def test_newscope - compile = mkcompile - graph = compile.instance_variable_get("@scope_graph") - assert_instance_of(Scope, compile.topscope, "Did not create top scope") - assert_instance_of(Puppet::SimpleGraph, graph, "Did not create graph") - - assert(graph.vertex?(compile.topscope), "The top scope is not a vertex in the graph") - - # Now that we've got the top scope, create a new, subscope - subscope = nil - assert_nothing_raised("Could not create subscope") do - subscope = compile.newscope(compile.topscope) - end - assert_instance_of(Scope, subscope, "Did not create subscope") - assert(graph.edge?(compile.topscope, subscope), "An edge between top scope and subscope was not added") - - # Make sure a scope can find its parent. - assert(compile.parent(subscope), "Could not look up parent scope on compile") - assert_equal(compile.topscope.object_id, compile.parent(subscope).object_id, "Did not get correct parent scope from compile") - assert_equal(compile.topscope.object_id, subscope.parent.object_id, "Scope did not correctly retrieve its parent scope") - - # Now create another, this time specifying options - another = nil - assert_nothing_raised("Could not create subscope") do - another = compile.newscope(subscope, :level => 5) - end - assert_equal(5, another.level, "did not set scope option correctly") - assert_instance_of(Scope, another, "Did not create second subscope") - assert(graph.edge?(subscope, another), "An edge between parent scope and second subscope was not added") - - # Make sure it can find its parent. - assert(compile.parent(another), "Could not look up parent scope of second subscope on compile") - assert_equal(subscope.object_id, compile.parent(another).object_id, "Did not get correct parent scope of second subscope from compile") - assert_equal(subscope.object_id, another.parent.object_id, "Second subscope did not correctly retrieve its parent scope") - - # And make sure both scopes show up in the right order in the search path - assert_equal([another.object_id, subscope.object_id, compile.topscope.object_id], another.scope_path.collect { |p| p.object_id }, - "Did not get correct scope path") - end - - # The heart of the action. - def test_compile - compile = mkcompile - [:set_node_parameters, :evaluate_main, :evaluate_ast_node, :evaluate_node_classes, :evaluate_generators, :fail_on_unevaluated, :finish].each do |method| - compile.expects(method) - end - assert_instance_of(Puppet::Node::Catalog, compile.compile, "Did not return the catalog") - end - - # Test setting the node's parameters into the top scope. - def test_set_node_parameters - compile = mkcompile - @node.parameters = {"a" => "b", "c" => "d"} - scope = compile.topscope - @node.parameters.each do |param, value| - scope.expects(:setvar).with(param, value) - end - - assert_nothing_raised("Could not call 'set_node_parameters'") do - compile.send(:set_node_parameters) - end - end - - # Test that we can evaluate the main class, which is the one named "" in namespace - # "". - def test_evaluate_main - compile = mkcompile - main_class = mock 'main_class' - compile.topscope.expects(:source=).with(main_class) - @parser.expects(:findclass).with("", "").returns(main_class) - - main_resource = mock 'main resource' - Puppet::Parser::Resource.expects(:new).with { |args| args[:title] == :main }.returns(main_resource) - - main_resource.expects(:evaluate) - - assert_nothing_raised("Could not call evaluate_main") do - compile.send(:evaluate_main) - end - end - - def test_evaluate_node_classes - compile = mkcompile - @node.classes = %w{one two three four} - compile.expects(:evaluate_classes).with(%w{one two three four}, compile.topscope) - assert_nothing_raised("could not call evaluate_node_classes") do - compile.send(:evaluate_node_classes) - end - end - - def test_evaluate_collections - compile = mkcompile - - colls = [] - - # Make sure we return false when there's nothing there. - assert(! compile.send(:evaluate_collections), "Returned true when there were no collections") - - # And when the collections fail to evaluate. - colls << mock("coll1-false") - colls << mock("coll2-false") - colls.each { |c| c.expects(:evaluate).returns(false) } - - compile.instance_variable_set("@collections", colls) - assert(! compile.send(:evaluate_collections), "Returned true when collections both evaluated nothing") - - # Now have one of the colls evaluate - colls.clear - colls << mock("coll1-one-true") - colls << mock("coll2-one-true") - colls[0].expects(:evaluate).returns(true) - colls[1].expects(:evaluate).returns(false) - assert(compile.send(:evaluate_collections), "Did not return true when one collection evaluated true") - - # And have them both eval true - colls.clear - colls << mock("coll1-both-true") - colls << mock("coll2-both-true") - colls[0].expects(:evaluate).returns(true) - colls[1].expects(:evaluate).returns(true) - assert(compile.send(:evaluate_collections), "Did not return true when both collections evaluated true") - end - - def test_unevaluated_resources - compile = mkcompile - resources = {} - compile.instance_variable_set("@resource_table", resources) - - # First test it when the table is empty - assert_nil(compile.send(:unevaluated_resources), "Somehow found unevaluated resources in an empty table") - - # Then add a builtin resources - resources["one"] = mock("builtin only") - resources["one"].expects(:builtin?).returns(true) - assert_nil(compile.send(:unevaluated_resources), "Considered a builtin resource unevaluated") - - # And do both builtin and non-builtin but already evaluated - resources.clear - resources["one"] = mock("builtin (with eval)") - resources["one"].expects(:builtin?).returns(true) - resources["two"] = mock("evaled (with builtin)") - resources["two"].expects(:builtin?).returns(false) - resources["two"].expects(:evaluated?).returns(true) - assert_nil(compile.send(:unevaluated_resources), "Considered either a builtin or evaluated resource unevaluated") - - # Now a single unevaluated resource. - resources.clear - resources["one"] = mock("unevaluated") - resources["one"].expects(:builtin?).returns(false) - resources["one"].expects(:evaluated?).returns(false) - assert_equal([resources["one"]], compile.send(:unevaluated_resources), "Did not find unevaluated resource") - - # With two uneval'ed resources, and an eval'ed one thrown in - resources.clear - resources["one"] = mock("unevaluated one") - resources["one"].expects(:builtin?).returns(false) - resources["one"].expects(:evaluated?).returns(false) - resources["two"] = mock("unevaluated two") - resources["two"].expects(:builtin?).returns(false) - resources["two"].expects(:evaluated?).returns(false) - resources["three"] = mock("evaluated") - resources["three"].expects(:builtin?).returns(false) - resources["three"].expects(:evaluated?).returns(true) - - result = compile.send(:unevaluated_resources) - %w{one two}.each do |name| - assert(result.include?(resources[name]), "Did not find %s in the unevaluated list" % name) - end - end - - def test_evaluate_definitions - # First try the case where there's nothing to return - compile = mkcompile - compile.expects(:unevaluated_resources).returns(nil) - - assert_nothing_raised("Could not test for unevaluated resources") do - assert(! compile.send(:evaluate_definitions), "evaluate_definitions returned true when no resources were evaluated") - end - - # Now try it with resources left to evaluate - resources = [] - res1 = mock("resource1") - res1.expects(:evaluate) - res2 = mock("resource2") - res2.expects(:evaluate) - resources << res1 << res2 - compile = mkcompile - compile.expects(:unevaluated_resources).returns(resources) - - assert_nothing_raised("Could not test for unevaluated resources") do - assert(compile.send(:evaluate_definitions), "evaluate_definitions returned false when resources were evaluated") - end - end - - def test_evaluate_generators - # First try the case where we have nothing to do - compile = mkcompile - compile.expects(:evaluate_definitions).returns(false) - compile.expects(:evaluate_collections).returns(false) - - assert_nothing_raised("Could not call :eval_iterate") do - compile.send(:evaluate_generators) - end - - # FIXME I could not get this test to work, but the code is short - # enough that I'm ok with it. - # It's important that collections are evaluated before definitions, - # so make sure that's the case by verifying that collections get tested - # twice but definitions only once. - #compile = mkcompile - #compile.expects(:evaluate_collections).returns(true).returns(false) - #compile.expects(:evaluate_definitions).returns(false) - #compile.send(:eval_iterate) - end - - def test_store - compile = mkcompile - Puppet.features.expects(:rails?).returns(true) - Puppet::Rails.expects(:connect) - - node = mock 'node' - resource_table = mock 'resources' - resource_table.expects(:values).returns(:resources) - compile.instance_variable_set("@node", node) - compile.instance_variable_set("@resource_table", resource_table) - compile.expects(:store_to_active_record).with(node, :resources) - compile.send(:store) - end - - def test_store_to_active_record - compile = mkcompile - node = mock 'node' - node.expects(:name).returns("myname") - Puppet::Rails::Host.stubs(:transaction).yields - Puppet::Rails::Host.expects(:store).with(node, :resources) - compile.send(:store_to_active_record, node, :resources) - end - - # Make sure that 'finish' gets called on all of our resources. - def test_finish - compile = mkcompile - table = compile.instance_variable_get("@resource_table") - - # Add a resource that does respond to :finish - yep = mock("finisher") - yep.expects(:respond_to?).with(:finish).returns(true) - yep.expects(:finish) - table["yep"] = yep - - # And one that does not - dnf = mock("dnf") - dnf.expects(:respond_to?).with(:finish).returns(false) - table["dnf"] = dnf - - compile.send(:finish) - end - - def test_verify_uniqueness - compile = mkcompile - - resources = compile.instance_variable_get("@resource_table") - resource = mock("noconflict") - resource.expects(:ref).returns("File[yay]") - assert_nothing_raised("Raised an exception when there should have been no conflict") do - compile.send(:verify_uniqueness, resource) - end - - # Now try the case where our type is isomorphic - resources["thing"] = true - - isoconflict = mock("isoconflict") - isoconflict.expects(:ref).returns("thing") - isoconflict.expects(:type).returns("testtype") - faketype = mock("faketype") - faketype.expects(:isomorphic?).returns(false) - faketype.expects(:name).returns("whatever") - Puppet::Type.expects(:type).with("testtype").returns(faketype) - assert_nothing_raised("Raised an exception when was a conflict in non-isomorphic types") do - compile.send(:verify_uniqueness, isoconflict) - end - - # Now test for when we actually have an exception - initial = mock("initial") - resources["thing"] = initial - initial.expects(:file).returns(false) - - conflict = mock("conflict") - conflict.expects(:ref).returns("thing").times(2) - conflict.expects(:type).returns("conflict") - conflict.expects(:file).returns(false) - conflict.expects(:line).returns(false) - - faketype = mock("faketype") - faketype.expects(:isomorphic?).returns(true) - Puppet::Type.expects(:type).with("conflict").returns(faketype) - assert_raise(Puppet::ParseError, "Did not fail when two isomorphic resources conflicted") do - compile.send(:verify_uniqueness, conflict) - end - end - - def test_store_resource - # Run once when there's no conflict - compile = mkcompile - table = compile.instance_variable_get("@resource_table") - resource = mock("resource") - resource.expects(:ref).returns("yay") - compile.expects(:verify_uniqueness).with(resource) - scope = stub("scope", :resource => mock('resource')) - - compile.catalog.expects(:add_edge!).with(scope.resource, resource) - - assert_nothing_raised("Could not store resource") do - compile.store_resource(scope, resource) - end - assert_equal(resource, table["yay"], "Did not store resource in table") - - # Now for conflicts - compile = mkcompile - table = compile.instance_variable_get("@resource_table") - resource = mock("resource") - compile.expects(:verify_uniqueness).with(resource).raises(ArgumentError) - - assert_raise(ArgumentError, "Did not raise uniqueness exception") do - compile.store_resource(scope, resource) - end - assert(table.empty?, "Conflicting resource was stored in table") - end - - def test_fail_on_unevaluated - compile = mkcompile - compile.expects(:fail_on_unevaluated_overrides) - compile.expects(:fail_on_unevaluated_resource_collections) - compile.send :fail_on_unevaluated - end - - def test_store_override - # First test the case when the resource is not present. - compile = mkcompile - overrides = compile.instance_variable_get("@resource_overrides") - override = Object.new - override.expects(:ref).returns(:myref).times(2) - override.expects(:override=).with(true) - - assert_nothing_raised("Could not call store_override") do - compile.store_override(override) - end - assert_instance_of(Array, overrides[:myref], "Overrides table is not a hash of arrays") - assert_equal(override, overrides[:myref][0], "Did not store override in appropriately named array") - - # And when the resource already exists. - resource = mock 'resource' - resources = compile.instance_variable_get("@resource_table") - resources[:resref] = resource - - override = mock 'override' - resource.expects(:merge).with(override) - override.expects(:override=).with(true) - override.expects(:ref).returns(:resref) - assert_nothing_raised("Could not call store_override when the resource already exists.") do - compile.store_override(override) - end - end - - def test_resource_overrides - compile = mkcompile - overrides = compile.instance_variable_get("@resource_overrides") - overrides[:test] = :yay - resource = mock 'resource' - resource.expects(:ref).returns(:test) - - assert_equal(:yay, compile.resource_overrides(resource), "Did not return overrides from table") - end - - def test_fail_on_unevaluated_resource_collections - compile = mkcompile - collections = compile.instance_variable_get("@collections") - - # Make sure we're fine when the list is empty - assert_nothing_raised("Failed when no collections were present") do - compile.send :fail_on_unevaluated_resource_collections - end - - # And that we're fine when we've got collections but with no resources - collections << mock('coll') - collections[0].expects(:resources).returns(nil) - assert_nothing_raised("Failed when no resource collections were present") do - compile.send :fail_on_unevaluated_resource_collections - end - - # But that we do fail when we've got resource collections left. - collections.clear - - # return both an array and a string, because that's tested internally - collections << mock('coll returns one') - collections[0].expects(:resources).returns(:something) - - collections << mock('coll returns many') - collections[1].expects(:resources).returns([:one, :two]) - - assert_raise(Puppet::ParseError, "Did not fail on unevaluated resource collections") do - compile.send :fail_on_unevaluated_resource_collections - end - end - - def test_fail_on_unevaluated_overrides - compile = mkcompile - overrides = compile.instance_variable_get("@resource_overrides") - - # Make sure we're fine when the list is empty - assert_nothing_raised("Failed when no collections were present") do - compile.send :fail_on_unevaluated_overrides - end - - # But that we fail if there are any overrides left in the table. - overrides[:yay] = [] - overrides[:foo] = [] - overrides[:bar] = [mock("override")] - overrides[:bar][0].expects(:ref).returns("yay") - assert_raise(Puppet::ParseError, "Failed to fail when overrides remain") do - compile.send :fail_on_unevaluated_overrides - end - end - - def test_find_resource - compile = mkcompile - resources = compile.instance_variable_get("@resource_table") - - assert_nothing_raised("Could not call findresource when the resource table was empty") do - assert_nil(compile.findresource("yay", "foo"), "Returned a non-existent resource") - assert_nil(compile.findresource("yay[foo]"), "Returned a non-existent resource") - end - - resources["Foo[bar]"] = :yay - assert_nothing_raised("Could not call findresource when the resource table was not empty") do - assert_equal(:yay, compile.findresource("foo", "bar"), "Returned a non-existent resource") - assert_equal(:yay, compile.findresource("Foo[bar]"), "Returned a non-existent resource") - end - end - - # #620 - Nodes and classes should conflict, else classes don't get evaluated - def test_nodes_and_classes_name_conflict - # Test node then class - compile = mkcompile - node = stub :nodescope? => true - klass = stub :nodescope? => false - compile.class_set("one", node) - assert_raise(Puppet::ParseError, "Did not fail when replacing node with class") do - compile.class_set("one", klass) - end - - # and class then node - compile = mkcompile - node = stub :nodescope? => true - klass = stub :nodescope? => false - compile.class_set("two", klass) - assert_raise(Puppet::ParseError, "Did not fail when replacing node with class") do - compile.class_set("two", node) - end - end -end diff --git a/test/language/resource.rb b/test/language/resource.rb index f129023eb..dbb1ab9f9 100755 --- a/test/language/resource.rb +++ b/test/language/resource.rb @@ -1,459 +1,456 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../lib/puppettest' require 'puppettest' require 'puppettest/resourcetesting' class TestResource < PuppetTest::TestCase include PuppetTest include PuppetTest::ParserTesting include PuppetTest::ResourceTesting Parser = Puppet::Parser AST = Parser::AST Resource = Puppet::Parser::Resource Reference = Puppet::Parser::Resource::Reference def setup super Puppet[:trace] = false end def teardown mocha_verify end def test_initialize args = {:type => "resource", :title => "testing", :scope => mkscope} # Check our arg requirements args.each do |name, value| try = args.dup try.delete(name) assert_raise(ArgumentError, "Did not fail when %s was missing" % name) do Parser::Resource.new(try) end end res = nil assert_nothing_raised do res = Parser::Resource.new(args) end ref = res.instance_variable_get("@ref") assert_equal("Resource", ref.type, "did not set resource type") assert_equal("testing", ref.title, "did not set resource title") end def test_merge res = mkresource other = mkresource # First try the case where the resource is not allowed to override res.source = "source1" other.source = "source2" other.source.expects(:child_of?).with("source1").returns(false) assert_raise(Puppet::ParseError, "Allowed unrelated resources to override") do res.merge(other) end # Next try it when the sources are equal. res.source = "source3" other.source = res.source other.source.expects(:child_of?).with("source3").never params = {:a => :b, :c => :d} other.expects(:params).returns(params) res.expects(:override_parameter).with(:b) res.expects(:override_parameter).with(:d) res.merge(other) # And then parentage is involved other = mkresource res.source = "source3" other.source = "source4" other.source.expects(:child_of?).with("source3").returns(true) params = {:a => :b, :c => :d} other.expects(:params).returns(params) res.expects(:override_parameter).with(:b) res.expects(:override_parameter).with(:d) res.merge(other) end # the [] method def test_array_accessors res = mkresource params = res.instance_variable_get("@params") assert_nil(res[:missing], "Found a missing parameter somehow") params[:something] = stub(:value => "yay") assert_equal("yay", res[:something], "Did not correctly call value on the parameter") res.expects(:title).returns(:mytitle) assert_equal(:mytitle, res[:title], "Did not call title when asked for it as a param") end # Make sure any defaults stored in the scope get added to our resource. def test_add_defaults res = mkresource params = res.instance_variable_get("@params") params[:a] = :b res.scope.expects(:lookupdefaults).with(res.type).returns(:a => :replaced, :c => :d) res.expects(:debug) res.send(:add_defaults) assert_equal(:d, params[:c], "Did not set default") assert_equal(:b, params[:a], "Replaced parameter with default") end def test_finish res = mkresource res.expects(:add_defaults) res.expects(:add_metaparams) res.expects(:validate) res.finish end # Make sure we paramcheck our params def test_validate res = mkresource params = res.instance_variable_get("@params") params[:one] = :two params[:three] = :four res.expects(:paramcheck).with(:one) res.expects(:paramcheck).with(:three) res.send(:validate) end def test_override_parameter res = mkresource params = res.instance_variable_get("@params") # There are three cases, with the second having two options: # No existing parameter. param = stub(:name => "myparam") res.send(:override_parameter, param) assert_equal(param, params["myparam"], "Override was not added to param list") # An existing parameter that we can override. source = stub(:child_of? => true) # Start out without addition params["param2"] = stub(:source => :whatever) param = stub(:name => "param2", :source => source, :add => false) res.send(:override_parameter, param) assert_equal(param, params["param2"], "Override was not added to param list") # Try with addition. params["param2"] = stub(:value => :a, :source => :whatever) param = stub(:name => "param2", :source => source, :add => true, :value => :b) param.expects(:value=).with([:a, :b]) res.send(:override_parameter, param) assert_equal(param, params["param2"], "Override was not added to param list") # And finally, make sure we throw an exception when the sources aren't related source = stub(:child_of? => false) params["param2"] = stub(:source => :whatever, :file => :f, :line => :l) old = params["param2"] param = stub(:name => "param2", :source => source, :file => :f, :line => :l) assert_raise(Puppet::ParseError, "Did not fail when params conflicted") do res.send(:override_parameter, param) end assert_equal(old, params["param2"], "Param was replaced irrespective of conflict") end def test_set_parameter res = mkresource params = res.instance_variable_get("@params") # First test the simple case: It's already a parameter param = mock('param') param.expects(:is_a?).with(Resource::Param).returns(true) param.expects(:name).returns("pname") res.send(:set_parameter, param) assert_equal(param, params["pname"], "Parameter was not added to hash") # Now the case where there's no value but it's not a param param = mock('param') param.expects(:is_a?).with(Resource::Param).returns(false) assert_raise(ArgumentError, "Did not fail when a non-param was passed") do res.send(:set_parameter, param) end # and the case where a value is passed in param = stub :name => "pname", :value => "whatever" Resource::Param.expects(:new).with(:name => "pname", :value => "myvalue", :source => res.source).returns(param) res.send(:set_parameter, "pname", "myvalue") assert_equal(param, params["pname"], "Did not put param in hash") end def test_paramcheck # There are three cases here: # It's a valid parameter res = mkresource ref = mock('ref') res.instance_variable_set("@ref", ref) klass = mock("class") ref.expects(:typeclass).returns(klass).times(4) klass.expects(:validattr?).with("good").returns(true) assert(res.send(:paramcheck, :good), "Did not allow valid param") # It's name or title klass.expects(:validattr?).with("name").returns(false) assert(res.send(:paramcheck, :name), "Did not allow name") klass.expects(:validattr?).with("title").returns(false) assert(res.send(:paramcheck, :title), "Did not allow title") # It's not actually allowed klass.expects(:validattr?).with("other").returns(false) res.expects(:fail) ref.expects(:type) res.send(:paramcheck, :other) end def test_to_transobject # First try translating a builtin resource. Make sure we use some references # and arrays, to make sure they translate correctly. source = mock("source") scope = mkscope scope.stubs(:tags).returns([]) refs = [] 4.times { |i| refs << Puppet::Parser::Resource::Reference.new(:title => "file%s" % i, :type => "file") } res = Parser::Resource.new :type => "file", :title => "/tmp", :source => source, :scope => scope, :params => paramify(source, :owner => "nobody", :group => %w{you me}, :require => refs[0], :ignore => %w{svn}, :subscribe => [refs[1], refs[2]], :notify => [refs[3]]) obj = nil assert_nothing_raised do obj = res.to_trans end assert_instance_of(Puppet::TransObject, obj) assert_equal(obj.type, res.type.downcase) assert_equal(obj.name, res.title) # TransObjects use strings, resources use symbols assert_equal("nobody", obj["owner"], "Single-value string was not passed correctly") assert_equal(%w{you me}, obj["group"], "Array of strings was not passed correctly") assert_equal("svn", obj["ignore"], "Array with single string was not turned into single value") assert_equal(["file", refs[0].title], obj["require"], "Resource reference was not passed correctly") assert_equal([["file", refs[1].title], ["file", refs[2].title]], obj["subscribe"], "Array of resource references was not passed correctly") assert_equal(["file", refs[3].title], obj["notify"], "Array with single resource reference was not turned into single value") end # FIXME This isn't a great test, but I need to move on. def test_to_transbucket bucket = mock("transbucket") source = mock("source") scope = mkscope res = Parser::Resource.new :type => "mydefine", :title => "yay", :source => source, :scope => scope result = res.to_trans assert_equal("yay", result.name, "did not set bucket name correctly") assert_equal("Mydefine", result.type, "did not set bucket type correctly") end def test_evaluate # First try the most common case, we're not a builtin type. res = mkresource ref = res.instance_variable_get("@ref") type = mock("type") ref.expects(:definedtype).returns(type) res.expects(:finish) res.scope = mock("scope") - config = mock("config") - res.scope.expects(:compile).returns(config) - config.expects(:delete_resource).with(res) type.expects(:evaluate_code).with(res) res.evaluate end def test_proxymethods res = Parser::Resource.new :type => "evaltest", :title => "yay", :source => mock("source"), :scope => mkscope assert_equal("Evaltest", res.type) assert_equal("yay", res.title) assert_equal(false, res.builtin?) end def test_reference_conversion # First try it as a normal string ref = Parser::Resource::Reference.new(:type => "file", :title => "/tmp/ref1") # Now create an obj that uses it res = mkresource :type => "file", :title => "/tmp/resource", :params => {:require => ref} res.scope = mkscope trans = nil assert_nothing_raised do trans = res.to_trans end assert_instance_of(Array, trans["require"]) assert_equal(["file", "/tmp/ref1"], trans["require"]) # Now try it when using an array of references. two = Parser::Resource::Reference.new(:type => "file", :title => "/tmp/ref2") res = mkresource :type => "file", :title => "/tmp/resource2", :params => {:require => [ref, two]} res.scope = mkscope trans = nil assert_nothing_raised do trans = res.to_trans end assert_instance_of(Array, trans["require"][0]) trans["require"].each do |val| assert_instance_of(Array, val) assert_equal("file", val[0]) assert(val[1] =~ /\/tmp\/ref[0-9]/, "Was %s instead of the file name" % val[1]) end end # This is a bit of a weird one -- the user should not actually know # that components exist, so we want references to act like they're not # builtin def test_components_are_not_builtin ref = Parser::Resource::Reference.new(:type => "component", :title => "yay") assert_nil(ref.builtintype, "Definition was considered builtin") end # The second part of #539 - make sure resources pass the arguments # correctly. def test_title_with_definitions parser = mkparser define = parser.newdefine "yayness", :code => resourcedef("file", "/tmp", "owner" => varref("name"), "mode" => varref("title")) klass = parser.findclass("", "") should = {:name => :owner, :title => :mode} [ {:name => "one", :title => "two"}, {:title => "three"}, ].each do |hash| config = mkcompile parser args = {:type => "yayness", :title => hash[:title], :source => klass, :scope => config.topscope} if hash[:name] args[:params] = {:name => hash[:name]} else args[:params] = {} # override the defaults end res = nil assert_nothing_raised("Could not create res with %s" % hash.inspect) do res = mkresource(args) end assert_nothing_raised("Could not eval res with %s" % hash.inspect) do res.evaluate end made = config.topscope.findresource("File[/tmp]") assert(made, "Did not create resource with %s" % hash.inspect) should.each do |orig, param| assert_equal(hash[orig] || hash[:title], made[param], "%s was not set correctly with %s" % [param, hash.inspect]) end end end # part of #629 -- the undef keyword. Make sure 'undef' params get skipped. def test_undef_and_to_hash res = mkresource :type => "file", :title => "/tmp/testing", :source => mock("source"), :scope => mkscope, :params => {:owner => :undef, :mode => "755"} hash = nil assert_nothing_raised("Could not convert resource with undef to hash") do hash = res.to_hash end assert_nil(hash[:owner], "got a value for an undef parameter") end # #643 - Make sure virtual defines result in virtual resources def test_virtual_defines parser = mkparser define = parser.newdefine("yayness", :code => resourcedef("file", varref("name"), "mode" => "644")) config = mkcompile(parser) res = mkresource :type => "yayness", :title => "foo", :params => {}, :scope => config.topscope res.virtual = true result = nil assert_nothing_raised("Could not evaluate defined resource") do result = res.evaluate end scope = res.scope newres = scope.findresource("File[foo]") assert(newres, "Could not find resource") assert(newres.virtual?, "Virtual defined resource generated non-virtual resources") # Now try it with exported resources res = mkresource :type => "yayness", :title => "bar", :params => {}, :scope => config.topscope res.exported = true result = nil assert_nothing_raised("Could not evaluate exported resource") do result = res.evaluate end scope = res.scope newres = scope.findresource("File[bar]") assert(newres, "Could not find resource") assert(newres.exported?, "Exported defined resource generated non-exported resources") assert(newres.virtual?, "Exported defined resource generated non-virtual resources") end # Make sure tags behave appropriately. def test_tags scope_resource = stub 'scope_resource', :tags => %w{srone srtwo} scope = stub 'scope', :resource => scope_resource resource = Puppet::Parser::Resource.new(:type => "file", :title => "yay", :scope => scope, :source => mock('source')) # Make sure we get the type and title %w{yay file}.each do |tag| assert(resource.tags.include?(tag), "Did not tag resource with %s" % tag) end # make sure we can only set legal tags ["an invalid tag", "-anotherinvalid", "bad*tag"].each do |tag| assert_raise(Puppet::ParseError, "Tag #{tag} was considered valid") do resource.tag tag end end # make sure good tags make it through. tags = %w{good-tag yaytag GoodTag another_tag a ab A} tags.each do |tag| assert_nothing_raised("Tag #{tag} was considered invalid") do resource.tag tag end end # make sure we get each of them. ptags = resource.tags tags.each do |tag| assert(ptags.include?(tag.downcase), "missing #{tag}") end end end diff --git a/test/language/scope.rb b/test/language/scope.rb index db9d465bf..b35687e66 100755 --- a/test/language/scope.rb +++ b/test/language/scope.rb @@ -1,510 +1,510 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../lib/puppettest' require 'mocha' require 'puppettest' require 'puppettest/parsertesting' require 'puppettest/resourcetesting' # so, what kind of things do we want to test? # we don't need to test function, since we're confident in the # library tests. We do, however, need to test how things are actually # working in the language. # so really, we want to do things like test that our ast is correct # and test whether we've got things in the right scopes class TestScope < Test::Unit::TestCase include PuppetTest::ParserTesting include PuppetTest::ResourceTesting def to_ary(hash) hash.collect { |key,value| [key,value] } end def test_variables config = mkcompile topscope = config.topscope midscope = config.newscope(topscope) botscope = config.newscope(midscope) scopes = {:top => topscope, :mid => midscope, :bot => botscope} # Set a variable in the top and make sure all three can get it topscope.setvar("first", "topval") scopes.each do |name, scope| assert_equal("topval", scope.lookupvar("first", false), "Could not find var in %s" % name) end # Now set a var in the midscope and make sure the mid and bottom can see it but not the top midscope.setvar("second", "midval") assert_equal(:undefined, scopes[:top].lookupvar("second", false), "Found child var in top scope") [:mid, :bot].each do |name| assert_equal("midval", scopes[name].lookupvar("second", false), "Could not find var in %s" % name) end # And set something in the bottom, and make sure we only find it there. botscope.setvar("third", "botval") [:top, :mid].each do |name| assert_equal(:undefined, scopes[name].lookupvar("third", false), "Found child var in top scope") end assert_equal("botval", scopes[:bot].lookupvar("third", false), "Could not find var in bottom scope") end def test_lookupvar parser = mkparser scope = mkscope :parser => parser # first do the plain lookups assert_equal("", scope.lookupvar("var"), "scope did not default to string") assert_equal("", scope.lookupvar("var", true), "scope ignored usestring setting") assert_equal(:undefined, scope.lookupvar("var", false), "scope ignored usestring setting when false") # Now set the var scope.setvar("var", "yep") assert_equal("yep", scope.lookupvar("var"), "did not retrieve value correctly") # Now test the parent lookups subscope = mkscope :parser => parser subscope.parent = scope assert_equal("", subscope.lookupvar("nope"), "scope did not default to string with parent") assert_equal("", subscope.lookupvar("nope", true), "scope ignored usestring setting with parent") assert_equal(:undefined, subscope.lookupvar("nope", false), "scope ignored usestring setting when false with parent") assert_equal("yep", subscope.lookupvar("var"), "did not retrieve value correctly from parent") # Now override the value in the subscope subscope.setvar("var", "sub") assert_equal("sub", subscope.lookupvar("var"), "did not retrieve overridden value correctly") # Make sure we punt when the var is qualified. Specify the usestring value, so we know it propagates. scope.expects(:lookup_qualified_var).with("one::two", false).returns(:punted) assert_equal(:punted, scope.lookupvar("one::two", false), "did not return the value of lookup_qualified_var") end def test_lookup_qualified_var parser = mkparser scope = mkscope :parser => parser scopes = {} classes = ["", "one", "one::two", "one::two::three"].each do |name| klass = parser.newclass(name) Puppet::Parser::Resource.new(:type => "class", :title => name, :scope => scope, :source => mock('source')).evaluate scopes[name] = scope.compile.class_scope(klass) end classes.each do |name| var = [name, "var"].join("::") scopes[name].expects(:lookupvar).with("var", false).returns(name) assert_equal(name, scope.send(:lookup_qualified_var, var, false), "did not get correct value from lookupvar") end end def test_declarative # set to declarative top = mkscope sub = mkscope(:parent => top) assert_nothing_raised { top.setvar("test","value") } assert_raise(Puppet::ParseError) { top.setvar("test","other") } assert_nothing_raised { sub.setvar("test","later") } assert_raise(Puppet::ParseError) { top.setvar("test","yeehaw") } end def test_setdefaults config = mkcompile scope = config.topscope defaults = scope.instance_variable_get("@defaults") # First the case where there are no defaults and we pass a single param param = stub :name => "myparam", :file => "f", :line => "l" scope.setdefaults(:mytype, param) assert_equal({"myparam" => param}, defaults[:mytype], "Did not set default correctly") # Now the case where we pass in multiple parameters param1 = stub :name => "one", :file => "f", :line => "l" param2 = stub :name => "two", :file => "f", :line => "l" scope.setdefaults(:newtype, [param1, param2]) assert_equal({"one" => param1, "two" => param2}, defaults[:newtype], "Did not set multiple defaults correctly") # And the case where there's actually a conflict. Use the first default for this. newparam = stub :name => "myparam", :file => "f", :line => "l" assert_raise(Puppet::ParseError, "Allowed resetting of defaults") do scope.setdefaults(:mytype, param) end assert_equal({"myparam" => param}, defaults[:mytype], "Replaced default even though there was a failure") end def test_lookupdefaults config = mkcompile top = config.topscope # Make a subscope sub = config.newscope(top) topdefs = top.instance_variable_get("@defaults") subdefs = sub.instance_variable_get("@defaults") # First add some defaults to our top scope topdefs[:t1] = {:p1 => :p2, :p3 => :p4} topdefs[:t2] = {:p5 => :p6} # Then the sub scope subdefs[:t1] = {:p1 => :p7, :p8 => :p9} subdefs[:t2] = {:p5 => :p10, :p11 => :p12} # Now make sure we get the correct list back result = nil assert_nothing_raised("Could not get defaults") do result = sub.lookupdefaults(:t1) end assert_equal(:p9, result[:p8], "Did not get child defaults") assert_equal(:p4, result[:p3], "Did not override parent defaults with child default") assert_equal(:p7, result[:p1], "Did not get parent defaults") end def test_parent config = mkcompile top = config.topscope # Make a subscope sub = config.newscope(top) assert_equal(top, sub.parent, "Did not find parent scope correctly") assert_equal(top, sub.parent, "Did not find parent scope on second call") end def test_strinterp # Make and evaluate our classes so the qualified lookups work parser = mkparser klass = parser.newclass("") scope = mkscope(:parser => parser) Puppet::Parser::Resource.new(:type => "class", :title => :main, :scope => scope, :source => mock('source')).evaluate assert_nothing_raised { scope.setvar("test","value") } scopes = {"" => scope} %w{one one::two one::two::three}.each do |name| klass = parser.newclass(name) Puppet::Parser::Resource.new(:type => "class", :title => name, :scope => scope, :source => mock('source')).evaluate scopes[name] = scope.compile.class_scope(klass) scopes[name].setvar("test", "value-%s" % name.sub(/.+::/,'')) end assert_equal("value", scope.lookupvar("::test"), "did not look up qualified value correctly") tests = { "string ${test}" => "string value", "string ${one::two::three::test}" => "string value-three", "string $one::two::three::test" => "string value-three", "string ${one::two::test}" => "string value-two", "string $one::two::test" => "string value-two", "string ${one::test}" => "string value-one", "string $one::test" => "string value-one", "string ${::test}" => "string value", "string $::test" => "string value", "string ${test} ${test} ${test}" => "string value value value", "string $test ${test} $test" => "string value value value", "string \\$test" => "string $test", '\\$test string' => "$test string", '$test string' => "value string", 'a testing $' => "a testing $", 'a testing \$' => "a testing $", "an escaped \\\n carriage return" => "an escaped carriage return", '\$' => "$", '\s' => "\s", '\t' => "\t", '\n' => "\n" } tests.each do |input, output| assert_nothing_raised("Failed to scan %s" % input.inspect) do assert_equal(output, scope.strinterp(input), 'did not parserret %s correctly' % input.inspect) end end logs = [] Puppet::Util::Log.close Puppet::Util::Log.newdestination(logs) # #523 %w{d f h l w z}.each do |l| string = "\\" + l assert_nothing_raised do assert_equal(string, scope.strinterp(string), 'did not parserret %s correctly' % string) end assert(logs.detect { |m| m.message =~ /Unrecognised escape/ }, "Did not get warning about escape sequence with %s" % string) logs.clear end end def test_tagfunction scope = mkscope resource = mock 'resource' scope.resource = resource resource.expects(:tag).with("yayness", "booness") scope.function_tag(%w{yayness booness}) end def test_includefunction parser = mkparser scope = mkscope :parser => parser myclass = parser.newclass "myclass" otherclass = parser.newclass "otherclass" function = Puppet::Parser::AST::Function.new( :name => "include", :ftype => :statement, :arguments => AST::ASTArray.new( :children => [nameobj("myclass"), nameobj("otherclass")] ) ) assert_nothing_raised do function.evaluate scope end scope.compile.send(:evaluate_generators) [myclass, otherclass].each do |klass| assert(scope.compile.class_scope(klass), "%s was not set" % klass.classname) end end def test_definedfunction parser = mkparser %w{one two}.each do |name| parser.newdefine name end scope = mkscope :parser => parser assert_nothing_raised { %w{one two file user}.each do |type| assert(scope.function_defined([type]), "Class #{type} was not considered defined") end assert(!scope.function_defined(["nopeness"]), "Class 'nopeness' was incorrectly considered defined") } end # Make sure we know what we consider to be truth. def test_truth assert_equal(true, Puppet::Parser::Scope.true?("a string"), "Strings not considered true") assert_equal(true, Puppet::Parser::Scope.true?(true), "True considered true") assert_equal(false, Puppet::Parser::Scope.true?(""), "Empty strings considered true") assert_equal(false, Puppet::Parser::Scope.true?(false), "false considered true") assert_equal(false, Puppet::Parser::Scope.true?(:undef), "undef considered true") end if defined? ActiveRecord # Verify that we recursively mark as exported the results of collectable # components. def test_exportedcomponents config = mkcompile parser = config.parser # Create a default source config.topscope.source = parser.newclass "", "" # And a scope resource - scope_res = stub 'scope_resource', :virtual? => true, :exported? => false, :tags => [] + scope_res = stub 'scope_resource', :virtual? => true, :exported? => false, :tags => [], :builtin? => true, :type => "eh", :title => "bee" config.topscope.resource = scope_res args = AST::ASTArray.new( :file => tempfile(), :line => rand(100), :children => [nameobj("arg")] ) # Create a top-level component parser.newdefine "one", :arguments => [%w{arg}], :code => AST::ASTArray.new( :children => [ resourcedef("file", "/tmp", {"owner" => varref("arg")}) ] ) # And a component that calls it parser.newdefine "two", :arguments => [%w{arg}], :code => AST::ASTArray.new( :children => [ resourcedef("one", "ptest", {"arg" => varref("arg")}) ] ) # And then a third component that calls the second parser.newdefine "three", :arguments => [%w{arg}], :code => AST::ASTArray.new( :children => [ resourcedef("two", "yay", {"arg" => varref("arg")}) ] ) # lastly, create an object that calls our third component obj = resourcedef("three", "boo", {"arg" => "parentfoo"}) # And mark it as exported obj.exported = true # And then evaluate it obj.evaluate config.topscope # And run the loop. config.send(:evaluate_generators) %w{File}.each do |type| objects = config.resources.find_all { |r| r.type == type and r.exported } assert(!objects.empty?, "Did not get an exported %s" % type) end end # Verify that we can both store and collect an object in the same # run, whether it's in the same scope as a collection or a different # scope. def test_storeandcollect Puppet[:storeconfigs] = true Puppet::Rails.init sleep 1 children = [] Puppet[:code] = " class yay { @@host { myhost: ip => \"192.168.0.2\" } } include yay @@host { puppet: ip => \"192.168.0.3\" } Host <<||>>" interp = nil assert_nothing_raised { interp = Puppet::Parser::Interpreter.new } config = nil # We run it twice because we want to make sure there's no conflict # if we pull it up from the database. node = mknode node.parameters = {"hostname" => node.name} 2.times { |i| assert_nothing_raised { config = interp.compile(node) } flat = config.extract.flatten %w{puppet myhost}.each do |name| assert(flat.find{|o| o.name == name }, "Did not find #{name}") end } end else $stderr.puts "No ActiveRecord -- skipping collection tests" end def test_namespaces scope = mkscope assert_equal([""], scope.namespaces, "Started out with incorrect namespaces") assert_nothing_raised { scope.add_namespace("fun::test") } assert_equal(["fun::test"], scope.namespaces, "Did not add namespace correctly") assert_nothing_raised { scope.add_namespace("yay::test") } assert_equal(["fun::test", "yay::test"], scope.namespaces, "Did not add extra namespace correctly") end def test_findclass_and_finddefine parser = mkparser # Make sure our scope calls the parser findclass method with # the right namespaces scope = mkscope :parser => parser parser.metaclass.send(:attr_accessor, :last) methods = [:findclass, :finddefine] methods.each do |m| parser.meta_def(m) do |namespace, name| @checked ||= [] @checked << [namespace, name] # Only return a value on the last call. if @last == namespace ret = @checked.dup @checked.clear return ret else return nil end end end test = proc do |should| parser.last = scope.namespaces[-1] methods.each do |method| result = scope.send(method, "testing") assert_equal(should, result, "did not get correct value from %s with namespaces %s" % [method, scope.namespaces.inspect]) end end # Start with the empty namespace assert_nothing_raised { test.call([["", "testing"]]) } # Now add a namespace scope.add_namespace("a") assert_nothing_raised { test.call([["a", "testing"]]) } # And another scope.add_namespace("b") assert_nothing_raised { test.call([["a", "testing"], ["b", "testing"]]) } end # #629 - undef should be "" or :undef def test_lookupvar_with_undef scope = mkscope scope.setvar("testing", :undef) assert_equal(:undef, scope.lookupvar("testing", false), "undef was not returned as :undef when not string") assert_equal("", scope.lookupvar("testing", true), "undef was not returned as '' when string") end end