From 3fb4cdb3b7bf531c94ce6c725d18f5f108d373d3 Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Sun, 26 Oct 2014 18:52:55 -0600 Subject: [PATCH] Add new RSpec acceptance tests to replace Cucumber --- Rakefile | 7 + .../active_model_integration_spec.rb | 20 ++ spec/acceptance/independent_matchers_spec.rb | 56 +++++ spec/acceptance/rails_integration_spec.rb | 139 ++++++++++++ spec/acceptance_spec_helper.rb | 21 ++ spec/support/acceptance/helpers.rb | 33 +++ .../helpers/active_model_helpers.rb | 7 + .../acceptance/helpers/array_helpers.rb | 13 ++ .../acceptance/helpers/base_helpers.rb | 14 ++ .../acceptance/helpers/command_helpers.rb | 51 +++++ .../acceptance/helpers/file_helpers.rb | 23 ++ .../support/acceptance/helpers/gem_helpers.rb | 31 +++ .../acceptance/helpers/minitest_helpers.rb | 21 ++ .../helpers/pluralization_helpers.rb | 13 ++ .../helpers/rails_version_helpers.rb | 9 + .../acceptance/helpers/rspec_helpers.rb | 26 +++ .../helpers/ruby_version_helpers.rb | 9 + .../acceptance/helpers/step_helpers.rb | 110 +++++++++ .../acceptance/matchers/have_output.rb | 31 +++ ...ndicate_number_of_tests_was_run_matcher.rb | 55 +++++ .../indicate_that_tests_were_run_matcher.rb | 103 +++++++++ spec/support/tests/bundle.rb | 94 ++++++++ spec/support/tests/command_runner.rb | 213 ++++++++++++++++++ spec/support/tests/filesystem.rb | 84 +++++++ spec/support/tests/version.rb | 45 ++++ spec/unit_spec_helper.rb | 2 +- 26 files changed, 1229 insertions(+), 1 deletion(-) create mode 100644 spec/acceptance/active_model_integration_spec.rb create mode 100644 spec/acceptance/independent_matchers_spec.rb create mode 100644 spec/acceptance/rails_integration_spec.rb create mode 100644 spec/acceptance_spec_helper.rb create mode 100644 spec/support/acceptance/helpers.rb create mode 100644 spec/support/acceptance/helpers/active_model_helpers.rb create mode 100644 spec/support/acceptance/helpers/array_helpers.rb create mode 100644 spec/support/acceptance/helpers/base_helpers.rb create mode 100644 spec/support/acceptance/helpers/command_helpers.rb create mode 100644 spec/support/acceptance/helpers/file_helpers.rb create mode 100644 spec/support/acceptance/helpers/gem_helpers.rb create mode 100644 spec/support/acceptance/helpers/minitest_helpers.rb create mode 100644 spec/support/acceptance/helpers/pluralization_helpers.rb create mode 100644 spec/support/acceptance/helpers/rails_version_helpers.rb create mode 100644 spec/support/acceptance/helpers/rspec_helpers.rb create mode 100644 spec/support/acceptance/helpers/ruby_version_helpers.rb create mode 100644 spec/support/acceptance/helpers/step_helpers.rb create mode 100644 spec/support/acceptance/matchers/have_output.rb create mode 100644 spec/support/acceptance/matchers/indicate_number_of_tests_was_run_matcher.rb create mode 100644 spec/support/acceptance/matchers/indicate_that_tests_were_run_matcher.rb create mode 100644 spec/support/tests/bundle.rb create mode 100644 spec/support/tests/command_runner.rb create mode 100644 spec/support/tests/filesystem.rb create mode 100644 spec/support/tests/version.rb diff --git a/Rakefile b/Rakefile index 583436b9..893dcad4 100644 --- a/Rakefile +++ b/Rakefile @@ -15,6 +15,13 @@ RSpec::Core::RakeTask.new('spec:unit') do |t| t.verbose = false end +RSpec::Core::RakeTask.new('spec:acceptance') do |t| + t.ruby_opts = '-w -r ./spec/report_warnings' + t.pattern = "spec/acceptance/**/*_spec.rb" + t.rspec_opts = '--color --format progress' + t.verbose = false +end + Cucumber::Rake::Task.new do |t| options = [] diff --git a/spec/acceptance/active_model_integration_spec.rb b/spec/acceptance/active_model_integration_spec.rb new file mode 100644 index 00000000..2f4971ea --- /dev/null +++ b/spec/acceptance/active_model_integration_spec.rb @@ -0,0 +1,20 @@ +require 'acceptance_spec_helper' + +describe 'shoulda-matchers integrates with an ActiveModel project' do + specify 'and loads without errors' do + create_active_model_project + + add_shoulda_matchers_to_project + + write_file 'load_dependencies.rb', <<-FILE + require 'active_model' + require 'shoulda-matchers' + + puts ActiveModel::VERSION::STRING + puts "Loaded all dependencies without errors" + FILE + + result = run_command('bundle exec ruby load_dependencies.rb') + expect(result).to have_output('Loaded all dependencies without errors') + end +end diff --git a/spec/acceptance/independent_matchers_spec.rb b/spec/acceptance/independent_matchers_spec.rb new file mode 100644 index 00000000..9d3847cd --- /dev/null +++ b/spec/acceptance/independent_matchers_spec.rb @@ -0,0 +1,56 @@ +require 'acceptance_spec_helper' + +describe 'shoulda-matchers has independent matchers' do + context 'specifically delegate_method' do + specify 'and integrates with a Ruby application that uses Minitest' do + create_generic_bundler_project + add_minitest_to_project + + write_file 'lib/post_office.rb', <<-FILE + class PostOffice + end + FILE + + write_file 'lib/courier.rb', <<-FILE + require 'forwardable' + + class Courier + extend Forwardable + + def_delegators :post_office, :deliver + + attr_reader :post_office + + def initialize(post_office) + @post_office = post_office + end + end + FILE + + write_minitest_test 'test/courier_test.rb' do |test_case_superclass| + <<-FILE + require "test_helper" + require "courier" + require "post_office" + + class CourierTest < #{test_case_superclass} + subject { Courier.new(post_office) } + + should delegate_method(:deliver).to(:post_office) + + def post_office + PostOffice.new + end + end + FILE + end + + result = run_n_unit_tests('test/courier_test.rb') + + expect(result).to indicate_number_of_tests_was_run(1) + expect(result).to have_output( + 'Courier should delegate #deliver to #post_office object' + ) + end + end +end diff --git a/spec/acceptance/rails_integration_spec.rb b/spec/acceptance/rails_integration_spec.rb new file mode 100644 index 00000000..cf8fabc8 --- /dev/null +++ b/spec/acceptance/rails_integration_spec.rb @@ -0,0 +1,139 @@ +require 'acceptance_spec_helper' + +describe 'shoulda-matchers integrates with Rails' do + before do + create_rails_application + + write_file 'db/migrate/1_create_users.rb', <<-FILE + class CreateUsers < ActiveRecord::Migration + def self.up + create_table :users do |t| + t.string :name + end + end + end + FILE + + run_rake_tasks!('db:migrate', 'db:test:prepare') + + write_file 'app/models/user.rb', <<-FILE + class User < ActiveRecord::Base + validates_presence_of :name + end + FILE + + write_file 'app/controllers/examples_controller.rb', <<-FILE + class ExamplesController < ApplicationController + def show + @example = 'hello' + render nothing: true + end + end + FILE + + configure_routes_with_single_wildcard_route + end + + specify 'in a project that uses Test::Unit' do + updating_bundle do + add_gems_for_n_unit + add_shoulda_matchers_to_project + end + + run_tests_for_n_unit + end + + specify 'in a project that uses RSpec' do + updating_bundle do + add_gems_for_rspec + add_shoulda_matchers_to_project + end + + run_tests_for_rspec + end + + specify 'in a project that uses Spring' do + unless bundle_includes?('spring') + skip "Spring isn't a dependency of this Appraisal" + end + + updating_bundle do + add_spring_to_project + add_gems_for_rspec + add_shoulda_matchers_to_project(manually: true) + end + + run_command_within_bundle!('spring stop') + + run_tests_for_rspec + end + + specify 'in a project that combines both RSpec and Test::Unit' do + updating_bundle do + add_gems_for_n_unit + add_gems_for_rspec + add_shoulda_matchers_to_project + end + + run_tests_for_n_unit + run_tests_for_rspec + end + + def add_gems_for_n_unit + add_gem 'shoulda-context' + end + + def add_gems_for_rspec + add_rspec_rails_to_project! + end + + def run_tests_for_n_unit + write_file 'test/unit/user_test.rb', <<-FILE + require 'test_helper' + + class UserTest < ActiveSupport::TestCase + should validate_presence_of(:name) + end + FILE + + write_file 'test/functional/examples_controller_test.rb', <<-FILE + require 'test_helper' + + class ExamplesControllerTest < ActionController::TestCase + def setup + get :show + end + + should respond_with(:success) + end + FILE + + result = run_n_unit_test_suite + + expect(result).to indicate_that_tests_were_run(unit: 1, functional: 1) + expect(result).to have_output('User should require name to be set') + expect(result).to have_output('should respond with 200') + end + + def run_tests_for_rspec + add_spec 'spec/models/user_spec.rb', <<-FILE + describe User do + it { should validate_presence_of(:name) } + end + FILE + + add_spec 'spec/controllers/examples_controller_spec.rb', <<-FILE + describe ExamplesController, "show" do + before { get :show } + + it { should respond_with(:success) } + end + FILE + + result = run_rspec_suite + + expect(result).to have_output('2 examples, 0 failures') + expect(result).to have_output('should require name to be set') + expect(result).to have_output('should respond with 200') + end +end diff --git a/spec/acceptance_spec_helper.rb b/spec/acceptance_spec_helper.rb new file mode 100644 index 00000000..e94463aa --- /dev/null +++ b/spec/acceptance_spec_helper.rb @@ -0,0 +1,21 @@ +require 'rspec/core' + +Dir[ File.join(File.expand_path('../support/acceptance/**/*.rb', __FILE__)) ].sort.each do |file| + require file +end + +RSpec.configure do |config| + config.expect_with :rspec do |c| + c.syntax = :expect + end + + if config.respond_to?(:infer_spec_type_from_file_location!) + config.infer_spec_type_from_file_location! + end + + AcceptanceTests::Helpers.configure_example_group(config) + + config.include AcceptanceTests::Matchers +end + +$VERBOSE = true diff --git a/spec/support/acceptance/helpers.rb b/spec/support/acceptance/helpers.rb new file mode 100644 index 00000000..7a580a44 --- /dev/null +++ b/spec/support/acceptance/helpers.rb @@ -0,0 +1,33 @@ +require_relative 'helpers/active_model_helpers' +require_relative 'helpers/base_helpers' +require_relative 'helpers/command_helpers' +require_relative 'helpers/gem_helpers' +require_relative 'helpers/rails_version_helpers' +require_relative 'helpers/rspec_helpers' +require_relative 'helpers/ruby_version_helpers' +require_relative 'helpers/step_helpers' + +module AcceptanceTests + module Helpers + def self.configure_example_group(example_group) + example_group.include(self) + + example_group.before do + fs.clean + end + + # example_group.around do |example| + # Bundler.with_clean_env { example.run } + # end + end + + include ActiveModelHelpers + include BaseHelpers + include CommandHelpers + include GemHelpers + include RailsVersionHelpers + include RspecHelpers + include RubyVersionHelpers + include StepHelpers + end +end diff --git a/spec/support/acceptance/helpers/active_model_helpers.rb b/spec/support/acceptance/helpers/active_model_helpers.rb new file mode 100644 index 00000000..ad64949f --- /dev/null +++ b/spec/support/acceptance/helpers/active_model_helpers.rb @@ -0,0 +1,7 @@ +module AcceptanceTests + module ActiveModelHelpers + def active_model_version + Bundler.definition.specs['activemodel'][0].version + end + end +end diff --git a/spec/support/acceptance/helpers/array_helpers.rb b/spec/support/acceptance/helpers/array_helpers.rb new file mode 100644 index 00000000..e9d3b2eb --- /dev/null +++ b/spec/support/acceptance/helpers/array_helpers.rb @@ -0,0 +1,13 @@ +module AcceptanceTests + module ArrayHelpers + def to_sentence(array) + if array.size == 1 + array[0] + elsif array.size == 2 + array.join(' and ') + else + to_sentence(array[1..-2].join(', '), [array[-1]]) + end + end + end +end diff --git a/spec/support/acceptance/helpers/base_helpers.rb b/spec/support/acceptance/helpers/base_helpers.rb new file mode 100644 index 00000000..e28fd1a7 --- /dev/null +++ b/spec/support/acceptance/helpers/base_helpers.rb @@ -0,0 +1,14 @@ +require_relative '../../tests/filesystem' +require_relative '../../tests/bundle' + +module AcceptanceTests + module BaseHelpers + def fs + @_fs ||= Tests::Filesystem.new + end + + def bundle + @_bundle ||= Tests::Bundle.new + end + end +end diff --git a/spec/support/acceptance/helpers/command_helpers.rb b/spec/support/acceptance/helpers/command_helpers.rb new file mode 100644 index 00000000..77ca4f49 --- /dev/null +++ b/spec/support/acceptance/helpers/command_helpers.rb @@ -0,0 +1,51 @@ +require_relative 'base_helpers' +require_relative '../../tests/command_runner' + +module AcceptanceTests + module CommandHelpers + include BaseHelpers + extend RSpec::Matchers::DSL + + def run_command(*args) + Tests::CommandRunner.run(*args) do |runner| + runner.directory = fs.project_directory + yield runner if block_given? + end + end + + def run_command!(*args) + run_command(*args) do |runner| + runner.run_successfully = true + yield runner if block_given? + end + end + + def run_command_within_bundle(*args) + run_command(*args) do |runner| + runner.command_prefix = 'bundle exec' + runner.env['BUNDLE_GEMFILE'] = fs.find_in_project('Gemfile').to_s + + runner.around_command do |run_command| + Bundler.with_clean_env(&run_command) + end + + yield runner if block_given? + end + end + + def run_command_within_bundle!(*args) + run_command_within_bundle(*args) do |runner| + runner.run_successfully = true + yield runner if block_given? + end + end + + def run_rake_tasks(*tasks) + run_command_within_bundle('rake', *tasks) + end + + def run_rake_tasks!(*tasks) + run_command_within_bundle!('rake', *tasks) + end + end +end diff --git a/spec/support/acceptance/helpers/file_helpers.rb b/spec/support/acceptance/helpers/file_helpers.rb new file mode 100644 index 00000000..62c3633f --- /dev/null +++ b/spec/support/acceptance/helpers/file_helpers.rb @@ -0,0 +1,23 @@ +require_relative 'base_helpers' + +module AcceptanceTests + module FileHelpers + include BaseHelpers + + def append_to_file(path, content, options = {}) + fs.append_to_file(path, content, options) + end + + def append_to_file_following(path, content_to_add, insertion_point) + fs.append_to_file_following(path, content_to_add, insertion_point) + end + + def remove_from_file(path, pattern) + fs.remove_from_file(path, pattern) + end + + def write_file(path, content) + fs.write(path, content) + end + end +end diff --git a/spec/support/acceptance/helpers/gem_helpers.rb b/spec/support/acceptance/helpers/gem_helpers.rb new file mode 100644 index 00000000..e3c3d542 --- /dev/null +++ b/spec/support/acceptance/helpers/gem_helpers.rb @@ -0,0 +1,31 @@ +require_relative 'base_helpers' +require_relative 'command_helpers' +require_relative 'file_helpers' + +module AcceptanceTests + module GemHelpers + include BaseHelpers + include CommandHelpers + include FileHelpers + + def add_gem(gem, *args) + bundle.add_gem(gem, *args) + end + + def install_gems + bundle.install_gems + end + + def updating_bundle(&block) + bundle.updating(&block) + end + + def bundle_version_of(gem) + bundle.version_of(gem) + end + + def bundle_includes?(gem) + bundle.includes?(gem) + end + end +end diff --git a/spec/support/acceptance/helpers/minitest_helpers.rb b/spec/support/acceptance/helpers/minitest_helpers.rb new file mode 100644 index 00000000..045a3988 --- /dev/null +++ b/spec/support/acceptance/helpers/minitest_helpers.rb @@ -0,0 +1,21 @@ +module AcceptanceTests + module MinitestHelpers + def minitest_test_case_superclass + if minitest_gte_5? + 'Minitest::Test' + else + 'MiniTest::Unit::TestCase' + end + end + + def minitest_gte_5? + if minitest_version + Gem::Requirement.new('>= 5').satisfied_by?(minitest_version) + end + end + + def minitest_version + Bundler.definition.specs['minitest'][0].version + end + end +end diff --git a/spec/support/acceptance/helpers/pluralization_helpers.rb b/spec/support/acceptance/helpers/pluralization_helpers.rb new file mode 100644 index 00000000..9aacd7ae --- /dev/null +++ b/spec/support/acceptance/helpers/pluralization_helpers.rb @@ -0,0 +1,13 @@ +module AcceptanceTests + module PluralizationHelpers + def pluralize(count, singular_version, plural_version = nil) + plural_version ||= singular_version + 's' + + if count == 1 + "#{count} #{singular_version}" + else + "#{count} #{plural_version}" + end + end + end +end diff --git a/spec/support/acceptance/helpers/rails_version_helpers.rb b/spec/support/acceptance/helpers/rails_version_helpers.rb new file mode 100644 index 00000000..055bf42d --- /dev/null +++ b/spec/support/acceptance/helpers/rails_version_helpers.rb @@ -0,0 +1,9 @@ +module AcceptanceTests + module RailsVersionHelpers + include GemHelpers + + def rails_version + bundle_version_of('rails') + end + end +end diff --git a/spec/support/acceptance/helpers/rspec_helpers.rb b/spec/support/acceptance/helpers/rspec_helpers.rb new file mode 100644 index 00000000..6d421631 --- /dev/null +++ b/spec/support/acceptance/helpers/rspec_helpers.rb @@ -0,0 +1,26 @@ +module AcceptanceTests + module RspecHelpers + include GemHelpers + + def rspec_rails_version + bundle_version_of('rspec-rails') + end + + def add_spec(path, content) + content = "require '#{spec_helper_require_path}'\n#{content}" + write_file path, content + end + + def spec_helper_require_path + if rspec_rails_version >= 3 + 'rails_helper' + else + 'spec_helper' + end + end + + def spec_helper_file_path + "spec/#{spec_helper_require_path}.rb" + end + end +end diff --git a/spec/support/acceptance/helpers/ruby_version_helpers.rb b/spec/support/acceptance/helpers/ruby_version_helpers.rb new file mode 100644 index 00000000..8725944b --- /dev/null +++ b/spec/support/acceptance/helpers/ruby_version_helpers.rb @@ -0,0 +1,9 @@ +require_relative '../../tests/version' + +module AcceptanceTests + module RubyVersionHelpers + def ruby_version + Tests::Version.new(RUBY_VERSION) + end + end +end diff --git a/spec/support/acceptance/helpers/step_helpers.rb b/spec/support/acceptance/helpers/step_helpers.rb new file mode 100644 index 00000000..de104af8 --- /dev/null +++ b/spec/support/acceptance/helpers/step_helpers.rb @@ -0,0 +1,110 @@ +require_relative 'file_helpers' +require_relative 'gem_helpers' +require_relative 'minitest_helpers' + +module AcceptanceTests + module StepHelpers + include FileHelpers + include GemHelpers + include MinitestHelpers + + extend RSpec::Matchers::DSL + + def create_active_model_project + create_generic_bundler_project + add_gem 'activemodel', active_model_version + end + + def create_generic_bundler_project + fs.create + run_command! 'bundle init' + end + + def add_shoulda_matchers_to_project(options = {}) + gem_options = { path: fs.root_directory } + + if options[:manually] + gem_options[:require] = false + end + + add_gem 'shoulda-matchers', gem_options + + if options[:manually] + append_to_file spec_helper_file_path, + "require 'shoulda/matchers'", + following: "require 'rspec/rails'" + end + end + + def add_minitest_to_project + add_gem 'shoulda-context' + add_gem 'minitest-reporters' + write_file 'test/test_helper.rb', <<-FILE + require 'minitest/autorun' + require 'minitest/reporters' + require 'shoulda/context' + require 'shoulda/matchers' + + Minitest::Reporters.use!(Minitest::Reporters::SpecReporter.new) + FILE + end + + def write_minitest_test(path, &block) + contents = block.call(minitest_test_case_superclass) + write_file(path, contents) + end + + def run_n_unit_tests(*paths) + run_command 'ruby -I lib -I test', *paths + end + + def run_n_unit_test_suite + run_rake_tasks('test', env: { TESTOPTS: '-v' }) + end + + def create_rails_application + command = "bundle exec rails new #{fs.project_directory} --skip-bundle" + + run_command!(command) do |runner| + runner.directory = nil + end + + updating_bundle do |bundle| + bundle.remove_gem 'turn' + bundle.remove_gem 'coffee-rails' + bundle.remove_gem 'uglifier' + + # if ruby_version >= '1.9.3' + # bundle.add_gem 'rake', '~> 0.9' + # run_command! 'bundle update rake --local' + # end + end + end + + def configure_routes_with_single_wildcard_route + write_file 'config/routes.rb', <<-FILE + Rails.application.routes.draw do + get ':controller(/:action(/:id(.:format)))' + end + FILE + end + + def add_rspec_rails_to_project! + add_gem 'rspec-rails', rspec_rails_version + run_command_within_bundle!('rails g rspec:install') + remove_from_file '.rspec', '--warnings' + end + + def run_rspec_suite + run_rake_tasks('spec', env: { SPEC_OPTS: '-fd' }) + end + + def add_spring_to_project + if rails_version < 4 + add_gem 'spring' + end + + add_gem 'spring-commands-rspec' + end + end +end diff --git a/spec/support/acceptance/matchers/have_output.rb b/spec/support/acceptance/matchers/have_output.rb new file mode 100644 index 00000000..68ab02d9 --- /dev/null +++ b/spec/support/acceptance/matchers/have_output.rb @@ -0,0 +1,31 @@ +module AcceptanceTests + module Matchers + def have_output(output) + HaveOutputMatcher.new(output) + end + + class HaveOutputMatcher + def initialize(output) + @output = output + end + + def matches?(runner) + @runner = runner + runner.has_output?(output) + end + + def failure_message + "Expected command to have output, but did not.\n\n" + + "Command: #{runner.formatted_command}\n\n" + + "Expected output:\n" + + output + "\n\n" + + "Actual output:\n" + + runner.elided_output + end + + protected + + attr_reader :output, :runner + end + end +end diff --git a/spec/support/acceptance/matchers/indicate_number_of_tests_was_run_matcher.rb b/spec/support/acceptance/matchers/indicate_number_of_tests_was_run_matcher.rb new file mode 100644 index 00000000..bb47e07e --- /dev/null +++ b/spec/support/acceptance/matchers/indicate_number_of_tests_was_run_matcher.rb @@ -0,0 +1,55 @@ +require_relative '../helpers/pluralization_helpers' +require_relative '../helpers/rails_version_helpers' + +module AcceptanceTests + module Matchers + def indicate_number_of_tests_was_run(expected_output) + IndicateNumberOfTestsWasRunMatcher.new(expected_output) + end + + class IndicateNumberOfTestsWasRunMatcher + include PluralizationHelpers + include RailsVersionHelpers + + def initialize(number) + @number = number + end + + def matches?(runner) + @runner = runner + expected_output === actual_output + end + + def failure_message + message = "Expected output to indicate that #{some_tests_were_run}.\n" + + "Expected output: #{expected_output}\n" + + if actual_output.empty? + message << 'Actual output: (empty)' + else + message << "Actual output:\n#{actual_output}" + end + + message + end + + protected + + attr_reader :number, :runner + + private + + def expected_output + /#{number} (tests|runs), #{number} assertions, 0 failures, 0 errors(, 0 skips)?/ + end + + def actual_output + runner.output + end + + def some_tests_were_run + pluralize(number, 'test was', 'tests were') + ' run' + end + end + end +end diff --git a/spec/support/acceptance/matchers/indicate_that_tests_were_run_matcher.rb b/spec/support/acceptance/matchers/indicate_that_tests_were_run_matcher.rb new file mode 100644 index 00000000..f8488431 --- /dev/null +++ b/spec/support/acceptance/matchers/indicate_that_tests_were_run_matcher.rb @@ -0,0 +1,103 @@ +require_relative '../helpers/array_helpers' +require_relative '../helpers/pluralization_helpers' +require_relative '../helpers/rails_version_helpers' + +module AcceptanceTests + module Matchers + def indicate_that_tests_were_run(series) + IndicateThatTestsWereRunMatcher.new(series) + end + + class IndicateThatTestsWereRunMatcher + include ArrayHelpers + include PluralizationHelpers + include RailsVersionHelpers + + def initialize(args) + @args = args + @series = args.values + end + + def matches?(runner) + @runner = runner + !matching_expected_output.nil? + end + + def failure_message + "Expected output to indicate that #{some_tests_were_run}.\n" + + "#{formatted_expected_output}\n" + + "#{formatted_actual_output}\n" + end + + protected + + attr_reader :args, :series, :runner + + private + + def expected_outputs + [ + expected_output_for_rails_3, + expected_output_for_turn, + expected_output_for_rails_4 + ] + end + + def matching_expected_output + @_matching_expected_output ||= + expected_outputs.detect do |expected_output| + actual_output =~ expected_output + end + end + + def expected_output_for_rails_3 + full_report = series.map do |number| + "#{number} tests, #{number} assertions, 0 failures, 0 errors(, 0 skips)?" + end.join('.+') + + Regexp.new(full_report, Regexp::MULTILINE) + end + + def expected_output_for_turn + full_report = series.map do |number| + "pass: #{number}, fail: 0, error: 0" + end.join('.+') + + Regexp.new(full_report, Regexp::MULTILINE) + end + + def expected_output_for_rails_4 + total = series.inject(:+) + /#{total} (tests|runs), #{total} assertions, 0 failures, 0 errors(, 0 skips)?/ + end + + def formatted_expected_output + if matching_expected_output + "Expected output:\n#{matching_actual_output}" + else + "Expected output: (n/a)" + end + end + + def actual_output + runner.output + end + + def formatted_actual_output + if actual_output.empty? + "Actual output: (empty)" + else + "Actual output:\n#{actual_output}" + end + end + + def some_tests_were_run + clauses = args.map do |type, number| + pluralize(number, "#{type} test was run", "#{type} tests were run") + end + + to_sentence(clauses) + end + end + end +end diff --git a/spec/support/tests/bundle.rb b/spec/support/tests/bundle.rb new file mode 100644 index 00000000..d659860c --- /dev/null +++ b/spec/support/tests/bundle.rb @@ -0,0 +1,94 @@ +require_relative 'filesystem' +require_relative 'command_runner' +require_relative 'version' + +module Tests + class Bundle + def initialize + @already_updating = false + @fs = Filesystem.new + end + + def updating(&block) + if already_updating? + yield self + return + end + + @already_updating = true + + yield self + + @already_updating = false + + install_gems + end + + def add_gem(gem, *args) + updating do + options = args.last.is_a?(Hash) ? args.pop : {} + version = args.shift + line = assemble_gem_line(gem, version, options) + fs.append_to_file('Gemfile', line) + end + end + + def remove_gem(gem) + updating do + fs.remove_from_file('Gemfile', /^gem ("|')gem\1/) + end + end + + def install_gems + CommandRunner.run!('bundle install --local') do |runner| + runner.retries = 5 + end + end + + def version_of(gem) + Version.new(Bundler.definition.specs[gem][0].version) + end + + def includes?(gem) + Bundler.definition.dependencies.any? do |dependency| + dependency.name == gem + end + end + + protected + + attr_reader :fs + + private + + def already_updating? + @already_updating + end + + def assemble_gem_line(gem, version, options) + formatted_options = options. + map { |key, value| "#{key}: #{formatted_value(value)}" }. + join(', ') + + line = %(gem '#{gem}') + + if version + line << %(, '#{version}') + end + + if options.any? + line << %(, #{formatted_options}) + end + + line << "\n" + end + + def formatted_value(value) + if value.is_a?(Pathname) + value.to_s.inspect + else + value.inspect + end + end + end +end diff --git a/spec/support/tests/command_runner.rb b/spec/support/tests/command_runner.rb new file mode 100644 index 00000000..9f9c569f --- /dev/null +++ b/spec/support/tests/command_runner.rb @@ -0,0 +1,213 @@ +require 'timeout' +require 'shellwords' + +module Tests + class CommandRunner + TimeoutError = Class.new(StandardError) + + def self.run(*args) + new(*args).tap do |runner| + yield runner + runner.call + end + end + + def self.run!(*args) + run(*args) do |runner| + runner.run_successfully = true + yield runner if block_given? + end + end + + attr_reader :status, :options, :env, :directory + attr_accessor :command_prefix, :run_quickly, :run_successfully, :retries, + :timeout + + def initialize(*args) + @reader, @writer = IO.pipe + options = (args.last.is_a?(Hash) ? args.pop : {}) + @args = args + @options = options.merge( + err: [:child, :out], + out: writer + ) + @env = extract_env_from(@options) + + @wrapper = ->(block) { block.call } + @command_prefix = '' + @directory = Dir.pwd + @run_quickly = false + @run_successfully = false + @retries = 1 + @num_times_run = 0 + @timeout = 20 + end + + def around_command(&block) + @wrapper = block + end + + def directory=(directory) + @directory = directory || Dir.pwd + end + + def formatted_command + [formatted_env, Shellwords.join(command)]. + select { |value| !value.empty? }. + join(' ') + end + + def call + possibly_retrying do + possibly_running_quickly do + debug { "\n\e[32mRunning command:\e[0m #{formatted_command}" } + wrapper.call(-> { run }) + debug { "\n" + divider('START') + output + divider('END') } + + if run_successfully && !success? + fail! + end + end + end + + self + end + + def stop + unless writer.closed? + writer.close + end + end + + def output + @_output ||= begin + stop + without_colors(reader.read) + end + end + + def elided_output + lines = output.split(/\n/) + new_lines = lines[0..4] + + if lines.size > 10 + new_lines << "(...#{lines.size - 10} more lines...)" + end + + new_lines << lines[-5..-1] + new_lines.join("\n") + end + + def success? + status.success? + end + + def exit_status + status.exitstatus + end + + def fail! + raise <<-MESSAGE +Command #{command.inspect} exited with status #{exit_status}. +Output: +#{divider('START') + output + divider('END')} + MESSAGE + end + + def has_output?(expected_output) + output.include?(expected_output) + end + + protected + + attr_reader :args, :reader, :writer, :wrapper + + private + + def extract_env_from(options) + options.delete(:env) { {} }.inject({}) do |hash, (key, value)| + hash[key.to_s] = value + hash + end + end + + def command + ([command_prefix] + args).flatten.flat_map do |word| + Shellwords.split(word) + end + end + + def formatted_env + env.map { |key, value| "#{key}=#{value.inspect}" }.join(' ') + end + + def run + Dir.chdir(directory) do + system(env, *command, options) + end + + @status = $? + end + + def possibly_running_quickly(&block) + if run_quickly + begin + Timeout.timeout(timeout, &block) + rescue Timeout::Error + stop + + message = + "Command timed out after #{timeout} seconds: #{formatted_command}\n" + + "Output:\n" + + elided_output + + raise TimeoutError, message + end + else + yield + end + end + + def possibly_retrying + begin + @num_times_run += 1 + yield + rescue => error + debug { "#{error.class}: #{error.message}" } + + if @num_times_run < @retries + sleep @num_times_run + retry + else + raise error + end + end + end + + def divider(title = '') + total_length = 72 + start_length = 3 + + string = '' + string << ('-' * start_length) + string << title + string << '-' * (total_length - start_length - title.length) + string << "\n" + string + end + + def without_colors(string) + string.gsub(/\e\[\d+(?:;\d+)?m(.+?)\e\[0m/, '\1') + end + + def debugging_enabled? + ENV['DEBUG_COMMANDS'] == '1' + end + + def debug(&block) + if debugging_enabled? + puts block.call + end + end + end +end diff --git a/spec/support/tests/filesystem.rb b/spec/support/tests/filesystem.rb new file mode 100644 index 00000000..70aa1352 --- /dev/null +++ b/spec/support/tests/filesystem.rb @@ -0,0 +1,84 @@ +require 'fileutils' + +module Tests + class Filesystem + PROJECT_NAME = 'test-project' + ROOT_DIRECTORY = Pathname.new('../../../..').expand_path(__FILE__) + TEMP_DIRECTORY = ROOT_DIRECTORY.join('tmp/acceptance') + PROJECT_DIRECTORY = TEMP_DIRECTORY.join(PROJECT_NAME) + + def root_directory + ROOT_DIRECTORY + end + + def temp_directory + TEMP_DIRECTORY + end + + def project_directory + PROJECT_DIRECTORY + end + + def within_project(&block) + Dir.chdir(project_directory, &block) + end + + def clean + if temp_directory.exist? + temp_directory.rmtree + end + end + + def create + project_directory.mkpath + end + + def find_in_project(path) + project_directory.join(path) + end + + def open(path, *args, &block) + find_in_project(path).open(*args, &block) + end + + def read(path) + find_in_project(path).read + end + + def write(path, content) + pathname = find_in_project(path) + pathname.dirname.mkpath + pathname.open('w') { |f| f.write(content) } + end + + def append_to_file(path, content, options = {}) + if options[:following] + append_to_file_following(path, content, options[:following]) + else + open(path, 'a') { |f| f.puts(content + "\n") } + end + end + + def append_to_file_following(path, content_to_add, insertion_point) + content_to_add = content_to_add + "\n" + + file_content = read(path) + file_lines = file_content.split("\n") + insertion_index = file_lines.find_index(insertion_point) + + if insertion_index.nil? + raise "Cannot find #{insertion_point.inspect} in #{path}" + end + + file_lines.insert(insertion_index + 1, content_to_add) + new_file_content = file_lines.join("\n") + write(path, new_file_content) + end + + def remove_from_file(path, pattern) + content = read(path) + content.sub!(/#{pattern}\n/, '') + write(path, content) + end + end +end diff --git a/spec/support/tests/version.rb b/spec/support/tests/version.rb new file mode 100644 index 00000000..4a2a66e4 --- /dev/null +++ b/spec/support/tests/version.rb @@ -0,0 +1,45 @@ +module Tests + class Version + def initialize(version) + @version = Gem::Version.new(version.to_s + '') + end + + def <(other_version) + compare?(:<, other_version) + end + + def <=(other_version) + compare?(:<=, other_version) + end + + def ==(other_version) + compare?(:==, other_version) + end + + def >=(other_version) + compare?(:>=, other_version) + end + + def >(other_version) + compare?(:>, other_version) + end + + def =~(other_version) + Gem::Requirement.new(other_version).satisfied_by?(version) + end + + def to_s + version.to_s + end + + protected + + attr_reader :version + + private + + def compare?(op, other_version) + Gem::Requirement.new("#{op} #{other_version}").satisfied_by?(version) + end + end +end diff --git a/spec/unit_spec_helper.rb b/spec/unit_spec_helper.rb index b93fce83..ac59a0a0 100644 --- a/spec/unit_spec_helper.rb +++ b/spec/unit_spec_helper.rb @@ -29,7 +29,7 @@ require 'rspec/rails' PROJECT_ROOT = File.expand_path('../..', __FILE__) $LOAD_PATH << File.join(PROJECT_ROOT, 'lib') -Dir[ File.join(File.expand_path('../support/unit/**/*.rb', __FILE__)) ].each do |file| +Dir[ File.join(File.expand_path('../support/unit/**/*.rb', __FILE__)) ].sort.each do |file| require file end