summary refs log tree commit homepage
diff options
context:
space:
mode:
authorEric Wong <normalperson@yhbt.net>2010-02-17 18:53:52 -0800
committerEric Wong <normalperson@yhbt.net>2010-02-17 18:53:52 -0800
commit5cef71dc6c640db414c41f59a5016fd3f5326bf9 (patch)
tree500cd48b0d92ba8b847ce6839d6889d5304fa11b
parent13598f977ec3b707bd1a8f2abb99825cb8f85b58 (diff)
-rw-r--r--GNUmakefile2
-rw-r--r--t/.gitignore2
-rw-r--r--t/GNUmakefile67
-rw-r--r--t/README42
-rwxr-xr-xt/bin/content-md5-put36
-rwxr-xr-xt/bin/sha1sum.rb23
-rwxr-xr-xt/bin/unused_listen40
-rwxr-xr-xt/bin/utee12
-rw-r--r--t/env.ru3
-rw-r--r--t/my-tap-lib.sh200
-rwxr-xr-xt/t0000-http-basic.sh50
-rw-r--r--t/test-lib.sh100
12 files changed, 577 insertions, 0 deletions
diff --git a/GNUmakefile b/GNUmakefile
index 3326aff..70707ce 100644
--- a/GNUmakefile
+++ b/GNUmakefile
@@ -58,6 +58,7 @@ lib/unicorn_http.$(DLEXT): $(ext)/unicorn_http.$(DLEXT)
         install -m644 $< $@
 http: lib/unicorn_http.$(DLEXT)
 
+test-install: $(test_prefix)/.stamp
 $(test_prefix)/.stamp: $(inst_deps)
         mkdir -p $(test_prefix)/.ccache
         tar cf - $(inst_deps) GIT-VERSION-GEN | \
@@ -282,3 +283,4 @@ gem install-gem: GIT-VERSION-FILE
 endif
 
 .PHONY: .FORCE-GIT-VERSION-FILE doc $(T) $(slow_tests) manifest man
