diff --git a/.bundle/config b/.bundle/config new file mode 100644 index 00000000..8ebbe30e --- /dev/null +++ b/.bundle/config @@ -0,0 +1,2 @@ +--- +BUNDLE_DISABLE_SHARED_GEMS: "1" diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..0bb98b1d --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +*.swp +tmp +pkg +.swo +*~ + diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..67ed1c19 --- /dev/null +++ b/Gemfile @@ -0,0 +1,5 @@ +source "http://rubygems.org" +gem "cucumber" +gem "aruba" +gem "rake" +gem "rspec" diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 00000000..c6b70145 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,38 @@ +GEM + remote: http://rubygems.org/ + specs: + aruba (0.2.4) + background_process + cucumber (~> 0.9.3) + background_process (1.2) + builder (2.1.2) + cucumber (0.9.4) + builder (~> 2.1.2) + diff-lcs (~> 1.1.2) + gherkin (~> 2.2.9) + json (~> 1.4.6) + term-ansicolor (~> 1.0.5) + diff-lcs (1.1.2) + gherkin (2.2.9) + json (~> 1.4.6) + term-ansicolor (~> 1.0.5) + json (1.4.6) + rake (0.8.7) + rspec (2.1.0) + rspec-core (~> 2.1.0) + rspec-expectations (~> 2.1.0) + rspec-mocks (~> 2.1.0) + rspec-core (2.1.0) + rspec-expectations (2.1.0) + diff-lcs (~> 1.1.2) + rspec-mocks (2.1.0) + term-ansicolor (1.0.5) + +PLATFORMS + ruby + +DEPENDENCIES + aruba + cucumber + rake + rspec diff --git a/Rakefile b/Rakefile new file mode 100644 index 00000000..167fc5f2 --- /dev/null +++ b/Rakefile @@ -0,0 +1,19 @@ +require 'rubygems' +require 'bundler/setup' +require 'rake/gempackagetask' +require 'cucumber/rake/task' + +eval("$specification = #{IO.read('appraisal.gemspec')}") +Rake::GemPackageTask.new($specification) do |package| + package.need_zip = true + package.need_tar = true +end + +Cucumber::Rake::Task.new(:cucumber) do |t| + t.fork = true + t.cucumber_opts = ['--format', (ENV['CUCUMBER_FORMAT'] || 'progress')] +end + +desc "Default: run the cucumber scenarios" +task :default => :cucumber + diff --git a/appraisal.gemspec b/appraisal.gemspec new file mode 100644 index 00000000..69787ffe --- /dev/null +++ b/appraisal.gemspec @@ -0,0 +1,26 @@ +Gem::Specification.new do |s| + s.name = %q{appraisal} + s.version = '0.1' + s.summary = %q{Find out how much your Ruby gems are worth} + s.description = %q{appraisal integrates with bundler and rake to test your library against different versions of dependencies in repeatable scenarios called "appraisals."} + + s.files = Dir['[A-Z]*', 'lib/**/*.rb', 'features/**/*'] + s.require_path = 'lib' + s.test_files = Dir['features/**/*'] + + s.has_rdoc = false + + s.authors = ["Joe Ferris"] + s.email = %q{jferris@thoughtbot.com} + s.homepage = "http://github.com/thoughtbot/appraisal" + + s.add_development_dependency('cucumber') + s.add_development_dependency('aruba') + + s.add_runtime_dependency('rake') + s.add_runtime_dependency('bundler') + + s.platform = Gem::Platform::RUBY + s.rubygems_version = %q{1.2.0} +end + diff --git a/features/appraisals.feature b/features/appraisals.feature new file mode 100644 index 00000000..3addbc93 --- /dev/null +++ b/features/appraisals.feature @@ -0,0 +1,57 @@ +Feature: run a rake task through several appraisals + + Background: + Given a directory named "projecto" + When I cd to "projecto" + And I write to "Gemfile" with: + """ + source "http://rubygems.org" + gem "rake" + gem "factory_girl" + """ + When I add "appraisal" from this project as a dependency + And I write to "Appraisals" with: + """ + appraise "1.3.2" do + gem "factory_girl", "1.3.2" + end + appraise "1.3.0" do + gem "factory_girl", "1.3.0" + end + """ + When I write to "Rakefile" with: + """ + require 'rubygems' + require 'bundler/setup' + require 'appraisal' + task :version do + require 'factory_girl' + puts "Loaded #{Factory::VERSION}" + end + task :default => :version + """ + When I successfully run "rake appraisal:install --trace" + + @disable-bundler + Scenario: run a specific task with one appraisal + When I successfully run "rake appraisal:1.3.0 version --trace" + Then the output should contain "Loaded 1.3.0" + + @disable-bundler + Scenario: run a specific task with all appraisals + When I successfully run "rake appraisal version --trace" + Then the output should contain "Loaded 1.3.0" + And the output should contain "Loaded 1.3.2" + And the output should not contain "Invoke version" + + @disable-bundler + Scenario: run the default task with one appraisal + When I successfully run "rake appraisal:1.3.0 --trace" + Then the output should contain "Loaded 1.3.0" + + @disable-bundler + Scenario: run the default task with all appraisals + When I successfully run "rake appraisal --trace" + Then the output should contain "Loaded 1.3.0" + And the output should contain "Loaded 1.3.2" + diff --git a/features/step_definitions/dependency_steps.rb b/features/step_definitions/dependency_steps.rb new file mode 100644 index 00000000..36864d48 --- /dev/null +++ b/features/step_definitions/dependency_steps.rb @@ -0,0 +1,4 @@ +When /^I add "([^"]*)" from this project as a dependency$/ do |gem_name| + append_to_file('Gemfile', %{\ngem "#{gem_name}", :path => "#{PROJECT_ROOT}"}) +end + diff --git a/features/support/env.rb b/features/support/env.rb new file mode 100644 index 00000000..2c40011a --- /dev/null +++ b/features/support/env.rb @@ -0,0 +1,4 @@ +require 'aruba' + +PROJECT_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..', '..')).freeze + diff --git a/lib/appraisal.rb b/lib/appraisal.rb new file mode 100644 index 00000000..131292db --- /dev/null +++ b/lib/appraisal.rb @@ -0,0 +1,4 @@ +require 'appraisal/task' + +Appraisal::Task.new + diff --git a/lib/appraisal/appraisal.rb b/lib/appraisal/appraisal.rb new file mode 100644 index 00000000..6737f434 --- /dev/null +++ b/lib/appraisal/appraisal.rb @@ -0,0 +1,46 @@ +require 'appraisal/gemfile' +require 'appraisal/command' +require 'fileutils' + +module Appraisal + # Represents one appraisal and its dependencies + class Appraisal + attr_reader :name, :gemfile + + def initialize(name, source_gemfile) + @name = name + @gemfile = source_gemfile.dup + end + + def gem(name, *requirements) + gemfile.gem(name, *requirements) + end + + def write_gemfile + ::File.open(gemfile_path, "w") do |file| + file.puts("# This file was generated by Appraisal") + file.puts + file.write(gemfile.to_s) + end + end + + def install + Command.new("bundle install --gemfile=#{gemfile_path}").run + end + + def gemfile_path + unless ::File.exist?(gemfile_root) + FileUtils.mkdir(gemfile_root) + end + + ::File.join(gemfile_root, "#{name}.gemfile") + end + + private + + def gemfile_root + "gemfiles" + end + end +end + diff --git a/lib/appraisal/command.rb b/lib/appraisal/command.rb new file mode 100644 index 00000000..71b2c11c --- /dev/null +++ b/lib/appraisal/command.rb @@ -0,0 +1,53 @@ +module Appraisal + # Executes commands with a clean environment + class Command + BUNDLER_ENV_VARS = %w(RUBYOPT BUNDLE_PATH BUNDLE_BIN_PATH BUNDLE_GEMFILE).freeze + + def self.from_args(gemfile) + command = ([$0] + ARGV.slice(1, ARGV.size)).join(' ') + new(command, gemfile) + end + + def initialize(command, gemfile = nil) + @original_env = {} + @gemfile = gemfile + @command = command + end + + def run + announce + with_clean_env { Kernel.system(@command) } + end + + def exec + announce + with_clean_env { Kernel.exec(@command) } + end + + private + + def with_clean_env + unset_bundler_env_vars + ENV['BUNDLE_GEMFILE'] = @gemfile + yield + ensure + restore_env + end + + def announce + puts ">> BUNDLE_GEMFILE=#{@gemfile} #{@command}" + end + + def unset_bundler_env_vars + BUNDLER_ENV_VARS.each do |key| + @original_env[key] = ENV[key] + ENV[key] = nil + end + end + + def restore_env + @original_env.each { |key, value| ENV[key] = value } + end + end +end + diff --git a/lib/appraisal/dependency.rb b/lib/appraisal/dependency.rb new file mode 100644 index 00000000..c44c6cd0 --- /dev/null +++ b/lib/appraisal/dependency.rb @@ -0,0 +1,27 @@ +module Appraisal + # Dependency on a gem and optional version requirements + class Dependency + attr_reader :name, :requirements + + def initialize(name, requirements) + @name = name + @requirements = requirements + end + + def to_s + gem_name = %{gem "#{name}"} + if requirements.nil? || requirements.empty? + gem_name + else + "#{gem_name}, #{inspect_requirements}" + end + end + + private + + def inspect_requirements + requirements.map { |requirement| requirement.inspect }.join(", ") + end + end +end + diff --git a/lib/appraisal/file.rb b/lib/appraisal/file.rb new file mode 100644 index 00000000..06926508 --- /dev/null +++ b/lib/appraisal/file.rb @@ -0,0 +1,41 @@ +require 'appraisal/appraisal' +require 'appraisal/gemfile' + +module Appraisal + # Loads and parses Appraisal files + class File + attr_reader :appraisals, :gemfile + + def self.each(&block) + new.each(&block) + end + + def initialize + @appraisals = [] + @gemfile = Gemfile.new + @gemfile.load('Gemfile') + run(IO.read(path)) + end + + def each(&block) + appraisals.each(&block) + end + + def appraise(name, &block) + @appraisals << Appraisal.new(name, gemfile).tap do |appraisal| + appraisal.instance_eval(&block) + end + end + + private + + def run(definitions) + instance_eval definitions, __FILE__, __LINE__ + end + + def path + 'Appraisals' + end + end +end + diff --git a/lib/appraisal/gemfile.rb b/lib/appraisal/gemfile.rb new file mode 100644 index 00000000..3916d22e --- /dev/null +++ b/lib/appraisal/gemfile.rb @@ -0,0 +1,42 @@ +require 'appraisal/dependency' + +module Appraisal + # Load bundler Gemfiles and merge dependencies + class Gemfile + attr_reader :dependencies + + def initialize + @dependencies = {} + end + + def load(path) + run(IO.read(path)) + end + + def run(definitions) + instance_eval(definitions, __FILE__, __LINE__) + end + + def gem(name, *requirements) + @dependencies[name] = Dependency.new(name, requirements) + end + + def source(source) + @source = source + end + + def to_s + %{source "#{@source}"\n} << + dependencies.values.map { |dependency| dependency.to_s }.join("\n") + end + + def dup + Gemfile.new.tap do |gemfile| + gemfile.source @source + dependencies.values.each do |dependency| + gemfile.gem(dependency.name, *dependency.requirements) + end + end + end + end +end diff --git a/lib/appraisal/task.rb b/lib/appraisal/task.rb new file mode 100644 index 00000000..11ce02ae --- /dev/null +++ b/lib/appraisal/task.rb @@ -0,0 +1,44 @@ +require 'appraisal/file' +require 'rake/tasklib' + +module Appraisal + # Defines tasks for installing appraisal dependencies and running other tasks + # for a given appraisal. + class Task < Rake::TaskLib + def initialize + namespace :appraisal do + desc "Generate a Gemfile for each appraisal" + task :gemfiles do + File.each do |appraisal| + appraisal.write_gemfile + end + end + + desc "Resolve and install dependencies for each appraisal" + task :install => :gemfiles do + File.each do |appraisal| + appraisal.install + end + end + + File.each do |appraisal| + desc "Run the given task for appraisal #{appraisal.name}" + task appraisal.name do + Command.from_args(appraisal.gemfile_path).exec + end + end + + task :all do + File.each do |appraisal| + Command.from_args(appraisal.gemfile_path).run + end + exit + end + end + + desc "Run the given task for all appraisals" + task :appraisal => "appraisal:all" + end + end +end +