From df8dd09a2e1bb5ad904661e0543d78c224d7f841 Mon Sep 17 00:00:00 2001 From: Ryan Trauntvein Date: Fri, 27 May 2016 13:10:16 -0700 Subject: [PATCH 1/5] Version v2.3.0-RC2 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 1572a71c..14ecaa49 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ FROM debian:jessie MAINTAINER Alt Three ARG cachet_ver -ENV cachet_ver master +ENV cachet_ver v2.3.0-RC2 # Using debian packages instead of compiling from scratch RUN DEBIAN_FRONTEND=noninteractive \ From 1af3448c11fece11fc36616d8080c12a3b219f0f Mon Sep 17 00:00:00 2001 From: Ryan Trauntvein Date: Tue, 31 May 2016 23:23:45 -0700 Subject: [PATCH 2/5] fix DB connect test message (#87) --- docker/entrypoint.sh | 4 ++-- docker/supervisord.conf | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index c8d7f78c..945359b6 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -1,5 +1,5 @@ #!/bin/bash -set -e +set -eo pipefail [[ $DEBUG == true ]] && set -x @@ -16,7 +16,7 @@ check_database_connection() { timeout=60 while ! ${prog} >/dev/null 2>&1 do - timeout=$(expr $timeout - 1) + timeout=$(( $timeout - 1 )) if [[ $timeout -eq 0 ]]; then echo echo "Could not connect to database server. Aborting..." diff --git a/docker/supervisord.conf b/docker/supervisord.conf index 45d547fd..222a98e1 100644 --- a/docker/supervisord.conf +++ b/docker/supervisord.conf @@ -15,7 +15,7 @@ supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface [supervisorctl] serverurl=unix:///var/run/supervisor.sock ; use a unix:// URL for a unix socket -[program:php-fpm7.0] +[program:php-fpm] command=/usr/sbin/php5-fpm -c /etc/php5/fpm [program:cron] From c412cef472ae11f12760a1bf82e8e92ee8733b8a Mon Sep 17 00:00:00 2001 From: Ryan Trauntvein Date: Tue, 31 May 2016 23:28:34 -0700 Subject: [PATCH 3/5] Rename docker dir to conf, closes #85 --- Dockerfile | 10 ++-- conf/.env.docker | 26 +++++++++ conf/crontab | 2 + conf/entrypoint.sh | 127 +++++++++++++++++++++++++++++++++++++++++ conf/nginx-site.conf | 30 ++++++++++ conf/php-fpm-pool.conf | 24 ++++++++ conf/supervisord.conf | 22 +++++++ 7 files changed, 236 insertions(+), 5 deletions(-) create mode 100644 conf/.env.docker create mode 100644 conf/crontab create mode 100755 conf/entrypoint.sh create mode 100644 conf/nginx-site.conf create mode 100644 conf/php-fpm-pool.conf create mode 100644 conf/supervisord.conf diff --git a/Dockerfile b/Dockerfile index 14ecaa49..97b5fee7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,15 +18,15 @@ RUN DEBIAN_FRONTEND=noninteractive \ apt-get clean && apt-get autoremove -q && \ rm -rf /var/lib/apt/lists/* /usr/share/doc /usr/share/man /tmp/* -COPY docker/php-fpm-pool.conf /etc/php5/fpm/pool.d/www.conf -COPY docker/supervisord.conf /etc/supervisor/supervisord.conf +COPY conf/php-fpm-pool.conf /etc/php5/fpm/pool.d/www.conf +COPY conf/supervisord.conf /etc/supervisor/supervisord.conf RUN sed -i -e "s/;daemonize\s*=\s*yes/daemonize = no/g" /etc/php5/fpm/php-fpm.conf RUN mkdir -p /var/www/html && \ chown -R www-data /var/www -COPY docker/crontab /etc/cron.d/artisan-schedule -COPY docker/entrypoint.sh /sbin/entrypoint.sh +COPY conf/crontab /etc/cron.d/artisan-schedule +COPY conf/entrypoint.sh /sbin/entrypoint.sh RUN chmod 0644 /etc/cron.d/artisan-schedule && \ touch /var/log/cron.log @@ -46,7 +46,7 @@ RUN wget https://github.com/cachethq/Cachet/archive/${cachet_ver}.tar.gz && \ rm -r ${cachet_ver}.tar.gz && \ php composer.phar install --no-dev -o -COPY docker/.env.docker /var/www/html/.env +COPY conf/.env.docker /var/www/html/.env VOLUME /var/www EXPOSE 8000 diff --git a/conf/.env.docker b/conf/.env.docker new file mode 100644 index 00000000..d565b8c6 --- /dev/null +++ b/conf/.env.docker @@ -0,0 +1,26 @@ +APP_ENV="{{APP_ENV}}" +APP_DEBUG="{{APP_DEBUG}}" +APP_URL="{{APP_URL}}" +APP_KEY={{APP_KEY}} + +DB_DRIVER="{{DB_DRIVER}}" +DB_HOST="{{DB_HOST}}" +DB_DATABASE="{{DB_DATABASE}}" +DB_USERNAME="{{DB_USERNAME}}" +DB_PASSWORD="{{DB_PASSWORD}}" + +CACHE_DRIVER="{{CACHE_DRIVER}}" +SESSION_DRIVER="{{SESSION_DRIVER}}" +QUEUE_DRIVER="{{QUEUE_DRIVER}}" + +MAIL_DRIVER="{{MAIL_DRIVER}}" +MAIL_HOST="{{MAIL_HOST}}" +MAIL_PORT="{{MAIL_PORT}}" +MAIL_USERNAME="{{MAIL_USERNAME}}" +MAIL_PASSWORD="{{MAIL_PASSWORD}}" +MAIL_ADDRESS="{{MAIL_ADDRESS}}" +MAIL_NAME="{{MAIL_NAME}}" + +REDIS_HOST="{{REDIS_HOST}}" +REDIS_DATABASE="{{REDIS_DATABASE}}" +REDIS_PORT="{{REDIS_PORT}}" diff --git a/conf/crontab b/conf/crontab new file mode 100644 index 00000000..a7549d7e --- /dev/null +++ b/conf/crontab @@ -0,0 +1,2 @@ +#minute hour mday month wday who command +* * * * * www-data php /var/www/html/artisan schedule:run >> /dev/null 2>&1 diff --git a/conf/entrypoint.sh b/conf/entrypoint.sh new file mode 100755 index 00000000..945359b6 --- /dev/null +++ b/conf/entrypoint.sh @@ -0,0 +1,127 @@ +#!/bin/bash +set -eo pipefail + +[[ $DEBUG == true ]] && set -x + +check_database_connection() { + case ${DB_DRIVER} in + mysql) + prog="mysqladmin -h ${DB_HOST} -u ${DB_USERNAME} ${DB_PASSWORD:+-p$DB_PASSWORD} status" + ;; + pgsql) + prog=$(find /usr/lib/postgresql/ -name pg_isready) + prog="${prog} -h ${DB_HOST} -U ${DB_USERNAME} -d ${DB_DATABASE} -t 1" + ;; + esac + timeout=60 + while ! ${prog} >/dev/null 2>&1 + do + timeout=$(( $timeout - 1 )) + if [[ $timeout -eq 0 ]]; then + echo + echo "Could not connect to database server. Aborting..." + return 1 + fi + echo -n "." + sleep 1 + done + echo +} + +initialize_system() { + APP_ENV=${APP_ENV:-development} + APP_DEBUG=${APP_DEBUG:-true} + APP_URL=${APP_URL:-http://localhost} + APP_KEY=${APP_KEY:-SECRET} + + DB_DRIVER=${DB_DRIVER:-pgsql} + DB_HOST=${DB_HOST:-postgres} + DB_DATABASE=${DB_DATABASE:-cachet} + DB_USERNAME=${DB_USERNAME:-postgres} + DB_PASSWORD=${DB_PASSWORD:-postgres} + DB_PORT=${DB_PORT:-5432} + + CACHE_DRIVER=${CACHE_DRIVER:-apc} + SESSION_DRIVER=${SESSION_DRIVER:-apc} + QUEUE_DRIVER=${QUEUE_DRIVER:-database} + + MAIL_DRIVER=${MAIL_DRIVER:-smtp} + MAIL_HOST=${MAIL_HOST:-mailtrap.io} + MAIL_PORT=${MAIL_PORT:-2525} + MAIL_USERNAME=${MAIL_USERNAME:-null} + MAIL_PASSWORD=${MAIL_PASSWORD:-null} + MAIL_ADDRESS=${MAIL_ADDRESS:-null} + MAIL_NAME=${MAIL_NAME:-null} + + REDIS_HOST=${REDIS_HOST:-null} + REDIS_DATABASE=${REDIS_DATABASE:-null} + REDIS_PORT=${REDIS_PORT:-null} + + # configure env file + + sed 's,{{APP_ENV}},'"${APP_ENV}"',g' -i /var/www/html/.env + sed 's,{{APP_DEBUG}},'"${APP_DEBUG}"',g' -i /var/www/html/.env + sed 's,{{APP_URL}},'"${APP_URL}"',g' -i /var/www/html/.env + sed 's,{{APP_KEY}},'${APP_KEY}',g' -i /var/www/html/.env + + sed 's,{{DB_DRIVER}},'"${DB_DRIVER}"',g' -i /var/www/html/.env + sed 's,{{DB_HOST}},'"${DB_HOST}"',g' -i /var/www/html/.env + sed 's,{{DB_DATABASE}},'"${DB_DATABASE}"',g' -i /var/www/html/.env + sed 's,{{DB_USERNAME}},'"${DB_USERNAME}"',g' -i /var/www/html/.env + sed 's,{{DB_PASSWORD}},'"${DB_PASSWORD}"',g' -i /var/www/html/.env + + sed 's,{{CACHE_DRIVER}},'"${CACHE_DRIVER}"',g' -i /var/www/html/.env + sed 's,{{SESSION_DRIVER}},'"${SESSION_DRIVER}"',g' -i /var/www/html/.env + sed 's,{{QUEUE_DRIVER}},'"${QUEUE_DRIVER}"',g' -i /var/www/html/.env + + sed 's,{{MAIL_DRIVER}},'"${MAIL_DRIVER}"',g' -i /var/www/html/.env + sed 's,{{MAIL_HOST}},'"${MAIL_HOST}"',g' -i /var/www/html/.env + sed 's,{{MAIL_PORT}},'"${MAIL_PORT}"',g' -i /var/www/html/.env + sed 's,{{MAIL_USERNAME}},'"${MAIL_USERNAME}"',g' -i /var/www/html/.env + sed 's,{{MAIL_PASSWORD}},'"${MAIL_PASSWORD}"',g' -i /var/www/html/.env + sed 's,{{MAIL_ADDRESS}},'"${MAIL_ADDRESS}"',g' -i /var/www/html/.env + sed 's,{{MAIL_NAME}},'"${MAIL_NAME}"',g' -i /var/www/html/.env + + sed 's,{{REDIS_HOST}},'"${REDIS_HOST}"',g' -i /var/www/html/.env + sed 's,{{REDIS_DATABASE}},'"${REDIS_DATABASE}"',g' -i /var/www/html/.env + sed 's,{{REDIS_PORT}},'"${REDIS_PORT}"',g' -i /var/www/html/.env + + php composer.phar install --no-dev -o + php artisan app:install + rm -rf bootstrap/cache/* + touch /var/www/.cachet-installed + start_system +} + +start_system() { + check_database_connection + [ -f "/var/www/.cachet-installed" ] && echo "Starting Cachet" || initialize_system + php artisan config:cache + sudo /usr/bin/supervisord -n -c /etc/supervisor/supervisord.conf +} + +case ${1} in + init|start) + + case ${1} in + start) + start_system + ;; + init) + initialize_system + ;; + esac + ;; + help) + echo "Available options:" + echo " start - Starts the Cachet server (default)" + echo " init - Initialize the Cachet server (e.g. create databases, compile assets)." + echo " help - Displays the help" + echo " [command] - Execute the specified command, eg. bash." + ;; + *) + exec "$@" + ;; +esac + +exit 0 diff --git a/conf/nginx-site.conf b/conf/nginx-site.conf new file mode 100644 index 00000000..3c01bebc --- /dev/null +++ b/conf/nginx-site.conf @@ -0,0 +1,30 @@ +server { + listen 8000 default; ## Listen for ipv4; this line is default and implied + + # Make site accessible from http://localhost/ + server_name localhost; + root /var/www/html/public; + + index index.html index.htm index.php; + + charset utf-8; + + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + # Pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 + location ~ \.php$ { + fastcgi_split_path_info ^(.+\.php)(/.+)$; + include fastcgi_params; + fastcgi_pass cachet:9000; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_index index.php; + fastcgi_keep_conn on; + } + + location ~ /\.ht { + deny all; + } + +} diff --git a/conf/php-fpm-pool.conf b/conf/php-fpm-pool.conf new file mode 100644 index 00000000..9d773222 --- /dev/null +++ b/conf/php-fpm-pool.conf @@ -0,0 +1,24 @@ +[www] +user = www-data +group = www-data + +listen = 9000 + +request_terminate_timeout = 120s + +pm = ondemand +pm.max_children = 5 +pm.process_idle_timeout = 10s +pm.max_requests = 500 +chdir = / + +env[DB_DRIVER] = $DB_DRIVER +env[DB_HOST] = $DB_HOST +env[DB_DATABASE] = $DB_DATABASE +env[DB_USERNAME] = $DB_USERNAME +env[DB_PASSWORD] = $DB_PASSWORD +env[CACHE_DRIVER] = $CACHE_DRIVER + + +[global] +daemonize = no diff --git a/conf/supervisord.conf b/conf/supervisord.conf new file mode 100644 index 00000000..222a98e1 --- /dev/null +++ b/conf/supervisord.conf @@ -0,0 +1,22 @@ +[supervisord] +logfile=/dev/null ; (main log file;default $CWD/supervisord.log) +logfile_maxbytes=0 ; (max main logfile bytes b4 rotation;default 50MB) +logfile_backups=0 ; (num of main logfile rotation backups;default 10) +loglevel=info ; (log level;default info; others: debug,warn,trace) +pidfile=/tmp/supervisord.pid ; (supervisord pidfile;default supervisord.pid) +nodaemon=true ; (start in foreground if true;default false) + +; the below section must remain in the config file for RPC +; (supervisorctl/web interface) to work, additional interfaces may be +; added by defining them in separate rpcinterface: sections +[rpcinterface:supervisor] +supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface + +[supervisorctl] +serverurl=unix:///var/run/supervisor.sock ; use a unix:// URL for a unix socket + +[program:php-fpm] +command=/usr/sbin/php5-fpm -c /etc/php5/fpm + +[program:cron] +command=/usr/sbin/cron -f From f0fea307f2853a1f3bdff0495f57e4e989c87374 Mon Sep 17 00:00:00 2001 From: Ryan Trauntvein Date: Mon, 6 Jun 2016 11:31:32 -0700 Subject: [PATCH 4/5] [WIP] Add bats QA tests (#90) Add bats QA tests --- .travis.yml | 16 +- Dockerfile | 2 +- conf/nginx-site.conf | 2 +- docker-compose.yml | 4 +- test/docker_helpers.bash | 66 +++++ test/lib/batslib.bash | 594 +++++++++++++++++++++++++++++++++++++++ test/lib/output.bash | 264 +++++++++++++++++ test/test.full.bats | 35 +++ test/test_helpers.bash | 57 ++++ 9 files changed, 1031 insertions(+), 9 deletions(-) create mode 100644 test/docker_helpers.bash create mode 100644 test/lib/batslib.bash create mode 100644 test/lib/output.bash create mode 100755 test/test.full.bats create mode 100644 test/test_helpers.bash diff --git a/.travis.yml b/.travis.yml index 6b051661..da7c5448 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,17 +1,22 @@ sudo: required -services: +services: - docker -env: +env: global: - DOCKER_VERSION=1.10.1-0~trusty - DOCKER_COMPOSE_VERSION=1.7.1 -before_install: +before_install: # list docker-engine versions - apt-cache madison docker-engine + # add bats repo https://github.com/tkuchiki/bats-travis-ci + - sudo add-apt-repository ppa:duggan/bats --yes + - sudo apt-get update -qq + - sudo apt-get install -qq bats + # upgrade docker-engine to specific version - sudo apt-get -o Dpkg::Options::="--force-confnew" install -y docker-engine=${DOCKER_VERSION} @@ -20,9 +25,10 @@ before_install: - curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose - chmod +x docker-compose - sudo mv docker-compose /usr/local/bin + - docker pull appropriate/curl:latest -script: - - docker-compose build +script: + - bats test/test.full.bats notifications: email: false diff --git a/Dockerfile b/Dockerfile index 97b5fee7..40c4bc71 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ FROM debian:jessie MAINTAINER Alt Three ARG cachet_ver -ENV cachet_ver v2.3.0-RC2 +ENV cachet_ver v2.3.0-RC5 # Using debian packages instead of compiling from scratch RUN DEBIAN_FRONTEND=noninteractive \ diff --git a/conf/nginx-site.conf b/conf/nginx-site.conf index 3c01bebc..a0eedbda 100644 --- a/conf/nginx-site.conf +++ b/conf/nginx-site.conf @@ -1,5 +1,5 @@ server { - listen 8000 default; ## Listen for ipv4; this line is default and implied + listen 80 default; ## Listen for ipv4; this line is default and implied # Make site accessible from http://localhost/ server_name localhost; diff --git a/docker-compose.yml b/docker-compose.yml index ca36c0ea..cb6655d4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,9 +4,9 @@ services: nginx: image: nginx:stable-alpine volumes: - - ./docker/nginx-site.conf:/etc/nginx/conf.d/default.conf + - ./conf/nginx-site.conf:/etc/nginx/conf.d/default.conf ports: - - 80:8000 + - 80:80 links: - cachet volumes_from: diff --git a/test/docker_helpers.bash b/test/docker_helpers.bash new file mode 100644 index 00000000..e0c281df --- /dev/null +++ b/test/docker_helpers.bash @@ -0,0 +1,66 @@ +## functions to help deal with docker + +# Removes container $1 +function docker_clean { + docker kill $1 &>/dev/null ||: + sleep .25s + docker rm -vf $1 &>/dev/null ||: + sleep .25s +} + +# get the ip of docker container $1 +function docker_ip { + docker inspect --format '{{ .NetworkSettings.Networks.docker_default.IPAddress }}' $1 +} + +# get the id of docker container $1 +function docker_id { + docker inspect --format '{{ .ID }}' $1 +} + +# get the running state of container $1 +# → true/false +# fails if the container does not exist +function docker_running_state { + docker inspect -f {{.State.Running}} $1 +} + +# get the docker container $1 PID +function docker_pid { + docker inspect --format {{.State.Pid}} $1 +} + +# asserts logs from container $1 contains $2 +function docker_assert_log { + local -r container=$1 + shift + run docker logs $container + assert_output -p "$*" +} + +# wait for a container to produce a given text in its log +# $1 container +# $2 timeout in second +# $* text to wait for +function docker_wait_for_log { + local -r container=$1 + local -ir timeout_sec=$2 + shift 2 + retry $(( $timeout_sec * 2 )) .5s docker_assert_log $container "$*" +} + +# Create a docker container named $1 which exposes the docker host unix +# socket over tcp on port 2375. +# +# $1 container name +function docker_tcp { + local container_name="$1" + docker_clean $container_name + docker run -d \ + --label bats-type="socat" \ + --name $container_name \ + --expose 2375 \ + -v /var/run/docker.sock:/var/run/docker.sock \ + rancher/socat-docker + docker run --label bats-type="docker" --link "$container_name:docker" docker:1.10 version +} diff --git a/test/lib/batslib.bash b/test/lib/batslib.bash new file mode 100644 index 00000000..66f3d48f --- /dev/null +++ b/test/lib/batslib.bash @@ -0,0 +1,594 @@ +# +# batslib.bash +# ------------ +# +# The Standard Library is a collection of test helpers intended to +# simplify testing. It contains the following types of test helpers. +# +# - Assertions are functions that perform a test and output relevant +# information on failure to help debugging. They return 1 on failure +# and 0 otherwise. +# +# All output is formatted for readability using the functions of +# `output.bash' and sent to the standard error. +# + + +######################################################################## +# ASSERTIONS +######################################################################## + +# Fail and display a message. When no parameters are specified, the +# message is read from the standard input. Other functions use this to +# report failure. +# +# Globals: +# none +# Arguments: +# $@ - [=STDIN] message +# Returns: +# 1 - always +# Inputs: +# STDIN - [=$@] message +# Outputs: +# STDERR - message +fail() { + (( $# == 0 )) && batslib_err || batslib_err "$@" + return 1 +} + +# Fail and display details if the expression evaluates to false. Details +# include the expression, `$status' and `$output'. +# +# NOTE: The expression must be a simple command. Compound commands, such +# as `[[', can be used only when executed with `bash -c'. +# +# Globals: +# status +# output +# Arguments: +# $1 - expression +# Returns: +# 0 - expression evaluates to TRUE +# 1 - otherwise +# Outputs: +# STDERR - details, on failure +assert() { + if ! "$@"; then + { local -ar single=( + 'expression' "$*" + 'status' "$status" + ) + local -ar may_be_multi=( + 'output' "$output" + ) + local -ir width="$( batslib_get_max_single_line_key_width \ + "${single[@]}" "${may_be_multi[@]}" )" + batslib_print_kv_single "$width" "${single[@]}" + batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}" + } | batslib_decorate 'assertion failed' \ + | fail + fi +} + +# Fail and display details if the expected and actual values do not +# equal. Details include both values. +# +# Globals: +# none +# Arguments: +# $1 - actual value +# $2 - expected value +# Returns: +# 0 - values equal +# 1 - otherwise +# Outputs: +# STDERR - details, on failure +assert_equal() { + if [[ $1 != "$2" ]]; then + batslib_print_kv_single_or_multi 8 \ + 'expected' "$2" \ + 'actual' "$1" \ + | batslib_decorate 'values do not equal' \ + | fail + fi +} + +# Fail and display details if `$status' is not 0. Details include +# `$status' and `$output'. +# +# Globals: +# status +# output +# Arguments: +# none +# Returns: +# 0 - `$status' is 0 +# 1 - otherwise +# Outputs: +# STDERR - details, on failure +assert_success() { + if (( status != 0 )); then + { local -ir width=6 + batslib_print_kv_single "$width" 'status' "$status" + batslib_print_kv_single_or_multi "$width" 'output' "$output" + } | batslib_decorate 'command failed' \ + | fail + fi +} + +# Fail and display details if `$status' is 0. Details include `$output'. +# +# Optionally, when the expected status is specified, fail when it does +# not equal `$status'. In this case, details include the expected and +# actual status, and `$output'. +# +# Globals: +# status +# output +# Arguments: +# $1 - [opt] expected status +# Returns: +# 0 - `$status' is not 0, or +# `$status' equals the expected status +# 1 - otherwise +# Outputs: +# STDERR - details, on failure +assert_failure() { + (( $# > 0 )) && local -r expected="$1" + if (( status == 0 )); then + batslib_print_kv_single_or_multi 6 'output' "$output" \ + | batslib_decorate 'command succeeded, but it was expected to fail' \ + | fail + elif (( $# > 0 )) && (( status != expected )); then + { local -ir width=8 + batslib_print_kv_single "$width" \ + 'expected' "$expected" \ + 'actual' "$status" + batslib_print_kv_single_or_multi "$width" \ + 'output' "$output" + } | batslib_decorate 'command failed as expected, but status differs' \ + | fail + fi +} + +# Fail and display details if the expected does not match the actual +# output or a fragment of it. +# +# By default, the entire output is matched. The assertion fails if the +# expected output does not equal `$output'. Details include both values. +# +# When `-l ' is used, only the -th line is matched. The +# assertion fails if the expected line does not equal +# `${lines[}'. Details include the compared lines and . +# +# When `-l' is used without the argument, the output is searched +# for the expected line. The expected line is matched against each line +# in `${lines[@]}'. If no match is found the assertion fails. Details +# include the expected line and `$output'. +# +# By default, literal matching is performed. Options `-p' and `-r' +# enable partial (i.e. substring) and extended regular expression +# matching, respectively. Specifying an invalid extended regular +# expression with `-r' displays an error. +# +# Options `-p' and `-r' are mutually exclusive. When used +# simultaneously, an error is displayed. +# +# Globals: +# output +# lines +# Options: +# -l - match against the -th element of `${lines[@]}' +# -l - search `${lines[@]}' for the expected line +# -p - partial matching +# -r - extended regular expression matching +# Arguments: +# $1 - expected output +# Returns: +# 0 - expected matches the actual output +# 1 - otherwise +# Outputs: +# STDERR - details, on failure +# error message, on error +assert_output() { + local -i is_match_line=0 + local -i is_match_contained=0 + local -i is_mode_partial=0 + local -i is_mode_regex=0 + + # Handle options. + while (( $# > 0 )); do + case "$1" in + -l) + if (( $# > 2 )) && [[ $2 =~ ^([0-9]|[1-9][0-9]+)$ ]]; then + is_match_line=1 + local -ri idx="$2" + shift + else + is_match_contained=1; + fi + shift + ;; + -p) is_mode_partial=1; shift ;; + -r) is_mode_regex=1; shift ;; + --) break ;; + *) break ;; + esac + done + + if (( is_match_line )) && (( is_match_contained )); then + echo "\`-l' and \`-l ' are mutually exclusive" \ + | batslib_decorate 'ERROR: assert_output' \ + | fail + return $? + fi + + if (( is_mode_partial )) && (( is_mode_regex )); then + echo "\`-p' and \`-r' are mutually exclusive" \ + | batslib_decorate 'ERROR: assert_output' \ + | fail + return $? + fi + + # Arguments. + local -r expected="$1" + + if (( is_mode_regex == 1 )) && [[ '' =~ $expected ]] || (( $? == 2 )); then + echo "Invalid extended regular expression: \`$expected'" \ + | batslib_decorate 'ERROR: assert_output' \ + | fail + return $? + fi + + # Matching. + if (( is_match_contained )); then + # Line contained in output. + if (( is_mode_regex )); then + local -i idx + for (( idx = 0; idx < ${#lines[@]}; ++idx )); do + [[ ${lines[$idx]} =~ $expected ]] && return 0 + done + { local -ar single=( + 'regex' "$expected" + ) + local -ar may_be_multi=( + 'output' "$output" + ) + local -ir width="$( batslib_get_max_single_line_key_width \ + "${single[@]}" "${may_be_multi[@]}" )" + batslib_print_kv_single "$width" "${single[@]}" + batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}" + } | batslib_decorate 'no output line matches regular expression' \ + | fail + elif (( is_mode_partial )); then + local -i idx + for (( idx = 0; idx < ${#lines[@]}; ++idx )); do + [[ ${lines[$idx]} == *"$expected"* ]] && return 0 + done + { local -ar single=( + 'substring' "$expected" + ) + local -ar may_be_multi=( + 'output' "$output" + ) + local -ir width="$( batslib_get_max_single_line_key_width \ + "${single[@]}" "${may_be_multi[@]}" )" + batslib_print_kv_single "$width" "${single[@]}" + batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}" + } | batslib_decorate 'no output line contains substring' \ + | fail + else + local -i idx + for (( idx = 0; idx < ${#lines[@]}; ++idx )); do + [[ ${lines[$idx]} == "$expected" ]] && return 0 + done + { local -ar single=( + 'line' "$expected" + ) + local -ar may_be_multi=( + 'output' "$output" + ) + local -ir width="$( batslib_get_max_single_line_key_width \ + "${single[@]}" "${may_be_multi[@]}" )" + batslib_print_kv_single "$width" "${single[@]}" + batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}" + } | batslib_decorate 'output does not contain line' \ + | fail + fi + elif (( is_match_line )); then + # Specific line. + if (( is_mode_regex )); then + if ! [[ ${lines[$idx]} =~ $expected ]]; then + batslib_print_kv_single 5 \ + 'index' "$idx" \ + 'regex' "$expected" \ + 'line' "${lines[$idx]}" \ + | batslib_decorate 'regular expression does not match line' \ + | fail + fi + elif (( is_mode_partial )); then + if [[ ${lines[$idx]} != *"$expected"* ]]; then + batslib_print_kv_single 9 \ + 'index' "$idx" \ + 'substring' "$expected" \ + 'line' "${lines[$idx]}" \ + | batslib_decorate 'line does not contain substring' \ + | fail + fi + else + if [[ ${lines[$idx]} != "$expected" ]]; then + batslib_print_kv_single 8 \ + 'index' "$idx" \ + 'expected' "$expected" \ + 'actual' "${lines[$idx]}" \ + | batslib_decorate 'line differs' \ + | fail + fi + fi + else + # Entire output. + if (( is_mode_regex )); then + if ! [[ $output =~ $expected ]]; then + batslib_print_kv_single_or_multi 6 \ + 'regex' "$expected" \ + 'output' "$output" \ + | batslib_decorate 'regular expression does not match output' \ + | fail + fi + elif (( is_mode_partial )); then + if [[ $output != *"$expected"* ]]; then + batslib_print_kv_single_or_multi 9 \ + 'substring' "$expected" \ + 'output' "$output" \ + | batslib_decorate 'output does not contain substring' \ + | fail + fi + else + if [[ $output != "$expected" ]]; then + batslib_print_kv_single_or_multi 8 \ + 'expected' "$expected" \ + 'actual' "$output" \ + | batslib_decorate 'output differs' \ + | fail + fi + fi + fi +} + +# Fail and display details if the unexpected matches the actual output +# or a fragment of it. +# +# By default, the entire output is matched. The assertion fails if the +# unexpected output equals `$output'. Details include `$output'. +# +# When `-l ' is used, only the -th line is matched. The +# assertion fails if the unexpected line equals `${lines[}'. +# Details include the compared line and . +# +# When `-l' is used without the argument, the output is searched +# for the unexpected line. The unexpected line is matched against each +# line in `${lines[]}'. If a match is found the assertion fails. +# Details include the unexpected line, the index where it was found and +# `$output' (with the unexpected line highlighted in it if `$output` is +# longer than one line). +# +# By default, literal matching is performed. Options `-p' and `-r' +# enable partial (i.e. substring) and extended regular expression +# matching, respectively. On failure, the substring or the regular +# expression is added to the details (if not already displayed). +# Specifying an invalid extended regular expression with `-r' displays +# an error. +# +# Options `-p' and `-r' are mutually exclusive. When used +# simultaneously, an error is displayed. +# +# Globals: +# output +# lines +# Options: +# -l - match against the -th element of `${lines[@]}' +# -l - search `${lines[@]}' for the unexpected line +# -p - partial matching +# -r - extended regular expression matching +# Arguments: +# $1 - unexpected output +# Returns: +# 0 - unexpected matches the actual output +# 1 - otherwise +# Outputs: +# STDERR - details, on failure +# error message, on error +refute_output() { + local -i is_match_line=0 + local -i is_match_contained=0 + local -i is_mode_partial=0 + local -i is_mode_regex=0 + + # Handle options. + while (( $# > 0 )); do + case "$1" in + -l) + if (( $# > 2 )) && [[ $2 =~ ^([0-9]|[1-9][0-9]+)$ ]]; then + is_match_line=1 + local -ri idx="$2" + shift + else + is_match_contained=1; + fi + shift + ;; + -L) is_match_contained=1; shift ;; + -p) is_mode_partial=1; shift ;; + -r) is_mode_regex=1; shift ;; + --) break ;; + *) break ;; + esac + done + + if (( is_match_line )) && (( is_match_contained )); then + echo "\`-l' and \`-l ' are mutually exclusive" \ + | batslib_decorate 'ERROR: refute_output' \ + | fail + return $? + fi + + if (( is_mode_partial )) && (( is_mode_regex )); then + echo "\`-p' and \`-r' are mutually exclusive" \ + | batslib_decorate 'ERROR: refute_output' \ + | fail + return $? + fi + + # Arguments. + local -r unexpected="$1" + + if (( is_mode_regex == 1 )) && [[ '' =~ $unexpected ]] || (( $? == 2 )); then + echo "Invalid extended regular expression: \`$unexpected'" \ + | batslib_decorate 'ERROR: refute_output' \ + | fail + return $? + fi + + # Matching. + if (( is_match_contained )); then + # Line contained in output. + if (( is_mode_regex )); then + local -i idx + for (( idx = 0; idx < ${#lines[@]}; ++idx )); do + if [[ ${lines[$idx]} =~ $unexpected ]]; then + { local -ar single=( + 'regex' "$unexpected" + 'index' "$idx" + ) + local -a may_be_multi=( + 'output' "$output" + ) + local -ir width="$( batslib_get_max_single_line_key_width \ + "${single[@]}" "${may_be_multi[@]}" )" + batslib_print_kv_single "$width" "${single[@]}" + if batslib_is_single_line "${may_be_multi[1]}"; then + batslib_print_kv_single "$width" "${may_be_multi[@]}" + else + may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" \ + | batslib_prefix \ + | batslib_mark '>' "$idx" )" + batslib_print_kv_multi "${may_be_multi[@]}" + fi + } | batslib_decorate 'no line should match the regular expression' \ + | fail + return $? + fi + done + elif (( is_mode_partial )); then + local -i idx + for (( idx = 0; idx < ${#lines[@]}; ++idx )); do + if [[ ${lines[$idx]} == *"$unexpected"* ]]; then + { local -ar single=( + 'substring' "$unexpected" + 'index' "$idx" + ) + local -a may_be_multi=( + 'output' "$output" + ) + local -ir width="$( batslib_get_max_single_line_key_width \ + "${single[@]}" "${may_be_multi[@]}" )" + batslib_print_kv_single "$width" "${single[@]}" + if batslib_is_single_line "${may_be_multi[1]}"; then + batslib_print_kv_single "$width" "${may_be_multi[@]}" + else + may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" \ + | batslib_prefix \ + | batslib_mark '>' "$idx" )" + batslib_print_kv_multi "${may_be_multi[@]}" + fi + } | batslib_decorate 'no line should contain substring' \ + | fail + return $? + fi + done + else + local -i idx + for (( idx = 0; idx < ${#lines[@]}; ++idx )); do + if [[ ${lines[$idx]} == "$unexpected" ]]; then + { local -ar single=( + 'line' "$unexpected" + 'index' "$idx" + ) + local -a may_be_multi=( + 'output' "$output" + ) + local -ir width="$( batslib_get_max_single_line_key_width \ + "${single[@]}" "${may_be_multi[@]}" )" + batslib_print_kv_single "$width" "${single[@]}" + if batslib_is_single_line "${may_be_multi[1]}"; then + batslib_print_kv_single "$width" "${may_be_multi[@]}" + else + may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" \ + | batslib_prefix \ + | batslib_mark '>' "$idx" )" + batslib_print_kv_multi "${may_be_multi[@]}" + fi + } | batslib_decorate 'line should not be in output' \ + | fail + return $? + fi + done + fi + elif (( is_match_line )); then + # Specific line. + if (( is_mode_regex )); then + if [[ ${lines[$idx]} =~ $unexpected ]] || (( $? == 0 )); then + batslib_print_kv_single 5 \ + 'index' "$idx" \ + 'regex' "$unexpected" \ + 'line' "${lines[$idx]}" \ + | batslib_decorate 'regular expression should not match line' \ + | fail + fi + elif (( is_mode_partial )); then + if [[ ${lines[$idx]} == *"$unexpected"* ]]; then + batslib_print_kv_single 9 \ + 'index' "$idx" \ + 'substring' "$unexpected" \ + 'line' "${lines[$idx]}" \ + | batslib_decorate 'line should not contain substring' \ + | fail + fi + else + if [[ ${lines[$idx]} == "$unexpected" ]]; then + batslib_print_kv_single 5 \ + 'index' "$idx" \ + 'line' "${lines[$idx]}" \ + | batslib_decorate 'line should differ' \ + | fail + fi + fi + else + # Entire output. + if (( is_mode_regex )); then + if [[ $output =~ $unexpected ]] || (( $? == 0 )); then + batslib_print_kv_single_or_multi 6 \ + 'regex' "$unexpected" \ + 'output' "$output" \ + | batslib_decorate 'regular expression should not match output' \ + | fail + fi + elif (( is_mode_partial )); then + if [[ $output == *"$unexpected"* ]]; then + batslib_print_kv_single_or_multi 9 \ + 'substring' "$unexpected" \ + 'output' "$output" \ + | batslib_decorate 'output should not contain substring' \ + | fail + fi + else + if [[ $output == "$unexpected" ]]; then + batslib_print_kv_single_or_multi 6 \ + 'output' "$output" \ + | batslib_decorate 'output equals, but it was expected to differ' \ + | fail + fi + fi + fi +} diff --git a/test/lib/output.bash b/test/lib/output.bash new file mode 100644 index 00000000..aa9cb876 --- /dev/null +++ b/test/lib/output.bash @@ -0,0 +1,264 @@ +# +# output.bash +# ----------- +# +# Private functions implementing output formatting. Used by public +# helper functions. +# + +# Print a message to the standard error. When no parameters are +# specified, the message is read from the standard input. +# +# Globals: +# none +# Arguments: +# $@ - [=STDIN] message +# Returns: +# none +# Inputs: +# STDIN - [=$@] message +# Outputs: +# STDERR - message +batslib_err() { + { if (( $# > 0 )); then + echo "$@" + else + cat - + fi + } >&2 +} + +# Count the number of lines in the given string. +# +# TODO(ztombol): Fix tests and remove this note after #93 is resolved! +# NOTE: Due to a bug in Bats, `batslib_count_lines "$output"' does not +# give the same result as `${#lines[@]}' when the output contains +# empty lines. +# See PR #93 (https://github.com/sstephenson/bats/pull/93). +# +# Globals: +# none +# Arguments: +# $1 - string +# Returns: +# none +# Outputs: +# STDOUT - number of lines +batslib_count_lines() { + local -i n_lines=0 + local line + while IFS='' read -r line || [[ -n $line ]]; do + (( ++n_lines )) + done < <(printf '%s' "$1") + echo "$n_lines" +} + +# Determine whether all strings are single-line. +# +# Globals: +# none +# Arguments: +# $@ - strings +# Returns: +# 0 - all strings are single-line +# 1 - otherwise +batslib_is_single_line() { + for string in "$@"; do + (( $(batslib_count_lines "$string") > 1 )) && return 1 + done + return 0 +} + +# Determine the length of the longest key that has a single-line value. +# +# This function is useful in determining the correct width of the key +# column in two-column format when some keys may have multi-line values +# and thus should be excluded. +# +# Globals: +# none +# Arguments: +# $odd - key +# $even - value of the previous key +# Returns: +# none +# Outputs: +# STDOUT - length of longest key +batslib_get_max_single_line_key_width() { + local -i max_len=-1 + while (( $# != 0 )); do + local -i key_len="${#1}" + batslib_is_single_line "$2" && (( key_len > max_len )) && max_len="$key_len" + shift 2 + done + echo "$max_len" +} + +# Print key-value pairs in two-column format. +# +# Keys are displayed in the first column, and their corresponding values +# in the second. To evenly line up values, the key column is fixed-width +# and its width is specified with the first parameter (possibly computed +# using `batslib_get_max_single_line_key_width'). +# +# Globals: +# none +# Arguments: +# $1 - width of key column +# $even - key +# $odd - value of the previous key +# Returns: +# none +# Outputs: +# STDOUT - formatted key-value pairs +batslib_print_kv_single() { + local -ir col_width="$1"; shift + while (( $# != 0 )); do + printf '%-*s : %s\n' "$col_width" "$1" "$2" + shift 2 + done +} + +# Print key-value pairs in multi-line format. +# +# The key is displayed first with the number of lines of its +# corresponding value in parenthesis. Next, starting on the next line, +# the value is displayed. For better readability, it is recommended to +# indent values using `batslib_prefix'. +# +# Globals: +# none +# Arguments: +# $odd - key +# $even - value of the previous key +# Returns: +# none +# Outputs: +# STDOUT - formatted key-value pairs +batslib_print_kv_multi() { + while (( $# != 0 )); do + printf '%s (%d lines):\n' "$1" "$( batslib_count_lines "$2" )" + printf '%s\n' "$2" + shift 2 + done +} + +# Print all key-value pairs in either two-column or multi-line format +# depending on whether all values are single-line. +# +# If all values are single-line, print all pairs in two-column format +# with the specified key column width (identical to using +# `batslib_print_kv_single'). +# +# Otherwise, print all pairs in multi-line format after indenting values +# with two spaces for readability (identical to using `batslib_prefix' +# and `batslib_print_kv_multi') +# +# Globals: +# none +# Arguments: +# $1 - width of key column (for two-column format) +# $even - key +# $odd - value of the previous key +# Returns: +# none +# Outputs: +# STDOUT - formatted key-value pairs +batslib_print_kv_single_or_multi() { + local -ir width="$1"; shift + local -a pairs=( "$@" ) + + local -a values=() + local -i i + for (( i=1; i < ${#pairs[@]}; i+=2 )); do + values+=( "${pairs[$i]}" ) + done + + if batslib_is_single_line "${values[@]}"; then + batslib_print_kv_single "$width" "${pairs[@]}" + else + local -i i + for (( i=1; i < ${#pairs[@]}; i+=2 )); do + pairs[$i]="$( batslib_prefix < <(printf '%s' "${pairs[$i]}") )" + done + batslib_print_kv_multi "${pairs[@]}" + fi +} + +# Prefix each line read from the standard input with the given string. +# +# Globals: +# none +# Arguments: +# $1 - [= ] prefix string +# Returns: +# none +# Inputs: +# STDIN - lines +# Outputs: +# STDOUT - prefixed lines +batslib_prefix() { + local -r prefix="${1:- }" + local line + while IFS='' read -r line || [[ -n $line ]]; do + printf '%s%s\n' "$prefix" "$line" + done +} + +# Mark select lines of the text read from the standard input by +# overwriting their beginning with the given string. +# +# Usually the input is indented by a few spaces using `batslib_prefix' +# first. +# +# Globals: +# none +# Arguments: +# $1 - marking string +# $@ - indices (zero-based) of lines to mark +# Returns: +# none +# Inputs: +# STDIN - lines +# Outputs: +# STDOUT - lines after marking +batslib_mark() { + local -r symbol="$1"; shift + # Sort line numbers. + set -- $( sort -nu <<< "$( printf '%d\n' "$@" )" ) + + local line + local -i idx=0 + while IFS='' read -r line || [[ -n $line ]]; do + if (( ${1:--1} == idx )); then + printf '%s\n' "${symbol}${line:${#symbol}}" + shift + else + printf '%s\n' "$line" + fi + (( ++idx )) + done +} + +# Enclose the input text in header and footer lines. +# +# The header contains the given string as title. The output is preceded +# and followed by an additional newline to make it stand out more. +# +# Globals: +# none +# Arguments: +# $1 - title +# Returns: +# none +# Inputs: +# STDIN - text +# Outputs: +# STDOUT - decorated text +batslib_decorate() { + echo + echo "-- $1 --" + cat - + echo '--' + echo +} \ No newline at end of file diff --git a/test/test.full.bats b/test/test.full.bats new file mode 100755 index 00000000..09f3168e --- /dev/null +++ b/test/test.full.bats @@ -0,0 +1,35 @@ +#!/usr/bin/env bats +load test_helpers +load docker_helpers +load "lib/batslib" +load "lib/output" + +@test "[$TEST_FILE] testing Cachet Docker image build" { + command docker-compose build cachet +} + +@test "[$TEST_FILE] testing Cachet docker-compose up" { + command docker-compose up -d +} + +@test "[$TEST_FILE] wait for Cachet startup" { + docker_wait_for_log docker_cachet_1 15 "INFO success: php-fpm entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)" +} + +@test "[$TEST_FILE] php artisan cachet:seed" { + command docker exec docker_cachet_1 php artisan cachet:seed +} + +@test "[$TEST_FILE] curl 200 test" { + run curl_container docker_nginx_1 /auth/login --head + assert_output -l 0 $'HTTP/1.1 200 OK\r' +} + +@test "[$TEST_FILE] login test" { + run curl_container docker_nginx_1 /auth/login --head --user test:test123 + assert_output -l 0 $'HTTP/1.1 200 OK\r' +} + +@test "[$TEST_FILE] stop all bats containers" { + stop_bats_containers +} diff --git a/test/test_helpers.bash b/test/test_helpers.bash new file mode 100644 index 00000000..d99f4ba7 --- /dev/null +++ b/test/test_helpers.bash @@ -0,0 +1,57 @@ +# Test if requirements are met +( + type docker &>/dev/null || ( echo "docker is not available"; exit 1 ) +)>&2 + +TEST_FILE=$(basename $BATS_TEST_FILENAME .bats) + +# stop all containers with the "bats-type" label (matching the optionally supplied value) +# +# $1 optional label value +function stop_bats_containers { + docker-compose stop +} + +# delete all containers +docker_cleanup() { + docker-compose rm -af +} + +# Send a HTTP request to container $1 for path $2 and +# Additional curl options can be passed as $@ +# +# $1 container name +# $2 HTTP path to query +# $@ additional options to pass to the curl command +function curl_container { + local -r container=$1 + local -r path=$2 + shift 2 + docker run --net=docker_default --label bats-type="curl" appropriate/curl --silent \ + --connect-timeout 5 \ + --max-time 20 \ + --retry 4 --retry-delay 5 \ + "$@" \ + http://$(docker_ip $container)${path} +} + +# Retry a command $1 times until it succeeds. Wait $2 seconds between retries. +function retry { + local attempts=$1 + shift + local delay=$1 + shift + local i + + for ((i=0; i < attempts; i++)); do + run "$@" + if [ "$status" -eq 0 ]; then + echo "$output" + return 0 + fi + sleep $delay + done + + echo "Command \"$@\" failed $attempts times. Status: $status. Output: $output" >&2 + false +} From cb2b8479f3580c002415617d03c1e85e5e0a8bdf Mon Sep 17 00:00:00 2001 From: Ryan Trauntvein Date: Mon, 6 Jun 2016 11:32:39 -0700 Subject: [PATCH 5/5] Backport recent image changes to master --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 40c4bc71..52b59322 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ FROM debian:jessie MAINTAINER Alt Three ARG cachet_ver -ENV cachet_ver v2.3.0-RC5 +ENV cachet_ver master # Using debian packages instead of compiling from scratch RUN DEBIAN_FRONTEND=noninteractive \