+.PHONY: test-install
diff --git a/t/.gitignore b/t/.gitignore
new file mode 100644
index 0000000..a0c1c36
--- /dev/null
+++ b/t/.gitignore
@@ -0,0 +1,2 @@
+/random_blob
+/.dep+*
diff --git a/t/GNUmakefile b/t/GNUmakefile
new file mode 100644
index 0000000..20e5b6e
--- /dev/null
+++ b/t/GNUmakefile
@@ -0,0 +1,67 @@
+# we can run tests in parallel with GNU make
+all::
+
+pid := $(shell echo $$PPID)
+
+RUBY = ruby
+-include ../local.mk
+ifeq ($(RUBY_VERSION),)
+  RUBY_VERSION := $(shell $(RUBY) -e 'puts RUBY_VERSION')
+endif
+
+ifeq ($(RUBY_VERSION),)
+  $(error unable to detect RUBY_VERSION)
+endif
+
+T = $(wildcard t[0-9][0-9][0-9][0-9]-*.sh)
+
+all:: $(T)
+
+# can't rely on "set -o pipefail" since we don't require bash or ksh93 :<
+t_pfx = trash/$@-$(RUBY_VERSION)
+TEST_OPTS =
+# TRACER = strace -f -o $(t_pfx).strace -s 100000
+# TRACER = /usr/bin/time -o $(t_pfx).time
+
+ifdef V
+  ifeq ($(V),2)
+    TEST_OPTS += --trace
+  else
+    TEST_OPTS += --verbose
+  endif
+endif
+
+random_blob:
+        dd if=/dev/urandom bs=1M count=30 of=$@.$(pid)
+        mv $@.$(pid) $@
+
+$(T): random_blob
+
+dependencies := socat curl
+deps := $(addprefix .dep+,$(dependencies))
+$(deps): dep_bin = $(lastword $(subst +, ,$@))
+$(deps):
+        @which $(dep_bin) > $@.$(pid) 2>/dev/null || :
+        @test -s $@.$(pid) || \
+          { echo >&2 "E '$(dep_bin)' not found in PATH=$(PATH)"; exit 1; }
+        @mv $@.$(pid) $@
+dep: $(deps)
+
+test_prefix := $(CURDIR)/../test/install-$(RUBY_VERSION)
+$(test_prefix)/.stamp:
+        $(MAKE) -C .. test-install
+
+$(T): export RUBY := $(RUBY)
+$(T): export PATH := $(test_prefix)/bin:$(PATH)
+$(T): export RUBYLIB := $(test_prefix)/lib:$(RUBYLIB)
+$(T): dep $(test_prefix)/.stamp trash/.gitignore
+        $(TRACER) $(SHELL) $(SH_TEST_OPTS) $@ $(TEST_OPTS)
+
+trash/.gitignore:
+        mkdir -p $(@D)
+        echo '*' > $@
+
+clean:
+        $(RM) -r trash/*
+
+.PHONY: $(T) clean
diff --git a/t/README b/t/README
new file mode 100644
index 0000000..095f106
--- /dev/null
+++ b/t/README
@@ -0,0 +1,42 @@
+= Unicorn integration test suite
+
+These are all integration tests that start the server on random, unused
+TCP ports or Unix domain sockets.  They're all designed to run
+concurrently with other tests to minimize test time, but tests may be
+run independently as well.
+
+We write our tests in Bourne shell because that's what we're
+comfortable writing integration tests with.
+
+== Requirements
+
+* {Ruby 1.8 or 1.9}[http://www.ruby-lang.org/] (duh!)
+* {GNU make}[http://www.gnu.org/software/make/]
+* {socat}[http://www.dest-unreach.org/socat/]
+* {curl}[http://curl.haxx.se/]
+* standard UNIX shell utilities (Bourne sh, awk, sed, grep, ...)
+
+We do not use bashisms or any non-portable, non-POSIX constructs
+in our shell code.  We use the "pipefail" option if available and
+mainly test with {ksh}[http://kornshell.com/], but occasionally
+with {dash}[http://gondor.apana.org.au/~herbert/dash/] and
+{bash}[http://www.gnu.org/software/bash/], too.
+
+== Running Tests
+
+To run the entire test suite with 8 tests running at once:
+
+  make -j8
+
+To run one individual test:
+
+  make t0000-simple-http.sh
+
+You may also increase verbosity by setting the "V" variable for
+GNU make.  To disable trapping of stdout/stderr:
+
+  make V=1
+
+To enable the "set -x" option in shell scripts to trace execution
+
+  make V=2
diff --git a/t/bin/content-md5-put b/t/bin/content-md5-put
new file mode 100755
index 0000000..01da0bb
--- /dev/null
+++ b/t/bin/content-md5-put
@@ -0,0 +1,36 @@
+#!/usr/bin/env ruby
+# -*- encoding: binary -*-
+# simple chunked HTTP PUT request generator (and just that),
+# it reads stdin and writes to stdout so socat can write to a
+# UNIX or TCP socket (or to another filter or file) along with
+# a Content-MD5 trailer.
+require 'digest/md5'
+$stdout.sync = $stderr.sync = true
+$stdout.binmode
+$stdin.binmode
+
+bs = ENV['bs'] ? ENV['bs'].to_i : 4096
+
+if ARGV.grep("--no-headers").empty?
+  $stdout.write(
+      "PUT / HTTP/1.1\r\n" \
+      "Host: example.com\r\n" \
+      "Transfer-Encoding: chunked\r\n" \
+      "Trailer: Content-MD5\r\n" \
+      "\r\n"
+    )
+end
+
+digest = Digest::MD5.new
+if buf = $stdin.readpartial(bs)
+  begin
+    digest.update(buf)
+    $stdout.write("%x\r\n" % [ buf.size ])
+    $stdout.write(buf)
+    $stdout.write("\r\n")
+  end while $stdin.read(bs, buf)
+end
+
+digest = [ digest.digest ].pack('m').strip
+$stdout.write("0\r\n")
+$stdout.write("Content-MD5: #{digest}\r\n\r\n")
diff --git a/t/bin/sha1sum.rb b/t/bin/sha1sum.rb
new file mode 100755
index 0000000..b602e79
--- /dev/null
+++ b/t/bin/sha1sum.rb
@@ -0,0 +1,23 @@
+#!/usr/bin/env ruby
+# -*- encoding: binary -*-
+
+# Reads from stdin and outputs the SHA1 hex digest of the input this is
+# ONLY used as a last resort, our test code will try to use sha1sum(1),
+# openssl(1), or gsha1sum(1) before falling back to using this.  We try
+# all options first because we have a strong and healthy distrust of our
+# Ruby abilities in general, and *especially* when it comes to
+# understanding (and trusting the implementation of) Ruby 1.9 encoding.
+
+require 'digest/sha1'
+$stdout.sync = $stderr.sync = true
+$stdout.binmode
+$stdin.binmode
+bs = 16384
+digest = Digest::SHA1.new
+if buf = $stdin.read(bs)
+  begin
+    digest.update(buf)
+  end while $stdin.read(bs, buf)
+end
+
+$stdout.syswrite("#{digest.hexdigest}\n")
diff --git a/t/bin/unused_listen b/t/bin/unused_listen
new file mode 100755
index 0000000..b638f54
--- /dev/null
+++ b/t/bin/unused_listen
@@ -0,0 +1,40 @@
+#!/usr/bin/env ruby
+# -*- encoding: binary -*-
+# this is to remain compatible with the unused_port function in the
+# Unicorn test/test_helper.rb file
+require 'socket'
+require 'tmpdir'
+
+default_port = 8080
+addr = ENV['UNICORN_TEST_ADDR'] || '127.0.0.1'
+retries = 100
+base = 5000
+port = sock = lock_path = nil
+
+begin
+  begin
+    port = base + rand(32768 - base)
+    while port == default_port
+      port = base + rand(32768 - base)
+    end
+
+    sock = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
+    sock.bind(Socket.pack_sockaddr_in(port, addr))
+    sock.listen(5)
+  rescue Errno::EADDRINUSE, Errno::EACCES
+    sock.close rescue nil
+    retry if (retries -= 1) >= 0
+  end
+
+  # since we'll end up closing the random port we just got, there's a race
+  # condition could allow the random port we just chose to reselect itself
+  # when running tests in parallel with gmake.  Create a lock file while
+  # we have the port here to ensure that does not happen.
+  lock_path = "#{Dir::tmpdir}/unicorn_test.#{addr}:#{port}.lock"
+  lock = File.open(lock_path, File::WRONLY|File::CREAT|File::EXCL, 0600)
+rescue Errno::EEXIST
+  sock.close rescue nil
+  retry
+end
+sock.close rescue nil
+puts %Q(listen=#{addr}:#{port} T_RM_LIST="$T_RM_LIST #{lock_path}")
diff --git a/t/bin/utee b/t/bin/utee
new file mode 100755
index 0000000..7b61fea
--- /dev/null
+++ b/t/bin/utee
@@ -0,0 +1,12 @@
+#!/usr/bin/env ruby
+# -*- encoding: binary -*-
+# tee(1) as distributed on most(all?) systems is buffered in luserspace
+# this only does unbuffered writes (with line-buffered input) to make
+# test output appear in real-time
+$stdin.binmode
+$stdout.binmode
+fp = File.open(ARGV.shift, "wb")
+$stdin.each_line do |line|
+  fp.syswrite line
+  $stdout.syswrite line
+end
diff --git a/t/env.ru b/t/env.ru
new file mode 100644
index 0000000..388412e
--- /dev/null
+++ b/t/env.ru
@@ -0,0 +1,3 @@
+use Rack::ContentLength
+use Rack::ContentType, "text/plain"
+run lambda { |env| [ 200, {}, [ env.inspect << "\n" ] ] }
diff --git a/t/my-tap-lib.sh b/t/my-tap-lib.sh
new file mode 100644
index 0000000..2d0c1a9
--- /dev/null
+++ b/t/my-tap-lib.sh
@@ -0,0 +1,200 @@
+#!/bin/sh
+# Copyright (c) 2009, 2010 Eric Wong <normalperson@yhbt.net>
+#
+# TAP-producing shell library for POSIX-compliant Bourne shells We do
+# not _rely_ on Bourne Again features, though we will use "set -o
+# pipefail" from ksh93 or bash 3 if available
+#
+# Only generic, non-project/non-language-specific stuff goes here.  We
+# only have POSIX dependencies for the core tests (without --verbose),
+# though we'll enable useful non-POSIX things if they're available.
+#
+# This test library is intentionally unforgiving, it does not support
+# skipping tests nor continuing after any failure.  Any failures
+# immediately halt execution as do any references to undefined
+# variables.
+#
+# When --verbose is specified, we always prefix stdout/stderr
+# output with "#" to avoid confusing TAP consumers.  Otherwise
+# the normal stdout/stderr streams are redirected to /dev/null
+
+# dup normal stdout(fd=1) and stderr (fd=2) to fd=3 and fd=4 respectively
+# normal TAP output goes to fd=3, nothing should go to fd=4
+exec 3>&1 4>&2
+
+# ensure a sane environment
+TZ=UTC LC_ALL=C LANG=C
+export LANG LC_ALL TZ
+unset CDPATH
+
+# pipefail is non-POSIX, but very useful in ksh93/bash
+( set -o pipefail 2>/dev/null ) && set -o pipefail
+
+SED=${SED-sed}
+
+# Unlike other test frameworks, we are unforgiving and bail immediately
+# on any failures.  We do this because we're lazy about error handling
+# and also because we believe anything broken should not be allowed to
+# propagate throughout the rest of the test
+set -e
+set -u
+
+# name of our test
+T=${0##*/}
+
+t_expect_nr=-1
+t_nr=0
+t_current=
+t_complete=false
+
+# list of files to remove unconditionally on exit
+T_RM_LIST=
+
+# list of files to remove only on successful exit
+T_OK_RM_LIST=
+
+# emit output to stdout, it'll be parsed by the TAP consumer
+# so it must be TAP-compliant output
+t_echo () {
+        echo >&3 "$@"
+}
+
+# emits non-parsed information to stdout, it will be prefixed with a '#'
+# to not throw off TAP consumers
+t_info () {
+        t_echo '#' "$@"
+}
+
+# exit with an error and print a diagnostic
+die () {
+        echo >&2 "$@"
+        exit 1
+}
+
+# our at_exit handler, it'll fire for all exits except SIGKILL (unavoidable)
+t_at_exit () {
+        code=$?
+        set +e
+        if test $code -eq 0
+        then
+                $t_complete || {
+                        t_info "t_done not called"
+                        code=1
+                }
+        elif test -n "$t_current"
+        then
+                t_echo "not ok $t_nr - $t_current"
+        fi
+        if test $t_expect_nr -ne -1
+        then
+                test $t_expect_nr -eq $t_nr || {
+                        t_info "planned $t_expect_nr tests but ran $t_nr"
+                        test $code -ne 0 || code=1
+                }
+        fi
+        $t_complete || {
+                t_info "unexpected test failure"
+                test $code -ne 0 || code=1
+        }
+        rm -f $T_RM_LIST
+        test $code -eq 0 && rm -f $T_OK_RM_LIST
+        set +x
+        exec >&3 2>&4
+        t_close_fds
+        exit $code
+}
+
+# close test-specific extra file descriptors
+t_close_fds () {
+        exec 3>&- 4>&-
+}
+
+# call this at the start of your test to specify the number of tests
+# you plan to run
+t_plan () {
+        test "$1" -ge 1 || die "must plan at least one test"
+        test $t_expect_nr -eq -1 || die "tried to plan twice in one test"
+        t_expect_nr=$1
+        shift
+        t_echo 1..$t_expect_nr "#" "$@"
+        trap t_at_exit EXIT
+}
+
+_t_checkup () {
+        test $t_expect_nr -le 0 && die "no tests planned"
+        test -n "$t_current" && t_echo "ok $t_nr - $t_current"
+        true
+}
+
+# finalizes any previously test and starts a new one
+t_begin () {
+        _t_checkup
+        t_nr=$(( $t_nr + 1 ))
+        t_current="$1"
+
+        # just in case somebody wanted to cheat us:
+        set -e
+}
+
+# finalizes the current test without starting a new one
+t_end () {
+        _t_checkup
+        t_current=
+}
+
+# run this to signify the end of your test
+t_done () {
+        _t_checkup
+        t_current=
+        t_complete=true
+        test $t_expect_nr -eq $t_nr || exit 1
+        exit 0
+}
+
+# create and assign named-pipes to variable _names_ passed to this function
+t_fifos () {
+        for _id in "$@"
+        do
+                _name=$_id
+                _tmp=$(mktemp -t $T.$$.$_id.XXXXXXXX)
+                eval "$_id=$_tmp"
+                rm -f $_tmp
+                mkfifo $_tmp
+                T_RM_LIST="$T_RM_LIST $_tmp"
+        done
+}
+
+t_verbose=false t_trace=false
+
+while test "$#" -ne 0
+do
+        arg="$1"
+        shift
+        case $arg in
+        -v|--verbose) t_verbose=true ;;
+        --trace) t_trace=true t_verbose=true ;;
+        *) die "Unknown option: $arg" ;;
+        esac
+done
+
+# we always only setup stdout, nothing should end up in the "real" stderr
+if $t_verbose
+then
+        if test x"$(which mktemp 2>/dev/null)" = x
+        then
+                die "mktemp(1) not available for --verbose"
+        fi
+        t_fifos t_stdout t_stderr
+
+        (
+                # use a subshell so seds are not waitable
+                $SED -e 's/^/#: /' $t_stdout &
+                $SED -e 's/^/#! /' $t_stderr &
+        ) &
+        exec > $t_stdout 2> $t_stderr
+else
+        exec > /dev/null 2> /dev/null
+fi
+
+$t_trace && set -x
+true
diff --git a/t/t0000-http-basic.sh b/t/t0000-http-basic.sh
new file mode 100755
index 0000000..01ead95
--- /dev/null
+++ b/t/t0000-http-basic.sh
@@ -0,0 +1,50 @@
+#!/bin/sh
+. ./test-lib.sh
+t_plan 8 "simple HTTP connection tests"
+
+t_begin "setup and start" && {
+        unicorn_setup
+        unicorn -D -c $unicorn_config env.ru
+        unicorn_wait_start
+}
+
+t_begin "single request" && {
+        curl -sSfv http://$listen/
+}
+
+t_begin "check stderr has no errors" && {
+        check_stderr
+}
+
+t_begin "HTTP/0.9 request should not return headers" && {
+        (
+                printf 'GET /\r\n'
+                cat $fifo > $tmp &
+                wait
+                echo ok > $ok
+        ) | socat - TCP:$listen > $fifo
+}
+
+t_begin "env.inspect should've put everything on one line" && {
+        test 1 -eq $(wc -l < $tmp)
+}
+
+t_begin "no headers in output" && {
+        if grep ^Connection: $tmp
+        then
+                die "Connection header found in $tmp"
+        elif grep ^HTTP/ $tmp
+        then
+                die "HTTP/ found in $tmp"
+        fi
+}
+
+t_begin "killing succeeds" && {
+        kill $unicorn_pid
+}
+
+t_begin "check stderr has no errors" && {
+        check_stderr
+}
+
+t_done
diff --git a/t/test-lib.sh b/t/test-lib.sh
new file mode 100644
index 0000000..60ace97
--- /dev/null
+++ b/t/test-lib.sh
@@ -0,0 +1,100 @@
+#!/bin/sh
+# Copyright (c) 2009 Rainbows! hackers
+# Copyright (c) 2010 Unicorn hackers
+. ./my-tap-lib.sh
+
+set +u
+set -e
+RUBY="${RUBY-ruby}"
+RUBY_VERSION=${RUBY_VERSION-$($RUBY -e 'puts RUBY_VERSION')}
+t_pfx=$PWD/trash/$T-$RUBY_VERSION
+set -u
+
+PATH=$PWD/bin:$PATH
+export PATH
+
+test -x $PWD/bin/unused_listen || die "must be run in 't' directory"
+
+wait_for_pid () {
+        path="$1"
+        nr=30
+        while ! test -s "$path" && test $nr -gt 0
+        do
+                nr=$(($nr - 1))
+                sleep 1
+        done
+}
+
+# given a list of variable names, create temporary files and assign
+# the pathnames to those variables
+rtmpfiles () {
+        for id in "$@"
+        do
+                name=$id
+                _tmp=$t_pfx.$id
+                eval "$id=$_tmp"
+
+                case $name in
+                *fifo)
+                        rm -f $_tmp
+                        mkfifo $_tmp
+                        T_RM_LIST="$T_RM_LIST $_tmp"
+                        ;;
+                *socket)
+                        rm -f $_tmp
+                        T_RM_LIST="$T_RM_LIST $_tmp"
+                        ;;
+                *)
+                        > $_tmp
+                        T_OK_RM_LIST="$T_OK_RM_LIST $_tmp"
+                        ;;
+                esac
+        done
+}
+
+dbgcat () {
+        id=$1
+        eval '_file=$'$id
+        echo "==> $id <=="
+        sed -e "s/^/$id:/" < $_file
+}
+
+check_stderr () {
+        set +u
+        _r_err=${1-${r_err}}
+        set -u
+        if grep -i Error $_r_err
+        then
+                die "Errors found in $_r_err"
+        elif grep SIGKILL $_r_err
+        then
+                die "SIGKILL found in $_r_err"
+        fi
+}
+
+# unicorn_setup
+unicorn_setup () {
+        eval $(unused_listen)
+        rtmpfiles unicorn_config pid r_err r_out fifo tmp ok
+        cat > $unicorn_config <<EOF
+listen "$listen"
+pid "$pid"
+stderr_path "$r_err"
+stdout_path "$r_out"
+EOF
+}
+
+unicorn_wait_start () {
+        # no need to play tricks with FIFOs since we got "ready_pipe" now
+        unicorn_pid=$(cat $pid)
+}
+
+rsha1 () {
+        _cmd="$(which sha1sum 2>/dev/null || :)"
+        test -n "$_cmd" || _cmd="$(which openssl 2>/dev/null || :) sha1"
+        test "$_cmd" != " sha1" || _cmd="$(which gsha1sum 2>/dev/null || :)"
+
+        # last resort, see comments in sha1sum.rb for reasoning
+        test -n "$_cmd" || _cmd=sha1sum.rb
+        expr "$($_cmd < random_blob)" : '\([a-f0-9]\{40\}\)'
+}