diff --git a/README_DEVELOPER.md b/README_DEVELOPER.md index 7aa070b22..f54e72ae1 100644 --- a/README_DEVELOPER.md +++ b/README_DEVELOPER.md @@ -1,768 +1,55 @@ # Developer README # This file is intended to provide a place for developers and contributors to document what other developers need to know about changes made to Puppet. ### Dependencies -# Internal Structures -## Two Types of Catalog - -When working on subsystems of Puppet that deal with the catalog it is important -to be aware of the two different types of Catalog. Developers will often find -this difference while working on the static compiler and types and providers. - -The two different types of catalog becomes relevant when writing spec tests -because we frequently need to wire up a fake catalog so that we can exercise -types, providers, or termini that filter the catalog. - -The two different types of catalogs are so-called "resource" catalogs and "RAL" -(resource abstraction layer) catalogs. At a high level, the resource catalog -is the in-memory object we serialize and transfer around the network. The -compiler terminus is expected to produce a resource catalog. The agent takes a -resource catalog and converts it into a RAL catalog. The RAL catalog is what -is used to apply the configuration model to the system. - -Resource dependency information is most easily obtained from a RAL catalog by -walking the graph instance produced by the `relationship_graph` method. - -### Resource Catalog - -If you're writing spec tests for something that deals with a catalog "server -side," a new catalog terminus for example, then you'll be dealing with a -resource catalog. You can produce a resource catalog suitable for spec tests -using something like this: - - let(:catalog) do - catalog = Puppet::Resource::Catalog.new("node-name-val") # NOT certname! - rsrc = Puppet::Resource.new("file", "sshd_config", - :parameters => { - :ensure => 'file', - :source => 'puppet:///modules/filetest/sshd_config', - } - ) - rsrc.file = 'site.pp' - rsrc.line = 21 - catalog.add_resource(rsrc) - end - -The resources in this catalog may be accessed using `catalog.resources`. -Resource dependencies are not easily walked using a resource catalog however. -To walk the dependency tree convert the catalog to a RAL catalog as described -in - -### RAL Catalog - -The resource catalog may be converted to a RAL catalog using `catalog.to_ral`. -The RAL catalog contains `Puppet::Type` instances instead of `Puppet::Resource` -instances as is the case with the resource catalog. - -One very useful feature of the RAL catalog are the methods to work with -resource relationships. For example: - - irb> catalog = catalog.to_ral - irb> graph = catalog.relationship_graph - irb> pp graph.edges - [{ Notify[alpha] => File[/tmp/file_20.txt] }, - { Notify[alpha] => File[/tmp/file_21.txt] }, - { Notify[alpha] => File[/tmp/file_22.txt] }, - { Notify[alpha] => File[/tmp/file_23.txt] }, - { Notify[alpha] => File[/tmp/file_24.txt] }, - { Notify[alpha] => File[/tmp/file_25.txt] }, - { Notify[alpha] => File[/tmp/file_26.txt] }, - { Notify[alpha] => File[/tmp/file_27.txt] }, - { Notify[alpha] => File[/tmp/file_28.txt] }, - { Notify[alpha] => File[/tmp/file_29.txt] }, - { File[/tmp/file_20.txt] => Notify[omega] }, - { File[/tmp/file_21.txt] => Notify[omega] }, - { File[/tmp/file_22.txt] => Notify[omega] }, - { File[/tmp/file_23.txt] => Notify[omega] }, - { File[/tmp/file_24.txt] => Notify[omega] }, - { File[/tmp/file_25.txt] => Notify[omega] }, - { File[/tmp/file_26.txt] => Notify[omega] }, - { File[/tmp/file_27.txt] => Notify[omega] }, - { File[/tmp/file_28.txt] => Notify[omega] }, - { File[/tmp/file_29.txt] => Notify[omega] }] - -If the `relationship_graph` method is throwing exceptions at you, there's a -good chance the catalog is not a RAL catalog. - -## Settings Catalog ## - -Be aware that Puppet creates a mini catalog and applies this catalog locally to -manage file resource from the settings. This behavior made it difficult and -time consuming to track down a race condition in -[2888](http://projects.puppetlabs.com/issues/2888). - -Even more surprising, the `File[puppetdlockfile]` resource is only added to the -settings catalog if the file exists on disk. This caused the race condition as -it will exist when a separate process holds the lock while applying the -catalog. - -It may be sufficient to simply be aware of the settings catalog and the -potential for race conditions it presents. An effective way to be reasonably -sure and track down the problem is to wrap the File.open method like so: - - # We're wrapping ourselves around the File.open method. - # As described at: http://goo.gl/lDsv6 - class File - WHITELIST = [ /pidlock.rb:39/ ] - - class << self - alias xxx_orig_open open - end - - def self.open(name, *rest, &block) - # Check the whitelist for any "good" File.open calls against the # - puppetdlock file - white_listed = caller(0).find do |line| - JJM_WHITELIST.find { |re| re.match(line) } - end - - # If you drop into IRB here, take a look at your caller, it might be - # the ghost in the machine you're looking for. - binding.pry if name =~ /puppetdlock/ and not white_listed - xxx_orig_open(name, *rest, &block) - end - end - -The settings catalog is populated by the `Puppet::Util::Settings#to\_catalog` -method. - -## Testing dependency version requirements - -Puppet is only compatible with certain versions of RSpec and Mocha. If you are -not using Bundler to install the required test libraries you must ensure that -you are using the right library versions. Using unsupported versions of Mocha -and RSpec will probably display many spurious failures. The supported versions -of RSpec and Mocha can be found in the project Gemfile. - -# A brief introduction to testing in Puppet - -Puppet relies heavily on automated testing to ensure that Puppet behaves as -expected and that new features don't interfere with existing behavior. There are -three primary sets of tests that Puppet uses: _unit tests_, _integration tests_, -and _acceptance tests_. - -- - - - -Unit tests are used to test the individual components of Puppet to ensure that -they function as expected in isolation. Unit tests are designed to hide the -actual system implementations and provide canned information so that only the -intended behavior is tested, rather than the targeted code and everything else -connected to it. Unit tests should never affect the state of the system that's -running the test. - -- - - - -Integration tests serve to test different units of code together to ensure that -they interact correctly. While individual methods might perform correctly, when -used with the rest of the system they might fail, so integration tests are a -higher level version of unit tests that serve to check the behavior of -individual subsystems. - -All of the unit and integration tests for Puppet are kept in the spec/ directory. - -- - - - -Acceptance tests are used to test high level behaviors of Puppet that deal with -a number of concerns and aren't easily tested with normal unit tests. Acceptance -tests function by changing system state and checking the system after -the fact to make sure that the intended behavior occurred. Because of this -acceptance tests can be destructive, so the systems being tested should be -throwaway systems. - -All of the acceptance tests for Puppet are kept in the acceptance/tests/ -directory. - -## Puppet Continuous integration - - * Travis-ci (unit tests only): https://travis-ci.org/puppetlabs/puppet/ - * Jenkins (unit and acceptance tests): https://jenkins.puppetlabs.com/view/Puppet%20FOSS/ - -## RSpec - -Puppet uses RSpec to perform unit and integration tests. RSpec handles a number -of concerns to make testing easier: - - * Executing examples and ensuring the actual behavior matches the expected behavior (examples) - * Grouping tests (describe and contexts) - * Setting up test environments and cleaning up afterwards (before and after blocks) - * Isolating tests (mocks and stubs) - -#### Examples and expectations - -At the most basic level, RSpec provides a framework for executing tests (which -are called examples) and ensuring that the actual behavior matches the expected -behavior (which are done with expectations) - -```ruby -# This is an example; it sets the test name and defines the test to run -specify "one equals one" do - # 'should' is an expectation; it adds a check to make sure that the left argument - # matches the right argument - 1.should == 1 -end - -# Examples can be declared with either 'it' or 'specify' -it "one doesn't equal two" do - 1.should_not == 2 -end -``` - -Good examples generally do as little setup as possible and only test one or two -things; it makes tests easier to understand and easier to debug. - -More complete documentation on expectations is available at https://www.relishapp.com/rspec/rspec-expectations/docs - -### Example groups - -Example groups are fairly self explanatory; they group similar examples into a -set. - -```ruby -describe "the number one" do - - it "is larger than zero" do - 1.should be > 0 - end - - it "is an odd number" do - 1.odd?.should be true - end - - it "is not nil" do - 1.should_not be_nil - end -end -``` - -Example groups have a number of uses that we'll get into later, but one of the -simplest demonstrations of what they do is how they help to format -documentation: - -``` -rspec ex.rb --format documentation - -the number one - is larger than zero - is an odd number - is not nil - -Finished in 0.00516 seconds -3 examples, 0 failures -``` - -### Setting up and tearing down tests - -Examples may require some setup before they can run, and might need to clean up -afterwards. `before` and `after` blocks can be used before this, and can be -used inside of example groups to limit how many examples they affect. - -```ruby - -describe "something that could warn" do - before :each do - # Disable warnings for this test - $VERBOSE = nil - end - - after do - # Enable warnings afterwards - $VERBOSE = true - end - - it "doesn't generate a warning" do - MY_CONSTANT = 1 - # reassigning a normally prints out 'warning: already initialized constant FOO' - MY_CONSTANT = 2 - end -end -``` - -### Setting up helper data - -Some examples may require setting up data before hand and making it available to -tests. RSpec provides helper methods with the `let` method call that can be used -inside of tests. - -```ruby -describe "a helper object" do - # This creates an array with three elements that we can retrieve in tests. A - # new copy will be made for each test. - let(:my_helper) do - ['foo', 'bar', 'baz'] - end - - it "should be an array" do - my_helper.should be_a_kind_of Array - end - - it "should have three elements" do - my_helper.should have(3).items - end -end -``` - -Like `before` blocks, helper objects like this are used to avoid doing a lot of -setup in individual examples and share setup between similar tests. - -### Isolating tests with stubs - -RSpec allows you to provide fake data during testing to make sure that -individual tests are only running the code being tested. You can stub out entire -objects, or just stub out individual methods on an object. When a method is -stubbed the method itself will never be called. - -While RSpec comes with its own stubbing framework, Puppet uses the Mocha -framework. - -A brief usage guide for Mocha is available at http://gofreerange.com/mocha/docs/#Usage, -and an overview of Mocha expectations is available at http://gofreerange.com/mocha/docs/Mocha/Expectation.html - -```ruby -describe "stubbing a method on an object" do - let(:my_helper) do - ['foo', 'bar', 'baz'] - end - - it 'should have three items before being stubbed' do - my_helper.size.should == 3 - end - - describe 'when stubbing the size' do - before do - my_helper.stubs(:size).returns 10 - end - - it 'should have the stubbed value for size' do - my_helper.size.should == 10 - end - end -end -``` - -Entire objects can be stubbed as well. - -```ruby -describe "stubbing an object" do - let(:my_helper) do - stub(:not_an_array, :size => 10) - end - - it 'should have the stubbed size' - my_helper.size.should == 10 - end -end -``` - -### Adding expectations with mocks - -It's possible to combine the concepts of stubbing and expectations so that a -method has to be called for the test to pass (like an expectation), and can -return a fixed value (like a stub). - -```ruby -describe "mocking a method on an object" do - let(:my_helper) do - ['foo', 'bar', 'baz'] - end - - describe "when mocking the size" do - before do - my_helper.expects(:size).returns 10 - end - - it "adds an expectation that a method was called" do - my_helper.size - end - end -end -``` - -Like stubs, entire objects can be mocked. - -```ruby -describe "mocking an object" do - let(:my_helper) do - mock(:not_an_array) - end - - before do - not_an_array.expects(:size).returns 10 - end - - it "adds an expectation that the method was called" do - not_an_array.size - end -end -``` -### Writing tests without side effects - -When properly written each test should be able to run in isolation, and tests -should be able to be run in any order. This makes tests more reliable and allows -a single test to be run if only that test is failing, instead of running all -17000+ tests each time something is changed. However, there are a number of ways -that can make tests fail when run in isolation or out of order. - -#### Using instance variables - -Puppet has a number of older tests that use `before` blocks and instance -variables to set up fixture data, instead of `let` blocks. These can retain -state between tests, which can lead to test failures when tests are run out of -order. - -```ruby -# test.rb -RSpec.configure do |c| - c.mock_framework = :mocha -end - -describe "fixture data" do - describe "using instance variables" do - - # BAD - before :all do - # This fixture will be created only once and will retain the `foo` stub - # between tests. - @fixture = stub 'test data' - end - - it "can be stubbed" do - @fixture.stubs(:foo).returns :bar - @fixture.foo.should == :bar - end - - it "should not keep state between tests" do - # The foo stub was added in the previous test and shouldn't be present - # in this test. - expect { @fixture.foo }.to raise_error - end - end - - describe "using `let` blocks" do - - # GOOD - # This will be recreated between tests so that state isn't retained. - let(:fixture) { stub 'test data' } - - it "can be stubbed" do - fixture.stubs(:foo).returns :bar - fixture.foo.should == :bar - end - - it "should not keep state between tests" do - # since let blocks are regenerated between tests, the foo stub added in - # the previous test will not be present here. - expect { fixture.foo }.to raise_error - end - end -end -``` - -``` -bundle exec rspec test.rb -fd - -fixture data - using instance variables - can be stubbed - should not keep state between tests (FAILED - 1) - using `let` blocks - can be stubbed - should not keep state between tests - -Failures: - - 1) fixture data using instance variables should not keep state between tests - Failure/Error: expect { @fixture.foo }.to raise_error - expected Exception but nothing was raised - # ./test.rb:17:in `block (3 levels) in ' - -Finished in 0.00248 seconds -4 examples, 1 failure - -Failed examples: - -rspec ./test.rb:16 # fixture data using instance variables should not keep state between tests -``` - - -### RSpec references - - * RSpec core docs: https://www.relishapp.com/rspec/rspec-core/docs - * RSpec guidelines with Ruby: http://betterspecs.org/ - -### Puppet-acceptance - -[beaker]: https://github.com/puppetlabs/beaker -[test::unit]: http://test-unit.rubyforge.org/ - -Puppet has a custom acceptance testing framework called [beaker][beaker] for -running acceptance tests. -Beaker runs the tests by configuring one or more VMs, copying the test cases -onto the VMs, performing the tests and collecting the results, and ensuring that -the results match the intended behavior. It uses [test::unit][test::unit] to -perform the actual assertions. - -For a detailed guide to running the acceptance tests locally on vagrant boxes, -see the `acceptance/README.md` document in this repo. - -# UTF-8 Handling # - -As Ruby 1.9 becomes more commonly used with Puppet, developers should be aware -of major changes to the way Strings and Regexp objects are handled. -Specifically, every instance of these two classes will have an encoding -attribute determined in a number of ways. - - * If the source file has an encoding specified in the magic comment at the - top, the instance will take on that encoding. - * Otherwise, the encoding will be determined by the LC\_LANG or LANG - environment variables. - * Otherwise, the encoding will default to ASCII-8BIT - -## References ## - -Excellent information about the differences between encodings in Ruby 1.8 and -Ruby 1.9 is published in this blog series: -[Understanding M17n](http://links.puppetlabs.com/understanding_m17n) - -## Encodings of Regexp and String instances ## - -In general, please be aware that Ruby 1.9 regular expressions need to be -compatible with the encoding of a string being used to match them. If they are -not compatible you can expect to receive and error such as: - - Encoding::CompatibilityError: incompatible encoding regexp match (ASCII-8BIT - regexp with UTF-8 string) - -In addition, some escape sequences were valid in Ruby 1.8 are no longer valid -in 1.9 if the regular expression is not marked as an ASCII-8BIT object. You -may expect errors like this in this situation: - - SyntaxError: (irb):7: invalid multibyte escape: /\xFF/ - -This error is particularly common when serializing a string to other -representations like JSON or YAML. To resolve the problem you can explicitly -mark the regular expression as ASCII-8BIT using the /n flag: - - "a" =~ /\342\230\203/n - -Finally, any time you're thinking of a string as an array of bytes rather than -an array of characters, common when escaping a string, you should work with -everything in ASCII-8BIT. Changing the encoding will not change the data -itself and allow the Regexp and the String to deal with bytes rather than -characters. - -Puppet provides a monkey patch to String which returns an encoding suitable for -byte manipulations: - - # Example of how to escape non ASCII printable characters for YAML. - >> snowman = "☃" - >> snowman.to_ascii8bit.gsub(/([\x80-\xFF])/n) { |x| "\\x#{x.unpack("C")[0].to_s(16)} } - => "\\xe2\\x98\\x83" - -If the Regexp is not marked as ASCII-8BIT using /n, then you can expect the -SyntaxError, invalid multibyte escape as mentioned above. - -# Windows # - -If you'd like to run Puppet from source on Windows platforms, the -include `ext/envpuppet.bat` will help. - -To quickly run Puppet from source, assuming you already have Ruby installed -from [rubyinstaller.org](http://rubyinstaller.org). - - C:\> cd C:\work\puppet - C:\work\puppet> set PATH=%PATH%;C:\work\puppet\ext - C:\work\puppet> envpuppet bundle install - C:\work\puppet> envpuppet puppet --version - 2.7.9 - -When writing a test that cannot possibly run on Windows, e.g. there is -no mount type on windows, do the following: - - describe Puppet::MyClass, :unless => Puppet.features.microsoft_windows? do - .. - end - -If the test doesn't currently pass on Windows, e.g. due to on going porting, then use an rspec conditional pending block: - - pending("porting to Windows", :if => Puppet.features.microsoft_windows?) do - - end - - pending("porting to Windows", :if => Puppet.features.microsoft_windows?) do - - end - -Then run the test as: - - C:\work\puppet> envpuppet bundle exec rspec spec - -## Common Issues ## - - * Don't assume file paths start with '/', as that is not a valid path on - Windows. Use Puppet::Util.absolute\_path? to validate that a path is fully - qualified. - - * Use File.expand\_path('/tmp') in tests to generate a fully qualified path - that is valid on POSIX and Windows. In the latter case, the current working - directory will be used to expand the path. - - * Always use binary mode when performing file I/O, unless you explicitly want - Ruby to translate between unix and dos line endings. For example, opening an - executable file in text mode will almost certainly corrupt the resulting - stream, as will occur when using: - - IO.open(path, 'r') { |f| ... } - IO.read(path) - - If in doubt, specify binary mode explicitly: - - IO.open(path, 'rb') - - * Don't assume file paths are separated by ':'. Use `File::PATH_SEPARATOR` - instead, which is ':' on POSIX and ';' on Windows. - - * On Windows, `File::SEPARATOR` is '/', and `File::ALT_SEPARATOR` is '\'. On - POSIX systems, `File::ALT_SEPARATOR` is nil. In general, use '/' as the - separator as most Windows APIs, e.g. CreateFile, accept both types of - separators. - - * Don't use waitpid/waitpid2 if you need the child process' exit code, - as the child process may exit before it has a chance to open the - child's HANDLE and retrieve its exit code. Use Puppet::Util.execute. - - * Don't assume 'C' drive. Use environment variables to look these up: - - "#{ENV['windir']}/system32/netsh.exe" # Configuration Directory # In Puppet 3.x we've simplified the behavior of selecting a configuration file to load. The intended behavior of reading `puppet.conf` is: 1. Use the explicit configuration provided by --confdir or --config if present 2. If running as root (`Puppet.features.root?`) then use the system `puppet.conf` 3. Otherwise, use `~/.puppet/puppet.conf`. When Puppet master is started from Rack, Puppet 3.x will read from ~/.puppet/puppet.conf by default. This is intended behavior. Rack configurations should start Puppet master with an explicit configuration directory using `ARGV << "--confdir" << "/etc/puppet"`. Please see the `ext/rack/config.ru` file for an up-to-date example. # Determining the Puppet Version If you need to programmatically work with the Puppet version, please use the following: require 'puppet/version' # Get the version baked into the sourcecode: version = Puppet.version # Set the version (e.g. in a Rakefile based on `git describe`) Puppet.version = '2.3.4' Please do not monkey patch the constant `Puppet::PUPPETVERSION` or obtain the version using the constant. The only supported way to set and get the Puppet version is through the accessor methods. -# Static Compiler - -The static compiler was added to Puppet in the 2.7.0 release. -[1](http://links.puppetlabs.com/static-compiler-announce) - -The static compiler is intended to provide a configuration catalog that -requires a minimal amount of network communication in order to apply the -catalog to the system. As implemented in Puppet 2.7.x and Puppet 3.0.x this -intention takes the form of replacing all of the source parameters of File -resources with a content parameter containing an address in the form of a -checksum. The expected behavior is that the process applying the catalog to -the node will retrieve the file content from the FileBucket instead of the -FileServer. - -The high level approach can be described as follows. The `StaticCompiler` is a -terminus that inserts itself between the "normal" compiler terminus and the -request. The static compiler takes the resource catalog produced by the -compiler and filters all File resources. Any file resource that contains a -source parameter with a value starting with 'puppet://' is filtered in the -following way in a "standard" single master / networked agents deployment -scenario: - - 1. The content, owner, group, and mode values are retrieved from th - FileServer by the master. - 2. The file content is stored in the file bucket on the master. - 3. The source parameter value is stripped from the File resource. - 4. The content parameter value is set in the File resource using the form - '{XXX}1234567890' which can be thought of as a content address indexed by - checksum. - 5. The owner, group and mode values are set in the File resource if they are - not already set. - 6. The filtered catalog is returned in the response. - -In addition to the catalog terminus, the process requesting the catalog needs -to obtain the file content. The default behavior of `puppet agent` is to -obtain file contents from the local client bucket. The method we expect users -to employ to reconfigure the agent to use the server bucket is to declare the -`Filebucket[puppet]` resource with the address of the master. For example: - - node default { - filebucket { puppet: - server => $server, - path => false, - } - class { filetest: } - } - -This special filebucket resource named "puppet" will cause the agent to fetch -file contents specified by checksum from the remote filebucket instead of the -default clientbucket. - -## Trying out the Static Compiler - -Create a module that recursively downloads something. The jeffmccune-filetest -module will recursively copy the rubygems source tree. - - $ bundle exec puppet module install jeffmccune-filetest - -Start the master with the StaticCompiler turned on: - - $ bundle exec puppet master \ - --catalog_terminus=static_compiler \ - --verbose \ - --no-daemonize - -Add the special Filebucket[puppet] resource: - - # site.pp - node default { - filebucket { puppet: server => $server, path => false } - class { filetest: } - } - -Get the static catalog: - - $ bundle exec puppet agent --test - -You should expect all file metadata to be contained in the catalog, including a -checksum representing the content. When managing an out of sync file resource, -the real contents should be fetched from the server instead of the -clientbucket. - Package Maintainers ===== Software Version API ----- Please see the public API regarding the software version as described in `lib/puppet/version.rb`. Puppet provides the means to easily specify the exact version of the software packaged using the VERSION file, for example: $ git describe --match "3.0.*" > lib/puppet/VERSION $ ruby -r puppet/version -e 'puts Puppet.version' 3.0.1-260-g9ca4e54 EOF diff --git a/docs/acceptance_tests.md b/docs/acceptance_tests.md index aa30ddb35..dcb2d7bab 100644 --- a/docs/acceptance_tests.md +++ b/docs/acceptance_tests.md @@ -1,218 +1,218 @@ Running Acceptance Tests Yourself ================================= Table of Contents ----------------- * [General Notes](#general-notes) * [Running Tests on the vcloud](#running-tests-on-the-vcloud) * [Running Tests on Vagrant Boxen](#running-tests-on-vagrant-boxen) General Notes ------------- -The rake tasks for running the tests are defined by the Rakefile in the same directory as this file. +The rake tasks for running the tests are defined by the Rakefile in the acceptance test directory. These tasks come with some documentation: `rake -T` will give short descriptions, and a `rake -D` will give full descriptions with information on ENV options required and optional for the various tasks. If you are setting up a new repository for acceptance, you will need to bundle install first. This step assumes you have ruby and the bundler gem installed. ```sh cd /path/to/repo/acceptance bundle install --path=.bundle/gems ``` Running Tests on the vcloud --------------------------- In order to use the Puppet Labs vcloud, you'll need to be a Puppet Labs employee. Community members should see the [guide to running the tests on vagrant boxen](#running-tests-on-local-vagrant-boxen). ### Authentication Normally the ci tasks are called from a prepared Jenkins job. If you are running this on your laptop, you will need this ssh private key in order for beaker to be able to log into the vms created from the hosts file: https://github.com/puppetlabs/puppetlabs-modules/blob/qa/secure/jenkins/id_rsa-acceptance https://github.com/puppetlabs/puppetlabs-modules/blob/qa/secure/jenkins/id_rsa-acceptance.pub TODO fetch these files directly from github, but am running into rate limits and then would also have to cross the issue of authentication. You will also need QA credentials to vsphere in a ~/.fog file. These credentials can be found on any of the Jenkins coordinator hosts. ### Packages In order to run the tests on hosts provisioned from packages produced by Delivery, you will need to reference a Puppet commit sha that has been packaged using Delivery's pl:jenkins:uber_build task. This is the snippet used by 'Puppet Packaging' Jenkins jobs: ```sh rake --trace package:implode rake --trace package:bootstrap rake --trace pl:jenkins:uber_build ``` The above Rake tasks were run from the root of a Puppet checkout. They are quoted just for reference. Typically if you are investigating a failure, you will have a SHA from a failed jenkins run which should correspond to a successful pipeline run, and you should not need to run the pipeline manually. A finished pipeline will have repository information available at http://builds.puppetlabs.lan/puppet/ So you can also browse this list and select a recent sha which has repo_configs/ available. When executing the ci:test:packages task, you must set the SHA, and also set CONFIG to point to a valid Beaker hosts_file. Configurations used in the Jenkins jobs are available under config/nodes ```sh bundle exec rake ci:test:packages SHA=abcdef CONFIG=config/nodes/rhel.yaml ``` Optionally you may set the TEST (TEST=a/test.rb,and/another/test.rb), and may pass additional OPTIONS to beaker (OPTIONS='--opt foo'). You may also edit a ./local_options.rb hash which will override config/ options, and in turn be overriden by commandline options set in the environment variables CONFIG, TEST and OPTIONS. This file is a ruby file containing a Ruby hash with configuration expected by Beaker. See Beaker source, and examples in config/. ### Git Alternatively you may provision via git clone by calling the ci:test:git task. Currently we don't have packages for Windows or Solaris from the Delivery pipeline, and must use ci:test:git to provision and test these platforms. #### Source Checkout for Different Fork If you have a branch pushed to your fork which you wish to test prior to merging into puppetlabs/puppet, you can do so be setting the FORK environment variable. So, if I have a branch 'issue/master/wonder-if-this-explodes' pushed to my jpartlow puppet fork that I want to test on Windows, I could invoke the following: ```sh bundle exec ci:test:git CONFIG=config/nodes/win2008r2.yaml SHA=issue/master/wonder-if-this-explodes FORK=jpartlow ``` #### Source Checkout for Local Branch See notes on running acceptance with Vagrant for more details on using a local git daemon. TODO Fix up the Rakefile's handling of git urls so that there is a simple way to specify both a branch on a github fork, and a branch on some other git server daemon, so that you have fewer steps when serving from a local git daemon. ### Preserving Hosts If you have local changes to puppet code (outside of acceptance/) that you don't want to repackage for time reasons, or you just want to ssh into the hosts after a test run, you can use the following sequence: bundle exec rake ci:test_and_preserve_hosts CONFIG=some/config.yaml SHA=12345 TEST=a/foo_test.rb to get the initial templates provisioned, and a local log/latest/preserve_config.yaml created for them. Then you can log into the hosts, or rerun tests against them by: bundle exec rake ci:test_against_preserved_hosts TEST=a/foo_test.rb This will use the existing hosts, uninstall and reinstall the puppet packages and rsync in any changes from your local source lib dir. To skip reinstalling the packages set SKIP_PACKAGE_REINSTALL=1. To skip rsyncing, set SKIP_RSYNC=1. To use rsync filters, create a file with your rsync filter settings and set RSYNC_FILTER_FILE to the name of that file. For example: include puppet include puppet/defaults.rb exclude * will ensure that only puppet/defaults.rb is copied. NOTE: By default these tasks provision with packages. Set TYPE=git to use source checkouts. ### Cleaning Up Preserved Hosts If you run a number of jobs with --preserve_hosts or vi ci:test_and_preserve_hosts, you may eventually generate a large number of stale vms. They should be reaped automatically by qa infrastructure within a day or so, but you may also run: bundle exec ci:destroy_preserved_hosts to clean them up sooner and free resources. There also may be scenarios where you want to specify the host(s) to destroy. E.g. you may want to destroy a subset of the hosts you've created. Or, if a test run terminates early, ci:destroy_preserved_hosts may not be able to derive the name of the vm to delete. In such cases you can specify host(s) to be deleted using the HOST_NAMES environment variable. E.g. HOST_NAMES=lvwwr9tdplg351u bundle exec rake ci:destroy_preserved_hosts HOST_NAMES=lvwwr9tdplg351u,ylrqjh5l6xvym4t bundle exec rake ci:destroy_preserved_hosts Running Tests on Vagrant Boxen ------------------------------ This guide assumes that you have an acceptable Ruby (i.e. 1.9+) installed along with the bundler gem, that you have the puppet repo checked out locally somewhere, and that the name of the checkout folder is `puppet`. I used Ruby 1.9.3-p484 Change to the `acceptance` directory in the root of the puppet repo: ```sh cd /path/to/repo/puppet/acceptance ``` Install the necessary gems with bundler: ```sh bundle install ``` Now you can get a list of test-related tasks you can run via rake: ```sh bundle exec rake -T ``` and view detailed information on the tasks with ```sh bundle exec rake -D ``` As an example, let's try running the acceptance tests using git as the code deployment mechanism. First, we'll have to create a beaker configuration file for a local vagrant box on which to run the tests. Here's what such a file could look like: ```yaml HOSTS: all-in-one: roles: - master - agent platform: centos-64-x64 hypervisor: vagrant ip: 192.168.80.100 box: centos-64-x64-vbox4210-nocm box_url: http://puppet-vagrant-boxes.puppetlabs.com/centos-64-x64-vbox4210-nocm.box CONFIG: ``` This defines a 64-bit CentOS 6.4 vagrant box that serves as both a puppet master and a puppet agent for the test roles. (For more information on beaker config files, see [beaker's README](https://github.com/puppetlabs/beaker/blob/master/README.md).) Save this file as `config/nodes/centos6-local.yaml`; we'll be needing it later. Since we have only provided a CentOS box, we don't have anywhere to run windows tests, therefore we'll have to skip those tests. That means we want to pass beaker a --tests argument that contains every directory and file in the `tests` directory besides the one called `windows`. We could pass this option on the command line, but it will be gigantic, so instead let's create a `local_options.rb` file that beaker will automatically read in. This file should contain a ruby hash of beaker's command-line flags to the corresponding flag arguments. Our hash will only contain the `tests` key, and its value will be a comma-seperated list of the other files and directories in `tests`. Here's an easy way to generate this file: ```sh echo "{tests: \"$(echo tests/* | sed -e 's| *tests/windows *||' -e 's/ /,/g')\"}" > local_options.rb" ``` The last thing that needs to be done before we can run the tests is to set up a way for the test box to check out our local changes for testing. We'll do this by starting a git daemon on our host. In another session, navigate to the folder that contains your checkout of the puppet repo, and then create the following symlink: ```sh ln -s . puppet.git ``` This works around the inflexible checkout path used by the test prep code. Now start the git daemon with ```sh git daemon --verbose --informative-errors --reuseaddr --export-all --base-path=. ``` after which you should see a message like `[32963] Ready to rumble` echoed to the console. Now we can finally run the tests! The rake task that we'll use is `ci:test:git`. Run ``` bundle exec rake -D ci:test:git ``` to read the full description of this task. From the description, we can see that we'll need to set a few environment variables: + CONFIG should be set to point to the CentOS beaker config file we created above. + SHA should be the SHA of the commit we want to test. + GIT_SERVER should be the IP address of the host (i.e. your machine) in the vagrant private network created for the test box. This is derived from the test box's ip by replacing the last octet with 1. For our example above, the host IP is 192.168.80.1 + FORK should be the path to a 'puppet.git' directory that points to the repo. In our case, this is the path to the symlink we created before, which is inside your puppet repo checkout, so FORK should just be the name of your checkout. We'll assume that the name is `puppet`. Putting it all together, we construct the following command-line invocation to run the tests: ```sh CONFIG=config/nodes/centos6-local.yaml SHA=#{test-commit-sha} GIT_SERVER='192.168.80.1' FORK='puppet' bundle exec rake --trace ci:test:git ``` Go ahead and run that sucker! Testing will take some time. After the testing finishes, you'll either see this line ``` systest completed successfully, thanks. ``` near the end of the output, indicating that all tests completed succesfully, or you'll see the end of a stack trace, indicating failed tests further up. diff --git a/docs/catalogs.md b/docs/catalogs.md new file mode 100644 index 000000000..6702a9b27 --- /dev/null +++ b/docs/catalogs.md @@ -0,0 +1,122 @@ +# Two Types of Catalog + +When working on subsystems of Puppet that deal with the catalog it is important +to be aware of the two different types of Catalog. Developers will often find +this difference while working on the static compiler and types and providers. + +The two different types of catalog becomes relevant when writing spec tests +because we frequently need to wire up a fake catalog so that we can exercise +types, providers, or termini that filter the catalog. + +The two different types of catalogs are so-called "resource" catalogs and "RAL" +(resource abstraction layer) catalogs. At a high level, the resource catalog +is the in-memory object we serialize and transfer around the network. The +compiler terminus is expected to produce a resource catalog. The agent takes a +resource catalog and converts it into a RAL catalog. The RAL catalog is what +is used to apply the configuration model to the system. + +Resource dependency information is most easily obtained from a RAL catalog by +walking the graph instance produced by the `relationship_graph` method. + +### Resource Catalog + +If you're writing spec tests for something that deals with a catalog "server +side," a new catalog terminus for example, then you'll be dealing with a +resource catalog. You can produce a resource catalog suitable for spec tests +using something like this: + + let(:catalog) do + catalog = Puppet::Resource::Catalog.new("node-name-val") # NOT certname! + rsrc = Puppet::Resource.new("file", "sshd_config", + :parameters => { + :ensure => 'file', + :source => 'puppet:///modules/filetest/sshd_config', + } + ) + rsrc.file = 'site.pp' + rsrc.line = 21 + catalog.add_resource(rsrc) + end + +The resources in this catalog may be accessed using `catalog.resources`. +Resource dependencies are not easily walked using a resource catalog however. +To walk the dependency tree convert the catalog to a RAL catalog as described +in + +### RAL Catalog + +The resource catalog may be converted to a RAL catalog using `catalog.to_ral`. +The RAL catalog contains `Puppet::Type` instances instead of `Puppet::Resource` +instances as is the case with the resource catalog. + +One very useful feature of the RAL catalog are the methods to work with +resource relationships. For example: + + irb> catalog = catalog.to_ral + irb> graph = catalog.relationship_graph + irb> pp graph.edges + [{ Notify[alpha] => File[/tmp/file_20.txt] }, + { Notify[alpha] => File[/tmp/file_21.txt] }, + { Notify[alpha] => File[/tmp/file_22.txt] }, + { Notify[alpha] => File[/tmp/file_23.txt] }, + { Notify[alpha] => File[/tmp/file_24.txt] }, + { Notify[alpha] => File[/tmp/file_25.txt] }, + { Notify[alpha] => File[/tmp/file_26.txt] }, + { Notify[alpha] => File[/tmp/file_27.txt] }, + { Notify[alpha] => File[/tmp/file_28.txt] }, + { Notify[alpha] => File[/tmp/file_29.txt] }, + { File[/tmp/file_20.txt] => Notify[omega] }, + { File[/tmp/file_21.txt] => Notify[omega] }, + { File[/tmp/file_22.txt] => Notify[omega] }, + { File[/tmp/file_23.txt] => Notify[omega] }, + { File[/tmp/file_24.txt] => Notify[omega] }, + { File[/tmp/file_25.txt] => Notify[omega] }, + { File[/tmp/file_26.txt] => Notify[omega] }, + { File[/tmp/file_27.txt] => Notify[omega] }, + { File[/tmp/file_28.txt] => Notify[omega] }, + { File[/tmp/file_29.txt] => Notify[omega] }] + +If the `relationship_graph` method is throwing exceptions at you, there's a +good chance the catalog is not a RAL catalog. + +## Settings Catalog ## + +Be aware that Puppet creates a mini catalog and applies this catalog locally to +manage file resource from the settings. This behavior made it difficult and +time consuming to track down a race condition in +[2888](http://projects.puppetlabs.com/issues/2888). + +Even more surprising, the `File[puppetdlockfile]` resource is only added to the +settings catalog if the file exists on disk. This caused the race condition as +it will exist when a separate process holds the lock while applying the +catalog. + +It may be sufficient to simply be aware of the settings catalog and the +potential for race conditions it presents. An effective way to be reasonably +sure and track down the problem is to wrap the File.open method like so: + + # We're wrapping ourselves around the File.open method. + # As described at: http://goo.gl/lDsv6 + class File + WHITELIST = [ /pidlock.rb:39/ ] + + class << self + alias xxx_orig_open open + end + + def self.open(name, *rest, &block) + # Check the whitelist for any "good" File.open calls against the # + puppetdlock file + white_listed = caller(0).find do |line| + JJM_WHITELIST.find { |re| re.match(line) } + end + + # If you drop into IRB here, take a look at your caller, it might be + # the ghost in the machine you're looking for. + binding.pry if name =~ /puppetdlock/ and not white_listed + xxx_orig_open(name, *rest, &block) + end + end + +The settings catalog is populated by the `Puppet::Util::Settings#to\_catalog` +method. diff --git a/docs/index.md b/docs/index.md index b7276af4a..283f4b28f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,17 @@ # Puppet Developer Documentation Setting up and running tests * [Quickstart Guide](quickstart.md) +* [RSpec Tutorial](rspec_tutorial.md) * [Running acceptance tests](acceptance_tests.md) + +Developer References + +* [Various Catalog Forms](catalogs.md) +* [Windows](windows.md) +* [Unicode and you](unicode.md) + +Misc + +* [Static Compiler](static_compiler.md) diff --git a/docs/rspec_tutorial.md b/docs/rspec_tutorial.md new file mode 100644 index 000000000..4545c0096 --- /dev/null +++ b/docs/rspec_tutorial.md @@ -0,0 +1,362 @@ +# A brief introduction to testing in Puppet + +Puppet relies heavily on automated testing to ensure that Puppet behaves as +expected and that new features don't interfere with existing behavior. There are +three primary sets of tests that Puppet uses: _unit tests_, _integration tests_, +and _acceptance tests_. + +- - - + +Unit tests are used to test the individual components of Puppet to ensure that +they function as expected in isolation. Unit tests are designed to hide the +actual system implementations and provide canned information so that only the +intended behavior is tested, rather than the targeted code and everything else +connected to it. Unit tests should never affect the state of the system that's +running the test. + +- - - + +Integration tests serve to test different units of code together to ensure that +they interact correctly. While individual methods might perform correctly, when +used with the rest of the system they might fail, so integration tests are a +higher level version of unit tests that serve to check the behavior of +individual subsystems. + +All of the unit and integration tests for Puppet are kept in the spec/ directory. + +- - - + +Acceptance tests are used to test high level behaviors of Puppet that deal with +a number of concerns and aren't easily tested with normal unit tests. Acceptance +tests function by changing system state and checking the system after +the fact to make sure that the intended behavior occurred. Because of this +acceptance tests can be destructive, so the systems being tested should be +throwaway systems. + +All of the acceptance tests for Puppet are kept in the acceptance/tests/ +directory. + +## Testing dependency version requirements + +Puppet is only compatible with certain versions of RSpec and Mocha. If you are +not using Bundler to install the required test libraries you must ensure that +you are using the right library versions. Using unsupported versions of Mocha +and RSpec will probably display many spurious failures. The supported versions +of RSpec and Mocha can be found in the project Gemfile. + +## Puppet Continuous integration + + * Travis-ci (unit tests only): https://travis-ci.org/puppetlabs/puppet/ + * Jenkins (unit and acceptance tests): https://jenkins.puppetlabs.com/view/Puppet%20FOSS/ + +## RSpec + +Puppet uses RSpec to perform unit and integration tests. RSpec handles a number +of concerns to make testing easier: + + * Executing examples and ensuring the actual behavior matches the expected behavior (examples) + * Grouping tests (describe and contexts) + * Setting up test environments and cleaning up afterwards (before and after blocks) + * Isolating tests (mocks and stubs) + +#### Examples and expectations + +At the most basic level, RSpec provides a framework for executing tests (which +are called examples) and ensuring that the actual behavior matches the expected +behavior (which are done with expectations) + +```ruby +# This is an example; it sets the test name and defines the test to run +specify "one equals one" do + # 'should' is an expectation; it adds a check to make sure that the left argument + # matches the right argument + 1.should == 1 +end + +# Examples can be declared with either 'it' or 'specify' +it "one doesn't equal two" do + 1.should_not == 2 +end +``` + +Good examples generally do as little setup as possible and only test one or two +things; it makes tests easier to understand and easier to debug. + +More complete documentation on expectations is available at https://www.relishapp.com/rspec/rspec-expectations/docs + +### Example groups + +Example groups are fairly self explanatory; they group similar examples into a +set. + +```ruby +describe "the number one" do + + it "is larger than zero" do + 1.should be > 0 + end + + it "is an odd number" do + 1.odd?.should be true + end + + it "is not nil" do + 1.should_not be_nil + end +end +``` + +Example groups have a number of uses that we'll get into later, but one of the +simplest demonstrations of what they do is how they help to format +documentation: + +``` +rspec ex.rb --format documentation + +the number one + is larger than zero + is an odd number + is not nil + +Finished in 0.00516 seconds +3 examples, 0 failures +``` + +### Setting up and tearing down tests + +Examples may require some setup before they can run, and might need to clean up +afterwards. `before` and `after` blocks can be used before this, and can be +used inside of example groups to limit how many examples they affect. + +```ruby + +describe "something that could warn" do + before :each do + # Disable warnings for this test + $VERBOSE = nil + end + + after do + # Enable warnings afterwards + $VERBOSE = true + end + + it "doesn't generate a warning" do + MY_CONSTANT = 1 + # reassigning a normally prints out 'warning: already initialized constant FOO' + MY_CONSTANT = 2 + end +end +``` + +### Setting up helper data + +Some examples may require setting up data before hand and making it available to +tests. RSpec provides helper methods with the `let` method call that can be used +inside of tests. + +```ruby +describe "a helper object" do + # This creates an array with three elements that we can retrieve in tests. A + # new copy will be made for each test. + let(:my_helper) do + ['foo', 'bar', 'baz'] + end + + it "should be an array" do + my_helper.should be_a_kind_of Array + end + + it "should have three elements" do + my_helper.should have(3).items + end +end +``` + +Like `before` blocks, helper objects like this are used to avoid doing a lot of +setup in individual examples and share setup between similar tests. + +### Isolating tests with stubs + +RSpec allows you to provide fake data during testing to make sure that +individual tests are only running the code being tested. You can stub out entire +objects, or just stub out individual methods on an object. When a method is +stubbed the method itself will never be called. + +While RSpec comes with its own stubbing framework, Puppet uses the Mocha +framework. + +A brief usage guide for Mocha is available at http://gofreerange.com/mocha/docs/#Usage, +and an overview of Mocha expectations is available at http://gofreerange.com/mocha/docs/Mocha/Expectation.html + +```ruby +describe "stubbing a method on an object" do + let(:my_helper) do + ['foo', 'bar', 'baz'] + end + + it 'should have three items before being stubbed' do + my_helper.size.should == 3 + end + + describe 'when stubbing the size' do + before do + my_helper.stubs(:size).returns 10 + end + + it 'should have the stubbed value for size' do + my_helper.size.should == 10 + end + end +end +``` + +Entire objects can be stubbed as well. + +```ruby +describe "stubbing an object" do + let(:my_helper) do + stub(:not_an_array, :size => 10) + end + + it 'should have the stubbed size' + my_helper.size.should == 10 + end +end +``` + +### Adding expectations with mocks + +It's possible to combine the concepts of stubbing and expectations so that a +method has to be called for the test to pass (like an expectation), and can +return a fixed value (like a stub). + +```ruby +describe "mocking a method on an object" do + let(:my_helper) do + ['foo', 'bar', 'baz'] + end + + describe "when mocking the size" do + before do + my_helper.expects(:size).returns 10 + end + + it "adds an expectation that a method was called" do + my_helper.size + end + end +end +``` + +Like stubs, entire objects can be mocked. + +```ruby +describe "mocking an object" do + let(:my_helper) do + mock(:not_an_array) + end + + before do + not_an_array.expects(:size).returns 10 + end + + it "adds an expectation that the method was called" do + not_an_array.size + end +end +``` +### Writing tests without side effects + +When properly written each test should be able to run in isolation, and tests +should be able to be run in any order. This makes tests more reliable and allows +a single test to be run if only that test is failing, instead of running all +17000+ tests each time something is changed. However, there are a number of ways +that can make tests fail when run in isolation or out of order. + +#### Using instance variables + +Puppet has a number of older tests that use `before` blocks and instance +variables to set up fixture data, instead of `let` blocks. These can retain +state between tests, which can lead to test failures when tests are run out of +order. + +```ruby +# test.rb +RSpec.configure do |c| + c.mock_framework = :mocha +end + +describe "fixture data" do + describe "using instance variables" do + + # BAD + before :all do + # This fixture will be created only once and will retain the `foo` stub + # between tests. + @fixture = stub 'test data' + end + + it "can be stubbed" do + @fixture.stubs(:foo).returns :bar + @fixture.foo.should == :bar + end + + it "should not keep state between tests" do + # The foo stub was added in the previous test and shouldn't be present + # in this test. + expect { @fixture.foo }.to raise_error + end + end + + describe "using `let` blocks" do + + # GOOD + # This will be recreated between tests so that state isn't retained. + let(:fixture) { stub 'test data' } + + it "can be stubbed" do + fixture.stubs(:foo).returns :bar + fixture.foo.should == :bar + end + + it "should not keep state between tests" do + # since let blocks are regenerated between tests, the foo stub added in + # the previous test will not be present here. + expect { fixture.foo }.to raise_error + end + end +end +``` + +``` +bundle exec rspec test.rb -fd + +fixture data + using instance variables + can be stubbed + should not keep state between tests (FAILED - 1) + using `let` blocks + can be stubbed + should not keep state between tests + +Failures: + + 1) fixture data using instance variables should not keep state between tests + Failure/Error: expect { @fixture.foo }.to raise_error + expected Exception but nothing was raised + # ./test.rb:17:in `block (3 levels) in ' + +Finished in 0.00248 seconds +4 examples, 1 failure + +Failed examples: + +rspec ./test.rb:16 # fixture data using instance variables should not keep state between tests +``` + +### RSpec references + + * RSpec core docs: https://www.relishapp.com/rspec/rspec-core/docs + * RSpec guidelines with Ruby: http://betterspecs.org/ + diff --git a/docs/static_compiler.md b/docs/static_compiler.md new file mode 100644 index 000000000..8a3270eab --- /dev/null +++ b/docs/static_compiler.md @@ -0,0 +1,83 @@ +# Static Compiler + +The static compiler was added to Puppet in the 2.7.0 release. +[1](http://links.puppetlabs.com/static-compiler-announce) + +The static compiler is intended to provide a configuration catalog that +requires a minimal amount of network communication in order to apply the +catalog to the system. As implemented in Puppet 2.7.x and Puppet 3.0.x this +intention takes the form of replacing all of the source parameters of File +resources with a content parameter containing an address in the form of a +checksum. The expected behavior is that the process applying the catalog to +the node will retrieve the file content from the FileBucket instead of the +FileServer. + +The high level approach can be described as follows. The `StaticCompiler` is a +terminus that inserts itself between the "normal" compiler terminus and the +request. The static compiler takes the resource catalog produced by the +compiler and filters all File resources. Any file resource that contains a +source parameter with a value starting with 'puppet://' is filtered in the +following way in a "standard" single master / networked agents deployment +scenario: + + 1. The content, owner, group, and mode values are retrieved from th + FileServer by the master. + 2. The file content is stored in the file bucket on the master. + 3. The source parameter value is stripped from the File resource. + 4. The content parameter value is set in the File resource using the form + '{XXX}1234567890' which can be thought of as a content address indexed by + checksum. + 5. The owner, group and mode values are set in the File resource if they are + not already set. + 6. The filtered catalog is returned in the response. + +In addition to the catalog terminus, the process requesting the catalog needs +to obtain the file content. The default behavior of `puppet agent` is to +obtain file contents from the local client bucket. The method we expect users +to employ to reconfigure the agent to use the server bucket is to declare the +`Filebucket[puppet]` resource with the address of the master. For example: + + node default { + filebucket { puppet: + server => $server, + path => false, + } + class { filetest: } + } + +This special filebucket resource named "puppet" will cause the agent to fetch +file contents specified by checksum from the remote filebucket instead of the +default clientbucket. + +## Trying out the Static Compiler + +Create a module that recursively downloads something. The jeffmccune-filetest +module will recursively copy the rubygems source tree. + + $ bundle exec puppet module install jeffmccune-filetest + +Start the master with the StaticCompiler turned on: + + $ bundle exec puppet master \ + --catalog_terminus=static_compiler \ + --verbose \ + --no-daemonize + +Add the special Filebucket[puppet] resource: + + # site.pp + node default { + filebucket { puppet: server => $server, path => false } + class { filetest: } + } + +Get the static catalog: + + $ bundle exec puppet agent --test + +You should expect all file metadata to be contained in the catalog, including a +checksum representing the content. When managing an out of sync file resource, +the real contents should be fetched from the server instead of the +clientbucket. + + diff --git a/docs/unicode.md b/docs/unicode.md new file mode 100644 index 000000000..a6c81df82 --- /dev/null +++ b/docs/unicode.md @@ -0,0 +1,56 @@ +# UTF-8 Handling # + +As Ruby 1.9 becomes more commonly used with Puppet, developers should be aware +of major changes to the way Strings and Regexp objects are handled. +Specifically, every instance of these two classes will have an encoding +attribute determined in a number of ways. + + * If the source file has an encoding specified in the magic comment at the + top, the instance will take on that encoding. + * Otherwise, the encoding will be determined by the LC\_LANG or LANG + environment variables. + * Otherwise, the encoding will default to ASCII-8BIT + +## References ## + +Excellent information about the differences between encodings in Ruby 1.8 and +Ruby 1.9 is published in this blog series: +[Understanding M17n](http://links.puppetlabs.com/understanding_m17n) + +## Encodings of Regexp and String instances ## + +In general, please be aware that Ruby 1.9 regular expressions need to be +compatible with the encoding of a string being used to match them. If they are +not compatible you can expect to receive and error such as: + + Encoding::CompatibilityError: incompatible encoding regexp match (ASCII-8BIT + regexp with UTF-8 string) + +In addition, some escape sequences were valid in Ruby 1.8 are no longer valid +in 1.9 if the regular expression is not marked as an ASCII-8BIT object. You +may expect errors like this in this situation: + + SyntaxError: (irb):7: invalid multibyte escape: /\xFF/ + +This error is particularly common when serializing a string to other +representations like JSON or YAML. To resolve the problem you can explicitly +mark the regular expression as ASCII-8BIT using the /n flag: + + "a" =~ /\342\230\203/n + +Finally, any time you're thinking of a string as an array of bytes rather than +an array of characters, common when escaping a string, you should work with +everything in ASCII-8BIT. Changing the encoding will not change the data +itself and allow the Regexp and the String to deal with bytes rather than +characters. + +Puppet provides a monkey patch to String which returns an encoding suitable for +byte manipulations: + + # Example of how to escape non ASCII printable characters for YAML. + >> snowman = "☃" + >> snowman.to_ascii8bit.gsub(/([\x80-\xFF])/n) { |x| "\\x#{x.unpack("C")[0].to_s(16)} } + => "\\xe2\\x98\\x83" + +If the Regexp is not marked as ASCII-8BIT using /n, then you can expect the +SyntaxError, invalid multibyte escape as mentioned above. diff --git a/docs/windows.md b/docs/windows.md new file mode 100644 index 000000000..1a61f8b45 --- /dev/null +++ b/docs/windows.md @@ -0,0 +1,73 @@ +# Windows # + +If you'd like to run Puppet from source on Windows platforms, the +include `ext/envpuppet.bat` will help. + +To quickly run Puppet from source, assuming you already have Ruby installed +from [rubyinstaller.org](http://rubyinstaller.org). + + C:\> cd C:\work\puppet + C:\work\puppet> set PATH=%PATH%;C:\work\puppet\ext + C:\work\puppet> envpuppet bundle install + C:\work\puppet> envpuppet puppet --version + 2.7.9 + +When writing a test that cannot possibly run on Windows, e.g. there is +no mount type on windows, do the following: + + describe Puppet::MyClass, :unless => Puppet.features.microsoft_windows? do + .. + end + +If the test doesn't currently pass on Windows, e.g. due to on going porting, then use an rspec conditional pending block: + + pending("porting to Windows", :if => Puppet.features.microsoft_windows?) do + + end + + pending("porting to Windows", :if => Puppet.features.microsoft_windows?) do + + end + +Then run the test as: + + C:\work\puppet> envpuppet bundle exec rspec spec + +## Common Issues ## + + * Don't assume file paths start with '/', as that is not a valid path on + Windows. Use Puppet::Util.absolute\_path? to validate that a path is fully + qualified. + + * Use File.expand\_path('/tmp') in tests to generate a fully qualified path + that is valid on POSIX and Windows. In the latter case, the current working + directory will be used to expand the path. + + * Always use binary mode when performing file I/O, unless you explicitly want + Ruby to translate between unix and dos line endings. For example, opening an + executable file in text mode will almost certainly corrupt the resulting + stream, as will occur when using: + + IO.open(path, 'r') { |f| ... } + IO.read(path) + + If in doubt, specify binary mode explicitly: + + IO.open(path, 'rb') + + * Don't assume file paths are separated by ':'. Use `File::PATH_SEPARATOR` + instead, which is ':' on POSIX and ';' on Windows. + + * On Windows, `File::SEPARATOR` is '/', and `File::ALT_SEPARATOR` is '\'. On + POSIX systems, `File::ALT_SEPARATOR` is nil. In general, use '/' as the + separator as most Windows APIs, e.g. CreateFile, accept both types of + separators. + + * Don't use waitpid/waitpid2 if you need the child process' exit code, + as the child process may exit before it has a chance to open the + child's HANDLE and retrieve its exit code. Use Puppet::Util.execute. + + * Don't assume 'C' drive. Use environment variables to look these up: + + "#{ENV['windir']}/system32/netsh.exe" +