require "spring/env" module Spring module Test class RailsVersion attr_reader :version def initialize(string) @version = Gem::Version.new(string) end def rails_3? version < Gem::Version.new("4.0.0") end alias needs_testunit? rails_3? def test_command needs_testunit? ? 'bin/testunit' : 'bin/rake test' end def controller_tests_dir rails_3? ? 'functional' : 'controllers' end def bundles_spring? version.segments.take(2) == [4, 1] || version > Gem::Version.new("4.1") end def major version.segments[0] end def minor version.segments[1] end def to_s version.to_s end end class Application DEFAULT_TIMEOUT = ENV['CI'] ? 30 : 10 attr_reader :root, :spring_env def initialize(root) @root = Pathname.new(root) @spring_env = Spring::Env.new(root) end def exists? root.exist? end def stdout @stdout ||= IO.pipe end def stderr @stderr ||= IO.pipe end def log_file @log_file ||= path("tmp/spring.log").open("w+") end def env @env ||= { "GEM_HOME" => gem_home.to_s, "GEM_PATH" => "", "HOME" => user_home.to_s, "RAILS_ENV" => nil, "RACK_ENV" => nil, "SPRING_LOG" => log_file.path } end def path(addition) root.join addition end def gemfile path "Gemfile" end def gem_home path "vendor/gems/#{RUBY_VERSION}" end def user_home path "user_home" end def spring gem_home.join "bin/spring" end def rails_version @rails_version ||= RailsVersion.new(gemfile.read.match(/gem 'rails', '(.*)'/)[1]) end def spring_test_command "#{rails_version.test_command} #{test}" end def stop_spring run "#{spring} stop" rescue Errno::ENOENT end def test path "test/#{rails_version.controller_tests_dir}/posts_controller_test.rb" end def controller path "app/controllers/posts_controller.rb" end def application_config path "config/application.rb" end def spring_config path "config/spring.rb" end def run(command, opts = {}) start_time = Time.now Bundler.with_clean_env do Process.spawn( env, command.to_s, out: stdout.last, err: stderr.last, in: :close, chdir: root.to_s, ) end _, status = Timeout.timeout(opts.fetch(:timeout, DEFAULT_TIMEOUT)) { Process.wait2 } if pid = spring_env.pid @server_pid = pid lines = `ps -A -o ppid= -o pid= | egrep '^\\s*#{@server_pid}'`.lines @application_pids = lines.map { |l| l.split.last.to_i } end output = read_streams puts dump_streams(command, output) if ENV["SPRING_DEBUG"] @times << (Time.now - start_time) if @times output.merge(status: status, command: command) rescue Timeout::Error => e raise e, "Output:\n\n#{dump_streams(command, read_streams)}" end def with_timing @times = [] yield ensure @times = nil end def last_time @times.last end def first_time @times.first end def timing_ratio last_time / first_time end def read_streams { stdout: read_stream(stdout.first), stderr: read_stream(stderr.first), log: read_stream(log_file) } end def read_stream(stream) output = "" while IO.select([stream], [], [], 0.5) && !stream.eof? output << stream.readpartial(10240) end output end def dump_streams(command, streams) output = "$ #{command}\n" streams.each do |name, stream| unless stream.chomp.empty? output << "--- #{name} ---\n" output << "#{stream.chomp}\n" end end output << "\n" output end def debug(artifacts) artifacts = artifacts.dup artifacts.delete :status dump_streams(artifacts.delete(:command), artifacts) end def await_reload raise "no pid" if @application_pids.nil? || @application_pids.empty? Timeout.timeout(DEFAULT_TIMEOUT) do sleep 0.1 while @application_pids.any? { |p| process_alive?(p) } end end def run!(command, options = {}) attempts = (options.delete(:retry) || 0) + 1 artifacts = nil until attempts == 0 || artifacts && artifacts[:status].success? artifacts = run(command, options) attempts -= 1 end if artifacts[:status].success? artifacts else raise "command failed\n\n#{debug(artifacts)}" end end def bundle run! "(gem list bundler | grep bundler) || gem install bundler", timeout: nil, retry: 2 run! "bundle update --retry=2", timeout: nil end private def process_alive?(pid) Process.kill 0, pid true rescue Errno::ESRCH false end end class ApplicationGenerator attr_reader :version_constraint, :version, :application def initialize(version_constraint) @version_constraint = version_constraint @version = RailsVersion.new(version_constraint.split(' ').last) @application = Application.new(root) @bundled = false end def root "#{TEST_ROOT}/apps/rails-#{version.major}-#{version.minor}-spring-#{Spring::VERSION}" end def system(command) if ENV["SPRING_DEBUG"] puts "$ #{command}\n" else command = "(#{command}) > /dev/null" end Kernel.system(command) or raise "command failed: #{command}" puts if ENV["SPRING_DEBUG"] end # Sporadic SSL errors keep causing test failures so there are anti-SSL workarounds here def generate Bundler.with_clean_env do system("gem list rails --installed --version '#{version_constraint}' || " \ "gem install rails --clear-sources --source http://rubygems.org --version '#{version_constraint}'") @version = RailsVersion.new(`ruby -e 'puts Gem::Specification.find_by_name("rails", "#{version_constraint}").version'`.chomp) skips = %w(--skip-bundle --skip-javascript --skip-sprockets) skips << "--skip-spring" if version.bundles_spring? system("rails _#{version}_ new #{application.root} #{skips.join(' ')}") raise "application generation failed" unless application.exists? FileUtils.mkdir_p(application.gem_home) FileUtils.mkdir_p(application.user_home) FileUtils.rm_rf(application.path("test/performance")) File.write(application.gemfile, "#{application.gemfile.read}gem 'spring', '#{Spring::VERSION}'\n") if version.needs_testunit? File.write(application.gemfile, "#{application.gemfile.read}gem 'spring-commands-testunit'\n") end File.write(application.gemfile, application.gemfile.read.sub("https://rubygems.org", "http://rubygems.org")) if application.path("bin").exist? FileUtils.cp_r(application.path("bin"), application.path("bin_original")) end end install_spring application.run! "bundle exec rails g scaffold post title:string" application.run! "bundle exec rake db:migrate db:test:clone" end def generate_if_missing generate unless application.exists? end def install_spring return if @installed system("gem build spring.gemspec 2>&1") application.run! "gem install ../../../spring-#{Spring::VERSION}.gem", timeout: nil application.bundle FileUtils.rm_rf application.path("bin") if application.path("bin_original").exist? FileUtils.cp_r application.path("bin_original"), application.path("bin") end application.run! "#{application.spring} binstub --all" @installed = true end def copy_to(path) system("rm -rf #{path}") system("cp -r #{application.root} #{path}") end end end end