diff --git a/test/integration/app.rb b/test/integration/app.rb new file mode 100644 index 0000000000..beefd7bef6 --- /dev/null +++ b/test/integration/app.rb @@ -0,0 +1,62 @@ +$stderr.puts "loading" +require 'sinatra' + +configure do + set :foo, :bar +end + +get '/app_file' do + content_type :txt + settings.app_file +end + +get '/ping' do + 'pong' +end + +get '/stream' do + stream do |out| + sleep 0.1 + out << "a" + sleep 1.2 + out << "b" + end +end + +get '/mainonly' do + object = Object.new + begin + object.send(:get, '/foo') { } + 'false' + rescue NameError + 'true' + end +end + +set :out, nil +get '/async' do + stream(:keep_open) { |o| (settings.out = o) << "hi!" } +end + +get '/send' do + settings.out << params[:msg] if params[:msg] + settings.out.close if params[:close] + "ok" +end + +class Subclass < Sinatra::Base + set :out, nil + get '/subclass/async' do + stream(:keep_open) { |o| (settings.out = o) << "hi!" } + end + + get '/subclass/send' do + settings.out << params[:msg] if params[:msg] + settings.out.close if params[:close] + "ok" + end +end + +use Subclass + +$stderr.puts "starting" diff --git a/test/integration_helper.rb b/test/integration_helper.rb new file mode 100644 index 0000000000..06acf53f29 --- /dev/null +++ b/test/integration_helper.rb @@ -0,0 +1,208 @@ +require 'sinatra/base' +require 'rbconfig' +require 'open-uri' +require 'net/http' + +module IntegrationHelper + class BaseServer + extend Enumerable + attr_accessor :server, :port, :pipe + alias name server + + def self.all + @all ||= [] + end + + def self.each(&block) + all.each(&block) + end + + def self.run(server, port) + new(server, port).run + end + + def app_file + File.expand_path('../integration/app.rb', __FILE__) + end + + def environment + "development" + end + + def initialize(server, port) + @installed, @pipe, @server, @port = nil, nil, server, port + Server.all << self + end + + def run + return unless installed? + kill + @log = "" + @pipe = IO.popen(command) + @started = Time.now + warn "#{server} up and running on port #{port}" if ping + at_exit { kill } + end + + def expect(str) + return if log.size < str.size or log[0, str.size] == str + raise "Server did not start properly:\n\n#{log}" + end + + def ping(timeout = 30) + loop do + return if alive? + if Time.now - @started > timeout + $stderr.puts command, log + fail "timeout" + else + expect "loading" + sleep 0.1 + end + end + end + + def alive? + 3.times { get('/ping') } + true + rescue Errno::ECONNREFUSED, Errno::ECONNRESET, EOFError, SystemCallError, OpenURI::HTTPError => error + false + end + + def get_stream(url = "/stream", &block) + Net::HTTP.start '127.0.0.1', port do |http| + request = Net::HTTP::Get.new url + http.request request do |response| + response.read_body(&block) + end + end + end + + def get(url) + open("http://127.0.0.1:#{port}#{url}").read + end + + def log + @log ||= "" + loop { @log << @pipe.read_nonblock(1) } + rescue Exception + @log + end + + def installed? + return @installed unless @installed.nil? + require server + @installed = true + rescue LoadError + warn "#{server} is not installed, skipping integration tests" + @installed = false + end + + def command + @command ||= begin + cmd = ["RACK_ENV=#{environment}", "exec"] + if RbConfig.respond_to? :ruby + cmd << RbConfig.ruby.inspect + else + file, dir = RbConfig::CONFIG.values_at('ruby_install_name', 'bindir') + cmd << File.expand_path(file, dir).inspect + end + cmd << "-I" << File.expand_path('../../lib', __FILE__).inspect + cmd << app_file.inspect << '-s' << server << '-o' << '127.0.0.1' << '-p' << port + cmd << "-e" << environment.to_s << '2>&1' + cmd.join " " + end + end + + def kill + return unless pipe + Process.kill("KILL", pipe.pid) + rescue NotImplementedError + system "kill -9 #{pipe.pid}" + rescue Errno::ESRCH + end + + def webrick? + name.to_s == "webrick" + end + end + + if RUBY_ENGINE == "jruby" + class JRubyServer < BaseServer + def start_vm + require 'java' + # Create a new container, set load paths and env + # SINGLETHREAD means create a new runtime + vm = org.jruby.embed.ScriptingContainer.new(org.jruby.embed.LocalContextScope::SINGLETHREAD) + vm.load_paths = [File.expand_path('../../lib', __FILE__)] + vm.environment = ENV.merge('RACK_ENV' => environment.to_s) + + # This ensures processing of RUBYOPT which activates Bundler + vm.provider.ruby_instance_config.process_arguments [] + vm.argv = ['-s', server.to_s, '-o', '127.0.0.1', '-p', port.to_s, '-e', environment.to_s] + + # Set stdout/stderr so we can retrieve log + @pipe = java.io.ByteArrayOutputStream.new + vm.output = java.io.PrintStream.new(@pipe) + vm.error = java.io.PrintStream.new(@pipe) + + Thread.new do + # Hack to ensure that Kernel#caller has the same info as + # when run from command-line, for Sintra::Application.app_file. + # Also, line numbers are zero-based in JRuby's parser + vm.provider.runtime.current_context.set_file_and_line(app_file, 0) + # Run the app + vm.run_scriptlet org.jruby.embed.PathType::ABSOLUTE, app_file + # terminate launches at_exit hooks which start server + vm.terminate + end + end + + def run + return unless installed? + kill + @thread = start_vm + @started = Time.now + warn "#{server} up and running on port #{port}" if ping + at_exit { kill } + end + + def log + String.from_java_bytes @pipe.to_byte_array + end + + def kill + @thread.kill if @thread + @thread = nil + end + end + Server = JRubyServer + else + Server = BaseServer + end + + def it(message, &block) + Server.each do |server| + next unless server.installed? + super "with #{server.name}: #{message}" do + self.server = server + server.run unless server.alive? + begin + instance_eval(&block) + rescue Exception => error + server.kill + raise error + end + end + end + end + + def self.extend_object(obj) + super + + base_port = 5000 + Process.pid % 100 + Sinatra::Base.server.each_with_index do |server, index| + Server.run(server, 5000+index) + end + end +end diff --git a/test/integration_test.rb b/test/integration_test.rb new file mode 100644 index 0000000000..1b482d486b --- /dev/null +++ b/test/integration_test.rb @@ -0,0 +1,31 @@ +require File.expand_path('../helper', __FILE__) +require File.expand_path('../integration_helper', __FILE__) +require 'timeout' + +# These tests start a real server and talk to it over TCP. +# Every test runs with every detected server. +# +# See test/integration/app.rb for the code of the app we test against. +class IntegrationTest < Test::Unit::TestCase + extend IntegrationHelper + attr_accessor :server + + it('sets the app_file') { assert_equal server.app_file, server.get("/app_file") } + + it 'logs once in development mode' do + random = "%064x" % Kernel.rand(2**256-1) + server.get "/ping?x=#{random}" + count = server.log.scan("GET /ping?x=#{random}").count + server.webrick? ? assert(count > 0) : assert_equal(1, count) + end + + it 'starts the correct server' do + exp = %r{ + ==\sSinatra/#{Sinatra::VERSION}\s + has\staken\sthe\sstage\son\s\d+\sfor\sdevelopment\s + with\sbackup\sfrom\s#{server} + }ix + + assert_match exp, server.log + end +end \ No newline at end of file