diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..adef125 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,10 @@ +Strainer CHANGELOG +================== + +v0.1.0 +------ +- enable loading of cookbook dependencies + +v0.0.4 +------ +- added `--fail-fast` option diff --git a/README.md b/README.md index 763b42d..4f06466 100644 --- a/README.md +++ b/README.md @@ -30,12 +30,16 @@ To strain, simply run the `strain` command and pass in the cookbooks to strain: # strains phantomjs and tmux $ bundle exec strain phantomjs tmux -This will run `knife test` and `foodcritic` against both of the cookbooks. You can pass in as many cookbooks are you'd like. +This will first detect the cookbook dependencies, copy the cookbook and all dependencies into a sandbox. Then `knife test` and `foodcritic` will be run against both of the cookbooks. You can pass in as many cookbooks as you'd like. -As of `v0.0.3`, there's an option for `--fail-fast` that will fail immediately when any strain command returns a non-zero exit code: +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 +This can save time, especially when running tests locally. This is *not* recommended on continuous integration. + Custom Foodcritic Rules ----------------------- I always advocate using both [Etsy Foodcritic Rules](https://github.com/etsy/foodcritic-rules) and [CustomInk Foodcritic Rules](https://github.com/customink/foodcritic-rules) in all your projects. I also advocate keeping them all as submodules in `[Chef Repo]/foodcritic/...`. This makes strainer unhappy... @@ -50,4 +54,3 @@ 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 -- **Dependencies** - Auto-detect dependent cookbooks and copy them over diff --git a/lib/strainer/runner.rb b/lib/strainer/runner.rb index 5656f6d..779c8fd 100644 --- a/lib/strainer/runner.rb +++ b/lib/strainer/runner.rb @@ -11,9 +11,9 @@ def initialize(sandbox, options = {}) @cookbooks.each do |cookbook| $stdout.puts - $stdout.puts Color.negative{ "# Straining '#{cookbook}'" }.to_s + $stdout.puts Color.negative{ "# Straining '#{cookbook.name}'" } - commands_for(cookbook).collect do |command| + commands_for(cookbook.name.to_s).collect do |command| success &= run(command) if fail_fast? && !success @@ -30,11 +30,11 @@ def initialize(sandbox, options = {}) end private - def commands_for(cookbook) - file = File.read( colanderfile_for(cookbook) ) + def commands_for(cookbook_name) + file = File.read( colanderfile_for(cookbook_name) ) file = file.strip - file = file.gsub('$COOKBOOK', cookbook) + file = file.gsub('$COOKBOOK', cookbook_name) file = file.gsub('$SANDBOX', @sandbox.sandbox_path) lines = file.split("\n").reject{|c| c.strip.empty?}.compact @@ -52,8 +52,8 @@ def commands_for(cookbook) end || [] end - def colanderfile_for(cookbook) - cookbook_level = File.join(@sandbox.sandbox_path(cookbook), 'Colanderfile') + 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) diff --git a/lib/strainer/sandbox.rb b/lib/strainer/sandbox.rb index 56aebab..b4cbc4d 100644 --- a/lib/strainer/sandbox.rb +++ b/lib/strainer/sandbox.rb @@ -1,3 +1,4 @@ +require 'chef' require 'fileutils' module Strainer @@ -5,24 +6,37 @@ class Sandbox attr_reader :cookbooks def initialize(cookbooks = [], options = {}) - @cookbooks = [cookbooks].flatten @options = options + @cookbooks = load_cookbooks([cookbooks].flatten) clear_sandbox create_sandbox end def cookbook_path(cookbook) - path = File.join(cookbooks_path, cookbook) + path = File.join(cookbooks_path, cookbook.name.to_s) raise "cookbook '#{cookbook}' was not found in #{cookbooks_path}" unless File.exists?(path) return path end def sandbox_path(cookbook = nil) - File.expand_path( File.join(%W(colander cookbooks #{cookbook})) ) + File.expand_path( File.join(%W(colander cookbooks #{cookbook.is_a?(::Chef::CookbookVersion) ? cookbook.name : cookbook})) ) 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 + + # 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 + def cookbooks_path @cookbooks_path ||= (@options[:cookbooks_path] || File.expand_path('cookbooks')) end @@ -45,7 +59,7 @@ def copy_globals end def copy_cookbooks - @cookbooks.each do |cookbook| + (cookbooks + cookbooks_dependencies).each do |cookbook| FileUtils.cp_r(cookbook_path(cookbook), sandbox_path) end end @@ -64,5 +78,29 @@ def place_knife_rb # create knife.rb File.open("#{chef_path}/knife.rb", 'w+'){ |f| f.write(contents) } 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 + end + end + end + end end end diff --git a/strainer.gemspec b/strainer.gemspec index 95b5c21..9185fa5 100644 --- a/strainer.gemspec +++ b/strainer.gemspec @@ -1,5 +1,5 @@ Gem::Specification.new do |gem| - gem.version = '0.0.4' + gem.version = '0.1.0' gem.authors = ['Seth Vargo'] gem.email = ['sethvargo@gmail.com'] gem.description = %q{Run isolated cookbook tests against your chef repository with Strainer.} @@ -12,6 +12,7 @@ Gem::Specification.new do |gem| gem.name = 'strainer' gem.require_paths = ['lib'] + gem.add_runtime_dependency 'chef', '~> 10.12.0' gem.add_runtime_dependency 'term-ansicolor', '~> 1.0.7' gem.add_development_dependency 'yard', '~> 0.8.2'