Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

release initial 1.0.0.rc1

  • Loading branch information...
commit e659a9413bce6e1167b109b62db8f70737aa6db4 1 parent b27cb0a
@sethvargo sethvargo authored
View
1  .gitignore
@@ -15,3 +15,4 @@ spec/reports
test/tmp
test/version_tmp
tmp
+sandbox/
View
6 CHANGELOG.md
@@ -1,6 +1,12 @@
Strainer CHANGELOG
==================
+v1.0.0
+------
+- Moved entirely to Berkshelf integration
+- Moved entirely to thor
+- **Breaking** - new command `strainer`, old command is deprecated
+
v0.2.1
------
- Support a wider range of chef versions
View
8 CONTRIBUTING.md
@@ -0,0 +1,8 @@
+Contributing
+============
+
+Needs Your Help
+---------------
+This is a list of features or problem *you* can help solve! Fork and submit a pull request to make Strain even better!
+
+- **Threading** - Run each cookbook's tests (or each cookbook tests test) in a separate thread
View
2  LICENSE
@@ -1,4 +1,4 @@
-Copyright 2012 CustomInk
+Copyright 2012 Seth Vargo, CustomInk, LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
View
52 README.md
@@ -4,67 +4,47 @@ Strainer is a gem for isolating and testing individual chef cookbooks. It allows
Usage
-----
-Strainer is a command line tool. The first thing you should do is add the following entry into your `.gitignore` and `chefignore` files:
+First, create a `Strainerfile`. Cookbook-level Strainerfiles will take precedence over root-level ones. For simplicity, let's just create one in the root of our chef repository:
- .colander
-
-The `.colander` directory is where strainer puts all your temporary files for testing. You should not commit it to source control, nor should you upload it if sharing this cookbook with the community.
-
-Next, create a `Colanderfile`. Cookbook-level Colanderfiles will take precedence over root-level ones. For simplicity, let's just create one in the root of our chef repository:
-
- # Colanderfile
+ # Strainerfile
knife test: bundle exec knife cookbook test $COOKBOOK
foodcritic: bundle exec foodcritic -f any cookbooks/$COOKBOOK
-`Colanderfile` exposes two variables:
+`Strainerfile` exposes two variables:
- `$COOKBOOK` - the current running cookbook
- `$SANDBOX` - the sandbox path
Just like foreman, the labels don't actually matter - they are only used in formatting the output.
-That `Colanderfile` will run [foodcritic](https://github.com/acrmp/foodcritic) and knife test. I recommend this as the bare minimum for a cookbook test.
+That `Strainerfile` will run [foodcritic](https://github.com/acrmp/foodcritic) and knife test. I recommend this as the bare minimum for a cookbook test.
-`Colanderfile`s commands are run in the context to the sandbox `.colander` directory. The sandbox is essentially a clone of your working directory. This can be a bit confusing. `knife cookbook test` requires that you run your command against the "root" directory, yet foodcrtitic and chefspec require you run inside an actual cookbook. Here's a quick example to clear up some confusion:
+`Strainerfile`s commands are run in the context to the sandbox directory. The sandbox is essentially a clone of your working directory. This can be a bit confusing. `knife cookbook test` requires that you run your command against the "root" directory, yet foodcrtitic and chefspec require you run inside an actual cookbook. Here's a quick example to clear up some confusion:
- # Colanderfile
+ # Strainerfile
knife test: bundle exec knife cookbook test $COOKBOOK
foodcritic: bundle exec foodcritic -f any $SANDBOX/$COOKBOOK
chefspec: bundle exec rspec $SANDBOX/$COOKBOOK
To strain, simply run the `strain` command and pass in the cookbook(s) to strain:
- $ bundle exec strain phantomjs tmux
-
-This will first detect the cookbook dependencies, copy the cookbook and all dependencies into a sandbox. It will execute the contents of the `Colanderfile` on each cookbook.
-
-Using Berkshelf
----------------
-[Berkshelf](http://berkshelf.com/) is a tool for managing multiple cookbooks. It works very similar to how a `Gemfile` works with Rubygems.
-
-You'll need to tell Berkshelf to install the cookbooks to a folder inside your local repository. This way, strainer can actually find them.
-
-For example, this will install your cookbooks into the `berks-cookbooks` directory:
-
- $ bundle exec berks install --path berks-cookbooks
-
-Finally, make sure that this path is **first** in your `.chef/knife.rb` file:
+ $ bundle exec strainer test phantomjs tmux
-```ruby
-# .chef/knife.rb
-current_dir = File.dirname(__FILE__)
-cookbook_path ["#{current_dir}/../berks-cookbooks", "#{current_dir}/../cookbooks"]
-```
+This will first detect the cookbook dependencies, copy the cookbook and all dependencies into a sandbox. It will execute the contents of the `Strainerfile` on each cookbook.
-Or pass it as an argument to the `strain` command:
+Berkshelf
+---------
+[Berkshelf](http://berkshelf.com/) is a tool for managing multiple cookbooks. It works very similar to how a `Gemfile` works with Rubygems. If you're already using Berkshelf, Strainer will work out of the box. If you're not using Berkshelf, Strainer will work out of the box.
- $ bundle exec strain phantomjs --cookbooks-path berks-cookbooks
+Librarian Chef
+--------------
+Strainer does not support librarian-chef, and I have no plans to implement this feature. PRs are welcome, but Strainer is closely tied to Berkshelf, intentionally.
Failing Quickly
---------------
As of `v0.0.4`, there's an option for `--fail-fast` that will fail immediately when any strain command returns a non-zero exit code:
- $ bundle exec strain phantomjs --fail-fast
+ $ bundle exec strainer test phantomjs --fail-fast
This can save time, especially when running tests locally. This is *not* recommended on continuous integration.
@@ -74,7 +54,7 @@ I always advocate using both [Etsy Foodcritic Rules](https://github.com/etsy/foo
Strainer runs everything in an isolated sandbox, inside your Chef Repo. When including additional foodcritic rules, you need to do something like this:
- # Colanderfile
+ # Strainerfile
foodcritic: bundle exec foodcritic -I foodcritic/* -f any $SANDBOX/$COOKBOOK
Needs Your Help
View
9 bin/strain
@@ -1,8 +1,9 @@
#!/usr/bin/env ruby
-
$:.unshift File.expand_path('../lib', __FILE__)
-
require 'strainer'
-require 'strainer/cli'
-Strainer::CLI.run(*ARGV)
+# This executable is deprecated
+#
+# @deprecated use `bundle exec strainer test` instead!
+Strainer.ui.deprecated 'The `strain` command is deprecated. Use `strainer test $COOKBOOKS` instead!'
+system "strainer test #{ARGV.join(' ')}"
View
11 bin/strainer
@@ -0,0 +1,11 @@
+#!/usr/bin/env ruby
+$:.unshift File.expand_path('../lib', __FILE__)
+require 'strainer'
+
+begin
+ Strainer::Cli.start
+rescue Strainer::Error::Base => e
+ Strainer.ui.error "#{e.class} - #{e}"
+ Strainer.ui.error " #{e.backtrace.join("\n ")}" if ENV['DEBUG']
+ exit e.status_code
+end
View
1  lib/berkshelf/extensions.rb
@@ -0,0 +1 @@
+require 'berkshelf/extensions/cached_cookbook'
View
20 lib/berkshelf/extensions/cached_cookbook.rb
@@ -0,0 +1,20 @@
+module Berkshelf
+ # Extensions on Berkshelf::CachedCookbook
+ #
+ # @author Seth Vargo <sethvargo@gmail.com>
+ class CachedCookbook
+ # @return [Pathname]
+ # the original location of this cookbook
+ attr_reader :original_path
+
+ # Allow overriding the path, but store the old path in another
+ # instance variable.
+ #
+ # @param [Pathname] location
+ # the new location for this cookbook
+ def path=(location)
+ @original_path = @path.dup
+ @path = location
+ end
+ end
+end
View
56 lib/strainer.rb
@@ -1,9 +1,55 @@
-require 'strainer/color'
-require 'strainer/runner'
-require 'strainer/sandbox'
+require 'berkshelf'
+require 'berkshelf/extensions'
+require 'pathname'
+require 'thor'
+
+require 'strainer/errors'
module Strainer
- def self.root
- @@root ||= File.expand_path('../../', __FILE__)
+ autoload :Cli, 'strainer/cli'
+ autoload :Command, 'strainer/command'
+ autoload :Runner, 'strainer/runner'
+ autoload :Sandbox, 'strainer/sandbox'
+ autoload :Strainerfile, 'strainer/strainerfile'
+ autoload :UI, 'strainer/ui'
+ autoload :Version, 'strainer/version'
+
+ class << self
+ # The root of the application
+ #
+ # @return [Pathname]
+ # the path to the root of Strainer
+ def root
+ @root ||= Pathname.new(File.expand_path('../../', __FILE__))
+ end
+
+ # The UI instance
+ #
+ # @return [Strainer::UI]
+ # an instance of the strainer UI
+ def ui
+ @ui ||= Strainer::UI.new
+ end
+
+ # Helper method to access a constant defined in Strainer::Sandbox that
+ # specifies the location of the sandbox
+ #
+ # @return [Pathname]
+ # the path to the sandbox
+ def sandbox_path
+ Strainer::Sandbox::SANDBOX
+ end
+
+ # Helper method to access a constant defined in Strainer::Strainerfile that
+ # specifies the filename for Strainer
+ #
+ # @return [String]
+ # the filename for Strainerfile
+ def strainerfile_name
+ Strainer::Strainerfile::FILENAME
+ end
end
end
+
+# Sync STDOUT to get "real-time" output
+STDOUT.sync = true
View
75 lib/strainer/cli.rb
@@ -1,52 +1,39 @@
-require 'optparse'
+require 'strainer'
module Strainer
- class CLI
- def self.run(*args)
- parse_options(*args)
-
- if @cookbooks.empty?
- puts Color.red { 'ERROR: You did not specify any cookbooks!' }
- else
- @sandbox = Strainer::Sandbox.new(@cookbooks, @options)
- @runner = Strainer::Runner.new(@sandbox, @options)
- end
+ # Use our own custom shell
+ Thor::Base.shell = Strainer::UI
+
+ # Cli runner for Strainer
+ #
+ # @author Seth Vargo <sethvargo@gmail.com>
+ class Cli < Thor
+ # global options
+ map ['-v', '--version'] => :version
+ class_option :cookbooks_path, :type => :string, :aliases => '-p', :desc => 'The path to the cookbook store', :banner => 'PATH'
+ class_option :config, :type => :string, :aliases => '-c', :desc => 'The path to the knife.rb config'
+
+ # strainer test *COOKBOOKS
+ method_option :except, :type => :array, :aliases => '-e', :desc => 'Strainerfile labels to ignore'
+ method_option :only, :type => :array, :aliases => '-o', :desc => 'Strainerfile labels to include'
+ method_option :fail_fast, :type => :boolean, :aliases => '-x', :desc => 'Stop termination immediately if a test fails', :banner => '', :default => false
+ desc 'test [COOKBOOKS]', 'Run tests against the given cookbooks'
+ def test(*cookbooks)
+ Strainer::Runner.new(cookbooks, options)
end
- private
- def self.parse_options(*args)
- @options = {}
-
- parser = OptionParser.new do |options|
- # remove OptionParsers Officious['version'] to avoid conflicts
- options.base.long.delete('version')
-
- options.on nil, '--fail-fast', 'Fail fast' do |ff|
- @options[:fail_fast] = ff
- end
-
- options.on '-p PATH', '--cookbooks-path PATH', 'Path to the cookbooks' do |cp|
- @options[:cookbooks_path] = cp
- end
-
- options.on '-h', '--help', 'Display this help screen' do
- puts options
- exit 0
- end
-
- options.on '-v', '--version', 'Display the current version' do
- require 'strainer/version'
- puts Strainer::VERSION
- exit 0
- end
- end
+ # strainer info
+ desc 'info', 'Display version and copyright information'
+ def info
+ Strainer.ui.info "Strainer (#{Strainer::VERSION})"
+ Strainer.ui.info "\n"
+ Strainer.ui.info File.read Strainer.root.join('LICENSE')
+ end
- # Get the cookbook names. The options that aren't read by optparser are assummed
- # to be cookbooks in this case.
- @cookbooks = []
- parser.order!(args) do |noopt|
- @cookbooks << noopt
- end
+ # strainer -v
+ desc 'version', 'Display the version information', hide: true
+ def version
+ Strainer.ui.info Strainer::VERSION
end
end
end
View
7 lib/strainer/color.rb
@@ -1,7 +0,0 @@
-require 'term/ansicolor'
-
-module Strainer
- class Color
- extend Term::ANSIColor
- end
-end
View
93 lib/strainer/command.rb
@@ -0,0 +1,93 @@
+module Strainer
+ # The Command class is responsible for a command (test) against a cookbook.
+ #
+ # @author Seth Vargo <sethvargo@gmail.com>
+ class Command
+ # List of colors to choose from when outputting labels
+ COLORS = %w(yellow blue magenta cyan).freeze
+
+ # @return [String]
+ # the "text" form of the command to run
+ attr_reader :command
+
+ # @return [String]
+ # the label for this command
+ attr_reader :label
+
+ # Parse a command out of the given string (line)
+ #
+ # @param [String] line
+ # the line to parse
+ # example: foodcritic -f any phantomjs
+ def initialize(line, cookbook, options = {})
+ @label, @command = line.split(':', 2).map(&:strip)
+ @cookbook = cookbook
+ end
+
+ # Run the given command against the cookbook
+ #
+ # @return [Boolean]
+ # `true` if the command exited successfully, `false` otherwise
+ def run!
+ title(label)
+
+ Dir.chdir Strainer.sandbox_path do
+ speak command
+ speak `#{command}`
+
+ unless $?.success?
+ Strainer.ui.error label_with_padding + Strainer.ui.set_color('Terminated with a non-zero exit status. Strainer assumes this is a failure.', :red)
+ Strainer.ui.error label_with_padding + Strainer.ui.set_color('FAILURE!', :red)
+ false
+ else
+ Strainer.ui.success label_with_padding + Strainer.ui.set_color('SUCCESS!', :green)
+ true
+ end
+ end
+ end
+
+ # Have this command output text, prefixing with its output with the
+ # command name
+ #
+ # @param [String] message
+ # the message to speak
+ # @param [Hash] options
+ # a list of options to pass along
+ def speak(message, options = {})
+ message.to_s.strip.split("\n").each do |line|
+ next if line.strip.empty?
+
+ line.gsub! Strainer.sandbox_path.to_s, @cookbook.original_path.dirname.to_s
+ Strainer.ui.say label_with_padding + line, options
+ end
+ end
+
+ private
+ # Return the color associated with this label
+ #
+ # @return [Symbol]
+ # the color (as a symbol) associated with this label
+ def color
+ @color ||= COLORS[label.length%COLORS.length].to_sym
+ end
+
+ # Update the current process name and terminal title with
+ # the given title.
+ #
+ # @param [String] title
+ # the title to update with
+ def title(title)
+ $0 = title
+ printf "\033]0;#{title}\007"
+ end
+
+ # Get the label corresponding to this command with spacial padding
+ #
+ # @return [String]
+ # the padding and colored label
+ def label_with_padding
+ padded_label = label[0..20].ljust(20) + ' | '
+ Strainer.ui.set_color padded_label, color
+ end
+ end
+end
View
27 lib/strainer/errors.rb
@@ -0,0 +1,27 @@
+module Strainer
+ module Error
+ # Base class for our custom errors to inherit from.
+ #
+ # @author Seth Vargo <sethvargo@gmail.com>
+ class Base < StandardError
+ # Helper method for creating errors using a given status code
+ #
+ # @param [Integer] code
+ # the status code for this error
+ def self.status_code(code)
+ define_method(:status_code) { code }
+ define_singleton_method(:status_code) { code }
+ end
+ end
+
+ # Raised when a required cookbook is not found.
+ #
+ # @author Seth Vargo <sethvargo@gmail.com>
+ class CookbookNotFound < Base; status_code(100); end
+
+ # Raised when Strainer is unable to find a Strainerfile.
+ #
+ # @author Seth Vargo <sethvargo@gmail.com>
+ class StrainerfileNotFound < Base; status_code(101); end
+ end
+end
View
177 lib/strainer/runner.rb
@@ -1,132 +1,87 @@
module Strainer
+ # The Runner class is responsible for executing the tests against cookbooks.
+ #
+ # @author Seth Vargo <sethvargo@gmail.com>
class Runner
- def initialize(sandbox, options = {})
- @sandbox = sandbox
- @cookbooks = @sandbox.cookbooks
- @options = options
-
- # need a variable at the root level to track whether the
- # build actually passed
- success = true
-
- @cookbooks.each do |cookbook|
- $stdout.puts
- $stdout.puts Color.negative{ "# Straining '#{cookbook.name}'" }
-
- commands_for(cookbook.name.to_s).collect do |command|
- success &= run(command)
-
- if fail_fast? && !success
- $stdout.puts [ label_with_padding(command[:label]), Color.red{ 'Exited early because --fail-fast was specified. Some tests may have been skipped!' } ].join(' ')
- abort
- end
+ # List taken from: http://wiki.opscode.com/display/chef/Chef+Configuration+Settings
+ # Listed in order of preferred preference
+ KNIFE_LOCATIONS = [
+ './.chef/knife.rb',
+ '~/.chef/knife.rb',
+ '/etc/chef/solo.rb',
+ '/etc/chef/client.rb'
+ ].freeze
+
+ class << self
+ # Perform a smart search for knife.rb chef configuration file
+ #
+ # @return [Pathname]
+ # the path to the chef configuration
+ def chef_config_path
+ @chef_config_path ||= begin
+ location = KNIFE_LOCATIONS.find{ |location| File.exists?(File.expand_path(location)) }
+ location ||= '~/.chef/knife.rb'
+
+ Pathname.new(File.expand_path(location))
end
-
- $stdout.puts
end
- # fail unless all commands returned successfully
- abort unless success
- end
-
- private
- def commands_for(cookbook_name)
- file = File.read( colanderfile_for(cookbook_name) )
-
- file = file.strip
- file = file.gsub('$COOKBOOK', cookbook_name)
- file = file.gsub('$SANDBOX', @sandbox.sandbox_path)
-
- # drop empty lines and comments
- lines = file.split("\n").reject{|c| c.strip.empty? || c.start_with?('#')}.compact
-
- # parse the line and split it into the label and command parts
+ # Set the chef_config_path
#
- # example line: foodcritic -f any phantomjs
- lines.collect do |line|
- split_line = line.split(':', 2)
-
- {
- :label => split_line[0].strip,
- :command => split_line[1].strip
- }
- end || []
- end
-
- def colanderfile_for(cookbook_name)
- cookbook_level = File.join(@sandbox.sandbox_path(cookbook_name), 'Colanderfile')
- root_level = File.expand_path('Colanderfile')
-
- if File.exists?(cookbook_level)
- cookbook_level
- elsif File.exists?(root_level)
- root_level
- else
- raise "Could not find Colanderfile in #{cookbook_level} or #{root_level}"
+ # @param [String] path
+ # the path to the config file
+ # @return [Pathname]
+ # the supplied string as a Pathname
+ def chef_config_path=(path)
+ @chef_config = nil
+ @chef_config_path = Pathname.new(path)
+ @chef_config_path
end
- end
- def label_with_padding(label)
- max_length = 12
- colors = [ :blue, :cyan, :magenta, :yellow ]
- color = colors[label.length%colors.length]
-
- Color.send(color) do
- "#{label[0...max_length].ljust(max_length)} | "
+ # Get the best chef configuration
+ #
+ # @return [Chef::Config]
+ # a chef configuration
+ def chef_config
+ @chef_config ||= begin
+ Chef::Config.from_file(chef_config_path)
+ Chef::Config
+ rescue
+ Chef::Config
+ end
end
end
- def run(command)
- Dir.chdir('.colander') do
- label = command[:label]
- command = command[:command]
- pretty_command = begin
- split = command.split(' ')
- path = split.pop
-
- if path =~ /\.colander/
- short_path = path.split('.colander').last[1..-1]
- else
- short_path = path
- end
+ # Creates a Strainer runner
+ #
+ # @param [Array<String>] cookbook_names
+ # an array of cookbook_names to test and load into the sandbox
+ # @param [Hash] options
+ # a list of options to pass along
+ def initialize(cookbook_names, options = {})
+ @options = options
+ @sandbox = Strainer::Sandbox.new(cookbook_names, @options)
+ @cookbooks = @sandbox.cookbooks
+ @report = {}
- split.push short_path
- split.join(' ')
- end
+ @cookbooks.each do |cookbook|
+ strainerfile = Strainer::Strainerfile.for(cookbook, options)
+ Strainer.ui.header("# Straining '#{cookbook.cookbook_name} (v#{cookbook.version})'")
- $stdout.puts [ label_with_padding(label), Color.bold{ Color.underscore{ pretty_command } } ].join(' ')
+ strainerfile.commands.each do |command|
+ success = command.run!
- result = format(label, `#{command}`)
- $stdout.puts result unless result.strip.empty?
+ @report[cookbook.cookbook_name] ||= {}
+ @report[cookbook.cookbook_name][command.label] = success
- if $?.success?
- $stdout.puts format(label, Color.green{'Success!'})
- $stdout.flush
- return true
- else
- $stdout.puts format(label, Color.red{'Failure!'})
- $stdout.flush
- return false
+ if @options[:fail_fast] && !success
+ Strainer.ui.fatal "Exited early because '--fail-fast' was specified. Some tests may have been skipped!"
+ abort
+ end
end
end
- end
-
- def format(label, data)
- data.to_s.strip.split("\n").collect do |line|
- if %w(fatal error alert).any?{ |e| line =~ /^#{e}/i }
- [ label_with_padding(label), Color.red{ line } ].join(' ')
- elsif %w(warn).any?{ |e| line =~ /^#{e}/i }
- [ label_with_padding(label), Color.yellow{ line } ].join(' ')
- elsif %w(info debug).any?{ |e| line =~ /^#{e}/i }
- [ label_with_padding(label), Color.cyan{ line } ].join(' ')
- else
- [ label_with_padding(label), line ].join(' ')
- end
- end.join("\n")
- end
- def fail_fast?
- @options[:fail_fast]
+ abort unless @report.values.collect(&:values).flatten.all?{|v| v == true}
end
end
end
View
205 lib/strainer/sandbox.rb
@@ -1,117 +1,156 @@
-require 'chef'
-require 'chef/knife'
-require 'fileutils'
-
module Strainer
+ # Manages the Strainer sandbox (playground) for isolated testing.
+ #
+ # @author Seth Vargo <sethvargo@gmail.com>
class Sandbox
+ # The path to the testing sandbox inside the gem.
+ SANDBOX = Strainer.root.join('sandbox').freeze
+
+ # @return [Array<Berkshelf::CachedCookbook>]
+ # an array of cookbooks in this sandbox
attr_reader :cookbooks
- def initialize(cookbooks = [], options = {})
- @options = options
- @cookbooks = load_cookbooks([cookbooks].flatten)
+ # Create a new sandbox for the given cookbooks
+ #
+ # @param [Array<String>] cookbook_names
+ # the list of cookbooks to copy into the sandbox
+ # @param [Hash] options
+ # a list of options to pass along
+ def initialize(cookbook_names, options = {})
+ @options = options
+ @cookbooks = load_cookbooks(cookbook_names)
+
+ reset_sandbox
+ copy_cookbooks
+ end
- clear_sandbox
+ # Clear out the existing sandbox and create the directories
+ def reset_sandbox
+ destroy_sandbox
create_sandbox
end
- def cookbook_path(cookbook)
- found_paths = cookbooks_path.select{ |path| File.exists?( File.join(path, cookbook.name.to_s) ) }
+ # Destroy the current sandbox, if it exists
+ def destroy_sandbox
+ FileUtils.rm_rf(SANDBOX) if File.directory?(SANDBOX)
+ end
- if found_paths.empty?
- puts Color.red { "Cookbook '#{cookbook.name}' was not found in #{cookbooks_path}" }
- exit(1)
- elsif found_paths.size > 1
- puts Color.yellow { "Cookbook '#{cookbook.name}' was found in multiple paths: #{found_paths}." }
- puts Color.yellow { 'Strainer can only handle one cookbook per path at this time. Pull requests are welcome :)' }
- exit(1)
+ # Create the sandbox unless it already exits
+ def create_sandbox
+ unless File.directory?(SANDBOX)
+ FileUtils.mkdir_p(SANDBOX)
+ copy_globals
+ place_knife_rb
end
-
- return File.join(found_paths[0], cookbook.name.to_s)
end
- def sandbox_path(cookbook = nil)
- File.expand_path( File.join(%W(.colander cookbooks #{cookbook.is_a?(::Chef::CookbookVersion) ? cookbook.name : cookbook})) )
+ # Copy over a whitelist of common files into our sandbox
+ def copy_globals
+ files = Dir[*%W(#{Strainer.strainerfile_name} foodcritic .rspec spec test)]
+ FileUtils.cp_r(files, SANDBOX)
end
- private
- # Load a specific cookbook by name
- def load_cookbook(cookbook_name)
- return cookbook_name if cookbook_name.is_a?(::Chef::CookbookVersion)
- loader = ::Chef::CookbookLoader.new(cookbooks_path)
- loader[cookbook_name]
- end
+ # Create a basic knife.rb file to ensure tests run successfully
+ def place_knife_rb
+ chef_path = SANDBOX.join('.chef')
+ FileUtils.mkdir_p(chef_path)
- # Load an array of cookbooks by name
- def load_cookbooks(cookbook_names)
- cookbook_names = [cookbook_names].flatten
- cookbook_names.collect{ |cookbook_name| load_cookbook(cookbook_name) }
- end
+ # Build the contents
+ contents = <<-EOH
+cache_type 'BasicFile'
+cache_options(:path => "\#{ENV['HOME']}/.chef/checksums")
+cookbook_path '#{SANDBOX}'
+EOH
- def cookbooks_path
- @cookbooks_path ||= [@options[:cookbooks_path] || ( File.exists?(knife_rb_path) ? (Chef::Config.from_file(knife_rb_path) && Chef::Config.cookbook_path.compact ) : nil) || File.expand_path('cookbooks')].flatten.collect{ |p| File.expand_path(p) }
+ # Create knife.rb
+ File.open("#{chef_path}/knife.rb", 'w+'){ |f| f.write(contents) }
end
- def clear_sandbox
- FileUtils.rm_rf(sandbox_path)
- end
+ # Copy all the cookbooks provided in {#initialize} to the isolated sandbox location
+ def copy_cookbooks
+ cookbooks_and_dependencies.each do |cookbook|
+ sandbox_path = SANDBOX.join(cookbook.cookbook_name)
- def create_sandbox
- FileUtils.mkdir_p(sandbox_path)
+ # Copy the files to our sandbox
+ FileUtils.cp_r(cookbook.path, sandbox_path)
- copy_globals
- copy_cookbooks
- place_knife_rb
+ # Override the @path location so we don't need to create a new object
+ cookbook.path = sandbox_path
+ end
end
- def copy_globals
- files = %w(.rspec spec test foodcritic)
- FileUtils.cp_r( Dir["{#{files.join(',')}}"], sandbox_path('..') )
+ # Load a cookbook from the given array of cookbook names
+ #
+ # @param [Array<String>] cookbook_names
+ # the list of cookbooks to search for
+ # @return [Array<Berkshelf::CachedCookbook>]
+ # the array of cached cookbooks
+ def load_cookbooks(cookbook_names)
+ cookbook_names.collect{ |cookbook_name| load_cookbook(cookbook_name) }
end
- def copy_cookbooks
- (cookbooks + cookbooks_dependencies).each do |cookbook|
- FileUtils.cp_r(cookbook_path(cookbook), sandbox_path)
- end
- end
+ # Load an individual cookbook by its name
+ #
+ # @param [String] cookbook_name
+ # the name of the cookbook to load
+ # @return [Berkshelf::CachedCookbook]
+ # the cached cookbook
+ # @raise [Strainer::Error::CookbookNotFound]
+ # when the cookbook was not found in any of the sources
+ def load_cookbook(cookbook_name)
+ cookbook = cookbooks_paths.collect do |path|
+ begin
+ Berkshelf::CachedCookbook.from_path(path.join(cookbook_name))
+ rescue Berkshelf::CookbookNotFound
+ # move onto the next source...
+ nil
+ end
+ end.compact.last
- def place_knife_rb
- chef_path = File.join(sandbox_path, '..','.chef')
- FileUtils.mkdir_p(chef_path)
+ cookbook ||= Berkshelf.cookbook_store.cookbooks(cookbook_name).last
- # build the contents
- contents = <<-EOH
-cache_type 'BasicFile'
-cache_options(:path => "\#{ENV['HOME']}/.chef/checksums")
-cookbook_path '#{sandbox_path}'
-EOH
+ unless cookbook
+ raise Strainer::Error::CookbookNotFound, "Could not find cookbook #{cookbook_name} in any of the sources."
+ end
- # create knife.rb
- File.open("#{chef_path}/knife.rb", 'w+'){ |f| f.write(contents) }
+ cookbook
end
- def knife_rb_path
- File.join(Chef::Knife.chef_config_dir, 'knife.rb')
+ # Dynamically builds a list of possible cookbook paths from the
+ # `@options` hash, Berkshelf config, and Chef config, and a logical
+ # guess
+ #
+ # @return [Array<Pathname>]
+ # a list of possible cookbook locations
+ def cookbooks_paths
+ @cookbooks_paths ||= begin
+ paths = [
+ @options[:cookbooks_path],
+ Strainer::Runner.chef_config.cookbook_path,
+ Berkshelf::Config.chef_config.cookbook_path,
+ 'cookbooks'
+ ].flatten.compact.map{ |path| Pathname.new(File.expand_path(path)) }.uniq
+
+ paths.select{ |path| File.exists?(path) }
+ end
end
- # Iterate over the cookbook's dependencies and ensure those cookbooks are
- # also included in our sandbox by adding them to the @cookbooks instance
- # variable. This method is actually semi-recursive because we append to the
- # end of the array on which we are iterating, ensuring we load all dependencies
- # dependencies.
- def cookbooks_dependencies
- @cookbooks_dependencies ||= begin
- $stdout.puts 'Loading cookbook dependencies...'
-
- loaded_dependencies = Hash.new(false)
-
- dependencies = @cookbooks.dup
-
- dependencies.each do |cookbook|
- cookbook.metadata.dependencies.keys.each do |dependency_name|
- unless loaded_dependencies[dependency_name]
- dependencies << load_cookbook(dependency_name)
- loaded_dependencies[dependency_name] = true
- end
+ # Collect all cookbooks and the dependencies specified in their metadata.rb
+ # for copying
+ #
+ # @return [Array<Berkshelf::CachedCookbook>]
+ # a list of cached cookbooks
+ def cookbooks_and_dependencies
+ loaded_dependencies = Hash.new(false)
+
+ dependencies = @cookbooks.dup
+ dependencies.each do |cookbook|
+ loaded_dependencies[cookbook.cookbook_name] = true
+
+ cookbook.metadata.dependencies.keys.each do |dependency_name|
+ unless loaded_dependencies[dependency_name]
+ dependencies << load_cookbook(dependency_name)
+ loaded_dependencies[dependency_name] = true
end
end
end
View
77 lib/strainer/strainerfile.rb
@@ -0,0 +1,77 @@
+module Strainer
+ # An instance of a Strainerfile.
+ #
+ # @author Seth Vargo <sethvargo@gmail.com>
+ class Strainerfile
+ # The filename for the Strainerfile
+ FILENAME = 'Strainerfile'
+
+ class << self
+ # (see #initialize)
+ def for(cookbook, options = {})
+ new(cookbook, options)
+ end
+ end
+
+ # Instantiate an instance of this class from a cookbook
+ #
+ # @param [Berkshelf::CachedCookbook] cookbook
+ # the cached cookbook to search for a Strainerfile
+ # @param [Hash] options
+ # a list of options to pass along
+ # @return [Strainerfile]
+ # an instance of this class
+ def initialize(cookbook, options = {})
+ @cookbook = cookbook
+ @options = options
+
+ locations = [
+ @cookbook.path.join(FILENAME),
+ Strainer.sandbox_path.join(FILENAME)
+ ]
+
+ @strainerfile = locations.find{ |location| File.exists?(location) }
+
+ raise Strainer::Error::StrainerfileNotFound, "Could not find a Strainerfile for cookbook '#{cookbook.cookbook_name}'." unless @strainerfile
+
+ load!
+ end
+
+ # Get the list of commands to run, filtered by the `@options` hash for either
+ # `:ignore` or `:only`
+ #
+ # @return [Array<Strainer::Command>]
+ # the list of commands to execute
+ def commands
+ @commands ||= if @options[:except]
+ @all_commands.reject{ |command| @options[:except].include?(command.label) }
+ elsif @options[:only]
+ @all_commands.select{ |command| @options[:only].include?(command.label) }
+ else
+ @all_commands
+ end
+ end
+
+ private
+ # Parse the given Strainerfile
+ def load!
+ return if @all_commands
+ contents = File.read @strainerfile
+ contents.strip!
+ contents.gsub! '$COOKBOOK', @cookbook.cookbook_name
+ contents.gsub! '$SANDBOX', Strainer.sandbox_path.to_s
+
+ # Drop empty lines and comments
+ lines = contents.split("\n")
+ lines.reject!{ |line| line.strip.empty? || line.strip.start_with?('#') }
+ lines.compact!
+ lines ||= []
+
+ # Parse the line and split it into the label and command parts
+ #
+ # @example Example Line
+ # foodcritic -f any phantomjs
+ @all_commands = lines.collect{ |line| Command.new(line, @cookbook, @options) }
+ end
+ end
+end
View
87 lib/strainer/ui.rb
@@ -0,0 +1,87 @@
+module Strainer
+ # Extend Thor::Shell::Color to provide nice helpers for outputting
+ # to the console.
+ #
+ # @author Seth Vargo <sethvargo@gmail.com>
+ class UI < ::Thor::Shell::Color
+ # Print the given message to STDOUT.
+ #
+ # @param [String]
+ # message the message to print
+ # @param [Symbol] color
+ # the color to use
+ # @param [Boolean] new_line
+ # include a new_line character
+ def say(message = '', color = nil, new_line = nil)
+ return if quiet?
+ super(message, color)
+ end
+ alias_method :info, :say
+
+ # Print the given message to STDOUT.
+ #
+ # @param [String] status
+ # the status to print
+ # @param [String] message
+ # the message to print
+ # @param [Boolean] log_status
+ # whether to log the status
+ def say_status(status, message, log_status = true)
+ return if quiet?
+ super(status, message, log_status)
+ end
+
+ # Print a header message
+ #
+ # @param [String] message
+ # the message to print
+ def header(message)
+ return if quiet?
+ say(message, [:black, :on_white])
+ end
+
+ # Print a green success message to STDOUT.
+ #
+ # @param [String] message
+ # the message to print
+ # @param [Symbol] color
+ # the color to use
+ def success(message, color = :green)
+ return if quiet?
+ say(message, color)
+ end
+
+ # Print a yellow warning message to STDOUT.
+ #
+ # @param [String] message
+ # the message to print
+ # @param [Symbol] color
+ # the color to use
+ def warn(message, color = :yellow)
+ return if quiet?
+ say(message, color)
+ end
+
+ # Print a red error message to the STDERR.
+ #
+ # @param [String] message
+ # the message to print
+ # @param [Symbol] color
+ # the color to use
+ def error(message, color = :red)
+ return if quiet?
+ message = set_color(message, *color) if color
+ super(message)
+ end
+ alias_method :fatal, :error
+
+ # Print a deprecation notice to STDERR.
+ #
+ # @param [String] message
+ # the message to print
+ def deprecated(message)
+ return if quiet?
+ error('DEPRECATION NOTICE: ' + message)
+ end
+ end
+end
View
3  lib/strainer/version.rb
@@ -1,3 +1,4 @@
module Strainer
- VERSION = '0.2.1'
+ # The current version of Strainer
+ VERSION = '1.0.0.rc1'
end
View
6 strainer.gemspec
@@ -15,8 +15,8 @@ Gem::Specification.new do |gem|
gem.name = 'strainer'
gem.require_paths = ['lib']
- gem.add_runtime_dependency 'chef', '>= 10.10'
- gem.add_runtime_dependency 'term-ansicolor', '~> 1.0.7'
+ gem.add_runtime_dependency 'berkshelf', '~> 1.0'
- gem.add_development_dependency 'yard', '~> 0.8.3'
+ gem.add_development_dependency 'redcarpet'
+ gem.add_development_dependency 'yard', '~> 0.8'
end
Please sign in to comment.
Something went wrong with that request. Please try again.