Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Initial import of nginx_thin spike

  • Loading branch information...
commit 51a81d2778266eb072f96846f28a2327336887bf 0 parents
@raggi authored
54 .autotest
@@ -0,0 +1,54 @@
+# -*- ruby -*-
+
+require 'autotest/restart'
+
+module Glob2Rexp
+ GLOB2REXP = {
+ '.' => '\.',
+ /(?:^|[^\\])\*/ => '\1.*'
+ }
+
+ def self.call(pattern)
+ GLOB2REXP.each { |from, to| pattern.gsub!(from, to) }
+ Regexp.new(pattern)
+ end
+end
+
+Autotest.add_hook :initialize do |at|
+ at.testlib = 'minitest/autorun'
+
+ # Don't track extras, this just burns cpu.
+ ignores = %w[]
+ ignores.concat File.read('.gitignore').split
+ ignores.delete_if { |pattern| pattern.nil? || pattern.empty? }
+ ignores.map! { |pattern| Glob2Rexp.call pattern }
+ ignores.each do |ignore|
+ at.add_exception ignore
+ end
+
+ # Make sure that we run all tests if the helper changes:
+ at.add_mapping(%r%^test/helper\.rb$%) do |f, _|
+ at.files_matching %r%test_.*\.rb%
+ end
+
+ # If the gemspec changes, we need to run the tests
+ at.add_mapping(%r%^.*\.gemspec$%) do |f, _|
+ at.files_matching %r%test_.*\.rb%
+ end
+
+ # If bundle did something, run all tests again
+ at.add_mapping(%r%^Gemfile\.lock$%) do |f, _|
+ at.files_matching %r%test_.*\.rb%
+ end
+end
+
+if File.exists?('Gemfile')
+ require 'autotest/bundler'
+
+ # If the Gemfile gets updated, run bundle install
+ Autotest.add_hook :updated do |at, *args|
+ if args.flatten.grep(%r%^Gemfile$|^.*\.gemspec$%).any?
+ system 'bundle'
+ end
+ end
+end
1  .gemtest
@@ -0,0 +1 @@
+
18 .gitignore
@@ -0,0 +1,18 @@
+coverage
+rdoc
+doc
+pkg
+test/tmp
+test/version_tmp
+tmp
+pkg
+*.gem
+*.rbc
+lib/bundler/man
+spec/reports
+.config
+InstalledFiles
+.bundle
+.yardoc
+_yardoc
+doc/
5 CHANGELOG.rdoc
@@ -0,0 +1,5 @@
+=== 1.0.0 / 2011-08-19
+
+* 1 major enhancement
+
+ * Birthday!
14 Manifest.txt
@@ -0,0 +1,14 @@
+.autotest
+.gemtest
+CHANGELOG.rdoc
+Manifest.txt
+README.rdoc
+Rakefile
+bin/nginx_thin
+config.ru
+lib/nginx_thin.rb
+lib/nginx_thin/nginx_server.rb
+lib/nginx_thin/thin_server.rb
+test/gemloader.rb
+test/helper.rb
+test/test_nginx_thin.rb
62 README.rdoc
@@ -0,0 +1,62 @@
+= nginx_thin
+
+http://rubygems.org/gems/nginx_thin
+{Project}[http://rubygems.org/gems/nginx_thin]
+{Documentation}[http://libraggi.rubyforge.org/nginx_thin/README_rdoc.html]
+{Wiki}[http://wiki.github.com/raggi/nginx_thin/]
+{Source Code}[http://github.com/raggi/nginx_thin/]
+{Issues}[http://github.com/raggi/nginx_thin/issues]
+{Rubyforge}[http://rubyforge.org/projects/libraggi]
+
+== DESCRIPTION:
+
+A wrapper around Thin and Nginx to support easily booting them together. This
+is most useful for integration test suites when you want to test through your
+nginx configuration. In some cases it may also provide a performance boost for
+such integration tests.
+
+== FEATURES/PROBLEMS:
+
+* Capybara integration (just call NginxThin.capybara in your setup)
+
+== SYNOPSIS:
+
+For easy Capybara integration:
+
+ NginxThin.capybara
+
+For other uses, see the documentation on NginxThin.
+
+== REQUIREMENTS:
+
+* nginx
+* thin
+
+== INSTALL:
+
+* gem install nginx_thin
+
+== LICENSE:
+
+(The MIT License)
+
+Copyright (c) 2011 raggi
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+'Software'), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22 Rakefile
@@ -0,0 +1,22 @@
+#!/usr/bin/env rake
+
+require 'hoe'
+Hoe.plugin :doofus, :git, :minitest, :gemspec2, :rubyforge
+
+Hoe.spec 'nginx_thin' do
+ developer 'raggi', 'raggi@rubyforge.org'
+ extra_dev_deps << %w(hoe-doofus >=1.0)
+ extra_dev_deps << %w(hoe-seattlerb >=1.2)
+ extra_dev_deps << %w(hoe-git >=1.3)
+ extra_dev_deps << %w(hoe-gemspec >=1.0.0)
+
+ extra_deps << %w(thin)
+
+ extra_dev_deps << %w(capybara)
+
+ self.extra_rdoc_files = FileList["**/*.rdoc"]
+ self.history_file = "CHANGELOG.rdoc"
+ self.readme_file = "README.rdoc"
+ self.rubyforge_name = 'libraggi'
+ self.testlib = :minitest
+end
23 bin/nginx_thin
@@ -0,0 +1,23 @@
+#!/usr/bin/env ruby
+
+config = ARGV.shift || ('config.ru' if File.exists?('config.ru'))
+port = ARGV.shift || 0
+root = ARGV.shift || Dir.pwd
+
+unless config && File.exists?(config) && File.directory?(root)
+ abort "Usage: nginx_thin config.ru [port] [root_path]"
+end
+
+require 'nginx_thin'
+
+app, opts = Rack::Builder.parse_file(config)
+
+server = NginxThin.new app, port, root
+server.start
+puts server.url
+puts "kill -INT #{$$} or CTRL+C to quit"
+puts "kill -HUP #{$$} to restart"
+trap(:INT) { server.stop }
+trap(:HUP) { server.stop; server.start }
+system "open #{server.url}"
+sleep
1  config.ru
@@ -0,0 +1 @@
+run proc { |env| [200, {'Content-Type' => 'text/plain'}, ['Nginx + Thin']] }
101 lib/nginx_thin.rb
@@ -0,0 +1,101 @@
+# = TODO
+# * Document more
+# * Support custom configs + urls
+# * Support multiple rack apps
+
+require 'thin'
+require 'tmpdir'
+require 'tempfile'
+require 'fileutils'
+require 'uri'
+
+class NginxThin
+ autoload :NginxServer, 'nginx_thin/nginx_server'
+ autoload :ThinServer, 'nginx_thin/thin_server'
+
+ VERSION = '1.0.0'
+
+ attr_reader :app_root, :rack_app, :url, :nginx_server, :rack_server, :port
+ attr_reader :forking
+
+ def initialize(rack_app, port = nil, app_root = nil, forking = true)
+ @rack_app = rack_app
+ @app_root = app_root || Dir.pwd + '/public'
+ @port = port == 0 ? nil : port
+ @uri = nil
+ @forking = forking
+ end
+
+ def forking?
+ @forking
+ end
+
+ def start
+ @rack_server = ThinServer.new(rack_app, socket, forking)
+ @rack_server.start
+
+ @nginx_server = NginxServer.new(app_root, socket, port)
+ @nginx_server.start
+
+ times = 0
+ until responsive?
+ times += 1
+ sleep 0.01
+ if times > 100
+ stop
+ states = "thin:#{thin_responsive?} nginx:#{nginx_responsive?}"
+ raise "Timed out booting NginxThin (#{states})"
+ end
+ end
+ end
+
+ def nginx_responsive?
+ TCPSocket.open('127.0.0.1', port) { |s| s.close; true }
+ rescue
+ nil
+ end
+
+ def thin_responsive?
+ File.exists?(socket)
+ end
+
+ def responsive?
+ thin_responsive? && nginx_responsive?
+ end
+
+ def stop
+ @nginx_server.stop if @nginx_server
+ # TODO improve handling if both error out
+ ensure
+ @rack_server.stop if @rack_server
+ end
+
+ def url
+ nginx_server.url
+ end
+
+ def port
+ @port || nginx_server && nginx_server.port
+ end
+
+ def socket
+ @socket ||= Dir.tmpdir + "/nginx.thin.#{@rack_app.object_id}.sock"
+ end
+
+ class <<self
+ attr_accessor :forking
+ end
+ @forking = true
+
+ def self.capybara(root = nil)
+ root ||= (Rails.root.to_s rescue Dir.pwd) + '/public'
+ root = File.expand_path(root)
+
+ Capybara.server do |app, port|
+ server = new(app, port, root, forking)
+ server.start
+ at_exit { server.stop }
+ end
+ end
+
+end
125 lib/nginx_thin/nginx_server.rb
@@ -0,0 +1,125 @@
+class NginxThin
+ class NginxServer
+ attr_reader :config_file, :port, :app_root, :socket
+
+ def initialize(app_root, socket, port = nil)
+ @app_root, @socket, @port = app_root, socket, port
+ @tmpdir = Dir.tmpdir + "/nginx_thin.#{$$}"
+ @config_file = @tmpdir + "/nginx_thin.#{$$}.conf"
+ @port ||= TCPServer.open(0) { |s| break s.addr[1] }
+ mkdirs
+ end
+
+ def mkdirs
+ FileUtils.mkdir_p @tmpdir + '/var/run'
+ FileUtils.mkdir_p @tmpdir + '/var/log'
+ end
+
+ def start
+ write_config
+ test_config
+ system "#{nginx} 2>/dev/null"
+ ensure
+ stop if $!
+ end
+
+ def stop
+ system "#{nginx} -s quit 2>/dev/null"
+ ensure
+ FileUtils.rm_rf @tmpdir if File.exists?(@tmpdir)
+ end
+
+ def test_config
+ out = `#{nginx} -t 2>&1`
+ unless $?.success?
+ system "open #{@config_file}"
+ $stderr.puts out
+ $stderr.puts File.read(@config_file)
+ raise "Nginx config failed"
+ end
+ end
+
+ def nginx
+ "nginx -c #{@config_file} -p #{@tmpdir}"
+ end
+
+ def pidfile
+ "#{@tmpdir}/nginx_thin.#{$$}.pid"
+ end
+
+ def write_config
+ open(@config_file, 'w+') do |file|
+ file.write <<-CONFIG
+ worker_processes 1;
+ daemon on;
+ pid #{pidfile};
+ error_log "#{@tmpdir}/error.log";
+
+ events { worker_connections 1024; }
+
+ http {
+ error_log "#{@tmpdir}/error.log";
+ access_log off;
+
+ sendfile on;
+ keepalive_timeout 0;
+ keepalive_requests 0;
+
+ upstream "nginx_thin#{$$}" {
+ server "unix:#{socket}";
+ }
+ include "#{mimes_file}";
+
+ server {
+
+ listen localhost:#{port};
+
+ # TODO: ssl sockets and how to identify url, etc
+ # listen localhost:\#{ssl_port} ssl;
+
+ server_name localhost 127.0.0.1;
+
+ root "#{app_root}";
+
+ location = ^/(?:robots\.txt|favicon\.ico)$ {
+ expires 1d;
+ try_files $uri =204;
+ }
+
+ location ~* \.(?:css|csv|f4v|gif|html|htm|ico|jpe?g|js|pdf|png|swf|txt|xml|zip|htc)$ {
+ if ( $args ~ ^[0-9]+$ ) {
+ expires max;
+ add_header Cache-Control public;
+ }
+ try_files $uri @app =404;
+ }
+
+ location / {
+ try_files $uri @app;
+ }
+
+ proxy_set_header Referer $http_referer;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
+ proxy_set_header Host $http_host;
+ proxy_redirect off;
+
+ location @app { proxy_pass http://nginx_thin#{$$}; }
+ }
+ }
+ CONFIG
+ end
+ end
+
+ def mimes_file
+ opts = `nginx -V 2>&1`.grep(/configure/).first.split(' --')
+ opts = Hash[opts.map! { |o| o.split('=') }]
+ File.dirname(opts['conf-path']) + '/mime.types'
+ end
+
+ def url
+ URI("http://localhost:#{port}")
+ end
+ end
+end
79 lib/nginx_thin/thin_server.rb
@@ -0,0 +1,79 @@
+class NginxThin
+ class ThinServer
+ attr_reader :server
+
+ def initialize(rack_app, socket, forking = false)
+ @socket = socket
+ @forking = forking
+ opts = {
+ :app => rack_app,
+ :server => :thin,
+ :Host => socket,
+ :environment => 'test'
+ }
+
+ if Rack.release.to_f < 1.3
+ Rack::Server.class_eval do
+ def initialize(options)
+ @options = options
+ @app = options[:app] if options[:app]
+ end
+ end
+ end
+
+ @server = Rack::Server.new(opts)
+ Thin::Logging.silent = true
+ # Thin::Logging.trace = true
+ end
+
+ def forking?
+ @forking
+ end
+
+ def start_forking
+ @cpid = fork do
+ trap(:INT) { EM.next_tick { EM.stop ; exit! } }
+ GC.start
+ ActiveRecord::Base.connection.reset! if Object.const_defined?(:ActiveRecord)
+ EM.run { @server.start }
+ end
+ ensure
+ stop if $! && @cpid
+ end
+
+ def stop_forking
+ Process.kill :INT, @cpid
+ Process.waitpid @cpid
+ rescue Errno::ESRCH, Errno::ECHILD
+ nil
+ end
+
+ def start_thread
+ @thread = Thread.new { EM.run }
+ @server.start
+ ensure
+ stop if $!
+ end
+
+ def stop_thread
+ EM.next_tick { EM.stop }
+ @thread.join
+ end
+
+ def start
+ if forking?
+ start_forking
+ else
+ start_thread
+ end
+ end
+
+ def stop
+ if forking?
+ stop_forking
+ else
+ stop_thread
+ end
+ end
+ end
+end
61 nginx_thin.gemspec
@@ -0,0 +1,61 @@
+# -*- encoding: utf-8 -*-
+
+Gem::Specification.new do |s|
+ s.name = %q{nginx_thin}
+ s.version = "1.0.0"
+
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
+ s.authors = [%q{raggi}]
+ s.date = %q{2011-08-19}
+ s.description = %q{A wrapper around Thin and Nginx to support easily booting them together. This
+is most useful for integration test suites when you want to test through your
+nginx configuration. In some cases it may also provide a performance boost for
+such integration tests.}
+ s.email = [%q{raggi@rubyforge.org}]
+ s.executables = [%q{nginx_thin}]
+ s.extra_rdoc_files = [%q{Manifest.txt}, %q{CHANGELOG.rdoc}, %q{README.rdoc}]
+ s.files = [%q{.autotest}, %q{.gemtest}, %q{CHANGELOG.rdoc}, %q{Manifest.txt}, %q{README.rdoc}, %q{Rakefile}, %q{bin/nginx_thin}, %q{config.ru}, %q{lib/nginx_thin.rb}, %q{lib/nginx_thin/nginx_server.rb}, %q{lib/nginx_thin/thin_server.rb}, %q{test/gemloader.rb}, %q{test/helper.rb}, %q{test/test_nginx_thin.rb}]
+ s.homepage = %q{http://rubygems.org/gems/nginx_thin}
+ s.rdoc_options = [%q{--main}, %q{README.rdoc}]
+ s.require_paths = [%q{lib}]
+ s.rubyforge_project = %q{libraggi}
+ s.rubygems_version = %q{1.8.5}
+ s.summary = %q{A wrapper around Thin and Nginx to support easily booting them together}
+ s.test_files = [%q{test/test_nginx_thin.rb}]
+
+ if s.respond_to? :specification_version then
+ s.specification_version = 3
+
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
+ s.add_runtime_dependency(%q<thin>, [">= 0"])
+ s.add_development_dependency(%q<minitest>, [">= 2.3.1"])
+ s.add_development_dependency(%q<rubyforge>, [">= 2.0.4"])
+ s.add_development_dependency(%q<hoe-doofus>, [">= 1.0"])
+ s.add_development_dependency(%q<hoe-seattlerb>, [">= 1.2"])
+ s.add_development_dependency(%q<hoe-git>, [">= 1.3"])
+ s.add_development_dependency(%q<hoe-gemspec>, [">= 1.0.0"])
+ s.add_development_dependency(%q<capybara>, [">= 0"])
+ s.add_development_dependency(%q<hoe>, ["~> 2.10"])
+ else
+ s.add_dependency(%q<thin>, [">= 0"])
+ s.add_dependency(%q<minitest>, [">= 2.3.1"])
+ s.add_dependency(%q<rubyforge>, [">= 2.0.4"])
+ s.add_dependency(%q<hoe-doofus>, [">= 1.0"])
+ s.add_dependency(%q<hoe-seattlerb>, [">= 1.2"])
+ s.add_dependency(%q<hoe-git>, [">= 1.3"])
+ s.add_dependency(%q<hoe-gemspec>, [">= 1.0.0"])
+ s.add_dependency(%q<capybara>, [">= 0"])
+ s.add_dependency(%q<hoe>, ["~> 2.10"])
+ end
+ else
+ s.add_dependency(%q<thin>, [">= 0"])
+ s.add_dependency(%q<minitest>, [">= 2.3.1"])
+ s.add_dependency(%q<rubyforge>, [">= 2.0.4"])
+ s.add_dependency(%q<hoe-doofus>, [">= 1.0"])
+ s.add_dependency(%q<hoe-seattlerb>, [">= 1.2"])
+ s.add_dependency(%q<hoe-git>, [">= 1.3"])
+ s.add_dependency(%q<hoe-gemspec>, [">= 1.0.0"])
+ s.add_dependency(%q<capybara>, [">= 0"])
+ s.add_dependency(%q<hoe>, ["~> 2.10"])
+ end
+end
11 test/gemloader.rb
@@ -0,0 +1,11 @@
+require 'rubygems'
+begin
+ project = File.basename(Dir['*.gemspec'].first, '.gemspec')
+ gemspec = File.expand_path("#{project}.gemspec", Dir.pwd)
+ spec = Gem::Specification.load(gemspec)
+ (spec.dependencies + spec.development_dependencies).each do |dep|
+ gem dep.name, dep.requirement.to_s
+ end
+rescue
+ warn "#{__FILE__}: Can't preload project dependencies: #{$!}"
+end
6 test/helper.rb
@@ -0,0 +1,6 @@
+require 'gemloader'
+require 'minitest/autorun'
+require 'minitest/spec'
+require "nginx_thin"
+require 'net/http/persistent'
+require 'capybara/dsl'
75 test/test_nginx_thin.rb
@@ -0,0 +1,75 @@
+require "helper"
+
+
+class TestNginxThin < MiniTest::Spec
+ describe "NginxThin" do
+ include Capybara::DSL
+
+ class RackApp
+ def call(env)
+ [200, {'Content-Type' => 'text/plain'}, ['hello world']]
+ end
+ end
+
+ def nginx_thin
+ @nginx_thin ||= begin
+ root = File.expand_path(File.dirname(__FILE__))
+ NginxThin.new(RackApp.new, nil, root)
+ end
+ end
+
+ def server_url
+ nginx_thin.url
+ end
+
+ def http
+ @http ||= Net::HTTP::Persistent.new
+ end
+
+ tests = lambda do
+ it "will serve up a rack app" do
+ begin
+ nginx_thin.start
+ result = http.request server_url + '/index'
+ result.body.must_equal 'hello world'
+ ensure
+ nginx_thin.stop
+ end
+ end
+
+ it "will serve up the app root" do
+ begin
+ nginx_thin.start
+ result = http.request server_url + "/#{File.basename __FILE__}"
+ result.body.must_equal File.read(__FILE__)
+ ensure
+ nginx_thin.stop
+ end
+ end
+
+ it "supports Capybara" do
+ NginxThin.capybara
+
+ Capybara.configure { |config| config.default_driver = :selenium }
+ Capybara.app = proc { |env| [200, {}, ['hello world']] }
+
+ visit '/'
+
+ page.text.must_equal 'hello world'
+ end
+ end
+
+ describe "in forking configuration" do
+ instance_eval(&tests)
+ end
+
+ describe "in threaded configuration" do
+ instance_eval(&tests)
+ end
+
+ describe "supports custom nginx configs" do
+ it "is pending"
+ end
+
+ end
+end

0 comments on commit 51a81d2

Please sign in to comment.
Something went wrong with that request. Please try again.