diff options
141 files changed, 13567 insertions, 7917 deletions
diff --git a/.hgignore b/.hgignore new file mode 100644 index 00000000..d4f37fde --- /dev/null +++ b/.hgignore @@ -0,0 +1,6 @@ +^build/ +^Makefile$ +\.pyc$ +\.cache$ +\.pytest_cache$ +__pycache__/ @@ -45,3 +45,4 @@ e0658022962c0d5b0a32a1b0e3090bdec328e3da 1.17.0-1 de07e42484ecc595050fcbd3581a64cc6b1c1de5 1.18.0-1 86cdf66f82745d8db35345368dcdb38c79a4f03a 1.19.0 79f364e9aa907b1d7768e0e6686ce0a80fe61f44 1.19.0-1 +0e985b30067380782125f1c479eda4ef909418df 1.20.0 @@ -1,4 +1,51 @@ +Changes with Unit 1.20.0 08 Oct 2020 + + *) Change: the PHP module is now initialized before chrooting; this + enables loading all extensions from the host system. + + *) Change: AVIF and APNG image formats added to the default MIME type + list. + + *) Change: functional tests migrated to the pytest framework. + + *) Feature: the Python module now fully supports applications that use + the ASGI 3.0 server interface. + + *) Feature: the Python module now has a built-in WebSocket server + implementation for applications, compatible with the HTTP & WebSocket + ASGI Message Format 2.1 specification. + + *) Feature: automatic mounting of an isolated "/tmp" file system into + chrooted application environments. + + *) Feature: the $host variable contains a normalized "Host" request + value. + + *) Feature: the "callable" option sets Python application callable + names. + + *) Feature: compatibility with PHP 8 RC 1. Thanks to Remi Collet. + + *) Feature: the "automount" option in the "isolation" object allows to + turn off the automatic mounting of language module dependencies. + + *) Bugfix: "pass"-ing requests to upstreams from a route was broken; the + bug had appeared in 1.19.0. Thanks to 洪志道 (Hong Zhi Dao) for + discovering and fixing it. + + *) Bugfix: the router process could crash during reconfiguration. + + *) Bugfix: a memory leak occurring in the router process; the bug had + appeared in 1.18.0. + + *) Bugfix: the "!" (non-empty) pattern was matched incorrectly; the bug + had appeared in 1.19.0. + + *) Bugfix: fixed building on platforms without sendfile() support, + notably NetBSD; the bug had appeared in 1.16.0. + + Changes with Unit 1.19.0 13 Aug 2020 *) Feature: reworked IPC between the router process and the applications diff --git a/auto/modules/java b/auto/modules/java index fa68f573..be8f443c 100644 --- a/auto/modules/java +++ b/auto/modules/java @@ -326,11 +326,11 @@ cat << END > $NXT_BUILD_DIR/$NXT_JAVA_MOUNTS_HEADER static const nxt_fs_mount_t nxt_java_mounts[] = { - {(u_char *) "proc", (u_char *) "/proc", (u_char *) "proc", 0, NULL}, + {(u_char *) "proc", (u_char *) "/proc", (u_char *) "proc", 0, NULL, 1}, {(u_char *) "$NXT_JAVA_LIBC_DIR", (u_char *) "$NXT_JAVA_LIBC_DIR", - (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL}, + (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL, 1}, {(u_char *) "$NXT_JAVA_HOME", (u_char *) "$NXT_JAVA_HOME", - (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL}, + (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL, 1}, }; diff --git a/auto/modules/php b/auto/modules/php index 75d60242..41eeb1c3 100644 --- a/auto/modules/php +++ b/auto/modules/php @@ -74,6 +74,8 @@ if /bin/sh -c "${NXT_PHP_CONFIG} --version" >> $NXT_AUTOCONF_ERR 2>&1; then $echo " found" NXT_PHP_VERSION="`${NXT_PHP_CONFIG} --version`" + NXT_PHP_EXT_DIR="`${NXT_PHP_CONFIG} --extension-dir`" + $echo " + PHP SAPI: [`${NXT_PHP_CONFIG} --php-sapis`]" NXT_PHP_MAJOR_VERSION=${NXT_PHP_VERSION%%.*} @@ -213,6 +215,7 @@ if grep ^$NXT_PHP_MODULE: $NXT_MAKEFILE 2>&1 > /dev/null; then exit 1; fi + $echo " + PHP module: ${NXT_PHP_MODULE}.unit.so" . auto/cc/deps diff --git a/auto/modules/python b/auto/modules/python index c14bf7e0..48f6e5ef 100644 --- a/auto/modules/python +++ b/auto/modules/python @@ -138,7 +138,7 @@ pyver = "python" + str(sys.version_info[0]) + "." + str(sys.version_info[1]) print("static const nxt_fs_mount_t nxt_python_mounts[] = {") -pattern = "{(u_char *) \"%s\", (u_char *) \"%s\", (u_char *) \"bind\", NXT_MS_BIND|NXT_MS_REC, NULL}," +pattern = "{(u_char *) \"%s\", (u_char *) \"%s\", (u_char *) \"bind\", NXT_MS_BIND|NXT_MS_REC, NULL, 1}," base = None for p in sys.path: if len(p) > 0: @@ -167,7 +167,13 @@ $echo " + Python module: ${NXT_PYTHON_MODULE}.unit.so" $echo >> $NXT_MAKEFILE NXT_PYTHON_MODULE_SRCS=" \ - src/nxt_python_wsgi.c \ + src/python/nxt_python.c \ + src/python/nxt_python_asgi.c \ + src/python/nxt_python_asgi_http.c \ + src/python/nxt_python_asgi_lifespan.c \ + src/python/nxt_python_asgi_str.c \ + src/python/nxt_python_asgi_websocket.c \ + src/python/nxt_python_wsgi.c \ " # The python module object files. @@ -185,6 +191,7 @@ for nxt_src in $NXT_PYTHON_MODULE_SRCS; do cat << END >> $NXT_MAKEFILE $NXT_BUILD_DIR/$nxt_obj: $nxt_src $NXT_VERSION_H + mkdir -p $NXT_BUILD_DIR/src/python \$(CC) -c \$(CFLAGS) -DNXT_PYTHON_MOUNTS_H=\"$NXT_PYTHON_MOUNTS_HEADER\" \\ \$(NXT_INCS) $NXT_PYTHON_INCLUDE \\ $nxt_dep_flags \\ diff --git a/auto/modules/ruby b/auto/modules/ruby index c1444f07..e0d54516 100644 --- a/auto/modules/ruby +++ b/auto/modules/ruby @@ -156,23 +156,23 @@ cat << END > $NXT_RUBY_MOUNTS_PATH static const nxt_fs_mount_t nxt_ruby_mounts[] = { {(u_char *) "$NXT_RUBY_RUBYHDRDIR", (u_char *) "$NXT_RUBY_RUBYHDRDIR", - (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL}, + (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL, 1}, {(u_char *) "$NXT_RUBY_ARCHHDRDIR", (u_char *) "$NXT_RUBY_ARCHHDRDIR", - (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL}, + (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL, 1}, {(u_char *) "$NXT_RUBY_SITEDIR", (u_char *) "$NXT_RUBY_SITEDIR", - (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL}, + (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL, 1}, {(u_char *) "$NXT_RUBY_LIBDIR", (u_char *) "$NXT_RUBY_LIBDIR", - (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL}, + (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL, 1}, {(u_char *) "$NXT_RUBY_TOPDIR", (u_char *) "$NXT_RUBY_TOPDIR", - (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL}, + (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL, 1}, {(u_char *) "$NXT_RUBY_PREFIXDIR", (u_char *) "$NXT_RUBY_PREFIXDIR", - (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL}, + (u_char *) "bind", NXT_MS_BIND | NXT_MS_REC, NULL, 1}, END for path in `echo $NXT_RUBY_GEMPATH | tr ':' '\n'`; do $echo "{(u_char *) \"$path\", (u_char *) \"$path\"," >> $NXT_RUBY_MOUNTS_PATH - $echo "(u_char *) \"bind\", NXT_MS_BIND | NXT_MS_REC, NULL}," >> $NXT_RUBY_MOUNTS_PATH + $echo "(u_char *) \"bind\", NXT_MS_BIND | NXT_MS_REC, NULL, 1}," >> $NXT_RUBY_MOUNTS_PATH done $echo "};" >> $NXT_RUBY_MOUNTS_PATH diff --git a/auto/sendfile b/auto/sendfile index a065f7b6..1c20db06 100644 --- a/auto/sendfile +++ b/auto/sendfile @@ -84,10 +84,8 @@ fi if [ $nxt_found = no ]; then - $echo - $echo "$0: error: no supported sendfile() found." - $echo - exit 1; + # No supported sendfile() found. Using our replacement. + nxt_found=yes fi diff --git a/auto/sources b/auto/sources index a61577dc..e44dc4bb 100644 --- a/auto/sources +++ b/auto/sources @@ -14,6 +14,7 @@ NXT_LIB_SRCS=" \ src/nxt_socket.c \ src/nxt_socketpair.c \ src/nxt_credential.c \ + src/nxt_isolation.c \ src/nxt_process.c \ src/nxt_process_title.c \ src/nxt_signal.c \ diff --git a/docs/changes.xml b/docs/changes.xml index 7c5016e6..2a954dbe 100644 --- a/docs/changes.xml +++ b/docs/changes.xml @@ -13,6 +13,133 @@ unit-perl unit-ruby unit-jsc-common unit-jsc8 unit-jsc10 unit-jsc11" + ver="1.20.0" rev="1" + date="2020-10-08" time="18:00:00 +0300" + packager="Andrei Belov <defan@nginx.com>"> + +<change> +<para> +NGINX Unit updated to 1.20.0. +</para> +</change> + +</changes> + + +<changes apply="unit" ver="1.20.0" rev="1" + date="2020-10-08" time="18:00:00 +0300" + packager="Andrei Belov <defan@nginx.com>"> + +<change type="change"> +<para> +the PHP module is now initialized before chrooting; this enables loading all +extensions from the host system. +</para> +</change> + +<change type="change"> +<para> +AVIF and APNG image formats added to the default MIME type list. +</para> +</change> + +<change type="change"> +<para> +functional tests migrated to the pytest framework. +</para> +</change> + +<change type="feature"> +<para> +the Python module now fully supports applications that use the ASGI 3.0 server +interface. +</para> +</change> + +<change type="feature"> +<para> +the Python module now has a built-in WebSocket server implementation for +applications, compatible with the HTTP & WebSocket ASGI Message Format 2.1 +specification. +</para> +</change> + +<change type="feature"> +<para> +automatic mounting of an isolated "/tmp" file system into chrooted application +environments. +</para> +</change> + +<change type="feature"> +<para> +the $host variable contains a normalized "Host" request value. +</para> +</change> + +<change type="feature"> +<para> +the "callable" option sets Python application callable names. +</para> +</change> + +<change type="feature"> +<para> +compatibility with PHP 8 RC 1. Thanks to Remi Collet. +</para> +</change> + +<change type="feature"> +<para> +the "automount" option in the "isolation" object allows to turn off the +automatic mounting of language module dependencies. +</para> +</change> + +<change type="bugfix"> +<para> +"pass"-ing requests to upstreams from a route was broken; the bug had appeared +in 1.19.0. Thanks to 洪志道 (Hong Zhi Dao) for discovering and fixing it. +</para> +</change> + +<change type="bugfix"> +<para> +the router process could crash during reconfiguration. +</para> +</change> + +<change type="bugfix"> +<para> +a memory leak occurring in the router process; the bug had appeared in 1.18.0. +</para> +</change> + +<change type="bugfix"> +<para> +the "!" (non-empty) pattern was matched incorrectly; +the bug had appeared in 1.19.0. +</para> +</change> + +<change type="bugfix"> +<para> +fixed building on platforms without sendfile() support, notably NetBSD; +the bug had appeared in 1.16.0. +</para> +</change> + +</changes> + + +<changes apply="unit-php + unit-python unit-python2.7 + unit-python3.4 unit-python3.5 unit-python3.6 unit-python3.7 + unit-python3.8 + unit-go + unit-perl + unit-ruby + unit-jsc-common unit-jsc8 unit-jsc10 unit-jsc11" ver="1.19.0" rev="1" date="2020-08-13" time="18:00:00 +0300" packager="Andrei Belov <defan@nginx.com>"> diff --git a/pkg/deb/Makefile b/pkg/deb/Makefile index 55bf7082..efeb642f 100644 --- a/pkg/deb/Makefile +++ b/pkg/deb/Makefile @@ -299,7 +299,7 @@ test: unit modules test -h debuild/unit-$(VERSION)/debian/build-unit/build/$${soname} || \ ln -fs `pwd`/$${so} debuild/unit-$(VERSION)/debian/build-unit/build/$${soname} ; \ done ; \ - ( cd debuild/unit-$(VERSION)/debian/build-unit && ./test/run.py ) ; \ + ( cd debuild/unit-$(VERSION)/debian/build-unit && env python3 -m pytest ) ; \ } test-debug: unit modules @@ -310,7 +310,7 @@ test-debug: unit modules test -h debuild/unit-$(VERSION)/debian/build-unit-debug/build/$${soname} || \ ln -fs `pwd`/$${so} debuild/unit-$(VERSION)/debian/build-unit-debug/build/$${soname} ; \ done ; \ - ( cd debuild/unit-$(VERSION)/debian/build-unit-debug && ./test/run.py ) ; \ + ( cd debuild/unit-$(VERSION)/debian/build-unit-debug && env python3 -m pytest ) ; \ } clean: diff --git a/pkg/docker/Dockerfile.full b/pkg/docker/Dockerfile.full index fc100635..724c24b6 100644 --- a/pkg/docker/Dockerfile.full +++ b/pkg/docker/Dockerfile.full @@ -2,7 +2,7 @@ FROM debian:buster-slim LABEL maintainer="NGINX Docker Maintainers <docker-maint@nginx.com>" -ENV UNIT_VERSION 1.19.0-1~buster +ENV UNIT_VERSION 1.20.0-1~buster RUN set -x \ && apt-get update \ @@ -21,8 +21,10 @@ RUN set -x \ done; \ test -z "$found" && echo >&2 "error: failed to fetch GPG key $NGINX_GPGKEY" && exit 1; \ apt-get remove --purge --auto-remove -y gnupg1 && rm -rf /var/lib/apt/lists/* \ +# work-around debian bug 863199 + && mkdir -p /usr/share/man/man1 \ && dpkgArch="$(dpkg --print-architecture)" \ - && unitPackages="unit=${UNIT_VERSION} unit-php=${UNIT_VERSION} unit-python2.7=${UNIT_VERSION} unit-python3.7=${UNIT_VERSION} unit-perl=${UNIT_VERSION} unit-ruby=${UNIT_VERSION}" \ + && unitPackages="unit=${UNIT_VERSION} unit-php=${UNIT_VERSION} unit-python2.7=${UNIT_VERSION} unit-python3.7=${UNIT_VERSION} unit-perl=${UNIT_VERSION} unit-ruby=${UNIT_VERSION} unit-jsc11=${UNIT_VERSION}" \ && case "$dpkgArch" in \ amd64|i386) \ # arches officialy built by upstream diff --git a/pkg/docker/Dockerfile.go1.11-dev b/pkg/docker/Dockerfile.go1.11-dev index 7c3f234e..17b1d2e9 100644 --- a/pkg/docker/Dockerfile.go1.11-dev +++ b/pkg/docker/Dockerfile.go1.11-dev @@ -2,7 +2,7 @@ FROM debian:buster-slim LABEL maintainer="NGINX Docker Maintainers <docker-maint@nginx.com>" -ENV UNIT_VERSION 1.19.0-1~buster +ENV UNIT_VERSION 1.20.0-1~buster RUN set -x \ && apt-get update \ @@ -21,6 +21,8 @@ RUN set -x \ done; \ test -z "$found" && echo >&2 "error: failed to fetch GPG key $NGINX_GPGKEY" && exit 1; \ apt-get remove --purge --auto-remove -y gnupg1 && rm -rf /var/lib/apt/lists/* \ +# work-around debian bug 863199 + && mkdir -p /usr/share/man/man1 \ && dpkgArch="$(dpkg --print-architecture)" \ && unitPackages="unit=${UNIT_VERSION} unit-go=${UNIT_VERSION} gcc" \ && case "$dpkgArch" in \ diff --git a/pkg/docker/Dockerfile.jsc11 b/pkg/docker/Dockerfile.jsc11 new file mode 100644 index 00000000..65b8ce0f --- /dev/null +++ b/pkg/docker/Dockerfile.jsc11 @@ -0,0 +1,95 @@ +FROM debian:buster-slim + +LABEL maintainer="NGINX Docker Maintainers <docker-maint@nginx.com>" + +ENV UNIT_VERSION 1.20.0-1~buster + +RUN set -x \ + && apt-get update \ + && apt-get install --no-install-recommends --no-install-suggests -y gnupg1 apt-transport-https ca-certificates \ + && \ + NGINX_GPGKEY=573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62; \ + found=''; \ + for server in \ + ha.pool.sks-keyservers.net \ + hkp://keyserver.ubuntu.com:80 \ + hkp://p80.pool.sks-keyservers.net:80 \ + pgp.mit.edu \ + ; do \ + echo "Fetching GPG key $NGINX_GPGKEY from $server"; \ + apt-key adv --keyserver "$server" --keyserver-options timeout=10 --recv-keys "$NGINX_GPGKEY" && found=yes && break; \ + done; \ + test -z "$found" && echo >&2 "error: failed to fetch GPG key $NGINX_GPGKEY" && exit 1; \ + apt-get remove --purge --auto-remove -y gnupg1 && rm -rf /var/lib/apt/lists/* \ +# work-around debian bug 863199 + && mkdir -p /usr/share/man/man1 \ + && dpkgArch="$(dpkg --print-architecture)" \ + && unitPackages="unit=${UNIT_VERSION} unit-jsc11=${UNIT_VERSION}" \ + && case "$dpkgArch" in \ + amd64|i386) \ +# arches officialy built by upstream + echo "deb https://packages.nginx.org/unit/debian/ buster unit" >> /etc/apt/sources.list.d/unit.list \ + && apt-get update \ + ;; \ + *) \ +# we're on an architecture upstream doesn't officially build for +# let's build binaries from the published source packages + echo "deb-src https://packages.nginx.org/unit/debian/ buster unit" >> /etc/apt/sources.list.d/unit.list \ + \ +# new directory for storing sources and .deb files + && tempDir="$(mktemp -d)" \ + && chmod 777 "$tempDir" \ +# (777 to ensure APT's "_apt" user can access it too) + \ +# save list of currently-installed packages so build dependencies can be cleanly removed later + && savedAptMark="$(apt-mark showmanual)" \ + \ +# build .deb files from upstream's source packages (which are verified by apt-get) + && apt-get update \ + && apt-get build-dep -y $unitPackages \ + && ( \ + cd "$tempDir" \ + && DEB_BUILD_OPTIONS="nocheck parallel=$(nproc)" \ + apt-get source --compile $unitPackages \ + ) \ +# we don't remove APT lists here because they get re-downloaded and removed later + \ +# reset apt-mark's "manual" list so that "purge --auto-remove" will remove all build dependencies +# (which is done after we install the built packages so we don't have to redownload any overlapping dependencies) + && apt-mark showmanual | xargs apt-mark auto > /dev/null \ + && { [ -z "$savedAptMark" ] || apt-mark manual $savedAptMark; } \ + \ +# create a temporary local APT repo to install from (so that dependency resolution can be handled by APT, as it should be) + && ls -lAFh "$tempDir" \ + && ( cd "$tempDir" && dpkg-scanpackages . > Packages ) \ + && grep '^Package: ' "$tempDir/Packages" \ + && echo "deb [ trusted=yes ] file://$tempDir ./" > /etc/apt/sources.list.d/temp.list \ +# work around the following APT issue by using "Acquire::GzipIndexes=false" (overriding "/etc/apt/apt.conf.d/docker-gzip-indexes") +# Could not open file /var/lib/apt/lists/partial/_tmp_tmp.ODWljpQfkE_._Packages - open (13: Permission denied) +# ... +# E: Failed to fetch store:/var/lib/apt/lists/partial/_tmp_tmp.ODWljpQfkE_._Packages Could not open file /var/lib/apt/lists/partial/_tmp_tmp.ODWljpQfkE_._Packages - open (13: Permission denied) + && apt-get -o Acquire::GzipIndexes=false update \ + ;; \ + esac \ + \ + && apt-get install --no-install-recommends --no-install-suggests -y \ + $unitPackages \ + curl \ + && apt-get remove --purge --auto-remove -y apt-transport-https && rm -rf /var/lib/apt/lists/* /etc/apt/sources.list.d/unit.list \ + \ +# if we have leftovers from building, let's purge them (including extra, unnecessary build deps) + && if [ -n "$tempDir" ]; then \ + apt-get purge -y --auto-remove \ + && rm -rf "$tempDir" /etc/apt/sources.list.d/temp.list; \ + fi + +# forward log to docker log collector +RUN ln -sf /dev/stdout /var/log/unit.log + +STOPSIGNAL SIGTERM + +COPY docker-entrypoint.sh /usr/local/bin/ +RUN mkdir /docker-entrypoint.d/ +ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] + +CMD ["unitd", "--no-daemon", "--control", "unix:/var/run/control.unit.sock"] diff --git a/pkg/docker/Dockerfile.minimal b/pkg/docker/Dockerfile.minimal index 48f1864c..5a10620a 100644 --- a/pkg/docker/Dockerfile.minimal +++ b/pkg/docker/Dockerfile.minimal @@ -2,7 +2,7 @@ FROM debian:buster-slim LABEL maintainer="NGINX Docker Maintainers <docker-maint@nginx.com>" -ENV UNIT_VERSION 1.19.0-1~buster +ENV UNIT_VERSION 1.20.0-1~buster RUN set -x \ && apt-get update \ @@ -21,6 +21,8 @@ RUN set -x \ done; \ test -z "$found" && echo >&2 "error: failed to fetch GPG key $NGINX_GPGKEY" && exit 1; \ apt-get remove --purge --auto-remove -y gnupg1 && rm -rf /var/lib/apt/lists/* \ +# work-around debian bug 863199 + && mkdir -p /usr/share/man/man1 \ && dpkgArch="$(dpkg --print-architecture)" \ && unitPackages="unit=${UNIT_VERSION}" \ && case "$dpkgArch" in \ diff --git a/pkg/docker/Dockerfile.perl5.28 b/pkg/docker/Dockerfile.perl5.28 index bff0ba0c..868527e5 100644 --- a/pkg/docker/Dockerfile.perl5.28 +++ b/pkg/docker/Dockerfile.perl5.28 @@ -2,7 +2,7 @@ FROM debian:buster-slim LABEL maintainer="NGINX Docker Maintainers <docker-maint@nginx.com>" -ENV UNIT_VERSION 1.19.0-1~buster +ENV UNIT_VERSION 1.20.0-1~buster RUN set -x \ && apt-get update \ @@ -21,6 +21,8 @@ RUN set -x \ done; \ test -z "$found" && echo >&2 "error: failed to fetch GPG key $NGINX_GPGKEY" && exit 1; \ apt-get remove --purge --auto-remove -y gnupg1 && rm -rf /var/lib/apt/lists/* \ +# work-around debian bug 863199 + && mkdir -p /usr/share/man/man1 \ && dpkgArch="$(dpkg --print-architecture)" \ && unitPackages="unit=${UNIT_VERSION} unit-perl=${UNIT_VERSION}" \ && case "$dpkgArch" in \ diff --git a/pkg/docker/Dockerfile.php7.3 b/pkg/docker/Dockerfile.php7.3 index 832baa5d..b5789dec 100644 --- a/pkg/docker/Dockerfile.php7.3 +++ b/pkg/docker/Dockerfile.php7.3 @@ -2,7 +2,7 @@ FROM debian:buster-slim LABEL maintainer="NGINX Docker Maintainers <docker-maint@nginx.com>" -ENV UNIT_VERSION 1.19.0-1~buster +ENV UNIT_VERSION 1.20.0-1~buster RUN set -x \ && apt-get update \ @@ -21,6 +21,8 @@ RUN set -x \ done; \ test -z "$found" && echo >&2 "error: failed to fetch GPG key $NGINX_GPGKEY" && exit 1; \ apt-get remove --purge --auto-remove -y gnupg1 && rm -rf /var/lib/apt/lists/* \ +# work-around debian bug 863199 + && mkdir -p /usr/share/man/man1 \ && dpkgArch="$(dpkg --print-architecture)" \ && unitPackages="unit=${UNIT_VERSION} unit-php=${UNIT_VERSION}" \ && case "$dpkgArch" in \ diff --git a/pkg/docker/Dockerfile.python2.7 b/pkg/docker/Dockerfile.python2.7 index 85f0add6..9bb83f7b 100644 --- a/pkg/docker/Dockerfile.python2.7 +++ b/pkg/docker/Dockerfile.python2.7 @@ -2,7 +2,7 @@ FROM debian:buster-slim LABEL maintainer="NGINX Docker Maintainers <docker-maint@nginx.com>" -ENV UNIT_VERSION 1.19.0-1~buster +ENV UNIT_VERSION 1.20.0-1~buster RUN set -x \ && apt-get update \ @@ -21,6 +21,8 @@ RUN set -x \ done; \ test -z "$found" && echo >&2 "error: failed to fetch GPG key $NGINX_GPGKEY" && exit 1; \ apt-get remove --purge --auto-remove -y gnupg1 && rm -rf /var/lib/apt/lists/* \ +# work-around debian bug 863199 + && mkdir -p /usr/share/man/man1 \ && dpkgArch="$(dpkg --print-architecture)" \ && unitPackages="unit=${UNIT_VERSION} unit-python2.7=${UNIT_VERSION}" \ && case "$dpkgArch" in \ diff --git a/pkg/docker/Dockerfile.python3.7 b/pkg/docker/Dockerfile.python3.7 index cefd15c1..99324844 100644 --- a/pkg/docker/Dockerfile.python3.7 +++ b/pkg/docker/Dockerfile.python3.7 @@ -2,7 +2,7 @@ FROM debian:buster-slim LABEL maintainer="NGINX Docker Maintainers <docker-maint@nginx.com>" -ENV UNIT_VERSION 1.19.0-1~buster +ENV UNIT_VERSION 1.20.0-1~buster RUN set -x \ && apt-get update \ @@ -21,6 +21,8 @@ RUN set -x \ done; \ test -z "$found" && echo >&2 "error: failed to fetch GPG key $NGINX_GPGKEY" && exit 1; \ apt-get remove --purge --auto-remove -y gnupg1 && rm -rf /var/lib/apt/lists/* \ +# work-around debian bug 863199 + && mkdir -p /usr/share/man/man1 \ && dpkgArch="$(dpkg --print-architecture)" \ && unitPackages="unit=${UNIT_VERSION} unit-python3.7=${UNIT_VERSION}" \ && case "$dpkgArch" in \ diff --git a/pkg/docker/Dockerfile.ruby2.5 b/pkg/docker/Dockerfile.ruby2.5 index 36f9594f..fef96867 100644 --- a/pkg/docker/Dockerfile.ruby2.5 +++ b/pkg/docker/Dockerfile.ruby2.5 @@ -2,7 +2,7 @@ FROM debian:buster-slim LABEL maintainer="NGINX Docker Maintainers <docker-maint@nginx.com>" -ENV UNIT_VERSION 1.19.0-1~buster +ENV UNIT_VERSION 1.20.0-1~buster RUN set -x \ && apt-get update \ @@ -21,6 +21,8 @@ RUN set -x \ done; \ test -z "$found" && echo >&2 "error: failed to fetch GPG key $NGINX_GPGKEY" && exit 1; \ apt-get remove --purge --auto-remove -y gnupg1 && rm -rf /var/lib/apt/lists/* \ +# work-around debian bug 863199 + && mkdir -p /usr/share/man/man1 \ && dpkgArch="$(dpkg --print-architecture)" \ && unitPackages="unit=${UNIT_VERSION} unit-ruby=${UNIT_VERSION}" \ && case "$dpkgArch" in \ diff --git a/pkg/docker/Dockerfile.tmpl b/pkg/docker/Dockerfile.tmpl index 3372ef6f..8b0e35e4 100644 --- a/pkg/docker/Dockerfile.tmpl +++ b/pkg/docker/Dockerfile.tmpl @@ -21,6 +21,8 @@ RUN set -x \ done; \ test -z "$found" && echo >&2 "error: failed to fetch GPG key $NGINX_GPGKEY" && exit 1; \ apt-get remove --purge --auto-remove -y gnupg1 && rm -rf /var/lib/apt/lists/* \ +# work-around debian bug 863199 + && mkdir -p /usr/share/man/man1 \ && dpkgArch="$(dpkg --print-architecture)" \ && unitPackages=@@UNITPACKAGES@@ \ && case "$dpkgArch" in \ diff --git a/pkg/docker/Makefile b/pkg/docker/Makefile index 7647e51b..aed5b8f7 100644 --- a/pkg/docker/Makefile +++ b/pkg/docker/Makefile @@ -12,7 +12,7 @@ CODENAME := buster UNIT_VERSION = $(VERSION)-$(RELEASE)~$(CODENAME) MODULES = python2.7 python3.7 php7.3 go1.11-dev perl5.28 ruby2.5 \ - full minimal + jsc11 full minimal MODULE_php7.3="unit=$${UNIT_VERSION} unit-php=$${UNIT_VERSION}" @@ -26,7 +26,9 @@ MODULE_perl5.28="unit=$${UNIT_VERSION} unit-perl=$${UNIT_VERSION}" MODULE_ruby2.5="unit=$${UNIT_VERSION} unit-ruby=$${UNIT_VERSION}" -MODULE_full="unit=$${UNIT_VERSION} unit-php=$${UNIT_VERSION} unit-python2.7=$${UNIT_VERSION} unit-python3.7=$${UNIT_VERSION} unit-perl=$${UNIT_VERSION} unit-ruby=$${UNIT_VERSION}" +MODULE_jsc11="unit=$${UNIT_VERSION} unit-jsc11=$${UNIT_VERSION}" + +MODULE_full="unit=$${UNIT_VERSION} unit-php=$${UNIT_VERSION} unit-python2.7=$${UNIT_VERSION} unit-python3.7=$${UNIT_VERSION} unit-perl=$${UNIT_VERSION} unit-ruby=$${UNIT_VERSION} unit-jsc11=$${UNIT_VERSION}" MODULE_minimal="unit=$${UNIT_VERSION}" @@ -36,7 +38,7 @@ default: @echo "valid targets: all build dockerfiles push tag export clean" dockerfiles: $(addprefix Dockerfile., $(MODULES)) -build: $(addprefix build-,$(MODULES)) +build: refresh-base $(addprefix build-,$(MODULES)) tag: $(addprefix tag-,$(MODULES)) push: $(addprefix push-,$(MODULES)) latest export: $(addsuffix .tar.gz,$(addprefix $(EXPORT_DIR)/nginx-unit-$(VERSION)-,$(MODULES))) $(addsuffix .tar.gz.sha512, $(addprefix $(EXPORT_DIR)/nginx-unit-$(VERSION)-,$(MODULES))) @@ -49,7 +51,7 @@ Dockerfile.%: ../../version > $@ build-%: Dockerfile.% - docker build -t unit:$(VERSION)-$* -f Dockerfile.$* . + docker build --no-cache -t unit:$(VERSION)-$* -f Dockerfile.$* . tag-%: build-% docker tag unit:$(VERSION)-$* nginx/unit:$(VERSION)-$* @@ -61,6 +63,9 @@ latest: docker tag nginx/unit:$(VERSION)-full nginx/unit:latest docker push nginx/unit:latest +refresh-base: + docker pull $(shell head -n 1 Dockerfile.tmpl | cut -d' ' -f 2) + $(EXPORT_DIR): mkdir -p $@ @@ -76,4 +81,4 @@ clean: rm -f $(addprefix Dockerfile., $(MODULES)) rm -rf $(EXPORT_DIR) -.PHONY: default all build dockerfiles latest push tag export clean +.PHONY: default all build dockerfiles latest push tag export clean refresh-base diff --git a/pkg/rpm/Makefile b/pkg/rpm/Makefile index 70100896..1944d58d 100644 --- a/pkg/rpm/Makefile +++ b/pkg/rpm/Makefile @@ -274,7 +274,7 @@ test: unit modules test -h rpmbuild/BUILD/unit-$(VERSION)/build-nodebug/$${soname} || \ ln -fs `pwd`/$${so} rpmbuild/BUILD/unit-$(VERSION)/build-nodebug/$${soname} ; \ done ; \ - ( cd rpmbuild/BUILD/unit-$(VERSION) && rm -f build && ln -s build-nodebug build && ./test/run.py ) ; \ + ( cd rpmbuild/BUILD/unit-$(VERSION) && rm -f build && ln -s build-nodebug build && env python3 -m pytest ) ; \ } test-debug: unit modules @@ -285,7 +285,7 @@ test-debug: unit modules test -h rpmbuild/BUILD/unit-$(VERSION)/build-debug/$${soname} || \ ln -fs `pwd`/$${so} rpmbuild/BUILD/unit-$(VERSION)/build-debug/$${soname} ; \ done ; \ - ( cd rpmbuild/BUILD/unit-$(VERSION) && rm -f build && ln -s build-debug build && ./test/run.py ) ; \ + ( cd rpmbuild/BUILD/unit-$(VERSION) && rm -f build && ln -s build-debug build && env python3 -m pytest ) ; \ } clean: diff --git a/src/nxt_application.c b/src/nxt_application.c index 57e4615e..6935346c 100644 --- a/src/nxt_application.c +++ b/src/nxt_application.c @@ -14,6 +14,7 @@ #include <nxt_application.h> #include <nxt_unit.h> #include <nxt_port_memory_int.h> +#include <nxt_isolation.h> #include <glob.h> @@ -41,45 +42,10 @@ static void nxt_discovery_quit(nxt_task_t *task, nxt_port_recv_msg_t *msg, void *data); static nxt_app_module_t *nxt_app_module_load(nxt_task_t *task, const char *name); -static nxt_int_t nxt_app_main_prefork(nxt_task_t *task, nxt_process_t *process, - nxt_mp_t *mp); static nxt_int_t nxt_app_setup(nxt_task_t *task, nxt_process_t *process); static nxt_int_t nxt_app_set_environment(nxt_conf_value_t *environment); static u_char *nxt_cstr_dup(nxt_mp_t *mp, u_char *dst, u_char *src); -#if (NXT_HAVE_ISOLATION_ROOTFS) -static nxt_int_t nxt_app_set_isolation_mounts(nxt_task_t *task, - nxt_process_t *process, nxt_str_t *app_type); -static nxt_int_t nxt_app_set_lang_mounts(nxt_task_t *task, - nxt_process_t *process, nxt_array_t *syspaths); -static nxt_int_t nxt_app_set_isolation_rootfs(nxt_task_t *task, - nxt_conf_value_t *isolation, nxt_process_t *process); -static nxt_int_t nxt_app_prepare_rootfs(nxt_task_t *task, - nxt_process_t *process); -#endif - -static nxt_int_t nxt_app_set_isolation(nxt_task_t *task, - nxt_conf_value_t *isolation, nxt_process_t *process); - -#if (NXT_HAVE_CLONE) -static nxt_int_t nxt_app_set_isolation_namespaces(nxt_task_t *task, - nxt_conf_value_t *isolation, nxt_process_t *process); -static nxt_int_t nxt_app_clone_flags(nxt_task_t *task, - nxt_conf_value_t *namespaces, nxt_clone_t *clone); -#endif - -#if (NXT_HAVE_CLONE_NEWUSER) -static nxt_int_t nxt_app_set_isolation_creds(nxt_task_t *task, - nxt_conf_value_t *isolation, nxt_process_t *process); -static nxt_int_t nxt_app_isolation_credential_map(nxt_task_t *task, - nxt_mp_t *mem_pool, nxt_conf_value_t *map_array, - nxt_clone_credential_map_t *map); -#endif - -#if (NXT_HAVE_PR_SET_NO_NEW_PRIVS) -static nxt_int_t nxt_app_set_isolation_new_privs(nxt_task_t *task, - nxt_conf_value_t *isolation, nxt_process_t *process); -#endif nxt_str_t nxt_server = nxt_string(NXT_SERVER); @@ -126,7 +92,7 @@ const nxt_process_init_t nxt_discovery_process = { const nxt_process_init_t nxt_app_process = { .type = NXT_PROCESS_APP, .setup = nxt_app_setup, - .prefork = nxt_app_main_prefork, + .prefork = nxt_isolation_main_prefork, .restart = 0, .start = NULL, /* set to module->start */ .port_handlers = &nxt_app_process_port_handlers, @@ -474,81 +440,6 @@ nxt_discovery_quit(nxt_task_t *task, nxt_port_recv_msg_t *msg, void *data) static nxt_int_t -nxt_app_main_prefork(nxt_task_t *task, nxt_process_t *process, nxt_mp_t *mp) -{ - nxt_int_t cap_setid; - nxt_int_t ret; - nxt_runtime_t *rt; - nxt_common_app_conf_t *app_conf; - - rt = task->thread->runtime; - app_conf = process->data.app; - cap_setid = rt->capabilities.setid; - - if (app_conf->isolation != NULL) { - ret = nxt_app_set_isolation(task, app_conf->isolation, process); - if (nxt_slow_path(ret != NXT_OK)) { - return ret; - } - } - -#if (NXT_HAVE_CLONE_NEWUSER) - if (nxt_is_clone_flag_set(process->isolation.clone.flags, NEWUSER)) { - cap_setid = 1; - } -#endif - -#if (NXT_HAVE_ISOLATION_ROOTFS) - if (process->isolation.rootfs != NULL) { - ret = nxt_app_set_isolation_mounts(task, process, &app_conf->type); - if (nxt_slow_path(ret != NXT_OK)) { - return ret; - } - } -#endif - - if (cap_setid) { - ret = nxt_process_creds_set(task, process, &app_conf->user, - &app_conf->group); - - if (nxt_slow_path(ret != NXT_OK)) { - return ret; - } - - } else { - if (!nxt_str_eq(&app_conf->user, (u_char *) rt->user_cred.user, - nxt_strlen(rt->user_cred.user))) - { - nxt_alert(task, "cannot set user \"%V\" for app \"%V\": " - "missing capabilities", &app_conf->user, &app_conf->name); - - return NXT_ERROR; - } - - if (app_conf->group.length > 0 - && !nxt_str_eq(&app_conf->group, (u_char *) rt->group, - nxt_strlen(rt->group))) - { - nxt_alert(task, "cannot set group \"%V\" for app \"%V\": " - "missing capabilities", &app_conf->group, - &app_conf->name); - - return NXT_ERROR; - } - } - -#if (NXT_HAVE_CLONE_NEWUSER) - ret = nxt_process_vldt_isolation_creds(task, process); - if (nxt_slow_path(ret != NXT_OK)) { - return ret; - } -#endif - - return NXT_OK; -} - - -static nxt_int_t nxt_app_setup(nxt_task_t *task, nxt_process_t *process) { nxt_int_t ret; @@ -594,13 +485,13 @@ nxt_app_setup(nxt_task_t *task, nxt_process_t *process) #if (NXT_HAVE_ISOLATION_ROOTFS) if (process->isolation.rootfs != NULL) { if (process->isolation.mounts != NULL) { - ret = nxt_app_prepare_rootfs(task, process); + ret = nxt_isolation_prepare_rootfs(task, process); if (nxt_slow_path(ret != NXT_OK)) { return ret; } } - ret = nxt_process_change_root(task, process); + ret = nxt_isolation_change_root(task, process); if (nxt_slow_path(ret != NXT_OK)) { return NXT_ERROR; } @@ -686,474 +577,6 @@ nxt_app_set_environment(nxt_conf_value_t *environment) } -static nxt_int_t -nxt_app_set_isolation(nxt_task_t *task, nxt_conf_value_t *isolation, - nxt_process_t *process) -{ -#if (NXT_HAVE_CLONE) - if (nxt_slow_path(nxt_app_set_isolation_namespaces(task, isolation, process) - != NXT_OK)) - { - return NXT_ERROR; - } -#endif - -#if (NXT_HAVE_CLONE_NEWUSER) - if (nxt_slow_path(nxt_app_set_isolation_creds(task, isolation, process) - != NXT_OK)) - { - return NXT_ERROR; - } -#endif - -#if (NXT_HAVE_ISOLATION_ROOTFS) - if (nxt_slow_path(nxt_app_set_isolation_rootfs(task, isolation, process) - != NXT_OK)) - { - return NXT_ERROR; - } -#endif - -#if (NXT_HAVE_PR_SET_NO_NEW_PRIVS) - if (nxt_slow_path(nxt_app_set_isolation_new_privs(task, isolation, process) - != NXT_OK)) - { - return NXT_ERROR; - } -#endif - - return NXT_OK; -} - - -#if (NXT_HAVE_CLONE) - -static nxt_int_t -nxt_app_set_isolation_namespaces(nxt_task_t *task, nxt_conf_value_t *isolation, - nxt_process_t *process) -{ - nxt_int_t ret; - nxt_conf_value_t *obj; - - static nxt_str_t nsname = nxt_string("namespaces"); - - obj = nxt_conf_get_object_member(isolation, &nsname, NULL); - if (obj != NULL) { - ret = nxt_app_clone_flags(task, obj, &process->isolation.clone); - if (nxt_slow_path(ret != NXT_OK)) { - return NXT_ERROR; - } - } - - return NXT_OK; -} - -#endif - - -#if (NXT_HAVE_CLONE_NEWUSER) - -static nxt_int_t -nxt_app_set_isolation_creds(nxt_task_t *task, nxt_conf_value_t *isolation, - nxt_process_t *process) -{ - nxt_int_t ret; - nxt_clone_t *clone; - nxt_conf_value_t *array; - - static nxt_str_t uidname = nxt_string("uidmap"); - static nxt_str_t gidname = nxt_string("gidmap"); - - clone = &process->isolation.clone; - - array = nxt_conf_get_object_member(isolation, &uidname, NULL); - if (array != NULL) { - ret = nxt_app_isolation_credential_map(task, process->mem_pool, array, - &clone->uidmap); - - if (nxt_slow_path(ret != NXT_OK)) { - return NXT_ERROR; - } - } - - array = nxt_conf_get_object_member(isolation, &gidname, NULL); - if (array != NULL) { - ret = nxt_app_isolation_credential_map(task, process->mem_pool, array, - &clone->gidmap); - - if (nxt_slow_path(ret != NXT_OK)) { - return NXT_ERROR; - } - } - - return NXT_OK; -} - - -static nxt_int_t -nxt_app_isolation_credential_map(nxt_task_t *task, nxt_mp_t *mp, - nxt_conf_value_t *map_array, nxt_clone_credential_map_t *map) -{ - nxt_int_t ret; - nxt_uint_t i; - nxt_conf_value_t *obj; - - static nxt_conf_map_t nxt_clone_map_entry_conf[] = { - { - nxt_string("container"), - NXT_CONF_MAP_INT, - offsetof(nxt_clone_map_entry_t, container), - }, - - { - nxt_string("host"), - NXT_CONF_MAP_INT, - offsetof(nxt_clone_map_entry_t, host), - }, - - { - nxt_string("size"), - NXT_CONF_MAP_INT, - offsetof(nxt_clone_map_entry_t, size), - }, - }; - - map->size = nxt_conf_array_elements_count(map_array); - - if (map->size == 0) { - return NXT_OK; - } - - map->map = nxt_mp_alloc(mp, map->size * sizeof(nxt_clone_map_entry_t)); - if (nxt_slow_path(map->map == NULL)) { - return NXT_ERROR; - } - - for (i = 0; i < map->size; i++) { - obj = nxt_conf_get_array_element(map_array, i); - - ret = nxt_conf_map_object(mp, obj, nxt_clone_map_entry_conf, - nxt_nitems(nxt_clone_map_entry_conf), - map->map + i); - if (nxt_slow_path(ret != NXT_OK)) { - nxt_alert(task, "clone map entry map error"); - return NXT_ERROR; - } - } - - return NXT_OK; -} - -#endif - -#if (NXT_HAVE_CLONE) - -static nxt_int_t -nxt_app_clone_flags(nxt_task_t *task, nxt_conf_value_t *namespaces, - nxt_clone_t *clone) -{ - uint32_t index; - nxt_str_t name; - nxt_int_t flag; - nxt_conf_value_t *value; - - index = 0; - - for ( ;; ) { - value = nxt_conf_next_object_member(namespaces, &name, &index); - - if (value == NULL) { - break; - } - - flag = 0; - -#if (NXT_HAVE_CLONE_NEWUSER) - if (nxt_str_eq(&name, "credential", 10)) { - flag = CLONE_NEWUSER; - } -#endif - -#if (NXT_HAVE_CLONE_NEWPID) - if (nxt_str_eq(&name, "pid", 3)) { - flag = CLONE_NEWPID; - } -#endif - -#if (NXT_HAVE_CLONE_NEWNET) - if (nxt_str_eq(&name, "network", 7)) { - flag = CLONE_NEWNET; - } -#endif - -#if (NXT_HAVE_CLONE_NEWUTS) - if (nxt_str_eq(&name, "uname", 5)) { - flag = CLONE_NEWUTS; - } -#endif - -#if (NXT_HAVE_CLONE_NEWNS) - if (nxt_str_eq(&name, "mount", 5)) { - flag = CLONE_NEWNS; - } -#endif - -#if (NXT_HAVE_CLONE_NEWCGROUP) - if (nxt_str_eq(&name, "cgroup", 6)) { - flag = CLONE_NEWCGROUP; - } -#endif - - if (!flag) { - nxt_alert(task, "unknown namespace flag: \"%V\"", &name); - return NXT_ERROR; - } - - if (nxt_conf_get_boolean(value)) { - clone->flags |= flag; - } - } - - return NXT_OK; -} - -#endif - - -#if (NXT_HAVE_ISOLATION_ROOTFS) - -static nxt_int_t -nxt_app_set_isolation_rootfs(nxt_task_t *task, nxt_conf_value_t *isolation, - nxt_process_t *process) -{ - nxt_str_t str; - nxt_conf_value_t *obj; - - static nxt_str_t rootfs_name = nxt_string("rootfs"); - - obj = nxt_conf_get_object_member(isolation, &rootfs_name, NULL); - if (obj != NULL) { - nxt_conf_get_string(obj, &str); - - if (nxt_slow_path(str.length <= 1 || str.start[0] != '/')) { - nxt_log(task, NXT_LOG_ERR, "rootfs requires an absolute path other " - "than \"/\" but given \"%V\"", &str); - - return NXT_ERROR; - } - - if (str.start[str.length - 1] == '/') { - str.length--; - } - - process->isolation.rootfs = nxt_mp_alloc(process->mem_pool, - str.length + 1); - - if (nxt_slow_path(process->isolation.rootfs == NULL)) { - return NXT_ERROR; - } - - nxt_memcpy(process->isolation.rootfs, str.start, str.length); - - process->isolation.rootfs[str.length] = '\0'; - } - - return NXT_OK; -} - - -static nxt_int_t -nxt_app_set_isolation_mounts(nxt_task_t *task, nxt_process_t *process, - nxt_str_t *app_type) -{ - nxt_int_t ret, cap_chroot; - nxt_runtime_t *rt; - nxt_app_lang_module_t *lang; - - rt = task->thread->runtime; - cap_chroot = rt->capabilities.chroot; - lang = nxt_app_lang_module(rt, app_type); - - nxt_assert(lang != NULL); - -#if (NXT_HAVE_CLONE_NEWUSER) - if (nxt_is_clone_flag_set(process->isolation.clone.flags, NEWUSER)) { - cap_chroot = 1; - } -#endif - - if (!cap_chroot) { - nxt_log(task, NXT_LOG_ERR, "The \"rootfs\" field requires privileges"); - return NXT_ERROR; - } - - if (lang->mounts != NULL && lang->mounts->nelts > 0) { - ret = nxt_app_set_lang_mounts(task, process, lang->mounts); - if (nxt_slow_path(ret != NXT_OK)) { - return NXT_ERROR; - } - } - - return NXT_OK; -} - - -static nxt_int_t -nxt_app_set_lang_mounts(nxt_task_t *task, nxt_process_t *process, - nxt_array_t *lang_mounts) -{ - u_char *p; - size_t i, n, rootfs_len, len; - nxt_mp_t *mp; - nxt_array_t *mounts; - const u_char *rootfs; - nxt_fs_mount_t *mnt, *lang_mnt; - - rootfs = process->isolation.rootfs; - rootfs_len = nxt_strlen(rootfs); - mp = process->mem_pool; - - /* copy to init mem pool */ - mounts = nxt_array_copy(mp, NULL, lang_mounts); - if (mounts == NULL) { - return NXT_ERROR; - } - - n = mounts->nelts; - mnt = mounts->elts; - lang_mnt = lang_mounts->elts; - - for (i = 0; i < n; i++) { - len = nxt_strlen(lang_mnt[i].dst); - - mnt[i].dst = nxt_mp_alloc(mp, rootfs_len + len + 1); - if (mnt[i].dst == NULL) { - return NXT_ERROR; - } - - p = nxt_cpymem(mnt[i].dst, rootfs, rootfs_len); - p = nxt_cpymem(p, lang_mnt[i].dst, len); - *p = '\0'; - } - - process->isolation.mounts = mounts; - - return NXT_OK; -} - - -static nxt_int_t -nxt_app_prepare_rootfs(nxt_task_t *task, nxt_process_t *process) -{ - size_t i, n; - nxt_int_t ret, hasproc; - struct stat st; - nxt_array_t *mounts; - const u_char *dst; - nxt_fs_mount_t *mnt; - - hasproc = 0; - -#if (NXT_HAVE_CLONE_NEWPID) && (NXT_HAVE_CLONE_NEWNS) - nxt_fs_mount_t mount; - - if (nxt_is_clone_flag_set(process->isolation.clone.flags, NEWPID) - && nxt_is_clone_flag_set(process->isolation.clone.flags, NEWNS)) - { - /* - * This mount point will automatically be gone when the namespace is - * destroyed. - */ - - mount.fstype = (u_char *) "proc"; - mount.src = (u_char *) "proc"; - mount.dst = (u_char *) "/proc"; - mount.data = (u_char *) ""; - mount.flags = 0; - - ret = nxt_fs_mkdir_all(mount.dst, S_IRWXU | S_IRWXG | S_IRWXO); - if (nxt_fast_path(ret == NXT_OK)) { - ret = nxt_fs_mount(task, &mount); - if (nxt_fast_path(ret == NXT_OK)) { - hasproc = 1; - } - - } else { - nxt_log(task, NXT_LOG_WARN, "mkdir(%s) %E", mount.dst, nxt_errno); - } - } -#endif - - mounts = process->isolation.mounts; - - n = mounts->nelts; - mnt = mounts->elts; - - for (i = 0; i < n; i++) { - dst = mnt[i].dst; - - if (nxt_slow_path(nxt_memcmp(mnt[i].fstype, "bind", 4) == 0 - && stat((const char *) mnt[i].src, &st) != 0)) - { - nxt_log(task, NXT_LOG_WARN, "host path not found: %s", mnt[i].src); - continue; - } - - if (hasproc && nxt_memcmp(mnt[i].fstype, "proc", 4) == 0 - && nxt_memcmp(mnt[i].dst, "/proc", 5) == 0) - { - continue; - } - - ret = nxt_fs_mkdir_all(dst, S_IRWXU | S_IRWXG | S_IRWXO); - if (nxt_slow_path(ret != NXT_OK)) { - nxt_alert(task, "mkdir(%s) %E", dst, nxt_errno); - goto undo; - } - - ret = nxt_fs_mount(task, &mnt[i]); - if (nxt_slow_path(ret != NXT_OK)) { - goto undo; - } - } - - return NXT_OK; - -undo: - - n = i + 1; - - for (i = 0; i < n; i++) { - nxt_fs_unmount(mnt[i].dst); - } - - return NXT_ERROR; -} - -#endif - - -#if (NXT_HAVE_PR_SET_NO_NEW_PRIVS) - -static nxt_int_t -nxt_app_set_isolation_new_privs(nxt_task_t *task, nxt_conf_value_t *isolation, - nxt_process_t *process) -{ - nxt_conf_value_t *obj; - - static nxt_str_t new_privs_name = nxt_string("new_privs"); - - obj = nxt_conf_get_object_member(isolation, &new_privs_name, NULL); - if (obj != NULL) { - process->isolation.new_privs = nxt_conf_get_boolean(obj); - } - - return NXT_OK; -} - -#endif - - static u_char * nxt_cstr_dup(nxt_mp_t *mp, u_char *dst, u_char *src) { diff --git a/src/nxt_application.h b/src/nxt_application.h index 3144dc3f..cb49a033 100644 --- a/src/nxt_application.h +++ b/src/nxt_application.h @@ -50,6 +50,7 @@ typedef struct { char *home; nxt_str_t path; nxt_str_t module; + char *callable; } nxt_python_app_conf_t; diff --git a/src/nxt_clone.h b/src/nxt_clone.h index e89fd82d..c2066ce6 100644 --- a/src/nxt_clone.h +++ b/src/nxt_clone.h @@ -42,9 +42,6 @@ pid_t nxt_clone(nxt_int_t flags); #if (NXT_HAVE_CLONE_NEWUSER) -#define NXT_CLONE_MNT(flags) \ - ((flags & CLONE_NEWNS) == CLONE_NEWNS) - NXT_EXPORT nxt_int_t nxt_clone_credential_map(nxt_task_t *task, pid_t pid, nxt_credential_t *creds, nxt_clone_t *clone); NXT_EXPORT nxt_int_t nxt_clone_vldt_credential_uidmap(nxt_task_t *task, diff --git a/src/nxt_conf_validation.c b/src/nxt_conf_validation.c index b5530b85..4364057b 100644 --- a/src/nxt_conf_validation.c +++ b/src/nxt_conf_validation.c @@ -496,12 +496,6 @@ static nxt_conf_vldt_object_t nxt_conf_vldt_app_limits_members[] = { NULL, NULL }, - { nxt_string("reschedule_timeout"), - NXT_CONF_VLDT_INTEGER, - 0, - NULL, - NULL }, - { nxt_string("requests"), NXT_CONF_VLDT_INTEGER, 0, @@ -622,6 +616,21 @@ static nxt_conf_vldt_object_t nxt_conf_vldt_app_procmap_members[] = { #endif +#if (NXT_HAVE_ISOLATION_ROOTFS) + +static nxt_conf_vldt_object_t nxt_conf_vldt_app_automount_members[] = { + { nxt_string("language_deps"), + NXT_CONF_VLDT_BOOLEAN, + 0, + NULL, + NULL }, + + NXT_CONF_VLDT_END +}; + +#endif + + static nxt_conf_vldt_object_t nxt_conf_vldt_app_isolation_members[] = { { nxt_string("namespaces"), NXT_CONF_VLDT_OBJECT, @@ -653,6 +662,12 @@ static nxt_conf_vldt_object_t nxt_conf_vldt_app_isolation_members[] = { NULL, NULL }, + { nxt_string("automount"), + NXT_CONF_VLDT_OBJECT, + 0, + &nxt_conf_vldt_object, + (void *) &nxt_conf_vldt_app_automount_members }, + #endif #if (NXT_HAVE_PR_SET_NO_NEW_PRIVS) @@ -758,6 +773,12 @@ static nxt_conf_vldt_object_t nxt_conf_vldt_python_members[] = { NULL, NULL }, + { nxt_string("callable"), + NXT_CONF_VLDT_STRING, + 0, + NULL, + NULL }, + NXT_CONF_VLDT_NEXT(&nxt_conf_vldt_common_members) }; @@ -1188,10 +1209,17 @@ static nxt_int_t nxt_conf_vldt_listener(nxt_conf_validation_t *vldt, nxt_str_t *name, nxt_conf_value_t *value) { - nxt_int_t ret; + nxt_int_t ret; + nxt_sockaddr_t *sa; - ret = nxt_conf_vldt_type(vldt, name, value, NXT_CONF_VLDT_OBJECT); + sa = nxt_sockaddr_parse(vldt->pool, name); + if (nxt_slow_path(sa == NULL)) { + return nxt_conf_vldt_error(vldt, + "The listener address \"%V\" is invalid.", + name); + } + ret = nxt_conf_vldt_type(vldt, name, value, NXT_CONF_VLDT_OBJECT); if (ret != NXT_OK) { return ret; } diff --git a/src/nxt_conn_write.c b/src/nxt_conn_write.c index d7a6a8da..bcf9e8fa 100644 --- a/src/nxt_conn_write.c +++ b/src/nxt_conn_write.c @@ -246,24 +246,47 @@ nxt_sendfile(int fd, int s, off_t pos, size_t size) { ssize_t res; -#ifdef NXT_HAVE_MACOSX_SENDFILE +#if (NXT_HAVE_MACOSX_SENDFILE) + off_t sent = size; int rc = sendfile(fd, s, pos, &sent, NULL, 0); res = (rc == 0 || sent > 0) ? sent : -1; -#endif -#ifdef NXT_HAVE_FREEBSD_SENDFILE +#elif (NXT_HAVE_FREEBSD_SENDFILE) + off_t sent = 0; int rc = sendfile(fd, s, pos, size, NULL, &sent, 0); res = (rc == 0 || sent > 0) ? sent : -1; -#endif -#ifdef NXT_HAVE_LINUX_SENDFILE +#elif (NXT_HAVE_LINUX_SENDFILE) + res = sendfile(s, fd, &pos, size); + +#else + + int err; + void *map; + off_t page_off; + + page_off = pos % nxt_pagesize; + + map = nxt_mem_mmap(NULL, size + page_off, PROT_READ, MAP_SHARED, fd, + pos - page_off); + if (nxt_slow_path(map == MAP_FAILED)) { + return -1; + } + + res = write(s, nxt_pointer_to(map, page_off), size); + + /* Backup and restore errno to catch socket errors in the upper level. */ + err = errno; + nxt_mem_munmap(map, size + page_off); + errno = err; + #endif return res; diff --git a/src/nxt_fs.c b/src/nxt_fs.c index fe271802..0228c25a 100644 --- a/src/nxt_fs.c +++ b/src/nxt_fs.c @@ -40,30 +40,31 @@ nxt_fs_mount(nxt_task_t *task, nxt_fs_mount_t *mnt) nxt_int_t nxt_fs_mount(nxt_task_t *task, nxt_fs_mount_t *mnt) { + u_char *data, *p, *end; + size_t iovlen; + nxt_int_t ret; const char *fstype; - uint8_t is_bind, is_proc; - struct iovec iov[8]; + struct iovec iov[128]; char errmsg[256]; - is_bind = nxt_strncmp(mnt->fstype, "bind", 4) == 0; - is_proc = nxt_strncmp(mnt->fstype, "proc", 4) == 0; + if (nxt_strncmp(mnt->fstype, "bind", 4) == 0) { + fstype = "nullfs"; - if (nxt_slow_path(!is_bind && !is_proc)) { - nxt_alert(task, "mount type \"%s\" not implemented.", mnt->fstype); - return NXT_ERROR; - } + } else if (nxt_strncmp(mnt->fstype, "proc", 4) == 0) { + fstype = "procfs"; - if (is_bind) { - fstype = "nullfs"; + } else if (nxt_strncmp(mnt->fstype, "tmpfs", 5) == 0) { + fstype = "tmpfs"; } else { - fstype = "procfs"; + nxt_alert(task, "mount type \"%s\" not implemented.", mnt->fstype); + return NXT_ERROR; } iov[0].iov_base = (void *) "fstype"; iov[0].iov_len = 7; iov[1].iov_base = (void *) fstype; - iov[1].iov_len = strlen(fstype) + 1; + iov[1].iov_len = nxt_strlen(fstype) + 1; iov[2].iov_base = (void *) "fspath"; iov[2].iov_len = 7; iov[3].iov_base = (void *) mnt->dst; @@ -77,12 +78,55 @@ nxt_fs_mount(nxt_task_t *task, nxt_fs_mount_t *mnt) iov[7].iov_base = (void *) errmsg; iov[7].iov_len = sizeof(errmsg); - if (nxt_slow_path(nmount(iov, 8, 0) < 0)) { - nxt_alert(task, "nmount(%p, 8, 0) %s", errmsg); - return NXT_ERROR; + iovlen = 8; + + data = NULL; + + if (mnt->data != NULL) { + data = (u_char *) nxt_strdup(mnt->data); + if (nxt_slow_path(data == NULL)) { + return NXT_ERROR; + } + + end = data - 1; + + do { + p = end + 1; + end = nxt_strchr(p, '='); + if (end == NULL) { + break; + } + + *end = '\0'; + + iov[iovlen++].iov_base = (void *) p; + iov[iovlen++].iov_len = (end - p) + 1; + + p = end + 1; + + end = nxt_strchr(p, ','); + if (end != NULL) { + *end = '\0'; + } + + iov[iovlen++].iov_base = (void *) p; + iov[iovlen++].iov_len = nxt_strlen(p) + 1; + + } while (end != NULL && nxt_nitems(iov) > (iovlen + 2)); } - return NXT_OK; + ret = NXT_OK; + + if (nxt_slow_path(nmount(iov, iovlen, 0) < 0)) { + nxt_alert(task, "nmount(%p, %d, 0) %s", iov, iovlen, errmsg); + ret = NXT_ERROR; + } + + if (data != NULL) { + free(data); + } + + return ret; } #endif diff --git a/src/nxt_fs.h b/src/nxt_fs.h index 85c78b27..bbd7ab9f 100644 --- a/src/nxt_fs.h +++ b/src/nxt_fs.h @@ -18,13 +18,38 @@ #define NXT_MS_REC 0 #endif +#ifdef MS_NOSUID +#define NXT_MS_NOSUID MS_NOSUID +#else +#define NXT_MS_NOSUID 0 +#endif + +#ifdef MS_NOEXEC +#define NXT_MS_NOEXEC MS_NOEXEC +#else +#define NXT_MS_NOEXEC 0 +#endif + +#ifdef MS_RELATIME +#define NXT_MS_RELATIME MS_RELATIME +#else +#define NXT_MS_RELATIME 0 +#endif + +#ifdef MS_NODEV +#define NXT_MS_NODEV MS_NODEV +#else +#define NXT_MS_NODEV 0 +#endif + typedef struct { - u_char *src; - u_char *dst; - u_char *fstype; - nxt_int_t flags; - u_char *data; + u_char *src; + u_char *dst; + u_char *fstype; + nxt_int_t flags; + u_char *data; + nxt_uint_t builtin; /* 1-bit */ } nxt_fs_mount_t; diff --git a/src/nxt_h1proto.c b/src/nxt_h1proto.c index b34be019..dc23d7c4 100644 --- a/src/nxt_h1proto.c +++ b/src/nxt_h1proto.c @@ -503,7 +503,10 @@ nxt_h1p_conn_request_init(nxt_task_t *task, void *obj, void *data) joint->count++; r->conf = joint; - c->local = joint->socket_conf->sockaddr; + + if (c->local == NULL) { + c->local = joint->socket_conf->sockaddr; + } nxt_h1p_conn_request_header_parse(task, c, h1p); return; @@ -734,9 +737,16 @@ nxt_h1p_connection(void *ctx, nxt_http_field_t *field, uintptr_t data) r = ctx; field->hopbyhop = 1; - if (field->value_length == 5 && nxt_memcmp(field->value, "close", 5) == 0) { + if (field->value_length == 5 + && nxt_memcasecmp(field->value, "close", 5) == 0) + { r->proto.h1->keepalive = 0; + } else if (field->value_length == 10 + && nxt_memcasecmp(field->value, "keep-alive", 10) == 0) + { + r->proto.h1->keepalive = 1; + } else if (field->value_length == 7 && nxt_memcasecmp(field->value, "upgrade", 7) == 0) { @@ -1749,7 +1759,15 @@ nxt_h1p_conn_timer_value(nxt_conn_t *c, uintptr_t data) joint = c->listen->socket.data; - return nxt_value_at(nxt_msec_t, joint->socket_conf, data); + if (nxt_fast_path(joint != NULL)) { + return nxt_value_at(nxt_msec_t, joint->socket_conf, data); + } + + /* + * Listening socket had been closed while + * connection was in keep-alive state. + */ + return 1; } @@ -1829,6 +1847,8 @@ nxt_h1p_idle_close(nxt_task_t *task, void *obj, void *data) nxt_debug(task, "h1p idle close"); + nxt_queue_remove(&c->link); + nxt_h1p_idle_response(task, c); } @@ -1863,10 +1883,9 @@ nxt_h1p_idle_timeout(nxt_task_t *task, void *obj, void *data) static void nxt_h1p_idle_response(nxt_task_t *task, nxt_conn_t *c) { - u_char *p; - size_t size; - nxt_buf_t *out, *last; - nxt_h1proto_t *h1p; + u_char *p; + size_t size; + nxt_buf_t *out, *last; size = nxt_length(NXT_H1P_IDLE_TIMEOUT) + nxt_http_date_cache.size @@ -1896,9 +1915,6 @@ nxt_h1p_idle_response(nxt_task_t *task, nxt_conn_t *c) last->completion_handler = nxt_h1p_idle_response_sent; last->parent = c; - h1p = c->socket.data; - h1p->conn_write_tail = &last->next; - c->write = out; c->write_state = &nxt_h1p_timeout_response_state; diff --git a/src/nxt_http_chunk_parse.c b/src/nxt_http_chunk_parse.c index 2164524b..be3a2023 100644 --- a/src/nxt_http_chunk_parse.c +++ b/src/nxt_http_chunk_parse.c @@ -74,7 +74,7 @@ nxt_http_chunk_parse(nxt_task_t *task, nxt_http_chunk_parse_t *hcp, goto next; } - /* ret == NXT_HTTP_CHUNK_END_ON_BORDER */ + /* ret == NXT_HTTP_CHUNK_END */ } ch = *hcp->pos++; diff --git a/src/nxt_http_proxy.c b/src/nxt_http_proxy.c index 34d0f36e..338d9fce 100644 --- a/src/nxt_http_proxy.c +++ b/src/nxt_http_proxy.c @@ -27,7 +27,6 @@ static void nxt_http_proxy_header_send(nxt_task_t *task, void *obj, void *data); static void nxt_http_proxy_header_sent(nxt_task_t *task, void *obj, void *data); static void nxt_http_proxy_header_read(nxt_task_t *task, void *obj, void *data); static void nxt_http_proxy_send_body(nxt_task_t *task, void *obj, void *data); -static void nxt_http_proxy_read(nxt_task_t *task, void *obj, void *data); static void nxt_http_proxy_buf_mem_completion(nxt_task_t *task, void *obj, void *data); static void nxt_http_proxy_error(nxt_task_t *task, void *obj, void *data); @@ -275,39 +274,16 @@ nxt_http_proxy_header_read(nxt_task_t *task, void *obj, void *data) } -static void -nxt_http_proxy_send_body(nxt_task_t *task, void *obj, void *data) -{ - nxt_buf_t *out; - nxt_http_peer_t *peer; - nxt_http_request_t *r; - - r = obj; - peer = data; - out = peer->body; - - if (out != NULL) { - peer->body = NULL; - nxt_http_request_send(task, r, out); - - } - - if (!peer->closed) { - nxt_http_proto[peer->protocol].peer_read(task, peer); - } -} - - static const nxt_http_request_state_t nxt_http_proxy_read_state nxt_aligned(64) = { - .ready_handler = nxt_http_proxy_read, + .ready_handler = nxt_http_proxy_send_body, .error_handler = nxt_http_proxy_error, }; static void -nxt_http_proxy_read(nxt_task_t *task, void *obj, void *data) +nxt_http_proxy_send_body(nxt_task_t *task, void *obj, void *data) { nxt_buf_t *out; nxt_http_peer_t *peer; @@ -316,9 +292,9 @@ nxt_http_proxy_read(nxt_task_t *task, void *obj, void *data) r = obj; peer = data; out = peer->body; - peer->body = NULL; if (out != NULL) { + peer->body = NULL; nxt_http_request_send(task, r, out); } diff --git a/src/nxt_http_route.c b/src/nxt_http_route.c index 0b2103cd..ae91076a 100644 --- a/src/nxt_http_route.c +++ b/src/nxt_http_route.c @@ -1085,10 +1085,6 @@ nxt_http_route_pattern_create(nxt_task_t *task, nxt_mp_t *mp, pattern->negative = 1; pattern->any = 0; - - if (test.length == 0) { - return NXT_OK; - } } if (test.length == 0) { @@ -1594,6 +1590,7 @@ nxt_http_action_t * nxt_http_action_create(nxt_task_t *task, nxt_router_temp_conf_t *tmcf, nxt_str_t *name) { + nxt_int_t ret; nxt_http_action_t *action; action = nxt_mp_alloc(tmcf->router_conf->mem_pool, @@ -1605,7 +1602,10 @@ nxt_http_action_create(nxt_task_t *task, nxt_router_temp_conf_t *tmcf, action->name = *name; action->handler = NULL; - nxt_http_action_resolve(task, tmcf, action); + ret = nxt_http_action_resolve(task, tmcf, action); + if (nxt_slow_path(ret != NXT_OK)) { + return NULL; + } return action; } diff --git a/src/nxt_http_static.c b/src/nxt_http_static.c index ee18be1b..5687ef2c 100644 --- a/src/nxt_http_static.c +++ b/src/nxt_http_static.c @@ -470,14 +470,17 @@ nxt_http_static_mtypes_init(nxt_mp_t *mp, nxt_lvlhsh_t *hash) { nxt_string("text/css"), ".css" }, { nxt_string("image/svg+xml"), ".svg" }, - { nxt_string("image/svg+xml"), ".svg" }, { nxt_string("image/webp"), ".webp" }, { nxt_string("image/png"), ".png" }, + { nxt_string("image/apng"), ".apng" }, { nxt_string("image/jpeg"), ".jpeg" }, { nxt_string("image/jpeg"), ".jpg" }, { nxt_string("image/gif"), ".gif" }, { nxt_string("image/x-icon"), ".ico" }, + { nxt_string("image/avif"), ".avif" }, + { nxt_string("image/avif-sequence"), ".avifs" }, + { nxt_string("font/woff"), ".woff" }, { nxt_string("font/woff2"), ".woff2" }, { nxt_string("font/otf"), ".otf" }, diff --git a/src/nxt_http_variables.c b/src/nxt_http_variables.c index 222d717c..1c0b561d 100644 --- a/src/nxt_http_variables.c +++ b/src/nxt_http_variables.c @@ -11,6 +11,8 @@ static nxt_int_t nxt_http_var_method(nxt_task_t *task, nxt_var_query_t *query, nxt_str_t *str, void *ctx); static nxt_int_t nxt_http_var_uri(nxt_task_t *task, nxt_var_query_t *query, nxt_str_t *str, void *ctx); +static nxt_int_t nxt_http_var_host(nxt_task_t *task, nxt_var_query_t *query, + nxt_str_t *str, void *ctx); static nxt_var_decl_t nxt_http_vars[] = { @@ -21,6 +23,10 @@ static nxt_var_decl_t nxt_http_vars[] = { { nxt_string("uri"), &nxt_http_var_uri, 0 }, + + { nxt_string("host"), + &nxt_http_var_host, + 0 }, }; @@ -57,3 +63,17 @@ nxt_http_var_uri(nxt_task_t *task, nxt_var_query_t *query, nxt_str_t *str, return NXT_OK; } + + +static nxt_int_t +nxt_http_var_host(nxt_task_t *task, nxt_var_query_t *query, nxt_str_t *str, + void *ctx) +{ + nxt_http_request_t *r; + + r = ctx; + + *str = r->host; + + return NXT_OK; +} diff --git a/src/nxt_isolation.c b/src/nxt_isolation.c new file mode 100644 index 00000000..ac7a37e8 --- /dev/null +++ b/src/nxt_isolation.c @@ -0,0 +1,1012 @@ +/* + * Copyright (C) NGINX, Inc. + */ + +#include <nxt_main.h> +#include <nxt_application.h> +#include <nxt_process.h> +#include <nxt_isolation.h> + +#if (NXT_HAVE_PIVOT_ROOT) +#include <mntent.h> +#endif + + +static nxt_int_t nxt_isolation_set(nxt_task_t *task, + nxt_conf_value_t *isolation, nxt_process_t *process); + +#if (NXT_HAVE_CLONE) +static nxt_int_t nxt_isolation_set_namespaces(nxt_task_t *task, + nxt_conf_value_t *isolation, nxt_process_t *process); +static nxt_int_t nxt_isolation_clone_flags(nxt_task_t *task, + nxt_conf_value_t *namespaces, nxt_clone_t *clone); +#endif + +#if (NXT_HAVE_CLONE_NEWUSER) +static nxt_int_t nxt_isolation_set_creds(nxt_task_t *task, + nxt_conf_value_t *isolation, nxt_process_t *process); +static nxt_int_t nxt_isolation_credential_map(nxt_task_t *task, + nxt_mp_t *mem_pool, nxt_conf_value_t *map_array, + nxt_clone_credential_map_t *map); +static nxt_int_t nxt_isolation_vldt_creds(nxt_task_t *task, + nxt_process_t *process); +#endif + +#if (NXT_HAVE_ISOLATION_ROOTFS) +static nxt_int_t nxt_isolation_set_rootfs(nxt_task_t *task, + nxt_conf_value_t *isolation, nxt_process_t *process); +static nxt_int_t nxt_isolation_set_automount(nxt_task_t *task, + nxt_conf_value_t *isolation, nxt_process_t *process); +static nxt_int_t nxt_isolation_set_mounts(nxt_task_t *task, + nxt_process_t *process, nxt_str_t *app_type); +static nxt_int_t nxt_isolation_set_lang_mounts(nxt_task_t *task, + nxt_process_t *process, nxt_array_t *syspaths); +static void nxt_isolation_unmount_all(nxt_task_t *task, nxt_process_t *process); + +#if (NXT_HAVE_PIVOT_ROOT) && (NXT_HAVE_CLONE_NEWNS) +static nxt_int_t nxt_isolation_pivot_root(nxt_task_t *task, const char *rootfs); +static nxt_int_t nxt_isolation_make_private_mount(nxt_task_t *task, + const char *rootfs); +nxt_inline int nxt_pivot_root(const char *new_root, const char *old_root); +#endif + +static nxt_int_t nxt_isolation_chroot(nxt_task_t *task, const char *path); +#endif + +#if (NXT_HAVE_PR_SET_NO_NEW_PRIVS) +static nxt_int_t nxt_isolation_set_new_privs(nxt_task_t *task, + nxt_conf_value_t *isolation, nxt_process_t *process); +#endif + + +nxt_int_t +nxt_isolation_main_prefork(nxt_task_t *task, nxt_process_t *process, + nxt_mp_t *mp) +{ + nxt_int_t cap_setid; + nxt_int_t ret; + nxt_runtime_t *rt; + nxt_common_app_conf_t *app_conf; + + rt = task->thread->runtime; + app_conf = process->data.app; + cap_setid = rt->capabilities.setid; + + if (app_conf->isolation != NULL) { + ret = nxt_isolation_set(task, app_conf->isolation, process); + if (nxt_slow_path(ret != NXT_OK)) { + return ret; + } + } + +#if (NXT_HAVE_CLONE_NEWUSER) + if (nxt_is_clone_flag_set(process->isolation.clone.flags, NEWUSER)) { + cap_setid = 1; + } +#endif + +#if (NXT_HAVE_ISOLATION_ROOTFS) + if (process->isolation.rootfs != NULL) { + ret = nxt_isolation_set_mounts(task, process, &app_conf->type); + if (nxt_slow_path(ret != NXT_OK)) { + return ret; + } + } +#endif + + if (cap_setid) { + ret = nxt_process_creds_set(task, process, &app_conf->user, + &app_conf->group); + + if (nxt_slow_path(ret != NXT_OK)) { + return ret; + } + + } else { + if (!nxt_str_eq(&app_conf->user, (u_char *) rt->user_cred.user, + nxt_strlen(rt->user_cred.user))) + { + nxt_alert(task, "cannot set user \"%V\" for app \"%V\": " + "missing capabilities", &app_conf->user, &app_conf->name); + + return NXT_ERROR; + } + + if (app_conf->group.length > 0 + && !nxt_str_eq(&app_conf->group, (u_char *) rt->group, + nxt_strlen(rt->group))) + { + nxt_alert(task, "cannot set group \"%V\" for app \"%V\": " + "missing capabilities", &app_conf->group, + &app_conf->name); + + return NXT_ERROR; + } + } + +#if (NXT_HAVE_CLONE_NEWUSER) + ret = nxt_isolation_vldt_creds(task, process); + if (nxt_slow_path(ret != NXT_OK)) { + return ret; + } +#endif + + return NXT_OK; +} + + +static nxt_int_t +nxt_isolation_set(nxt_task_t *task, nxt_conf_value_t *isolation, + nxt_process_t *process) +{ +#if (NXT_HAVE_CLONE) + if (nxt_slow_path(nxt_isolation_set_namespaces(task, isolation, process) + != NXT_OK)) + { + return NXT_ERROR; + } +#endif + +#if (NXT_HAVE_CLONE_NEWUSER) + if (nxt_slow_path(nxt_isolation_set_creds(task, isolation, process) + != NXT_OK)) + { + return NXT_ERROR; + } +#endif + +#if (NXT_HAVE_ISOLATION_ROOTFS) + if (nxt_slow_path(nxt_isolation_set_rootfs(task, isolation, process) + != NXT_OK)) + { + return NXT_ERROR; + } + + if (nxt_slow_path(nxt_isolation_set_automount(task, isolation, process) + != NXT_OK)) + { + return NXT_ERROR; + } +#endif + +#if (NXT_HAVE_PR_SET_NO_NEW_PRIVS) + if (nxt_slow_path(nxt_isolation_set_new_privs(task, isolation, process) + != NXT_OK)) + { + return NXT_ERROR; + } +#endif + + return NXT_OK; +} + + +#if (NXT_HAVE_CLONE) + +static nxt_int_t +nxt_isolation_set_namespaces(nxt_task_t *task, nxt_conf_value_t *isolation, + nxt_process_t *process) +{ + nxt_int_t ret; + nxt_conf_value_t *obj; + + static nxt_str_t nsname = nxt_string("namespaces"); + + obj = nxt_conf_get_object_member(isolation, &nsname, NULL); + if (obj != NULL) { + ret = nxt_isolation_clone_flags(task, obj, &process->isolation.clone); + if (nxt_slow_path(ret != NXT_OK)) { + return NXT_ERROR; + } + } + + return NXT_OK; +} + +#endif + + +#if (NXT_HAVE_CLONE_NEWUSER) + +static nxt_int_t +nxt_isolation_set_creds(nxt_task_t *task, nxt_conf_value_t *isolation, + nxt_process_t *process) +{ + nxt_int_t ret; + nxt_clone_t *clone; + nxt_conf_value_t *array; + + static nxt_str_t uidname = nxt_string("uidmap"); + static nxt_str_t gidname = nxt_string("gidmap"); + + clone = &process->isolation.clone; + + array = nxt_conf_get_object_member(isolation, &uidname, NULL); + if (array != NULL) { + ret = nxt_isolation_credential_map(task, process->mem_pool, array, + &clone->uidmap); + + if (nxt_slow_path(ret != NXT_OK)) { + return NXT_ERROR; + } + } + + array = nxt_conf_get_object_member(isolation, &gidname, NULL); + if (array != NULL) { + ret = nxt_isolation_credential_map(task, process->mem_pool, array, + &clone->gidmap); + + if (nxt_slow_path(ret != NXT_OK)) { + return NXT_ERROR; + } + } + + return NXT_OK; +} + + +static nxt_int_t +nxt_isolation_credential_map(nxt_task_t *task, nxt_mp_t *mp, + nxt_conf_value_t *map_array, nxt_clone_credential_map_t *map) +{ + nxt_int_t ret; + nxt_uint_t i; + nxt_conf_value_t *obj; + + static nxt_conf_map_t nxt_clone_map_entry_conf[] = { + { + nxt_string("container"), + NXT_CONF_MAP_INT, + offsetof(nxt_clone_map_entry_t, container), + }, + + { + nxt_string("host"), + NXT_CONF_MAP_INT, + offsetof(nxt_clone_map_entry_t, host), + }, + + { + nxt_string("size"), + NXT_CONF_MAP_INT, + offsetof(nxt_clone_map_entry_t, size), + }, + }; + + map->size = nxt_conf_array_elements_count(map_array); + + if (map->size == 0) { + return NXT_OK; + } + + map->map = nxt_mp_alloc(mp, map->size * sizeof(nxt_clone_map_entry_t)); + if (nxt_slow_path(map->map == NULL)) { + return NXT_ERROR; + } + + for (i = 0; i < map->size; i++) { + obj = nxt_conf_get_array_element(map_array, i); + + ret = nxt_conf_map_object(mp, obj, nxt_clone_map_entry_conf, + nxt_nitems(nxt_clone_map_entry_conf), + map->map + i); + if (nxt_slow_path(ret != NXT_OK)) { + nxt_alert(task, "clone map entry map error"); + return NXT_ERROR; + } + } + + return NXT_OK; +} + + +static nxt_int_t +nxt_isolation_vldt_creds(nxt_task_t *task, nxt_process_t *process) +{ + nxt_int_t ret; + nxt_clone_t *clone; + nxt_credential_t *creds; + + clone = &process->isolation.clone; + creds = process->user_cred; + + if (clone->uidmap.size == 0 && clone->gidmap.size == 0) { + return NXT_OK; + } + + if (!nxt_is_clone_flag_set(clone->flags, NEWUSER)) { + if (nxt_slow_path(clone->uidmap.size > 0)) { + nxt_log(task, NXT_LOG_ERR, "\"uidmap\" is set but " + "\"isolation.namespaces.credential\" is false or unset"); + + return NXT_ERROR; + } + + if (nxt_slow_path(clone->gidmap.size > 0)) { + nxt_log(task, NXT_LOG_ERR, "\"gidmap\" is set but " + "\"isolation.namespaces.credential\" is false or unset"); + + return NXT_ERROR; + } + + return NXT_OK; + } + + ret = nxt_clone_vldt_credential_uidmap(task, &clone->uidmap, creds); + if (nxt_slow_path(ret != NXT_OK)) { + return NXT_ERROR; + } + + return nxt_clone_vldt_credential_gidmap(task, &clone->gidmap, creds); +} + +#endif + + +#if (NXT_HAVE_CLONE) + +static nxt_int_t +nxt_isolation_clone_flags(nxt_task_t *task, nxt_conf_value_t *namespaces, + nxt_clone_t *clone) +{ + uint32_t index; + nxt_str_t name; + nxt_int_t flag; + nxt_conf_value_t *value; + + index = 0; + + for ( ;; ) { + value = nxt_conf_next_object_member(namespaces, &name, &index); + + if (value == NULL) { + break; + } + + flag = 0; + +#if (NXT_HAVE_CLONE_NEWUSER) + if (nxt_str_eq(&name, "credential", 10)) { + flag = CLONE_NEWUSER; + } +#endif + +#if (NXT_HAVE_CLONE_NEWPID) + if (nxt_str_eq(&name, "pid", 3)) { + flag = CLONE_NEWPID; + } +#endif + +#if (NXT_HAVE_CLONE_NEWNET) + if (nxt_str_eq(&name, "network", 7)) { + flag = CLONE_NEWNET; + } +#endif + +#if (NXT_HAVE_CLONE_NEWUTS) + if (nxt_str_eq(&name, "uname", 5)) { + flag = CLONE_NEWUTS; + } +#endif + +#if (NXT_HAVE_CLONE_NEWNS) + if (nxt_str_eq(&name, "mount", 5)) { + flag = CLONE_NEWNS; + } +#endif + +#if (NXT_HAVE_CLONE_NEWCGROUP) + if (nxt_str_eq(&name, "cgroup", 6)) { + flag = CLONE_NEWCGROUP; + } +#endif + + if (!flag) { + nxt_alert(task, "unknown namespace flag: \"%V\"", &name); + return NXT_ERROR; + } + + if (nxt_conf_get_boolean(value)) { + clone->flags |= flag; + } + } + + return NXT_OK; +} + +#endif + + +#if (NXT_HAVE_ISOLATION_ROOTFS) + +static nxt_int_t +nxt_isolation_set_rootfs(nxt_task_t *task, nxt_conf_value_t *isolation, + nxt_process_t *process) +{ + nxt_str_t str; + nxt_conf_value_t *obj; + + static nxt_str_t rootfs_name = nxt_string("rootfs"); + + obj = nxt_conf_get_object_member(isolation, &rootfs_name, NULL); + if (obj != NULL) { + nxt_conf_get_string(obj, &str); + + if (nxt_slow_path(str.length <= 1 || str.start[0] != '/')) { + nxt_log(task, NXT_LOG_ERR, "rootfs requires an absolute path other " + "than \"/\" but given \"%V\"", &str); + + return NXT_ERROR; + } + + if (str.start[str.length - 1] == '/') { + str.length--; + } + + process->isolation.rootfs = nxt_mp_alloc(process->mem_pool, + str.length + 1); + + if (nxt_slow_path(process->isolation.rootfs == NULL)) { + return NXT_ERROR; + } + + nxt_memcpy(process->isolation.rootfs, str.start, str.length); + + process->isolation.rootfs[str.length] = '\0'; + } + + return NXT_OK; +} + + +static nxt_int_t +nxt_isolation_set_automount(nxt_task_t *task, nxt_conf_value_t *isolation, + nxt_process_t *process) +{ + nxt_conf_value_t *conf, *value; + nxt_process_automount_t *automount; + + static nxt_str_t automount_name = nxt_string("automount"); + static nxt_str_t langdeps_name = nxt_string("language_deps"); + + automount = &process->isolation.automount; + + automount->language_deps = 1; + + conf = nxt_conf_get_object_member(isolation, &automount_name, NULL); + if (conf != NULL) { + value = nxt_conf_get_object_member(conf, &langdeps_name, NULL); + if (value != NULL) { + automount->language_deps = nxt_conf_get_boolean(value); + } + } + + return NXT_OK; +} + + +static nxt_int_t +nxt_isolation_set_mounts(nxt_task_t *task, nxt_process_t *process, + nxt_str_t *app_type) +{ + nxt_int_t ret, cap_chroot; + nxt_runtime_t *rt; + nxt_app_lang_module_t *lang; + + rt = task->thread->runtime; + cap_chroot = rt->capabilities.chroot; + lang = nxt_app_lang_module(rt, app_type); + + nxt_assert(lang != NULL); + +#if (NXT_HAVE_CLONE_NEWUSER) + if (nxt_is_clone_flag_set(process->isolation.clone.flags, NEWUSER)) { + cap_chroot = 1; + } +#endif + + if (!cap_chroot) { + nxt_log(task, NXT_LOG_ERR, "The \"rootfs\" field requires privileges"); + return NXT_ERROR; + } + + ret = nxt_isolation_set_lang_mounts(task, process, lang->mounts); + if (nxt_slow_path(ret != NXT_OK)) { + return NXT_ERROR; + } + + process->isolation.cleanup = nxt_isolation_unmount_all; + + return NXT_OK; +} + + +static nxt_int_t +nxt_isolation_set_lang_mounts(nxt_task_t *task, nxt_process_t *process, + nxt_array_t *lang_mounts) +{ + u_char *p; + size_t i, n, rootfs_len, len; + nxt_mp_t *mp; + nxt_array_t *mounts; + const u_char *rootfs; + nxt_fs_mount_t *mnt, *lang_mnt; + + mp = process->mem_pool; + + /* copy to init mem pool */ + mounts = nxt_array_copy(mp, NULL, lang_mounts); + if (mounts == NULL) { + return NXT_ERROR; + } + + n = mounts->nelts; + mnt = mounts->elts; + lang_mnt = lang_mounts->elts; + + rootfs = process->isolation.rootfs; + rootfs_len = nxt_strlen(rootfs); + + for (i = 0; i < n; i++) { + len = nxt_strlen(lang_mnt[i].dst); + + mnt[i].dst = nxt_mp_alloc(mp, rootfs_len + len + 1); + if (nxt_slow_path(mnt[i].dst == NULL)) { + return NXT_ERROR; + } + + p = nxt_cpymem(mnt[i].dst, rootfs, rootfs_len); + p = nxt_cpymem(p, lang_mnt[i].dst, len); + *p = '\0'; + } + + mnt = nxt_array_add(mounts); + if (nxt_slow_path(mnt == NULL)) { + return NXT_ERROR; + } + + mnt->src = (u_char *) "tmpfs"; + mnt->fstype = (u_char *) "tmpfs"; + mnt->flags = NXT_MS_NOSUID | NXT_MS_NODEV | NXT_MS_NOEXEC | NXT_MS_RELATIME; + mnt->data = (u_char *) "size=1m,mode=777"; + mnt->builtin = 1; + + mnt->dst = nxt_mp_nget(mp, rootfs_len + nxt_length("/tmp") + 1); + if (nxt_slow_path(mnt->dst == NULL)) { + return NXT_ERROR; + } + + p = nxt_cpymem(mnt->dst, rootfs, rootfs_len); + p = nxt_cpymem(p, "/tmp", 4); + *p = '\0'; + +#if (NXT_HAVE_CLONE_NEWPID) && (NXT_HAVE_CLONE_NEWNS) + + if (nxt_is_clone_flag_set(process->isolation.clone.flags, NEWPID) + && nxt_is_clone_flag_set(process->isolation.clone.flags, NEWNS)) + { + mnt = nxt_array_add(mounts); + if (nxt_slow_path(mnt == NULL)) { + return NXT_ERROR; + } + + mnt->fstype = (u_char *) "proc"; + mnt->src = (u_char *) "proc"; + + mnt->dst = nxt_mp_nget(mp, rootfs_len + nxt_length("/proc") + 1); + if (nxt_slow_path(mnt->dst == NULL)) { + return NXT_ERROR; + } + + p = nxt_cpymem(mnt->dst, rootfs, rootfs_len); + p = nxt_cpymem(p, "/proc", 5); + *p = '\0'; + + mnt->data = (u_char *) ""; + mnt->flags = 0; + } +#endif + + process->isolation.mounts = mounts; + + return NXT_OK; +} + + +void +nxt_isolation_unmount_all(nxt_task_t *task, nxt_process_t *process) +{ + size_t i, n; + nxt_array_t *mounts; + nxt_fs_mount_t *mnt; + nxt_process_automount_t *automount; + + nxt_debug(task, "unmount all (%s)", process->name); + + automount = &process->isolation.automount; + mounts = process->isolation.mounts; + n = mounts->nelts; + mnt = mounts->elts; + + for (i = 0; i < n; i++) { + if (mnt[i].builtin && !automount->language_deps) { + continue; + } + + nxt_fs_unmount(mnt[i].dst); + } +} + + +nxt_int_t +nxt_isolation_prepare_rootfs(nxt_task_t *task, nxt_process_t *process) +{ + size_t i, n; + nxt_int_t ret; + struct stat st; + nxt_array_t *mounts; + const u_char *dst; + nxt_fs_mount_t *mnt; + nxt_process_automount_t *automount; + + automount = &process->isolation.automount; + mounts = process->isolation.mounts; + + n = mounts->nelts; + mnt = mounts->elts; + + for (i = 0; i < n; i++) { + dst = mnt[i].dst; + + if (mnt[i].builtin && !automount->language_deps) { + continue; + } + + if (nxt_slow_path(nxt_memcmp(mnt[i].fstype, "bind", 4) == 0 + && stat((const char *) mnt[i].src, &st) != 0)) + { + nxt_log(task, NXT_LOG_WARN, "host path not found: %s", mnt[i].src); + continue; + } + + ret = nxt_fs_mkdir_all(dst, S_IRWXU | S_IRWXG | S_IRWXO); + if (nxt_slow_path(ret != NXT_OK)) { + nxt_alert(task, "mkdir(%s) %E", dst, nxt_errno); + goto undo; + } + + ret = nxt_fs_mount(task, &mnt[i]); + if (nxt_slow_path(ret != NXT_OK)) { + goto undo; + } + } + + return NXT_OK; + +undo: + + n = i + 1; + + for (i = 0; i < n; i++) { + nxt_fs_unmount(mnt[i].dst); + } + + return NXT_ERROR; +} + + +#if (NXT_HAVE_PIVOT_ROOT) && (NXT_HAVE_CLONE_NEWNS) + +nxt_int_t +nxt_isolation_change_root(nxt_task_t *task, nxt_process_t *process) +{ + char *rootfs; + nxt_int_t ret; + + rootfs = (char *) process->isolation.rootfs; + + nxt_debug(task, "change root: %s", rootfs); + + if (nxt_is_clone_flag_set(process->isolation.clone.flags, NEWNS)) { + ret = nxt_isolation_pivot_root(task, rootfs); + + } else { + ret = nxt_isolation_chroot(task, rootfs); + } + + if (nxt_fast_path(ret == NXT_OK)) { + if (nxt_slow_path(chdir("/") < 0)) { + nxt_alert(task, "chdir(\"/\") %E", nxt_errno); + return NXT_ERROR; + } + } + + return ret; +} + + +/* + * pivot_root(2) can only be safely used with containers, otherwise it can + * umount(2) the global root filesystem and screw up the machine. + */ + +static nxt_int_t +nxt_isolation_pivot_root(nxt_task_t *task, const char *path) +{ + /* + * This implementation makes use of a kernel trick that works for ages + * and now documented in Linux kernel 5. + * https://lore.kernel.org/linux-man/87r24piwhm.fsf@x220.int.ebiederm.org/T/ + */ + + if (nxt_slow_path(mount("", "/", "", MS_SLAVE|MS_REC, "") != 0)) { + nxt_alert(task, "mount(\"/\", MS_SLAVE|MS_REC) failed: %E", nxt_errno); + return NXT_ERROR; + } + + if (nxt_slow_path(nxt_isolation_make_private_mount(task, path) != NXT_OK)) { + return NXT_ERROR; + } + + if (nxt_slow_path(mount(path, path, "bind", MS_BIND|MS_REC, "") != 0)) { + nxt_alert(task, "error bind mounting rootfs %E", nxt_errno); + return NXT_ERROR; + } + + if (nxt_slow_path(chdir(path) != 0)) { + nxt_alert(task, "failed to chdir(%s) %E", path, nxt_errno); + return NXT_ERROR; + } + + if (nxt_slow_path(nxt_pivot_root(".", ".") != 0)) { + nxt_alert(task, "failed to pivot_root %E", nxt_errno); + return NXT_ERROR; + } + + /* + * Demote the oldroot mount to avoid unmounts getting propagated to + * the host. + */ + if (nxt_slow_path(mount("", ".", "", MS_SLAVE | MS_REC, NULL) != 0)) { + nxt_alert(task, "failed to bind mount rootfs %E", nxt_errno); + return NXT_ERROR; + } + + if (nxt_slow_path(umount2(".", MNT_DETACH) != 0)) { + nxt_alert(task, "failed to umount old root directory %E", nxt_errno); + return NXT_ERROR; + } + + return NXT_OK; +} + + +static nxt_int_t +nxt_isolation_make_private_mount(nxt_task_t *task, const char *rootfs) +{ + char *parent_mnt; + FILE *procfile; + u_char **mounts; + size_t len; + uint8_t *shared; + nxt_int_t ret, index, nmounts; + struct mntent *ent; + + static const char *mount_path = "/proc/self/mounts"; + + ret = NXT_ERROR; + ent = NULL; + shared = NULL; + procfile = NULL; + parent_mnt = NULL; + + nmounts = 256; + + mounts = nxt_malloc(nmounts * sizeof(uintptr_t)); + if (nxt_slow_path(mounts == NULL)) { + goto fail; + } + + shared = nxt_malloc(nmounts); + if (nxt_slow_path(shared == NULL)) { + goto fail; + } + + procfile = setmntent(mount_path, "r"); + if (nxt_slow_path(procfile == NULL)) { + nxt_alert(task, "failed to open %s %E", mount_path, nxt_errno); + + goto fail; + } + + index = 0; + +again: + + for ( ; index < nmounts; index++) { + ent = getmntent(procfile); + if (ent == NULL) { + nmounts = index; + break; + } + + mounts[index] = (u_char *) strdup(ent->mnt_dir); + shared[index] = hasmntopt(ent, "shared") != NULL; + } + + if (ent != NULL) { + /* there are still entries to be read */ + + nmounts *= 2; + mounts = nxt_realloc(mounts, nmounts); + if (nxt_slow_path(mounts == NULL)) { + goto fail; + } + + shared = nxt_realloc(shared, nmounts); + if (nxt_slow_path(shared == NULL)) { + goto fail; + } + + goto again; + } + + for (index = 0; index < nmounts; index++) { + if (nxt_strcmp(mounts[index], rootfs) == 0) { + parent_mnt = (char *) rootfs; + break; + } + } + + if (parent_mnt == NULL) { + len = nxt_strlen(rootfs); + + parent_mnt = nxt_malloc(len + 1); + if (parent_mnt == NULL) { + goto fail; + } + + nxt_memcpy(parent_mnt, rootfs, len); + parent_mnt[len] = '\0'; + + if (parent_mnt[len - 1] == '/') { + parent_mnt[len - 1] = '\0'; + len--; + } + + for ( ;; ) { + for (index = 0; index < nmounts; index++) { + if (nxt_strcmp(mounts[index], parent_mnt) == 0) { + goto found; + } + } + + if (len == 1 && parent_mnt[0] == '/') { + nxt_alert(task, "parent mount not found"); + goto fail; + } + + /* parent dir */ + while (parent_mnt[len - 1] != '/' && len > 0) { + len--; + } + + if (nxt_slow_path(len == 0)) { + nxt_alert(task, "parent mount not found"); + goto fail; + } + + if (len == 1) { + parent_mnt[len] = '\0'; /* / */ + } else { + parent_mnt[len - 1] = '\0'; /* /<path> */ + } + } + } + +found: + + if (shared[index]) { + if (nxt_slow_path(mount("", parent_mnt, "", MS_PRIVATE, "") != 0)) { + nxt_alert(task, "mount(\"\", \"%s\", MS_PRIVATE) %E", parent_mnt, + nxt_errno); + + goto fail; + } + } + + ret = NXT_OK; + +fail: + + if (procfile != NULL) { + endmntent(procfile); + } + + if (mounts != NULL) { + for (index = 0; index < nmounts; index++) { + nxt_free(mounts[index]); + } + + nxt_free(mounts); + } + + if (shared != NULL) { + nxt_free(shared); + } + + if (parent_mnt != NULL && parent_mnt != rootfs) { + nxt_free(parent_mnt); + } + + return ret; +} + + +nxt_inline int +nxt_pivot_root(const char *new_root, const char *old_root) +{ + return syscall(__NR_pivot_root, new_root, old_root); +} + + +#else /* !(NXT_HAVE_PIVOT_ROOT) || !(NXT_HAVE_CLONE_NEWNS) */ + + +nxt_int_t +nxt_isolation_change_root(nxt_task_t *task, nxt_process_t *process) +{ + char *rootfs; + + rootfs = (char *) process->isolation.rootfs; + + nxt_debug(task, "change root: %s", rootfs); + + if (nxt_fast_path(nxt_isolation_chroot(task, rootfs) == NXT_OK)) { + if (nxt_slow_path(chdir("/") < 0)) { + nxt_alert(task, "chdir(\"/\") %E", nxt_errno); + return NXT_ERROR; + } + + return NXT_OK; + } + + return NXT_ERROR; +} + +#endif + + +static nxt_int_t +nxt_isolation_chroot(nxt_task_t *task, const char *path) +{ + if (nxt_slow_path(chroot(path) < 0)) { + nxt_alert(task, "chroot(%s) %E", path, nxt_errno); + return NXT_ERROR; + } + + return NXT_OK; +} + +#endif /* NXT_HAVE_ISOLATION_ROOTFS */ + + +#if (NXT_HAVE_PR_SET_NO_NEW_PRIVS) + +static nxt_int_t +nxt_isolation_set_new_privs(nxt_task_t *task, nxt_conf_value_t *isolation, + nxt_process_t *process) +{ + nxt_conf_value_t *obj; + + static nxt_str_t new_privs_name = nxt_string("new_privs"); + + obj = nxt_conf_get_object_member(isolation, &new_privs_name, NULL); + if (obj != NULL) { + process->isolation.new_privs = nxt_conf_get_boolean(obj); + } + + return NXT_OK; +} + +#endif diff --git a/src/nxt_isolation.h b/src/nxt_isolation.h new file mode 100644 index 00000000..88a5f9e1 --- /dev/null +++ b/src/nxt_isolation.h @@ -0,0 +1,18 @@ +/* + * Copyright (C) NGINX, Inc. + */ + +#ifndef _NXT_ISOLATION_H_ +#define _NXT_ISOLATION_H_ + + +nxt_int_t nxt_isolation_main_prefork(nxt_task_t *task, nxt_process_t *process, + nxt_mp_t *mp); + +#if (NXT_HAVE_ISOLATION_ROOTFS) +nxt_int_t nxt_isolation_prepare_rootfs(nxt_task_t *task, + nxt_process_t *process); +nxt_int_t nxt_isolation_change_root(nxt_task_t *task, nxt_process_t *process); +#endif + +#endif /* _NXT_ISOLATION_H_ */ diff --git a/src/nxt_lvlhsh.c b/src/nxt_lvlhsh.c index ec433341..d10dbc58 100644 --- a/src/nxt_lvlhsh.c +++ b/src/nxt_lvlhsh.c @@ -1015,17 +1015,3 @@ nxt_lvlhsh_retrieve(nxt_lvlhsh_t *lh, const nxt_lvlhsh_proto_t *proto, return NULL; } - - -void * -nxt_lvlhsh_alloc(void *data, size_t size) -{ - return nxt_memalign(size, size); -} - - -void -nxt_lvlhsh_free(void *data, void *p) -{ - nxt_free(p); -} diff --git a/src/nxt_main_process.c b/src/nxt_main_process.c index 48eb2abb..d2edab1d 100644 --- a/src/nxt_main_process.c +++ b/src/nxt_main_process.c @@ -191,6 +191,12 @@ static nxt_conf_map_t nxt_python_app_conf[] = { NXT_CONF_MAP_STR, offsetof(nxt_common_app_conf_t, u.python.module), }, + + { + nxt_string("callable"), + NXT_CONF_MAP_CSTRZ, + offsetof(nxt_common_app_conf_t, u.python.callable), + }, }; @@ -878,11 +884,9 @@ nxt_main_cleanup_process(nxt_task_t *task, nxt_pid_t pid) return; } -#if (NXT_HAVE_ISOLATION_ROOTFS) - if (process->isolation.rootfs != NULL && process->isolation.mounts) { - (void) nxt_process_unmount_all(task, process); + if (process->isolation.cleanup != NULL) { + process->isolation.cleanup(task, process); } -#endif name = process->name; stream = process->stream; @@ -1292,6 +1296,8 @@ nxt_main_port_modules_handler(nxt_task_t *task, nxt_port_recv_msg_t *msg) goto fail; } + mnt->builtin = 1; + ret = nxt_conf_map_object(rt->mem_pool, value, nxt_app_lang_mounts_map, nxt_nitems(nxt_app_lang_mounts_map), mnt); diff --git a/src/nxt_malloc.c b/src/nxt_malloc.c index 910ef7cd..fed58e96 100644 --- a/src/nxt_malloc.c +++ b/src/nxt_malloc.c @@ -81,6 +81,22 @@ nxt_realloc(void *p, size_t size) } +/* nxt_lvlhsh_* functions moved here to avoid references from nxt_lvlhsh.c. */ + +void * +nxt_lvlhsh_alloc(void *data, size_t size) +{ + return nxt_memalign(size, size); +} + + +void +nxt_lvlhsh_free(void *data, void *p) +{ + nxt_free(p); +} + + #if (NXT_DEBUG) void diff --git a/src/nxt_php_sapi.c b/src/nxt_php_sapi.c index bc8341f4..234ceef8 100644 --- a/src/nxt_php_sapi.c +++ b/src/nxt_php_sapi.c @@ -77,13 +77,20 @@ typedef void (*zif_handler)(INTERNAL_FUNCTION_PARAMETERS); #endif +static nxt_int_t nxt_php_setup(nxt_task_t *task, nxt_process_t *process, + nxt_common_app_conf_t *conf); static nxt_int_t nxt_php_start(nxt_task_t *task, nxt_process_data_t *data); static nxt_int_t nxt_php_set_target(nxt_task_t *task, nxt_php_target_t *target, nxt_conf_value_t *conf); +static nxt_int_t nxt_php_set_ini_path(nxt_task_t *task, nxt_str_t *path, + char *workdir); static void nxt_php_set_options(nxt_task_t *task, nxt_conf_value_t *options, int type); static nxt_int_t nxt_php_alter_option(nxt_str_t *name, nxt_str_t *value, int type); +#ifdef NXT_PHP8 +static void nxt_php_disable_functions(nxt_str_t *str); +#endif static void nxt_php_disable(nxt_task_t *task, const char *type, nxt_str_t *value, char **ptr, nxt_php_disable_t disable); @@ -252,7 +259,7 @@ NXT_EXPORT nxt_app_module_t nxt_app_module = { PHP_VERSION, NULL, 0, - NULL, + nxt_php_setup, nxt_php_start, }; @@ -267,55 +274,20 @@ static void ***tsrm_ls; static nxt_int_t -nxt_php_start(nxt_task_t *task, nxt_process_data_t *data) +nxt_php_setup(nxt_task_t *task, nxt_process_t *process, + nxt_common_app_conf_t *conf) { - u_char *p; - uint32_t next; - nxt_str_t ini_path, name; - nxt_int_t ret; - nxt_uint_t n; - nxt_unit_ctx_t *unit_ctx; - nxt_unit_init_t php_init; - nxt_conf_value_t *value; - nxt_php_app_conf_t *c; - nxt_common_app_conf_t *conf; + nxt_str_t ini_path; + nxt_int_t ret; + nxt_conf_value_t *value; + nxt_php_app_conf_t *c; static nxt_str_t file_str = nxt_string("file"); static nxt_str_t user_str = nxt_string("user"); static nxt_str_t admin_str = nxt_string("admin"); - conf = data->app; c = &conf->u.php; - n = (c->targets != NULL) ? nxt_conf_object_members_count(c->targets) : 1; - - nxt_php_targets = nxt_zalloc(sizeof(nxt_php_target_t) * n); - if (nxt_slow_path(nxt_php_targets == NULL)) { - return NXT_ERROR; - } - - if (c->targets != NULL) { - next = 0; - - for (n = 0; /* void */; n++) { - value = nxt_conf_next_object_member(c->targets, &name, &next); - if (value == NULL) { - break; - } - - ret = nxt_php_set_target(task, &nxt_php_targets[n], value); - if (nxt_slow_path(ret != NXT_OK)) { - return NXT_ERROR; - } - } - - } else { - ret = nxt_php_set_target(task, &nxt_php_targets[0], conf->self); - if (nxt_slow_path(ret != NXT_OK)) { - return NXT_ERROR; - } - } - #ifdef ZTS #if PHP_VERSION_ID >= 70400 @@ -345,15 +317,12 @@ nxt_php_start(nxt_task_t *task, nxt_process_data_t *data) if (value != NULL) { nxt_conf_get_string(value, &ini_path); - p = nxt_malloc(ini_path.length + 1); - if (nxt_slow_path(p == NULL)) { + ret = nxt_php_set_ini_path(task, &ini_path, + conf->working_directory); + + if (nxt_slow_path(ret != NXT_OK)) { return NXT_ERROR; } - - nxt_php_sapi_module.php_ini_path_override = (char *) p; - - p = nxt_cpymem(p, ini_path.start, ini_path.length); - *p = '\0'; } } @@ -370,6 +339,55 @@ nxt_php_start(nxt_task_t *task, nxt_process_data_t *data) nxt_php_set_options(task, value, ZEND_INI_USER); } + return NXT_OK; +} + + +static nxt_int_t +nxt_php_start(nxt_task_t *task, nxt_process_data_t *data) +{ + uint32_t next; + nxt_int_t ret; + nxt_str_t name; + nxt_uint_t n; + nxt_unit_ctx_t *unit_ctx; + nxt_unit_init_t php_init; + nxt_conf_value_t *value; + nxt_php_app_conf_t *c; + nxt_common_app_conf_t *conf; + + conf = data->app; + c = &conf->u.php; + + n = (c->targets != NULL) ? nxt_conf_object_members_count(c->targets) : 1; + + nxt_php_targets = nxt_zalloc(sizeof(nxt_php_target_t) * n); + if (nxt_slow_path(nxt_php_targets == NULL)) { + return NXT_ERROR; + } + + if (c->targets != NULL) { + next = 0; + + for (n = 0; /* void */; n++) { + value = nxt_conf_next_object_member(c->targets, &name, &next); + if (value == NULL) { + break; + } + + ret = nxt_php_set_target(task, &nxt_php_targets[n], value); + if (nxt_slow_path(ret != NXT_OK)) { + return NXT_ERROR; + } + } + + } else { + ret = nxt_php_set_target(task, &nxt_php_targets[0], conf->self); + if (nxt_slow_path(ret != NXT_OK)) { + return NXT_ERROR; + } + } + ret = nxt_unit_default_init(task, &php_init); if (nxt_slow_path(ret != NXT_OK)) { nxt_alert(task, "nxt_unit_default_init() failed"); @@ -386,9 +404,8 @@ nxt_php_start(nxt_task_t *task, nxt_process_data_t *data) nxt_php_unit_ctx = unit_ctx; - nxt_unit_run(unit_ctx); - - nxt_unit_done(unit_ctx); + nxt_unit_run(nxt_php_unit_ctx); + nxt_unit_done(nxt_php_unit_ctx); exit(0); @@ -510,6 +527,46 @@ nxt_php_set_target(nxt_task_t *task, nxt_php_target_t *target, } +static nxt_int_t +nxt_php_set_ini_path(nxt_task_t *task, nxt_str_t *ini_path, char *workdir) +{ + size_t wdlen; + u_char *p, *start; + + if (ini_path->start[0] == '/' || workdir == NULL) { + p = nxt_malloc(ini_path->length + 1); + if (nxt_slow_path(p == NULL)) { + return NXT_ERROR; + } + + start = p; + + } else { + wdlen = nxt_strlen(workdir); + + p = nxt_malloc(wdlen + ini_path->length + 2); + if (nxt_slow_path(p == NULL)) { + return NXT_ERROR; + } + + start = p; + + p = nxt_cpymem(p, workdir, wdlen); + + if (workdir[wdlen - 1] != '/') { + *p++ = '/'; + } + } + + p = nxt_cpymem(p, ini_path->start, ini_path->length); + *p = '\0'; + + nxt_php_sapi_module.php_ini_path_override = (char *) start; + + return NXT_OK; +} + + static void nxt_php_set_options(nxt_task_t *task, nxt_conf_value_t *options, int type) { @@ -535,9 +592,13 @@ nxt_php_set_options(nxt_task_t *task, nxt_conf_value_t *options, int type) } if (nxt_str_eq(&name, "disable_functions", 17)) { +#ifdef NXT_PHP8 + nxt_php_disable_functions(&value); +#else nxt_php_disable(task, "function", &value, &PG(disable_functions), zend_disable_function); +#endif continue; } @@ -626,6 +687,29 @@ nxt_php_alter_option(nxt_str_t *name, nxt_str_t *value, int type) #endif +#ifdef NXT_PHP8 + +static void +nxt_php_disable_functions(nxt_str_t *str) +{ + char *p; + + p = nxt_malloc(str->length + 1); + if (nxt_slow_path(p == NULL)) { + return; + } + + nxt_memcpy(p, str->start, str->length); + p[str->length] = '\0'; + + zend_disable_functions(p); + + nxt_free(p); +} + +#endif + + static void nxt_php_disable(nxt_task_t *task, const char *type, nxt_str_t *value, char **ptr, nxt_php_disable_t disable) diff --git a/src/nxt_process.c b/src/nxt_process.c index 9bfae395..9be7974f 100644 --- a/src/nxt_process.c +++ b/src/nxt_process.c @@ -17,10 +17,6 @@ #include <sys/prctl.h> #endif -#if (NXT_HAVE_PIVOT_ROOT) -#include <mntent.h> -#endif - static nxt_int_t nxt_process_setup(nxt_task_t *task, nxt_process_t *process); static nxt_int_t nxt_process_child_fixup(nxt_task_t *task, nxt_process_t *process); @@ -33,16 +29,6 @@ static void nxt_process_created_ok(nxt_task_t *task, nxt_port_recv_msg_t *msg, static void nxt_process_created_error(nxt_task_t *task, nxt_port_recv_msg_t *msg, void *data); -#if (NXT_HAVE_ISOLATION_ROOTFS) -static nxt_int_t nxt_process_chroot(nxt_task_t *task, const char *path); - -#if (NXT_HAVE_PIVOT_ROOT) && (NXT_HAVE_CLONE_NEWNS) -static nxt_int_t nxt_process_pivot_root(nxt_task_t *task, const char *rootfs); -static nxt_int_t nxt_process_private_mount(nxt_task_t *task, - const char *rootfs); -static int nxt_pivot_root(const char *new_root, const char *old_root); -#endif -#endif /* A cached process pid. */ nxt_pid_t nxt_pid; @@ -398,51 +384,6 @@ nxt_process_core_setup(nxt_task_t *task, nxt_process_t *process) } -#if (NXT_HAVE_CLONE_NEWUSER) - -nxt_int_t -nxt_process_vldt_isolation_creds(nxt_task_t *task, nxt_process_t *process) -{ - nxt_int_t ret; - nxt_clone_t *clone; - nxt_credential_t *creds; - - clone = &process->isolation.clone; - creds = process->user_cred; - - if (clone->uidmap.size == 0 && clone->gidmap.size == 0) { - return NXT_OK; - } - - if (!nxt_is_clone_flag_set(clone->flags, NEWUSER)) { - if (nxt_slow_path(clone->uidmap.size > 0)) { - nxt_log(task, NXT_LOG_ERR, "\"uidmap\" is set but " - "\"isolation.namespaces.credential\" is false or unset"); - - return NXT_ERROR; - } - - if (nxt_slow_path(clone->gidmap.size > 0)) { - nxt_log(task, NXT_LOG_ERR, "\"gidmap\" is set but " - "\"isolation.namespaces.credential\" is false or unset"); - - return NXT_ERROR; - } - - return NXT_OK; - } - - ret = nxt_clone_vldt_credential_uidmap(task, &clone->uidmap, creds); - if (nxt_slow_path(ret != NXT_OK)) { - return NXT_ERROR; - } - - return nxt_clone_vldt_credential_gidmap(task, &clone->gidmap, creds); -} - -#endif - - nxt_int_t nxt_process_creds_set(nxt_task_t *task, nxt_process_t *process, nxt_str_t *user, nxt_str_t *group) @@ -525,329 +466,6 @@ nxt_process_apply_creds(nxt_task_t *task, nxt_process_t *process) } -#if (NXT_HAVE_ISOLATION_ROOTFS) - - -#if (NXT_HAVE_PIVOT_ROOT) && (NXT_HAVE_CLONE_NEWNS) - - -nxt_int_t -nxt_process_change_root(nxt_task_t *task, nxt_process_t *process) -{ - char *rootfs; - nxt_int_t ret; - - rootfs = (char *) process->isolation.rootfs; - - nxt_debug(task, "change root: %s", rootfs); - - if (NXT_CLONE_MNT(process->isolation.clone.flags)) { - ret = nxt_process_pivot_root(task, rootfs); - } else { - ret = nxt_process_chroot(task, rootfs); - } - - if (nxt_fast_path(ret == NXT_OK)) { - if (nxt_slow_path(chdir("/") < 0)) { - nxt_alert(task, "chdir(\"/\") %E", nxt_errno); - return NXT_ERROR; - } - } - - return ret; -} - - -#else - - -nxt_int_t -nxt_process_change_root(nxt_task_t *task, nxt_process_t *process) -{ - char *rootfs; - - rootfs = (char *) process->isolation.rootfs; - - nxt_debug(task, "change root: %s", rootfs); - - if (nxt_fast_path(nxt_process_chroot(task, rootfs) == NXT_OK)) { - if (nxt_slow_path(chdir("/") < 0)) { - nxt_alert(task, "chdir(\"/\") %E", nxt_errno); - return NXT_ERROR; - } - - return NXT_OK; - } - - return NXT_ERROR; -} - - -#endif - - -static nxt_int_t -nxt_process_chroot(nxt_task_t *task, const char *path) -{ - if (nxt_slow_path(chroot(path) < 0)) { - nxt_alert(task, "chroot(%s) %E", path, nxt_errno); - return NXT_ERROR; - } - - return NXT_OK; -} - - -void -nxt_process_unmount_all(nxt_task_t *task, nxt_process_t *process) -{ - size_t i, n; - nxt_array_t *mounts; - nxt_fs_mount_t *mnt; - - nxt_debug(task, "unmount all (%s)", process->name); - - mounts = process->isolation.mounts; - n = mounts->nelts; - mnt = mounts->elts; - - for (i = 0; i < n; i++) { - nxt_fs_unmount(mnt[i].dst); - } -} - - -#if (NXT_HAVE_PIVOT_ROOT) && (NXT_HAVE_CLONE_NEWNS) - -/* - * pivot_root(2) can only be safely used with containers, otherwise it can - * umount(2) the global root filesystem and screw up the machine. - */ - -static nxt_int_t -nxt_process_pivot_root(nxt_task_t *task, const char *path) -{ - /* - * This implementation makes use of a kernel trick that works for ages - * and now documented in Linux kernel 5. - * https://lore.kernel.org/linux-man/87r24piwhm.fsf@x220.int.ebiederm.org/T/ - */ - - if (nxt_slow_path(mount("", "/", "", MS_SLAVE|MS_REC, "") != 0)) { - nxt_alert(task, "failed to make / a slave mount %E", nxt_errno); - return NXT_ERROR; - } - - if (nxt_slow_path(nxt_process_private_mount(task, path) != NXT_OK)) { - return NXT_ERROR; - } - - if (nxt_slow_path(mount(path, path, "bind", MS_BIND|MS_REC, "") != 0)) { - nxt_alert(task, "error bind mounting rootfs %E", nxt_errno); - return NXT_ERROR; - } - - if (nxt_slow_path(chdir(path) != 0)) { - nxt_alert(task, "failed to chdir(%s) %E", path, nxt_errno); - return NXT_ERROR; - } - - if (nxt_slow_path(nxt_pivot_root(".", ".") != 0)) { - nxt_alert(task, "failed to pivot_root %E", nxt_errno); - return NXT_ERROR; - } - - /* - * Make oldroot a slave mount to avoid unmounts getting propagated to the - * host. - */ - if (nxt_slow_path(mount("", ".", "", MS_SLAVE | MS_REC, NULL) != 0)) { - nxt_alert(task, "failed to bind mount rootfs %E", nxt_errno); - return NXT_ERROR; - } - - if (nxt_slow_path(umount2(".", MNT_DETACH) != 0)) { - nxt_alert(task, "failed to umount old root directory %E", nxt_errno); - return NXT_ERROR; - } - - return NXT_OK; -} - - -static nxt_int_t -nxt_process_private_mount(nxt_task_t *task, const char *rootfs) -{ - char *parent_mnt; - FILE *procfile; - u_char **mounts; - size_t len; - uint8_t *shared; - nxt_int_t ret, index, nmounts; - struct mntent *ent; - - static const char *mount_path = "/proc/self/mounts"; - - ret = NXT_ERROR; - ent = NULL; - shared = NULL; - procfile = NULL; - parent_mnt = NULL; - - nmounts = 256; - - mounts = nxt_malloc(nmounts * sizeof(uintptr_t)); - if (nxt_slow_path(mounts == NULL)) { - goto fail; - } - - shared = nxt_malloc(nmounts); - if (nxt_slow_path(shared == NULL)) { - goto fail; - } - - procfile = setmntent(mount_path, "r"); - if (nxt_slow_path(procfile == NULL)) { - nxt_alert(task, "failed to open %s %E", mount_path, nxt_errno); - - goto fail; - } - - index = 0; - -again: - - for ( ; index < nmounts; index++) { - ent = getmntent(procfile); - if (ent == NULL) { - nmounts = index; - break; - } - - mounts[index] = (u_char *) strdup(ent->mnt_dir); - shared[index] = hasmntopt(ent, "shared") != NULL; - } - - if (ent != NULL) { - /* there are still entries to be read */ - - nmounts *= 2; - mounts = nxt_realloc(mounts, nmounts); - if (nxt_slow_path(mounts == NULL)) { - goto fail; - } - - shared = nxt_realloc(shared, nmounts); - if (nxt_slow_path(shared == NULL)) { - goto fail; - } - - goto again; - } - - for (index = 0; index < nmounts; index++) { - if (nxt_strcmp(mounts[index], rootfs) == 0) { - parent_mnt = (char *) rootfs; - break; - } - } - - if (parent_mnt == NULL) { - len = nxt_strlen(rootfs); - - parent_mnt = nxt_malloc(len + 1); - if (parent_mnt == NULL) { - goto fail; - } - - nxt_memcpy(parent_mnt, rootfs, len); - parent_mnt[len] = '\0'; - - if (parent_mnt[len - 1] == '/') { - parent_mnt[len - 1] = '\0'; - len--; - } - - for ( ;; ) { - for (index = 0; index < nmounts; index++) { - if (nxt_strcmp(mounts[index], parent_mnt) == 0) { - goto found; - } - } - - if (len == 1 && parent_mnt[0] == '/') { - nxt_alert(task, "parent mount not found"); - goto fail; - } - - /* parent dir */ - while (parent_mnt[len - 1] != '/' && len > 0) { - len--; - } - - if (nxt_slow_path(len == 0)) { - nxt_alert(task, "parent mount not found"); - goto fail; - } - - if (len == 1) { - parent_mnt[len] = '\0'; /* / */ - } else { - parent_mnt[len - 1] = '\0'; /* /<path> */ - } - } - } - -found: - - if (shared[index]) { - if (nxt_slow_path(mount("", parent_mnt, "", MS_PRIVATE, "") != 0)) { - nxt_alert(task, "mount(\"\", \"%s\", MS_PRIVATE) %E", parent_mnt, - nxt_errno); - - goto fail; - } - } - - ret = NXT_OK; - -fail: - - if (procfile != NULL) { - endmntent(procfile); - } - - if (mounts != NULL) { - for (index = 0; index < nmounts; index++) { - nxt_free(mounts[index]); - } - - nxt_free(mounts); - } - - if (shared != NULL) { - nxt_free(shared); - } - - if (parent_mnt != NULL && parent_mnt != rootfs) { - nxt_free(parent_mnt); - } - - return ret; -} - - -static int -nxt_pivot_root(const char *new_root, const char *old_root) -{ - return syscall(__NR_pivot_root, new_root, old_root); -} - -#endif - -#endif - - static nxt_int_t nxt_process_send_ready(nxt_task_t *task, nxt_process_t *process) { diff --git a/src/nxt_process.h b/src/nxt_process.h index ecd813e2..d9b4dff1 100644 --- a/src/nxt_process.h +++ b/src/nxt_process.h @@ -60,6 +60,9 @@ typedef enum { typedef struct nxt_port_mmap_s nxt_port_mmap_t; +typedef struct nxt_process_s nxt_process_t; +typedef void (*nxt_isolation_cleanup_t)(nxt_task_t *task, + nxt_process_t *process); typedef struct { @@ -69,21 +72,30 @@ typedef struct { nxt_port_mmap_t *elts; } nxt_port_mmaps_t; + typedef struct { - u_char *rootfs; - nxt_array_t *mounts; /* of nxt_mount_t */ + uint8_t language_deps; /* 1-byte */ +} nxt_process_automount_t; + + +typedef struct { + u_char *rootfs; + nxt_process_automount_t automount; + nxt_array_t *mounts; /* of nxt_mount_t */ + + nxt_isolation_cleanup_t cleanup; #if (NXT_HAVE_CLONE) - nxt_clone_t clone; + nxt_clone_t clone; #endif #if (NXT_HAVE_PR_SET_NO_NEW_PRIVS) - uint8_t new_privs; /* 1 bit */ + uint8_t new_privs; /* 1 bit */ #endif } nxt_process_isolation_t; -typedef struct { +struct nxt_process_s { nxt_pid_t pid; const char *name; nxt_queue_t ports; /* of nxt_port_t */ @@ -103,7 +115,7 @@ typedef struct { nxt_process_data_t data; nxt_process_isolation_t isolation; -} nxt_process_t; +}; typedef nxt_int_t (*nxt_process_prefork_t)(nxt_task_t *task, @@ -178,17 +190,6 @@ nxt_int_t nxt_process_creds_set(nxt_task_t *task, nxt_process_t *process, nxt_str_t *user, nxt_str_t *group); nxt_int_t nxt_process_apply_creds(nxt_task_t *task, nxt_process_t *process); -#if (NXT_HAVE_CLONE_NEWUSER) -nxt_int_t nxt_process_vldt_isolation_creds(nxt_task_t *task, - nxt_process_t *process); -#endif - -nxt_int_t nxt_process_change_root(nxt_task_t *task, nxt_process_t *process); - -#if (NXT_HAVE_ISOLATION_ROOTFS) -void nxt_process_unmount_all(nxt_task_t *task, nxt_process_t *process); -#endif - #if (NXT_HAVE_SETPROCTITLE) #define nxt_process_title(task, fmt, ...) \ diff --git a/src/nxt_router.c b/src/nxt_router.c index 0e1de6fa..a3218047 100644 --- a/src/nxt_router.c +++ b/src/nxt_router.c @@ -24,7 +24,6 @@ typedef struct { uint32_t max_processes; uint32_t spare_processes; nxt_msec_t timeout; - nxt_msec_t res_timeout; nxt_msec_t idle_timeout; uint32_t requests; nxt_conf_value_t *limits_value; @@ -260,7 +259,7 @@ static const nxt_str_t empty_prefix = nxt_string(""); static const nxt_str_t *nxt_app_msg_prefix[] = { &empty_prefix, - &http_prefix, + &empty_prefix, &http_prefix, &http_prefix, &http_prefix, @@ -1158,12 +1157,6 @@ static nxt_conf_map_t nxt_router_app_limits_conf[] = { }, { - nxt_string("reschedule_timeout"), - NXT_CONF_MAP_MSEC, - offsetof(nxt_router_app_conf_t, res_timeout), - }, - - { nxt_string("requests"), NXT_CONF_MAP_INT32, offsetof(nxt_router_app_conf_t, requests), @@ -1423,7 +1416,6 @@ nxt_router_conf_create(nxt_task_t *task, nxt_router_temp_conf_t *tmcf, apcf.max_processes = 1; apcf.spare_processes = 0; apcf.timeout = 0; - apcf.res_timeout = 1000; apcf.idle_timeout = 15000; apcf.requests = 0; apcf.limits_value = NULL; @@ -1505,8 +1497,6 @@ nxt_router_conf_create(nxt_task_t *task, nxt_router_temp_conf_t *tmcf, nxt_debug(task, "application type: %V", &apcf.type); nxt_debug(task, "application processes: %D", apcf.processes); nxt_debug(task, "application request timeout: %M", apcf.timeout); - nxt_debug(task, "application reschedule timeout: %M", - apcf.res_timeout); nxt_debug(task, "application requests: %D", apcf.requests); lang = nxt_app_lang_module(task->thread->runtime, &apcf.type); @@ -1537,7 +1527,6 @@ nxt_router_conf_create(nxt_task_t *task, nxt_router_temp_conf_t *tmcf, app->max_pending_processes = apcf.spare_processes ? apcf.spare_processes : 1; app->timeout = apcf.timeout; - app->res_timeout = apcf.res_timeout * 1000000; app->idle_timeout = apcf.idle_timeout; app->max_requests = apcf.requests; @@ -1736,6 +1725,10 @@ nxt_router_conf_create(nxt_task_t *task, nxt_router_temp_conf_t *tmcf, tmcf->router_conf, &lscf.application); } + + if (nxt_slow_path(skcf->action == NULL)) { + goto fail; + } } } @@ -4948,6 +4941,9 @@ nxt_router_app_prepare_request(nxt_task_t *task, nxt_debug(task, "queue is not empty"); } + buf->is_port_mmap_sent = 1; + buf->mem.pos = buf->mem.free; + } else { nxt_alert(task, "stream #%uD, app '%V': failed to send app message", req_rpc_data->stream, &app->name); diff --git a/src/nxt_router.h b/src/nxt_router.h index 81b3538c..512f1810 100644 --- a/src/nxt_router.h +++ b/src/nxt_router.h @@ -126,7 +126,6 @@ struct nxt_app_s { uint32_t max_requests; nxt_msec_t timeout; - nxt_nsec_t res_timeout; nxt_msec_t idle_timeout; nxt_str_t *targets; diff --git a/src/nxt_runtime.c b/src/nxt_runtime.c index 435276a0..44970b34 100644 --- a/src/nxt_runtime.c +++ b/src/nxt_runtime.c @@ -84,7 +84,11 @@ nxt_runtime_create(nxt_task_t *task) lang->version = (u_char *) ""; lang->file = NULL; lang->module = &nxt_external_module; - lang->mounts = NULL; + + lang->mounts = nxt_array_create(mp, 1, sizeof(nxt_fs_mount_t)); + if (nxt_slow_path(lang->mounts == NULL)) { + goto fail; + } listen_sockets = nxt_array_create(mp, 1, sizeof(nxt_listen_socket_t)); if (nxt_slow_path(listen_sockets == NULL)) { diff --git a/src/nxt_string.h b/src/nxt_string.h index 3f9192e2..7e02f59a 100644 --- a/src/nxt_string.h +++ b/src/nxt_string.h @@ -31,6 +31,16 @@ nxt_strlen(s) \ #define \ +nxt_strdup(s) \ + strdup((char *) s) + + +#define \ +nxt_strchr(buf, delim) \ + (u_char *) strchr((char *) buf, delim) + + +#define \ nxt_memzero(buf, length) \ (void) memset(buf, 0, length) diff --git a/src/nxt_unit.c b/src/nxt_unit.c index 6b7d631d..f75d61bc 100644 --- a/src/nxt_unit.c +++ b/src/nxt_unit.c @@ -74,7 +74,8 @@ static void nxt_unit_request_info_free(nxt_unit_request_info_impl_t *req); static nxt_unit_websocket_frame_impl_t *nxt_unit_websocket_frame_get( nxt_unit_ctx_t *ctx); static void nxt_unit_websocket_frame_release(nxt_unit_websocket_frame_t *ws); -static void nxt_unit_websocket_frame_free(nxt_unit_websocket_frame_impl_t *ws); +static void nxt_unit_websocket_frame_free(nxt_unit_ctx_t *ctx, + nxt_unit_websocket_frame_impl_t *ws); static nxt_unit_mmap_buf_t *nxt_unit_mmap_buf_get(nxt_unit_ctx_t *ctx); static void nxt_unit_mmap_buf_release(nxt_unit_mmap_buf_t *mmap_buf); static int nxt_unit_mmap_buf_send(nxt_unit_request_info_t *req, @@ -119,8 +120,7 @@ static void nxt_unit_mmap_release(nxt_unit_ctx_t *ctx, nxt_port_mmap_header_t *hdr, void *start, uint32_t size); static int nxt_unit_send_shm_ack(nxt_unit_ctx_t *ctx, pid_t pid); -static nxt_unit_process_t *nxt_unit_process_get(nxt_unit_impl_t *lib, - pid_t pid); +static nxt_unit_process_t *nxt_unit_process_get(nxt_unit_ctx_t *ctx, pid_t pid); static nxt_unit_process_t *nxt_unit_process_find(nxt_unit_impl_t *lib, pid_t pid, int remove); static nxt_unit_process_t *nxt_unit_process_pop_first(nxt_unit_impl_t *lib); @@ -184,6 +184,9 @@ static nxt_unit_request_info_t *nxt_unit_request_hash_find( nxt_unit_ctx_t *ctx, uint32_t stream, int remove); static char * nxt_unit_snprint_prefix(char *p, char *end, pid_t pid, int level); +static void *nxt_unit_lvlhsh_alloc(void *data, size_t size); +static void nxt_unit_lvlhsh_free(void *data, void *p); +static int nxt_unit_memcasecmp(const void *p1, const void *p2, size_t length); struct nxt_unit_mmap_buf_s { @@ -531,7 +534,8 @@ nxt_unit_create(nxt_unit_init_t *init) nxt_unit_impl_t *lib; nxt_unit_callbacks_t *cb; - lib = malloc(sizeof(nxt_unit_impl_t) + init->request_data_size); + lib = nxt_unit_malloc(NULL, + sizeof(nxt_unit_impl_t) + init->request_data_size); if (nxt_slow_path(lib == NULL)) { nxt_unit_alert(NULL, "failed to allocate unit struct"); @@ -586,7 +590,7 @@ nxt_unit_create(nxt_unit_init_t *init) fail: - free(lib); + nxt_unit_free(NULL, lib); return NULL; } @@ -710,7 +714,7 @@ nxt_unit_lib_release(nxt_unit_impl_t *lib) nxt_unit_mmaps_destroy(&lib->incoming); nxt_unit_mmaps_destroy(&lib->outgoing); - free(lib); + nxt_unit_free(NULL, lib); } } @@ -1388,7 +1392,7 @@ nxt_unit_request_check_response_port(nxt_unit_request_info_t *req, return NXT_UNIT_AGAIN; } - port_impl = malloc(sizeof(nxt_unit_port_impl_t)); + port_impl = nxt_unit_malloc(ctx, sizeof(nxt_unit_port_impl_t)); if (nxt_slow_path(port_impl == NULL)) { nxt_unit_alert(ctx, "check_response_port: malloc(%d) failed", (int) sizeof(nxt_unit_port_impl_t)); @@ -1412,7 +1416,7 @@ nxt_unit_request_check_response_port(nxt_unit_request_info_t *req, pthread_mutex_unlock(&lib->mutex); - free(port); + nxt_unit_free(ctx, port); return NXT_UNIT_ERROR; } @@ -1426,7 +1430,7 @@ nxt_unit_request_check_response_port(nxt_unit_request_info_t *req, pthread_mutex_unlock(&lib->mutex); - free(port); + nxt_unit_free(ctx, port); return NXT_UNIT_ERROR; } @@ -1634,8 +1638,8 @@ nxt_unit_request_info_get(nxt_unit_ctx_t *ctx) if (nxt_queue_is_empty(&ctx_impl->free_req)) { pthread_mutex_unlock(&ctx_impl->mutex); - req_impl = malloc(sizeof(nxt_unit_request_info_impl_t) - + lib->request_data_size); + req_impl = nxt_unit_malloc(ctx, sizeof(nxt_unit_request_info_impl_t) + + lib->request_data_size); if (nxt_slow_path(req_impl == NULL)) { return NULL; } @@ -1722,7 +1726,7 @@ nxt_unit_request_info_free(nxt_unit_request_info_impl_t *req_impl) nxt_queue_remove(&req_impl->link); if (req_impl != &ctx_impl->req) { - free(req_impl); + nxt_unit_free(&ctx_impl->ctx, req_impl); } } @@ -1741,7 +1745,7 @@ nxt_unit_websocket_frame_get(nxt_unit_ctx_t *ctx) if (nxt_queue_is_empty(&ctx_impl->free_ws)) { pthread_mutex_unlock(&ctx_impl->mutex); - ws_impl = malloc(sizeof(nxt_unit_websocket_frame_impl_t)); + ws_impl = nxt_unit_malloc(ctx, sizeof(nxt_unit_websocket_frame_impl_t)); if (nxt_slow_path(ws_impl == NULL)) { return NULL; } @@ -1783,11 +1787,12 @@ nxt_unit_websocket_frame_release(nxt_unit_websocket_frame_t *ws) static void -nxt_unit_websocket_frame_free(nxt_unit_websocket_frame_impl_t *ws_impl) +nxt_unit_websocket_frame_free(nxt_unit_ctx_t *ctx, + nxt_unit_websocket_frame_impl_t *ws_impl) { nxt_queue_remove(&ws_impl->link); - free(ws_impl); + nxt_unit_free(ctx, ws_impl); } @@ -1815,42 +1820,66 @@ nxt_unit_field_hash(const char *name, size_t name_length) void nxt_unit_request_group_dup_fields(nxt_unit_request_info_t *req) { + char *name; uint32_t i, j; nxt_unit_field_t *fields, f; nxt_unit_request_t *r; + static nxt_str_t content_length = nxt_string("content-length"); + static nxt_str_t content_type = nxt_string("content-type"); + static nxt_str_t cookie = nxt_string("cookie"); + nxt_unit_req_debug(req, "group_dup_fields"); r = req->request; fields = r->fields; for (i = 0; i < r->fields_count; i++) { + name = nxt_unit_sptr_get(&fields[i].name); switch (fields[i].hash) { case NXT_UNIT_HASH_CONTENT_LENGTH: - r->content_length_field = i; + if (fields[i].name_length == content_length.length + && nxt_unit_memcasecmp(name, content_length.start, + content_length.length) == 0) + { + r->content_length_field = i; + } + break; case NXT_UNIT_HASH_CONTENT_TYPE: - r->content_type_field = i; + if (fields[i].name_length == content_type.length + && nxt_unit_memcasecmp(name, content_type.start, + content_type.length) == 0) + { + r->content_type_field = i; + } + break; case NXT_UNIT_HASH_COOKIE: - r->cookie_field = i; + if (fields[i].name_length == cookie.length + && nxt_unit_memcasecmp(name, cookie.start, + cookie.length) == 0) + { + r->cookie_field = i; + } + break; - }; + } for (j = i + 1; j < r->fields_count; j++) { - if (fields[i].hash != fields[j].hash) { - continue; - } - - if (j == i + 1) { + if (fields[i].hash != fields[j].hash + || fields[i].name_length != fields[j].name_length + || nxt_unit_memcasecmp(name, + nxt_unit_sptr_get(&fields[j].name), + fields[j].name_length) != 0) + { continue; } f = fields[j]; - f.name.offset += (j - (i + 1)) * sizeof(f); f.value.offset += (j - (i + 1)) * sizeof(f); while (j > i + 1) { @@ -1862,6 +1891,9 @@ nxt_unit_request_group_dup_fields(nxt_unit_request_info_t *req) fields[j] = f; + /* Assign the same name pointer for further grouping simplicity. */ + nxt_unit_sptr_set(&fields[j].name, name); + i++; } } @@ -2297,7 +2329,7 @@ nxt_unit_mmap_buf_get(nxt_unit_ctx_t *ctx) if (ctx_impl->free_buf == NULL) { pthread_mutex_unlock(&ctx_impl->mutex); - mmap_buf = malloc(sizeof(nxt_unit_mmap_buf_t)); + mmap_buf = nxt_unit_malloc(ctx, sizeof(nxt_unit_mmap_buf_t)); if (nxt_slow_path(mmap_buf == NULL)) { return NULL; } @@ -2615,7 +2647,7 @@ nxt_unit_free_outgoing_buf(nxt_unit_mmap_buf_t *mmap_buf) } if (mmap_buf->free_ptr != NULL) { - free(mmap_buf->free_ptr); + nxt_unit_free(&mmap_buf->ctx_impl->ctx, mmap_buf->free_ptr); mmap_buf->free_ptr = NULL; } @@ -2657,7 +2689,7 @@ nxt_unit_read_buf_get_impl(nxt_unit_ctx_impl_t *ctx_impl) return rbuf; } - rbuf = malloc(sizeof(nxt_unit_read_buf_t)); + rbuf = nxt_unit_malloc(&ctx_impl->ctx, sizeof(nxt_unit_read_buf_t)); if (nxt_fast_path(rbuf != NULL)) { rbuf->ctx_impl = ctx_impl; @@ -3016,7 +3048,7 @@ nxt_unit_request_preread(nxt_unit_request_info_t *req, size_t size) return NULL; } - mmap_buf->free_ptr = malloc(size); + mmap_buf->free_ptr = nxt_unit_malloc(req->ctx, size); if (nxt_slow_path(mmap_buf->free_ptr == NULL)) { nxt_unit_req_alert(req, "preread: failed to allocate buf memory"); nxt_unit_mmap_buf_release(mmap_buf); @@ -3288,7 +3320,7 @@ int nxt_unit_websocket_retain(nxt_unit_websocket_frame_t *ws) { char *b; - size_t size; + size_t size, hsize; nxt_unit_websocket_frame_impl_t *ws_impl; ws_impl = nxt_container_of(ws, nxt_unit_websocket_frame_impl_t, ws); @@ -3299,19 +3331,30 @@ nxt_unit_websocket_retain(nxt_unit_websocket_frame_t *ws) size = ws_impl->buf->buf.end - ws_impl->buf->buf.start; - b = malloc(size); + b = nxt_unit_malloc(ws->req->ctx, size); if (nxt_slow_path(b == NULL)) { return NXT_UNIT_ERROR; } memcpy(b, ws_impl->buf->buf.start, size); + hsize = nxt_websocket_frame_header_size(b); + ws_impl->buf->buf.start = b; - ws_impl->buf->buf.free = b; + ws_impl->buf->buf.free = b + hsize; ws_impl->buf->buf.end = b + size; ws_impl->buf->free_ptr = b; + ws_impl->ws.header = (nxt_websocket_header_t *) b; + + if (ws_impl->ws.header->mask) { + ws_impl->ws.mask = (uint8_t *) b + hsize - 4; + + } else { + ws_impl->ws.mask = NULL; + } + return NXT_UNIT_OK; } @@ -3796,7 +3839,8 @@ nxt_unit_get_outgoing_buf(nxt_unit_ctx_t *ctx, nxt_unit_port_t *port, mmap_buf->plain_ptr = local_buf; } else { - mmap_buf->free_ptr = malloc(size + sizeof(nxt_port_msg_t)); + mmap_buf->free_ptr = nxt_unit_malloc(ctx, + size + sizeof(nxt_port_msg_t)); if (nxt_slow_path(mmap_buf->free_ptr == NULL)) { return NXT_UNIT_ERROR; } @@ -3966,7 +4010,7 @@ nxt_unit_process_release(nxt_unit_process_t *process) if (c == 1) { nxt_unit_debug(NULL, "destroy process #%d", (int) process->pid); - free(process); + nxt_unit_free(NULL, process); } } @@ -3983,7 +4027,7 @@ nxt_unit_mmaps_destroy(nxt_unit_mmaps_t *mmaps) munmap(mm->hdr, PORT_MMAP_SIZE); } - free(mmaps->elts); + nxt_unit_free(NULL, mmaps->elts); } pthread_mutex_destroy(&mmaps->mutex); @@ -4255,8 +4299,8 @@ nxt_unit_lvlhsh_pid_test(nxt_lvlhsh_query_t *lhq, void *data) static const nxt_lvlhsh_proto_t lvlhsh_processes_proto nxt_aligned(64) = { NXT_LVLHSH_DEFAULT, nxt_unit_lvlhsh_pid_test, - nxt_lvlhsh_alloc, - nxt_lvlhsh_free, + nxt_unit_lvlhsh_alloc, + nxt_unit_lvlhsh_free, }; @@ -4271,11 +4315,14 @@ nxt_unit_process_lhq_pid(nxt_lvlhsh_query_t *lhq, pid_t *pid) static nxt_unit_process_t * -nxt_unit_process_get(nxt_unit_impl_t *lib, pid_t pid) +nxt_unit_process_get(nxt_unit_ctx_t *ctx, pid_t pid) { + nxt_unit_impl_t *lib; nxt_unit_process_t *process; nxt_lvlhsh_query_t lhq; + lib = nxt_container_of(ctx->unit, nxt_unit_impl_t, unit); + nxt_unit_process_lhq_pid(&lhq, &pid); if (nxt_lvlhsh_find(&lib->processes, &lhq) == NXT_OK) { @@ -4285,9 +4332,9 @@ nxt_unit_process_get(nxt_unit_impl_t *lib, pid_t pid) return process; } - process = malloc(sizeof(nxt_unit_process_t)); + process = nxt_unit_malloc(ctx, sizeof(nxt_unit_process_t)); if (nxt_slow_path(process == NULL)) { - nxt_unit_alert(NULL, "failed to allocate process for #%d", (int) pid); + nxt_unit_alert(ctx, "failed to allocate process for #%d", (int) pid); return NULL; } @@ -4308,9 +4355,9 @@ nxt_unit_process_get(nxt_unit_impl_t *lib, pid_t pid) break; default: - nxt_unit_alert(NULL, "process %d insert failed", (int) pid); + nxt_unit_alert(ctx, "process %d insert failed", (int) pid); - free(process); + nxt_unit_free(ctx, process); process = NULL; break; } @@ -4881,7 +4928,8 @@ nxt_unit_ctx_alloc(nxt_unit_ctx_t *ctx, void *data) lib = nxt_container_of(ctx->unit, nxt_unit_impl_t, unit); - new_ctx = malloc(sizeof(nxt_unit_ctx_impl_t) + lib->request_data_size); + new_ctx = nxt_unit_malloc(ctx, sizeof(nxt_unit_ctx_impl_t) + + lib->request_data_size); if (nxt_slow_path(new_ctx == NULL)) { nxt_unit_alert(ctx, "failed to allocate context"); @@ -4890,7 +4938,7 @@ nxt_unit_ctx_alloc(nxt_unit_ctx_t *ctx, void *data) rc = nxt_unit_ctx_init(lib, new_ctx, data); if (nxt_slow_path(rc != NXT_UNIT_OK)) { - free(new_ctx); + nxt_unit_free(ctx, new_ctx); return NULL; } @@ -4969,7 +5017,7 @@ nxt_unit_ctx_free(nxt_unit_ctx_impl_t *ctx_impl) while (ctx_impl->free_buf != NULL) { mmap_buf = ctx_impl->free_buf; nxt_unit_mmap_buf_unlink(mmap_buf); - free(mmap_buf); + nxt_unit_free(&ctx_impl->ctx, mmap_buf); } nxt_queue_each(req_impl, &ctx_impl->free_req, @@ -4982,7 +5030,7 @@ nxt_unit_ctx_free(nxt_unit_ctx_impl_t *ctx_impl) nxt_queue_each(ws_impl, &ctx_impl->free_ws, nxt_unit_websocket_frame_impl_t, link) { - nxt_unit_websocket_frame_free(ws_impl); + nxt_unit_websocket_frame_free(&ctx_impl->ctx, ws_impl); } nxt_queue_loop; @@ -4996,7 +5044,7 @@ nxt_unit_ctx_free(nxt_unit_ctx_impl_t *ctx_impl) } if (ctx_impl != &lib->main_ctx) { - free(ctx_impl); + nxt_unit_free(&lib->main_ctx.ctx, ctx_impl); } nxt_unit_lib_release(lib); @@ -5048,7 +5096,7 @@ nxt_unit_create_port(nxt_unit_ctx_t *ctx) pthread_mutex_lock(&lib->mutex); - process = nxt_unit_process_get(lib, lib->pid); + process = nxt_unit_process_get(ctx, lib->pid); if (nxt_slow_path(process == NULL)) { pthread_mutex_unlock(&lib->mutex); @@ -5181,7 +5229,7 @@ nxt_inline void nxt_unit_port_release(nxt_unit_port_t *port) : sizeof(nxt_port_queue_t)); } - free(port_impl); + nxt_unit_free(NULL, port_impl); } } @@ -5288,7 +5336,7 @@ nxt_unit_add_port(nxt_unit_ctx_t *ctx, nxt_unit_port_t *port, void *queue) port->id.pid, port->id.id, port->in_fd, port->out_fd, queue); - process = nxt_unit_process_get(lib, port->id.pid); + process = nxt_unit_process_get(ctx, port->id.pid); if (nxt_slow_path(process == NULL)) { goto unlock; } @@ -5297,7 +5345,7 @@ nxt_unit_add_port(nxt_unit_ctx_t *ctx, nxt_unit_port_t *port, void *queue) process->next_port_id = port->id.id + 1; } - new_port = malloc(sizeof(nxt_unit_port_impl_t)); + new_port = nxt_unit_malloc(ctx, sizeof(nxt_unit_port_impl_t)); if (nxt_slow_path(new_port == NULL)) { nxt_unit_alert(ctx, "add_port: %d,%d malloc() failed", port->id.pid, port->id.id); @@ -5312,7 +5360,7 @@ nxt_unit_add_port(nxt_unit_ctx_t *ctx, nxt_unit_port_t *port, void *queue) nxt_unit_alert(ctx, "add_port: %d,%d hash_add failed", port->id.pid, port->id.id); - free(new_port); + nxt_unit_free(ctx, new_port); new_port = NULL; @@ -5716,10 +5764,6 @@ retry: nxt_unit_debug(ctx, "port{%d,%d} recv %d read_queue", (int) port->id.pid, (int) port->id.id, (int) rbuf->size); - if (port_impl->from_socket) { - nxt_unit_warn(ctx, "port protocol warning: READ_QUEUE after READ_SOCKET"); - } - goto retry; } @@ -5977,8 +6021,8 @@ nxt_unit_port_hash_test(nxt_lvlhsh_query_t *lhq, void *data) static const nxt_lvlhsh_proto_t lvlhsh_ports_proto nxt_aligned(64) = { NXT_LVLHSH_DEFAULT, nxt_unit_port_hash_test, - nxt_lvlhsh_alloc, - nxt_lvlhsh_free, + nxt_unit_lvlhsh_alloc, + nxt_unit_lvlhsh_free, }; @@ -6076,8 +6120,8 @@ nxt_unit_request_hash_test(nxt_lvlhsh_query_t *lhq, void *data) static const nxt_lvlhsh_proto_t lvlhsh_requests_proto nxt_aligned(64) = { NXT_LVLHSH_DEFAULT, nxt_unit_request_hash_test, - nxt_lvlhsh_alloc, - nxt_lvlhsh_free, + nxt_unit_lvlhsh_alloc, + nxt_unit_lvlhsh_free, }; @@ -6158,7 +6202,9 @@ nxt_unit_request_hash_find(nxt_unit_ctx_t *ctx, uint32_t stream, int remove) case NXT_OK: req_impl = nxt_container_of(lhq.value, nxt_unit_request_info_impl_t, req); - req_impl->in_hash = 0; + if (remove) { + req_impl->in_hash = 0; + } return lhq.value; @@ -6307,29 +6353,84 @@ nxt_unit_snprint_prefix(char *p, char *end, pid_t pid, int level) } -/* The function required by nxt_lvlhsh_alloc() and nxt_lvlvhsh_free(). */ - -void * -nxt_memalign(size_t alignment, size_t size) +static void * +nxt_unit_lvlhsh_alloc(void *data, size_t size) { - void *p; - nxt_err_t err; + int err; + void *p; - err = posix_memalign(&p, alignment, size); + err = posix_memalign(&p, size, size); if (nxt_fast_path(err == 0)) { + nxt_unit_debug(NULL, "posix_memalign(%d, %d): %p", + (int) size, (int) size, p); return p; } + nxt_unit_alert(NULL, "posix_memalign(%d, %d) failed: %s (%d)", + (int) size, (int) size, strerror(err), err); return NULL; } -#if (NXT_DEBUG) + +static void +nxt_unit_lvlhsh_free(void *data, void *p) +{ + nxt_unit_free(NULL, p); +} + + +void * +nxt_unit_malloc(nxt_unit_ctx_t *ctx, size_t size) +{ + void *p; + + p = malloc(size); + + if (nxt_fast_path(p != NULL)) { + nxt_unit_debug(ctx, "malloc(%d): %p", (int) size, p); + + } else { + nxt_unit_alert(ctx, "malloc(%d) failed: %s (%d)", + (int) size, strerror(errno), errno); + } + + return p; +} + void -nxt_free(void *p) +nxt_unit_free(nxt_unit_ctx_t *ctx, void *p) { + nxt_unit_debug(ctx, "free(%p)", p); + free(p); } -#endif + +static int +nxt_unit_memcasecmp(const void *p1, const void *p2, size_t length) +{ + u_char c1, c2; + nxt_int_t n; + const u_char *s1, *s2; + + s1 = p1; + s2 = p2; + + while (length-- != 0) { + c1 = *s1++; + c2 = *s2++; + + c1 = nxt_lowcase(c1); + c2 = nxt_lowcase(c2); + + n = c1 - c2; + + if (n != 0) { + return n; + } + } + + return 0; +} diff --git a/src/nxt_unit.h b/src/nxt_unit.h index 67244cf4..e90f0781 100644 --- a/src/nxt_unit.h +++ b/src/nxt_unit.h @@ -187,7 +187,7 @@ struct nxt_unit_read_info_s { /* * Initialize Unit application library with necessary callbacks and - * ready/reply port parameters, send 'READY' response to master. + * ready/reply port parameters, send 'READY' response to main. */ nxt_unit_ctx_t *nxt_unit_init(nxt_unit_init_t *); @@ -322,6 +322,10 @@ int nxt_unit_websocket_retain(nxt_unit_websocket_frame_t *ws); void nxt_unit_websocket_done(nxt_unit_websocket_frame_t *ws); +void *nxt_unit_malloc(nxt_unit_ctx_t *ctx, size_t size); + +void nxt_unit_free(nxt_unit_ctx_t *ctx, void *p); + #if defined __has_attribute #if __has_attribute(format) diff --git a/src/nxt_upstream.c b/src/nxt_upstream.c index c8ecbbe6..9f81b286 100644 --- a/src/nxt_upstream.c +++ b/src/nxt_upstream.c @@ -86,11 +86,11 @@ nxt_upstream_find(nxt_upstreams_t *upstreams, nxt_str_t *name, action->u.upstream_number = i; action->handler = nxt_upstream_handler; - return NXT_DECLINED; + return NXT_OK; } } - return NXT_OK; + return NXT_DECLINED; } diff --git a/src/python/nxt_python.c b/src/python/nxt_python.c new file mode 100644 index 00000000..01534a47 --- /dev/null +++ b/src/python/nxt_python.c @@ -0,0 +1,340 @@ + +/* + * Copyright (C) NGINX, Inc. + */ + + +#include <Python.h> + +#include <nxt_main.h> +#include <nxt_router.h> +#include <nxt_unit.h> + +#include <python/nxt_python.h> + +#include NXT_PYTHON_MOUNTS_H + + +static nxt_int_t nxt_python_start(nxt_task_t *task, + nxt_process_data_t *data); +static void nxt_python_atexit(void); + +static uint32_t compat[] = { + NXT_VERNUM, NXT_DEBUG, +}; + + +NXT_EXPORT nxt_app_module_t nxt_app_module = { + sizeof(compat), + compat, + nxt_string("python"), + PY_VERSION, + nxt_python_mounts, + nxt_nitems(nxt_python_mounts), + NULL, + nxt_python_start, +}; + +static PyObject *nxt_py_stderr_flush; +PyObject *nxt_py_application; + +#if PY_MAJOR_VERSION == 3 +static wchar_t *nxt_py_home; +#else +static char *nxt_py_home; +#endif + + +static nxt_int_t +nxt_python_start(nxt_task_t *task, nxt_process_data_t *data) +{ + int rc, asgi; + char *nxt_py_module; + size_t len; + PyObject *obj, *pypath, *module; + const char *callable; + nxt_unit_ctx_t *unit_ctx; + nxt_unit_init_t python_init; + nxt_common_app_conf_t *app_conf; + nxt_python_app_conf_t *c; +#if PY_MAJOR_VERSION == 3 + char *path; + size_t size; + nxt_int_t pep405; + + static const char pyvenv[] = "/pyvenv.cfg"; + static const char bin_python[] = "/bin/python"; +#endif + + app_conf = data->app; + c = &app_conf->u.python; + + if (c->home != NULL) { + len = nxt_strlen(c->home); + +#if PY_MAJOR_VERSION == 3 + + path = nxt_malloc(len + sizeof(pyvenv)); + if (nxt_slow_path(path == NULL)) { + nxt_alert(task, "Failed to allocate memory"); + return NXT_ERROR; + } + + nxt_memcpy(path, c->home, len); + nxt_memcpy(path + len, pyvenv, sizeof(pyvenv)); + + pep405 = (access(path, R_OK) == 0); + + nxt_free(path); + + if (pep405) { + size = (len + sizeof(bin_python)) * sizeof(wchar_t); + + } else { + size = (len + 1) * sizeof(wchar_t); + } + + nxt_py_home = nxt_malloc(size); + if (nxt_slow_path(nxt_py_home == NULL)) { + nxt_alert(task, "Failed to allocate memory"); + return NXT_ERROR; + } + + if (pep405) { + mbstowcs(nxt_py_home, c->home, len); + mbstowcs(nxt_py_home + len, bin_python, sizeof(bin_python)); + Py_SetProgramName(nxt_py_home); + + } else { + mbstowcs(nxt_py_home, c->home, len + 1); + Py_SetPythonHome(nxt_py_home); + } + +#else + nxt_py_home = nxt_malloc(len + 1); + if (nxt_slow_path(nxt_py_home == NULL)) { + nxt_alert(task, "Failed to allocate memory"); + return NXT_ERROR; + } + + nxt_memcpy(nxt_py_home, c->home, len + 1); + Py_SetPythonHome(nxt_py_home); +#endif + } + + Py_InitializeEx(0); + + module = NULL; + obj = NULL; + + obj = PySys_GetObject((char *) "stderr"); + if (nxt_slow_path(obj == NULL)) { + nxt_alert(task, "Python failed to get \"sys.stderr\" object"); + goto fail; + } + + nxt_py_stderr_flush = PyObject_GetAttrString(obj, "flush"); + if (nxt_slow_path(nxt_py_stderr_flush == NULL)) { + nxt_alert(task, "Python failed to get \"flush\" attribute of " + "\"sys.stderr\" object"); + goto fail; + } + + /* obj is a Borrowed reference. */ + + if (c->path.length > 0) { + obj = PyString_FromStringAndSize((char *) c->path.start, + c->path.length); + + if (nxt_slow_path(obj == NULL)) { + nxt_alert(task, "Python failed to create string object \"%V\"", + &c->path); + goto fail; + } + + pypath = PySys_GetObject((char *) "path"); + + if (nxt_slow_path(pypath == NULL)) { + nxt_alert(task, "Python failed to get \"sys.path\" list"); + goto fail; + } + + if (nxt_slow_path(PyList_Insert(pypath, 0, obj) != 0)) { + nxt_alert(task, "Python failed to insert \"%V\" into \"sys.path\"", + &c->path); + goto fail; + } + + Py_DECREF(obj); + } + + obj = Py_BuildValue("[s]", "unit"); + if (nxt_slow_path(obj == NULL)) { + nxt_alert(task, "Python failed to create the \"sys.argv\" list"); + goto fail; + } + + if (nxt_slow_path(PySys_SetObject((char *) "argv", obj) != 0)) { + nxt_alert(task, "Python failed to set the \"sys.argv\" list"); + goto fail; + } + + Py_CLEAR(obj); + + nxt_py_module = nxt_alloca(c->module.length + 1); + nxt_memcpy(nxt_py_module, c->module.start, c->module.length); + nxt_py_module[c->module.length] = '\0'; + + module = PyImport_ImportModule(nxt_py_module); + if (nxt_slow_path(module == NULL)) { + nxt_alert(task, "Python failed to import module \"%s\"", nxt_py_module); + nxt_python_print_exception(); + goto fail; + } + + callable = (c->callable != NULL) ? c->callable : "application"; + + obj = PyDict_GetItemString(PyModule_GetDict(module), callable); + if (nxt_slow_path(obj == NULL)) { + nxt_alert(task, "Python failed to get \"%s\" " + "from module \"%s\"", callable, nxt_py_module); + goto fail; + } + + if (nxt_slow_path(PyCallable_Check(obj) == 0)) { + nxt_alert(task, "\"%s\" in module \"%s\" " + "is not a callable object", callable, nxt_py_module); + goto fail; + } + + nxt_py_application = obj; + obj = NULL; + + Py_INCREF(nxt_py_application); + + Py_CLEAR(module); + + nxt_unit_default_init(task, &python_init); + + python_init.shm_limit = data->app->shm_limit; + + asgi = nxt_python_asgi_check(nxt_py_application); + + if (asgi) { + rc = nxt_python_asgi_init(task, &python_init); + + } else { + rc = nxt_python_wsgi_init(task, &python_init); + } + + if (nxt_slow_path(rc == NXT_ERROR)) { + goto fail; + } + + unit_ctx = nxt_unit_init(&python_init); + if (nxt_slow_path(unit_ctx == NULL)) { + goto fail; + } + + if (asgi) { + rc = nxt_python_asgi_run(unit_ctx); + + } else { + rc = nxt_python_wsgi_run(unit_ctx); + } + + nxt_unit_done(unit_ctx); + + nxt_python_atexit(); + + exit(rc); + + return NXT_OK; + +fail: + + Py_XDECREF(obj); + Py_XDECREF(module); + + nxt_python_atexit(); + + return NXT_ERROR; +} + + +nxt_int_t +nxt_python_init_strings(nxt_python_string_t *pstr) +{ + PyObject *obj; + + while (pstr->string.start != NULL) { + obj = PyString_FromStringAndSize((char *) pstr->string.start, + pstr->string.length); + if (nxt_slow_path(obj == NULL)) { + return NXT_ERROR; + } + + PyUnicode_InternInPlace(&obj); + + *pstr->object_p = obj; + + pstr++; + } + + return NXT_OK; +} + + +void +nxt_python_done_strings(nxt_python_string_t *pstr) +{ + PyObject *obj; + + while (pstr->string.start != NULL) { + obj = *pstr->object_p; + + Py_XDECREF(obj); + *pstr->object_p = NULL; + + pstr++; + } +} + + +static void +nxt_python_atexit(void) +{ + nxt_python_wsgi_done(); + nxt_python_asgi_done(); + + Py_XDECREF(nxt_py_stderr_flush); + Py_XDECREF(nxt_py_application); + + Py_Finalize(); + + if (nxt_py_home != NULL) { + nxt_free(nxt_py_home); + } +} + + +void +nxt_python_print_exception(void) +{ + PyErr_Print(); + +#if PY_MAJOR_VERSION == 3 + /* The backtrace may be buffered in sys.stderr file object. */ + { + PyObject *result; + + result = PyObject_CallFunction(nxt_py_stderr_flush, NULL); + if (nxt_slow_path(result == NULL)) { + PyErr_Clear(); + return; + } + + Py_DECREF(result); + } +#endif +} diff --git a/src/python/nxt_python.h b/src/python/nxt_python.h new file mode 100644 index 00000000..3211026b --- /dev/null +++ b/src/python/nxt_python.h @@ -0,0 +1,60 @@ + +/* + * Copyright (C) NGINX, Inc. + */ + +#ifndef _NXT_PYTHON_H_INCLUDED_ +#define _NXT_PYTHON_H_INCLUDED_ + + +#include <Python.h> +#include <nxt_main.h> +#include <nxt_unit.h> + + +#if PY_MAJOR_VERSION == 3 +#define NXT_PYTHON_BYTES_TYPE "bytestring" + +#define PyString_FromStringAndSize(str, size) \ + PyUnicode_DecodeLatin1((str), (size), "strict") +#define PyString_AS_STRING PyUnicode_DATA + +#else +#define NXT_PYTHON_BYTES_TYPE "string" + +#define PyBytes_FromStringAndSize PyString_FromStringAndSize +#define PyBytes_Check PyString_Check +#define PyBytes_GET_SIZE PyString_GET_SIZE +#define PyBytes_AS_STRING PyString_AS_STRING +#define PyUnicode_InternInPlace PyString_InternInPlace +#define PyUnicode_AsUTF8 PyString_AS_STRING +#endif + +#if PY_MAJOR_VERSION == 3 && PY_MINOR_VERSION >= 5 +#define NXT_HAVE_ASGI 1 +#endif + +extern PyObject *nxt_py_application; + +typedef struct { + nxt_str_t string; + PyObject **object_p; +} nxt_python_string_t; + + +nxt_int_t nxt_python_init_strings(nxt_python_string_t *pstr); +void nxt_python_done_strings(nxt_python_string_t *pstr); + +void nxt_python_print_exception(void); + +nxt_int_t nxt_python_wsgi_init(nxt_task_t *task, nxt_unit_init_t *init); +int nxt_python_wsgi_run(nxt_unit_ctx_t *ctx); +void nxt_python_wsgi_done(void); + +int nxt_python_asgi_check(PyObject *obj); +nxt_int_t nxt_python_asgi_init(nxt_task_t *task, nxt_unit_init_t *init); +nxt_int_t nxt_python_asgi_run(nxt_unit_ctx_t *ctx); +void nxt_python_asgi_done(void); + + +#endif /* _NXT_PYTHON_H_INCLUDED_ */ diff --git a/src/python/nxt_python_asgi.c b/src/python/nxt_python_asgi.c new file mode 100644 index 00000000..72408ea1 --- /dev/null +++ b/src/python/nxt_python_asgi.c @@ -0,0 +1,1227 @@ + +/* + * Copyright (C) NGINX, Inc. + */ + + +#include <python/nxt_python.h> + +#if (NXT_HAVE_ASGI) + +#include <nxt_main.h> +#include <nxt_unit.h> +#include <nxt_unit_request.h> +#include <nxt_unit_response.h> +#include <python/nxt_python_asgi.h> +#include <python/nxt_python_asgi_str.h> + + +static void nxt_py_asgi_request_handler(nxt_unit_request_info_t *req); + +static PyObject *nxt_py_asgi_create_http_scope(nxt_unit_request_info_t *req); +static PyObject *nxt_py_asgi_create_address(nxt_unit_sptr_t *sptr, uint8_t len, + uint16_t port); +static PyObject *nxt_py_asgi_create_header(nxt_unit_field_t *f); +static PyObject *nxt_py_asgi_create_subprotocols(nxt_unit_field_t *f); + +static int nxt_py_asgi_add_port(nxt_unit_ctx_t *ctx, nxt_unit_port_t *port); +static void nxt_py_asgi_remove_port(nxt_unit_t *lib, nxt_unit_port_t *port); +static void nxt_py_asgi_quit(nxt_unit_ctx_t *ctx); +static void nxt_py_asgi_shm_ack_handler(nxt_unit_ctx_t *ctx); + +static PyObject *nxt_py_asgi_port_read(PyObject *self, PyObject *args); + + +PyObject *nxt_py_loop_run_until_complete; +PyObject *nxt_py_loop_create_future; +PyObject *nxt_py_loop_create_task; + +nxt_queue_t nxt_py_asgi_drain_queue; + +static PyObject *nxt_py_loop_call_soon; +static PyObject *nxt_py_quit_future; +static PyObject *nxt_py_quit_future_set_result; +static PyObject *nxt_py_loop_add_reader; +static PyObject *nxt_py_loop_remove_reader; +static PyObject *nxt_py_port_read; + +static PyMethodDef nxt_py_port_read_method = + {"unit_port_read", nxt_py_asgi_port_read, METH_VARARGS, ""}; + +#define NXT_UNIT_HASH_WS_PROTOCOL 0xED0A + + +int +nxt_python_asgi_check(PyObject *obj) +{ + int res; + PyObject *call; + PyCodeObject *code; + + if (PyFunction_Check(obj)) { + code = (PyCodeObject *) PyFunction_GET_CODE(obj); + + return (code->co_flags & CO_COROUTINE) != 0; + } + + if (PyMethod_Check(obj)) { + obj = PyMethod_GET_FUNCTION(obj); + + code = (PyCodeObject *) PyFunction_GET_CODE(obj); + + return (code->co_flags & CO_COROUTINE) != 0; + } + + call = PyObject_GetAttrString(obj, "__call__"); + + if (call == NULL) { + return 0; + } + + if (PyFunction_Check(call)) { + code = (PyCodeObject *) PyFunction_GET_CODE(call); + + res = (code->co_flags & CO_COROUTINE) != 0; + + } else { + if (PyMethod_Check(call)) { + obj = PyMethod_GET_FUNCTION(call); + + code = (PyCodeObject *) PyFunction_GET_CODE(obj); + + res = (code->co_flags & CO_COROUTINE) != 0; + + } else { + res = 0; + } + } + + Py_DECREF(call); + + return res; +} + + +nxt_int_t +nxt_python_asgi_init(nxt_task_t *task, nxt_unit_init_t *init) +{ + PyObject *asyncio, *loop, *get_event_loop; + nxt_int_t rc; + + nxt_debug(task, "asgi_init"); + + if (nxt_slow_path(nxt_py_asgi_str_init() != NXT_OK)) { + nxt_alert(task, "Python failed to init string objects"); + return NXT_ERROR; + } + + asyncio = PyImport_ImportModule("asyncio"); + if (nxt_slow_path(asyncio == NULL)) { + nxt_alert(task, "Python failed to import module 'asyncio'"); + nxt_python_print_exception(); + return NXT_ERROR; + } + + loop = NULL; + get_event_loop = PyDict_GetItemString(PyModule_GetDict(asyncio), + "get_event_loop"); + if (nxt_slow_path(get_event_loop == NULL)) { + nxt_alert(task, + "Python failed to get 'get_event_loop' from module 'asyncio'"); + goto fail; + } + + if (nxt_slow_path(PyCallable_Check(get_event_loop) == 0)) { + nxt_alert(task, "'asyncio.get_event_loop' is not a callable object"); + goto fail; + } + + loop = PyObject_CallObject(get_event_loop, NULL); + if (nxt_slow_path(loop == NULL)) { + nxt_alert(task, "Python failed to call 'asyncio.get_event_loop'"); + goto fail; + } + + nxt_py_loop_create_task = PyObject_GetAttrString(loop, "create_task"); + if (nxt_slow_path(nxt_py_loop_create_task == NULL)) { + nxt_alert(task, "Python failed to get 'loop.create_task'"); + goto fail; + } + + if (nxt_slow_path(PyCallable_Check(nxt_py_loop_create_task) == 0)) { + nxt_alert(task, "'loop.create_task' is not a callable object"); + goto fail; + } + + nxt_py_loop_add_reader = PyObject_GetAttrString(loop, "add_reader"); + if (nxt_slow_path(nxt_py_loop_add_reader == NULL)) { + nxt_alert(task, "Python failed to get 'loop.add_reader'"); + goto fail; + } + + if (nxt_slow_path(PyCallable_Check(nxt_py_loop_add_reader) == 0)) { + nxt_alert(task, "'loop.add_reader' is not a callable object"); + goto fail; + } + + nxt_py_loop_remove_reader = PyObject_GetAttrString(loop, "remove_reader"); + if (nxt_slow_path(nxt_py_loop_remove_reader == NULL)) { + nxt_alert(task, "Python failed to get 'loop.remove_reader'"); + goto fail; + } + + if (nxt_slow_path(PyCallable_Check(nxt_py_loop_remove_reader) == 0)) { + nxt_alert(task, "'loop.remove_reader' is not a callable object"); + goto fail; + } + + nxt_py_loop_call_soon = PyObject_GetAttrString(loop, "call_soon"); + if (nxt_slow_path(nxt_py_loop_call_soon == NULL)) { + nxt_alert(task, "Python failed to get 'loop.call_soon'"); + goto fail; + } + + if (nxt_slow_path(PyCallable_Check(nxt_py_loop_call_soon) == 0)) { + nxt_alert(task, "'loop.call_soon' is not a callable object"); + goto fail; + } + + nxt_py_loop_run_until_complete = PyObject_GetAttrString(loop, + "run_until_complete"); + if (nxt_slow_path(nxt_py_loop_run_until_complete == NULL)) { + nxt_alert(task, "Python failed to get 'loop.run_until_complete'"); + goto fail; + } + + if (nxt_slow_path(PyCallable_Check(nxt_py_loop_run_until_complete) == 0)) { + nxt_alert(task, "'loop.run_until_complete' is not a callable object"); + goto fail; + } + + nxt_py_loop_create_future = PyObject_GetAttrString(loop, "create_future"); + if (nxt_slow_path(nxt_py_loop_create_future == NULL)) { + nxt_alert(task, "Python failed to get 'loop.create_future'"); + goto fail; + } + + if (nxt_slow_path(PyCallable_Check(nxt_py_loop_create_future) == 0)) { + nxt_alert(task, "'loop.create_future' is not a callable object"); + goto fail; + } + + nxt_py_quit_future = PyObject_CallObject(nxt_py_loop_create_future, NULL); + if (nxt_slow_path(nxt_py_quit_future == NULL)) { + nxt_alert(task, "Python failed to create Future "); + nxt_python_print_exception(); + goto fail; + } + + nxt_py_quit_future_set_result = PyObject_GetAttrString(nxt_py_quit_future, + "set_result"); + if (nxt_slow_path(nxt_py_quit_future_set_result == NULL)) { + nxt_alert(task, "Python failed to get 'future.set_result'"); + goto fail; + } + + if (nxt_slow_path(PyCallable_Check(nxt_py_quit_future_set_result) == 0)) { + nxt_alert(task, "'future.set_result' is not a callable object"); + goto fail; + } + + nxt_py_port_read = PyCFunction_New(&nxt_py_port_read_method, NULL); + if (nxt_slow_path(nxt_py_port_read == NULL)) { + nxt_alert(task, "Python failed to initialize the 'port_read' function"); + goto fail; + } + + nxt_queue_init(&nxt_py_asgi_drain_queue); + + if (nxt_slow_path(nxt_py_asgi_http_init(task) == NXT_ERROR)) { + goto fail; + } + + if (nxt_slow_path(nxt_py_asgi_websocket_init(task) == NXT_ERROR)) { + goto fail; + } + + rc = nxt_py_asgi_lifespan_startup(task); + if (nxt_slow_path(rc == NXT_ERROR)) { + goto fail; + } + + init->callbacks.request_handler = nxt_py_asgi_request_handler; + init->callbacks.data_handler = nxt_py_asgi_http_data_handler; + init->callbacks.websocket_handler = nxt_py_asgi_websocket_handler; + init->callbacks.close_handler = nxt_py_asgi_websocket_close_handler; + init->callbacks.quit = nxt_py_asgi_quit; + init->callbacks.shm_ack_handler = nxt_py_asgi_shm_ack_handler; + init->callbacks.add_port = nxt_py_asgi_add_port; + init->callbacks.remove_port = nxt_py_asgi_remove_port; + + Py_DECREF(loop); + Py_DECREF(asyncio); + + return NXT_OK; + +fail: + + Py_XDECREF(loop); + Py_DECREF(asyncio); + + return NXT_ERROR; +} + + +nxt_int_t +nxt_python_asgi_run(nxt_unit_ctx_t *ctx) +{ + PyObject *res; + + res = PyObject_CallFunctionObjArgs(nxt_py_loop_run_until_complete, + nxt_py_quit_future, NULL); + if (nxt_slow_path(res == NULL)) { + nxt_unit_alert(ctx, "Python failed to call loop.run_until_complete"); + nxt_python_print_exception(); + + return NXT_ERROR; + } + + Py_DECREF(res); + + nxt_py_asgi_lifespan_shutdown(); + + return NXT_OK; +} + + +static void +nxt_py_asgi_request_handler(nxt_unit_request_info_t *req) +{ + PyObject *scope, *res, *task, *receive, *send, *done, *asgi; + + if (req->request->websocket_handshake) { + asgi = nxt_py_asgi_websocket_create(req); + + } else { + asgi = nxt_py_asgi_http_create(req); + } + + if (nxt_slow_path(asgi == NULL)) { + nxt_unit_req_alert(req, "Python failed to create asgi object"); + nxt_unit_request_done(req, NXT_UNIT_ERROR); + + return; + } + + receive = PyObject_GetAttrString(asgi, "receive"); + if (nxt_slow_path(receive == NULL)) { + nxt_unit_req_alert(req, "Python failed to get 'receive' method"); + nxt_unit_request_done(req, NXT_UNIT_ERROR); + + goto release_asgi; + } + + send = PyObject_GetAttrString(asgi, "send"); + if (nxt_slow_path(receive == NULL)) { + nxt_unit_req_alert(req, "Python failed to get 'send' method"); + nxt_unit_request_done(req, NXT_UNIT_ERROR); + + goto release_receive; + } + + done = PyObject_GetAttrString(asgi, "_done"); + if (nxt_slow_path(receive == NULL)) { + nxt_unit_req_alert(req, "Python failed to get '_done' method"); + nxt_unit_request_done(req, NXT_UNIT_ERROR); + + goto release_send; + } + + scope = nxt_py_asgi_create_http_scope(req); + if (nxt_slow_path(scope == NULL)) { + nxt_unit_request_done(req, NXT_UNIT_ERROR); + + goto release_done; + } + + req->data = asgi; + + res = PyObject_CallFunctionObjArgs(nxt_py_application, + scope, receive, send, NULL); + if (nxt_slow_path(res == NULL)) { + nxt_unit_req_error(req, "Python failed to call the application"); + nxt_python_print_exception(); + nxt_unit_request_done(req, NXT_UNIT_ERROR); + + goto release_scope; + } + + if (nxt_slow_path(!PyCoro_CheckExact(res))) { + nxt_unit_req_error(req, "Application result type is not a coroutine"); + nxt_unit_request_done(req, NXT_UNIT_ERROR); + + Py_DECREF(res); + + goto release_scope; + } + + task = PyObject_CallFunctionObjArgs(nxt_py_loop_create_task, res, NULL); + if (nxt_slow_path(task == NULL)) { + nxt_unit_req_error(req, "Python failed to call the create_task"); + nxt_python_print_exception(); + nxt_unit_request_done(req, NXT_UNIT_ERROR); + + Py_DECREF(res); + + goto release_scope; + } + + Py_DECREF(res); + + res = PyObject_CallMethodObjArgs(task, nxt_py_add_done_callback_str, done, + NULL); + if (nxt_slow_path(res == NULL)) { + nxt_unit_req_error(req, + "Python failed to call 'task.add_done_callback'"); + nxt_python_print_exception(); + nxt_unit_request_done(req, NXT_UNIT_ERROR); + + goto release_task; + } + + Py_DECREF(res); +release_task: + Py_DECREF(task); +release_scope: + Py_DECREF(scope); +release_done: + Py_DECREF(done); +release_send: + Py_DECREF(send); +release_receive: + Py_DECREF(receive); +release_asgi: + Py_DECREF(asgi); +} + + +static PyObject * +nxt_py_asgi_create_http_scope(nxt_unit_request_info_t *req) +{ + char *p, *target, *query; + uint32_t target_length, i; + PyObject *scope, *v, *type, *scheme; + PyObject *headers, *header; + nxt_unit_field_t *f; + nxt_unit_request_t *r; + + static const nxt_str_t ws_protocol = nxt_string("sec-websocket-protocol"); + +#define SET_ITEM(dict, key, value) \ + if (nxt_slow_path(PyDict_SetItem(dict, nxt_py_ ## key ## _str, value) \ + == -1)) \ + { \ + nxt_unit_req_alert(req, "Python failed to set '" \ + #dict "." #key "' item"); \ + goto fail; \ + } + + v = NULL; + headers = NULL; + + r = req->request; + + if (r->websocket_handshake) { + type = nxt_py_websocket_str; + scheme = r->tls ? nxt_py_wss_str : nxt_py_ws_str; + + } else { + type = nxt_py_http_str; + scheme = r->tls ? nxt_py_https_str : nxt_py_http_str; + } + + scope = nxt_py_asgi_new_scope(req, type, nxt_py_2_1_str); + if (nxt_slow_path(scope == NULL)) { + return NULL; + } + + p = nxt_unit_sptr_get(&r->version); + SET_ITEM(scope, http_version, p[7] == '1' ? nxt_py_1_1_str + : nxt_py_1_0_str) + SET_ITEM(scope, scheme, scheme) + + v = PyString_FromStringAndSize(nxt_unit_sptr_get(&r->method), + r->method_length); + if (nxt_slow_path(v == NULL)) { + nxt_unit_req_alert(req, "Python failed to create 'method' string"); + goto fail; + } + + SET_ITEM(scope, method, v) + Py_DECREF(v); + + v = PyUnicode_DecodeUTF8(nxt_unit_sptr_get(&r->path), r->path_length, + "replace"); + if (nxt_slow_path(v == NULL)) { + nxt_unit_req_alert(req, "Python failed to create 'path' string"); + goto fail; + } + + SET_ITEM(scope, path, v) + Py_DECREF(v); + + target = nxt_unit_sptr_get(&r->target); + query = nxt_unit_sptr_get(&r->query); + + if (r->query.offset != 0) { + target_length = query - target - 1; + + } else { + target_length = r->target_length; + } + + v = PyBytes_FromStringAndSize(target, target_length); + if (nxt_slow_path(v == NULL)) { + nxt_unit_req_alert(req, "Python failed to create 'raw_path' string"); + goto fail; + } + + SET_ITEM(scope, raw_path, v) + Py_DECREF(v); + + v = PyBytes_FromStringAndSize(query, r->query_length); + if (nxt_slow_path(v == NULL)) { + nxt_unit_req_alert(req, "Python failed to create 'query' string"); + goto fail; + } + + SET_ITEM(scope, query_string, v) + Py_DECREF(v); + + v = nxt_py_asgi_create_address(&r->remote, r->remote_length, 0); + if (nxt_slow_path(v == NULL)) { + nxt_unit_req_alert(req, "Python failed to create 'client' pair"); + goto fail; + } + + SET_ITEM(scope, client, v) + Py_DECREF(v); + + v = nxt_py_asgi_create_address(&r->local, r->local_length, 80); + if (nxt_slow_path(v == NULL)) { + nxt_unit_req_alert(req, "Python failed to create 'server' pair"); + goto fail; + } + + SET_ITEM(scope, server, v) + Py_DECREF(v); + + v = NULL; + + headers = PyTuple_New(r->fields_count); + if (nxt_slow_path(headers == NULL)) { + nxt_unit_req_alert(req, "Python failed to create 'headers' object"); + goto fail; + } + + for (i = 0; i < r->fields_count; i++) { + f = r->fields + i; + + header = nxt_py_asgi_create_header(f); + if (nxt_slow_path(header == NULL)) { + nxt_unit_req_alert(req, "Python failed to create 'header' pair"); + goto fail; + } + + PyTuple_SET_ITEM(headers, i, header); + + if (f->hash == NXT_UNIT_HASH_WS_PROTOCOL + && f->name_length == ws_protocol.length + && f->value_length > 0 + && r->websocket_handshake) + { + v = nxt_py_asgi_create_subprotocols(f); + if (nxt_slow_path(v == NULL)) { + nxt_unit_req_alert(req, "Failed to create subprotocols"); + goto fail; + } + + SET_ITEM(scope, subprotocols, v); + Py_DECREF(v); + } + } + + SET_ITEM(scope, headers, headers) + Py_DECREF(headers); + + return scope; + +fail: + + Py_XDECREF(v); + Py_XDECREF(headers); + Py_DECREF(scope); + + return NULL; + +#undef SET_ITEM +} + + +static PyObject * +nxt_py_asgi_create_address(nxt_unit_sptr_t *sptr, uint8_t len, uint16_t port) +{ + char *p, *s; + PyObject *pair, *v; + + pair = PyTuple_New(2); + if (nxt_slow_path(pair == NULL)) { + return NULL; + } + + p = nxt_unit_sptr_get(sptr); + s = memchr(p, ':', len); + + v = PyString_FromStringAndSize(p, s == NULL ? len : s - p); + if (nxt_slow_path(v == NULL)) { + Py_DECREF(pair); + + return NULL; + } + + PyTuple_SET_ITEM(pair, 0, v); + + if (s != NULL) { + p += len; + v = PyLong_FromString(s + 1, &p, 10); + + } else { + v = PyLong_FromLong(port); + } + + if (nxt_slow_path(v == NULL)) { + Py_DECREF(pair); + + return NULL; + } + + PyTuple_SET_ITEM(pair, 1, v); + + return pair; +} + + +static PyObject * +nxt_py_asgi_create_header(nxt_unit_field_t *f) +{ + char c, *name; + uint8_t pos; + PyObject *header, *v; + + header = PyTuple_New(2); + if (nxt_slow_path(header == NULL)) { + return NULL; + } + + name = nxt_unit_sptr_get(&f->name); + + for (pos = 0; pos < f->name_length; pos++) { + c = name[pos]; + if (c >= 'A' && c <= 'Z') { + name[pos] = (c | 0x20); + } + } + + v = PyBytes_FromStringAndSize(name, f->name_length); + if (nxt_slow_path(v == NULL)) { + Py_DECREF(header); + + return NULL; + } + + PyTuple_SET_ITEM(header, 0, v); + + v = PyBytes_FromStringAndSize(nxt_unit_sptr_get(&f->value), + f->value_length); + if (nxt_slow_path(v == NULL)) { + Py_DECREF(header); + + return NULL; + } + + PyTuple_SET_ITEM(header, 1, v); + + return header; +} + + +static PyObject * +nxt_py_asgi_create_subprotocols(nxt_unit_field_t *f) +{ + char *v; + uint32_t i, n, start; + PyObject *res, *proto; + + v = nxt_unit_sptr_get(&f->value); + n = 1; + + for (i = 0; i < f->value_length; i++) { + if (v[i] == ',') { + n++; + } + } + + res = PyTuple_New(n); + if (nxt_slow_path(res == NULL)) { + return NULL; + } + + n = 0; + start = 0; + + for (i = 0; i < f->value_length; ) { + if (v[i] != ',') { + i++; + + continue; + } + + if (i - start > 0) { + proto = PyString_FromStringAndSize(v + start, i - start); + if (nxt_slow_path(proto == NULL)) { + goto fail; + } + + PyTuple_SET_ITEM(res, n, proto); + + n++; + } + + do { + i++; + } while (i < f->value_length && v[i] == ' '); + + start = i; + } + + if (i - start > 0) { + proto = PyString_FromStringAndSize(v + start, i - start); + if (nxt_slow_path(proto == NULL)) { + goto fail; + } + + PyTuple_SET_ITEM(res, n, proto); + } + + return res; + +fail: + + Py_DECREF(res); + + return NULL; +} + + +static int +nxt_py_asgi_add_port(nxt_unit_ctx_t *ctx, nxt_unit_port_t *port) +{ + int nb; + PyObject *res; + + if (port->in_fd == -1) { + return NXT_UNIT_OK; + } + + nb = 1; + + if (nxt_slow_path(ioctl(port->in_fd, FIONBIO, &nb) == -1)) { + nxt_unit_alert(ctx, "ioctl(%d, FIONBIO, 0) failed: %s (%d)", + port->in_fd, strerror(errno), errno); + + return NXT_UNIT_ERROR; + } + + nxt_unit_debug(ctx, "asgi_add_port %d %p %p", port->in_fd, ctx, port); + + res = PyObject_CallFunctionObjArgs(nxt_py_loop_add_reader, + PyLong_FromLong(port->in_fd), + nxt_py_port_read, + PyLong_FromVoidPtr(ctx), + PyLong_FromVoidPtr(port), NULL); + if (nxt_slow_path(res == NULL)) { + nxt_unit_alert(ctx, "Python failed to add_reader"); + + return NXT_UNIT_ERROR; + } + + Py_DECREF(res); + + return NXT_UNIT_OK; +} + + +static void +nxt_py_asgi_remove_port(nxt_unit_t *lib, nxt_unit_port_t *port) +{ + PyObject *res; + + nxt_unit_debug(NULL, "asgi_remove_port %d %p", port->in_fd, port); + + if (port->in_fd == -1) { + return; + } + + res = PyObject_CallFunctionObjArgs(nxt_py_loop_remove_reader, + PyLong_FromLong(port->in_fd), NULL); + if (nxt_slow_path(res == NULL)) { + nxt_unit_alert(NULL, "Python failed to remove_reader"); + } + + Py_DECREF(res); +} + + +static void +nxt_py_asgi_quit(nxt_unit_ctx_t *ctx) +{ + PyObject *res; + + nxt_unit_debug(ctx, "asgi_quit %p", ctx); + + res = PyObject_CallFunctionObjArgs(nxt_py_quit_future_set_result, + PyLong_FromLong(0), NULL); + if (nxt_slow_path(res == NULL)) { + nxt_unit_alert(ctx, "Python failed to set_result"); + } + + Py_DECREF(res); +} + + +static void +nxt_py_asgi_shm_ack_handler(nxt_unit_ctx_t *ctx) +{ + int rc; + nxt_queue_link_t *lnk; + + while (!nxt_queue_is_empty(&nxt_py_asgi_drain_queue)) { + lnk = nxt_queue_first(&nxt_py_asgi_drain_queue); + + rc = nxt_py_asgi_http_drain(lnk); + if (rc == NXT_UNIT_AGAIN) { + break; + } + + nxt_queue_remove(lnk); + } +} + + +static PyObject * +nxt_py_asgi_port_read(PyObject *self, PyObject *args) +{ + int rc; + PyObject *arg; + Py_ssize_t n; + nxt_unit_ctx_t *ctx; + nxt_unit_port_t *port; + + n = PyTuple_GET_SIZE(args); + + if (n != 2) { + nxt_unit_alert(NULL, + "nxt_py_asgi_port_read: invalid number of arguments %d", + (int) n); + + return PyErr_Format(PyExc_TypeError, "invalid number of arguments"); + } + + arg = PyTuple_GET_ITEM(args, 0); + if (nxt_slow_path(arg == NULL || PyLong_Check(arg) == 0)) { + return PyErr_Format(PyExc_TypeError, + "the first argument is not a long"); + } + + ctx = PyLong_AsVoidPtr(arg); + + arg = PyTuple_GET_ITEM(args, 1); + if (nxt_slow_path(arg == NULL || PyLong_Check(arg) == 0)) { + return PyErr_Format(PyExc_TypeError, + "the second argument is not a long"); + } + + port = PyLong_AsVoidPtr(arg); + + nxt_unit_debug(ctx, "asgi_port_read %p %p", ctx, port); + + rc = nxt_unit_process_port_msg(ctx, port); + + if (nxt_slow_path(rc == NXT_UNIT_ERROR)) { + return PyErr_Format(PyExc_RuntimeError, + "error processing port message"); + } + + Py_RETURN_NONE; +} + + +PyObject * +nxt_py_asgi_enum_headers(PyObject *headers, nxt_py_asgi_enum_header_cb cb, + void *data) +{ + int i; + PyObject *iter, *header, *h_iter, *name, *val, *res; + + iter = PyObject_GetIter(headers); + if (nxt_slow_path(iter == NULL)) { + return PyErr_Format(PyExc_TypeError, "'headers' is not an iterable"); + } + + for (i = 0; /* void */; i++) { + header = PyIter_Next(iter); + if (header == NULL) { + break; + } + + h_iter = PyObject_GetIter(header); + if (nxt_slow_path(h_iter == NULL)) { + Py_DECREF(header); + Py_DECREF(iter); + + return PyErr_Format(PyExc_TypeError, + "'headers' item #%d is not an iterable", i); + } + + name = PyIter_Next(h_iter); + if (nxt_slow_path(name == NULL || !PyBytes_Check(name))) { + Py_XDECREF(name); + Py_DECREF(h_iter); + Py_DECREF(header); + Py_DECREF(iter); + + return PyErr_Format(PyExc_TypeError, + "'headers' item #%d 'name' is not a byte string", i); + } + + val = PyIter_Next(h_iter); + if (nxt_slow_path(val == NULL || !PyBytes_Check(val))) { + Py_XDECREF(val); + Py_DECREF(h_iter); + Py_DECREF(header); + Py_DECREF(iter); + + return PyErr_Format(PyExc_TypeError, + "'headers' item #%d 'value' is not a byte string", i); + } + + res = cb(data, i, name, val); + + Py_DECREF(name); + Py_DECREF(val); + Py_DECREF(h_iter); + Py_DECREF(header); + + if (nxt_slow_path(res == NULL)) { + Py_DECREF(iter); + + return NULL; + } + + Py_DECREF(res); + } + + Py_DECREF(iter); + + Py_RETURN_NONE; +} + + +PyObject * +nxt_py_asgi_calc_size(void *data, int i, PyObject *name, PyObject *val) +{ + nxt_py_asgi_calc_size_ctx_t *ctx; + + ctx = data; + + ctx->fields_count++; + ctx->fields_size += PyBytes_GET_SIZE(name) + PyBytes_GET_SIZE(val); + + Py_RETURN_NONE; +} + + +PyObject * +nxt_py_asgi_add_field(void *data, int i, PyObject *name, PyObject *val) +{ + int rc; + char *name_str, *val_str; + uint32_t name_len, val_len; + nxt_off_t content_length; + nxt_unit_request_info_t *req; + nxt_py_asgi_add_field_ctx_t *ctx; + + name_str = PyBytes_AS_STRING(name); + name_len = PyBytes_GET_SIZE(name); + + val_str = PyBytes_AS_STRING(val); + val_len = PyBytes_GET_SIZE(val); + + ctx = data; + req = ctx->req; + + rc = nxt_unit_response_add_field(req, name_str, name_len, + val_str, val_len); + if (nxt_slow_path(rc != NXT_UNIT_OK)) { + return PyErr_Format(PyExc_RuntimeError, + "failed to add header #%d", i); + } + + if (req->response->fields[i].hash == NXT_UNIT_HASH_CONTENT_LENGTH) { + content_length = nxt_off_t_parse((u_char *) val_str, val_len); + if (nxt_slow_path(content_length < 0)) { + nxt_unit_req_error(req, "failed to parse Content-Length " + "value %.*s", (int) val_len, val_str); + + return PyErr_Format(PyExc_ValueError, + "Failed to parse Content-Length: '%.*s'", + (int) val_len, val_str); + } + + ctx->content_length = content_length; + } + + Py_RETURN_NONE; +} + + +PyObject * +nxt_py_asgi_set_result_soon(nxt_unit_request_info_t *req, PyObject *future, + PyObject *result) +{ + PyObject *set_result, *res; + + if (nxt_slow_path(result == NULL)) { + Py_DECREF(future); + + return NULL; + } + + set_result = PyObject_GetAttrString(future, "set_result"); + if (nxt_slow_path(set_result == NULL)) { + nxt_unit_req_alert(req, "failed to get 'set_result' for future"); + + Py_CLEAR(future); + + goto cleanup; + } + + if (nxt_slow_path(PyCallable_Check(set_result) == 0)) { + nxt_unit_req_alert(req, "'future.set_result' is not a callable"); + + Py_CLEAR(future); + + goto cleanup; + } + + res = PyObject_CallFunctionObjArgs(nxt_py_loop_call_soon, set_result, + result, NULL); + if (nxt_slow_path(res == NULL)) { + nxt_unit_req_alert(req, "Python failed to call 'loop.call_soon'"); + nxt_python_print_exception(); + + Py_CLEAR(future); + } + + Py_XDECREF(res); + +cleanup: + + Py_DECREF(set_result); + Py_DECREF(result); + + return future; +} + + +PyObject * +nxt_py_asgi_new_msg(nxt_unit_request_info_t *req, PyObject *type) +{ + PyObject *msg; + + msg = PyDict_New(); + if (nxt_slow_path(msg == NULL)) { + nxt_unit_req_alert(req, "Python failed to create message dict"); + nxt_python_print_exception(); + + return PyErr_Format(PyExc_RuntimeError, + "failed to create message dict"); + } + + if (nxt_slow_path(PyDict_SetItem(msg, nxt_py_type_str, type) == -1)) { + nxt_unit_req_alert(req, "Python failed to set 'msg.type' item"); + + Py_DECREF(msg); + + return PyErr_Format(PyExc_RuntimeError, + "failed to set 'msg.type' item"); + } + + return msg; +} + + +PyObject * +nxt_py_asgi_new_scope(nxt_unit_request_info_t *req, PyObject *type, + PyObject *spec_version) +{ + PyObject *scope, *asgi; + + scope = PyDict_New(); + if (nxt_slow_path(scope == NULL)) { + nxt_unit_req_alert(req, "Python failed to create 'scope' dict"); + nxt_python_print_exception(); + + return PyErr_Format(PyExc_RuntimeError, + "failed to create 'scope' dict"); + } + + if (nxt_slow_path(PyDict_SetItem(scope, nxt_py_type_str, type) == -1)) { + nxt_unit_req_alert(req, "Python failed to set 'scope.type' item"); + + Py_DECREF(scope); + + return PyErr_Format(PyExc_RuntimeError, + "failed to set 'scope.type' item"); + } + + asgi = PyDict_New(); + if (nxt_slow_path(asgi == NULL)) { + nxt_unit_req_alert(req, "Python failed to create 'asgi' dict"); + nxt_python_print_exception(); + + Py_DECREF(scope); + + return PyErr_Format(PyExc_RuntimeError, + "failed to create 'asgi' dict"); + } + + if (nxt_slow_path(PyDict_SetItem(scope, nxt_py_asgi_str, asgi) == -1)) { + nxt_unit_req_alert(req, "Python failed to set 'scope.asgi' item"); + + Py_DECREF(asgi); + Py_DECREF(scope); + + return PyErr_Format(PyExc_RuntimeError, + "failed to set 'scope.asgi' item"); + } + + if (nxt_slow_path(PyDict_SetItem(asgi, nxt_py_version_str, + nxt_py_3_0_str) == -1)) + { + nxt_unit_req_alert(req, "Python failed to set 'asgi.version' item"); + + Py_DECREF(asgi); + Py_DECREF(scope); + + return PyErr_Format(PyExc_RuntimeError, + "failed to set 'asgi.version' item"); + } + + if (nxt_slow_path(PyDict_SetItem(asgi, nxt_py_spec_version_str, + spec_version) == -1)) + { + nxt_unit_req_alert(req, + "Python failed to set 'asgi.spec_version' item"); + + Py_DECREF(asgi); + Py_DECREF(scope); + + return PyErr_Format(PyExc_RuntimeError, + "failed to set 'asgi.spec_version' item"); + } + + Py_DECREF(asgi); + + return scope; +} + + +void +nxt_py_asgi_dealloc(PyObject *self) +{ + PyObject_Del(self); +} + + +PyObject * +nxt_py_asgi_await(PyObject *self) +{ + Py_INCREF(self); + return self; +} + + +PyObject * +nxt_py_asgi_iter(PyObject *self) +{ + Py_INCREF(self); + return self; +} + + +PyObject * +nxt_py_asgi_next(PyObject *self) +{ + return NULL; +} + + +void +nxt_python_asgi_done(void) +{ + nxt_py_asgi_str_done(); + + Py_XDECREF(nxt_py_quit_future); + Py_XDECREF(nxt_py_quit_future_set_result); + Py_XDECREF(nxt_py_loop_run_until_complete); + Py_XDECREF(nxt_py_loop_create_future); + Py_XDECREF(nxt_py_loop_create_task); + Py_XDECREF(nxt_py_loop_call_soon); + Py_XDECREF(nxt_py_loop_add_reader); + Py_XDECREF(nxt_py_loop_remove_reader); + Py_XDECREF(nxt_py_port_read); +} + +#else /* !(NXT_HAVE_ASGI) */ + + +int +nxt_python_asgi_check(PyObject *obj) +{ + return 0; +} + + +nxt_int_t +nxt_python_asgi_init(nxt_task_t *task, nxt_unit_init_t *init) +{ + nxt_alert(task, "ASGI not implemented"); + return NXT_ERROR; +} + + +nxt_int_t +nxt_python_asgi_run(nxt_unit_ctx_t *ctx) +{ + nxt_unit_alert(ctx, "ASGI not implemented"); + return NXT_ERROR; +} + + +void +nxt_python_asgi_done(void) +{ +} + +#endif /* NXT_HAVE_ASGI */ diff --git a/src/python/nxt_python_asgi.h b/src/python/nxt_python_asgi.h new file mode 100644 index 00000000..24337c37 --- /dev/null +++ b/src/python/nxt_python_asgi.h @@ -0,0 +1,60 @@ + +/* + * Copyright (C) NGINX, Inc. + */ + +#ifndef _NXT_PYTHON_ASGI_H_INCLUDED_ +#define _NXT_PYTHON_ASGI_H_INCLUDED_ + + +typedef PyObject * (*nxt_py_asgi_enum_header_cb)(void *ctx, int i, + PyObject *name, PyObject *val); + +typedef struct { + uint32_t fields_count; + uint32_t fields_size; +} nxt_py_asgi_calc_size_ctx_t; + +typedef struct { + nxt_unit_request_info_t *req; + uint64_t content_length; +} nxt_py_asgi_add_field_ctx_t; + +PyObject *nxt_py_asgi_enum_headers(PyObject *headers, + nxt_py_asgi_enum_header_cb cb, void *data); + +PyObject *nxt_py_asgi_calc_size(void *data, int i, PyObject *n, PyObject *v); +PyObject *nxt_py_asgi_add_field(void *data, int i, PyObject *n, PyObject *v); + +PyObject *nxt_py_asgi_set_result_soon(nxt_unit_request_info_t *req, + PyObject *future, PyObject *result); +PyObject *nxt_py_asgi_new_msg(nxt_unit_request_info_t *req, PyObject *type); +PyObject *nxt_py_asgi_new_scope(nxt_unit_request_info_t *req, PyObject *type, + PyObject *spec_version); + +void nxt_py_asgi_dealloc(PyObject *self); +PyObject *nxt_py_asgi_await(PyObject *self); +PyObject *nxt_py_asgi_iter(PyObject *self); +PyObject *nxt_py_asgi_next(PyObject *self); + +nxt_int_t nxt_py_asgi_http_init(nxt_task_t *task); +PyObject *nxt_py_asgi_http_create(nxt_unit_request_info_t *req); +void nxt_py_asgi_http_data_handler(nxt_unit_request_info_t *req); +int nxt_py_asgi_http_drain(nxt_queue_link_t *lnk); + +nxt_int_t nxt_py_asgi_websocket_init(nxt_task_t *task); +PyObject *nxt_py_asgi_websocket_create(nxt_unit_request_info_t *req); +void nxt_py_asgi_websocket_handler(nxt_unit_websocket_frame_t *ws); +void nxt_py_asgi_websocket_close_handler(nxt_unit_request_info_t *req); + +nxt_int_t nxt_py_asgi_lifespan_startup(nxt_task_t *task); +nxt_int_t nxt_py_asgi_lifespan_shutdown(void); + +extern PyObject *nxt_py_loop_run_until_complete; +extern PyObject *nxt_py_loop_create_future; +extern PyObject *nxt_py_loop_create_task; + +extern nxt_queue_t nxt_py_asgi_drain_queue; + + +#endif /* _NXT_PYTHON_ASGI_H_INCLUDED_ */ diff --git a/src/python/nxt_python_asgi_http.c b/src/python/nxt_python_asgi_http.c new file mode 100644 index 00000000..b07d61d6 --- /dev/null +++ b/src/python/nxt_python_asgi_http.c @@ -0,0 +1,591 @@ + +/* + * Copyright (C) NGINX, Inc. + */ + + +#include <python/nxt_python.h> + +#if (NXT_HAVE_ASGI) + +#include <nxt_main.h> +#include <nxt_unit.h> +#include <nxt_unit_request.h> +#include <python/nxt_python_asgi.h> +#include <python/nxt_python_asgi_str.h> + + +typedef struct { + PyObject_HEAD + nxt_unit_request_info_t *req; + nxt_queue_link_t link; + PyObject *receive_future; + PyObject *send_future; + uint64_t content_length; + uint64_t bytes_sent; + int complete; + PyObject *send_body; + Py_ssize_t send_body_off; +} nxt_py_asgi_http_t; + + +static PyObject *nxt_py_asgi_http_receive(PyObject *self, PyObject *none); +static PyObject *nxt_py_asgi_http_read_msg(nxt_py_asgi_http_t *http); +static PyObject *nxt_py_asgi_http_send(PyObject *self, PyObject *dict); +static PyObject *nxt_py_asgi_http_response_start(nxt_py_asgi_http_t *http, + PyObject *dict); +static PyObject *nxt_py_asgi_http_response_body(nxt_py_asgi_http_t *http, + PyObject *dict); +static PyObject *nxt_py_asgi_http_done(PyObject *self, PyObject *future); + + +static PyMethodDef nxt_py_asgi_http_methods[] = { + { "receive", nxt_py_asgi_http_receive, METH_NOARGS, 0 }, + { "send", nxt_py_asgi_http_send, METH_O, 0 }, + { "_done", nxt_py_asgi_http_done, METH_O, 0 }, + { NULL, NULL, 0, 0 } +}; + +static PyAsyncMethods nxt_py_asgi_async_methods = { + .am_await = nxt_py_asgi_await, +}; + +static PyTypeObject nxt_py_asgi_http_type = { + PyVarObject_HEAD_INIT(NULL, 0) + + .tp_name = "unit._asgi_http", + .tp_basicsize = sizeof(nxt_py_asgi_http_t), + .tp_dealloc = nxt_py_asgi_dealloc, + .tp_as_async = &nxt_py_asgi_async_methods, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_doc = "unit ASGI HTTP request object", + .tp_iter = nxt_py_asgi_iter, + .tp_iternext = nxt_py_asgi_next, + .tp_methods = nxt_py_asgi_http_methods, +}; + +static Py_ssize_t nxt_py_asgi_http_body_buf_size = 32 * 1024 * 1024; + + +nxt_int_t +nxt_py_asgi_http_init(nxt_task_t *task) +{ + if (nxt_slow_path(PyType_Ready(&nxt_py_asgi_http_type) != 0)) { + nxt_alert(task, "Python failed to initialize the 'http' type object"); + return NXT_ERROR; + } + + return NXT_OK; +} + + +PyObject * +nxt_py_asgi_http_create(nxt_unit_request_info_t *req) +{ + nxt_py_asgi_http_t *http; + + http = PyObject_New(nxt_py_asgi_http_t, &nxt_py_asgi_http_type); + + if (nxt_fast_path(http != NULL)) { + http->req = req; + http->receive_future = NULL; + http->send_future = NULL; + http->content_length = -1; + http->bytes_sent = 0; + http->complete = 0; + http->send_body = NULL; + http->send_body_off = 0; + } + + return (PyObject *) http; +} + + +static PyObject * +nxt_py_asgi_http_receive(PyObject *self, PyObject *none) +{ + PyObject *msg, *future; + nxt_py_asgi_http_t *http; + nxt_unit_request_info_t *req; + + http = (nxt_py_asgi_http_t *) self; + req = http->req; + + nxt_unit_req_debug(req, "asgi_http_receive"); + + msg = nxt_py_asgi_http_read_msg(http); + if (nxt_slow_path(msg == NULL)) { + return NULL; + } + + future = PyObject_CallObject(nxt_py_loop_create_future, NULL); + if (nxt_slow_path(future == NULL)) { + nxt_unit_req_alert(req, "Python failed to create Future object"); + nxt_python_print_exception(); + + Py_DECREF(msg); + + return PyErr_Format(PyExc_RuntimeError, + "failed to create Future object"); + } + + if (msg != Py_None) { + return nxt_py_asgi_set_result_soon(req, future, msg); + } + + http->receive_future = future; + Py_INCREF(http->receive_future); + + Py_DECREF(msg); + + return future; +} + + +static PyObject * +nxt_py_asgi_http_read_msg(nxt_py_asgi_http_t *http) +{ + char *body_buf; + ssize_t read_res; + PyObject *msg, *body; + Py_ssize_t size; + nxt_unit_request_info_t *req; + + req = http->req; + + size = req->content_length; + + if (size > nxt_py_asgi_http_body_buf_size) { + size = nxt_py_asgi_http_body_buf_size; + } + + if (size > 0) { + body = PyBytes_FromStringAndSize(NULL, size); + if (nxt_slow_path(body == NULL)) { + nxt_unit_req_alert(req, "Python failed to create body byte string"); + nxt_python_print_exception(); + + return PyErr_Format(PyExc_RuntimeError, + "failed to create Bytes object"); + } + + body_buf = PyBytes_AS_STRING(body); + + read_res = nxt_unit_request_read(req, body_buf, size); + + } else { + body = NULL; + read_res = 0; + } + + if (read_res > 0 || read_res == size) { + msg = nxt_py_asgi_new_msg(req, nxt_py_http_request_str); + if (nxt_slow_path(msg == NULL)) { + Py_XDECREF(body); + + return NULL; + } + +#define SET_ITEM(dict, key, value) \ + if (nxt_slow_path(PyDict_SetItem(dict, nxt_py_ ## key ## _str, value) \ + == -1)) \ + { \ + nxt_unit_req_alert(req, \ + "Python failed to set '" #dict "." #key "' item"); \ + PyErr_SetString(PyExc_RuntimeError, \ + "Python failed to set '" #dict "." #key "' item"); \ + goto fail; \ + } + + if (body != NULL) { + SET_ITEM(msg, body, body) + } + + if (req->content_length > 0) { + SET_ITEM(msg, more_body, Py_True) + } + +#undef SET_ITEM + + Py_XDECREF(body); + + return msg; + } + + Py_XDECREF(body); + + Py_RETURN_NONE; + +fail: + + Py_DECREF(msg); + Py_XDECREF(body); + + return NULL; +} + + +static PyObject * +nxt_py_asgi_http_send(PyObject *self, PyObject *dict) +{ + PyObject *type; + const char *type_str; + Py_ssize_t type_len; + nxt_py_asgi_http_t *http; + + static const nxt_str_t response_start = nxt_string("http.response.start"); + static const nxt_str_t response_body = nxt_string("http.response.body"); + + http = (nxt_py_asgi_http_t *) self; + + type = PyDict_GetItem(dict, nxt_py_type_str); + if (nxt_slow_path(type == NULL || !PyUnicode_Check(type))) { + nxt_unit_req_error(http->req, "asgi_http_send: " + "'type' is not a unicode string"); + return PyErr_Format(PyExc_TypeError, "'type' is not a unicode string"); + } + + type_str = PyUnicode_AsUTF8AndSize(type, &type_len); + + nxt_unit_req_debug(http->req, "asgi_http_send type is '%.*s'", + (int) type_len, type_str); + + if (type_len == (Py_ssize_t) response_start.length + && memcmp(type_str, response_start.start, type_len) == 0) + { + return nxt_py_asgi_http_response_start(http, dict); + } + + if (type_len == (Py_ssize_t) response_body.length + && memcmp(type_str, response_body.start, type_len) == 0) + { + return nxt_py_asgi_http_response_body(http, dict); + } + + nxt_unit_req_error(http->req, "asgi_http_send: unexpected 'type': '%.*s'", + (int) type_len, type_str); + + return PyErr_Format(PyExc_AssertionError, "unexpected 'type': '%U'", type); +} + + +static PyObject * +nxt_py_asgi_http_response_start(nxt_py_asgi_http_t *http, PyObject *dict) +{ + int rc; + PyObject *status, *headers, *res; + nxt_py_asgi_calc_size_ctx_t calc_size_ctx; + nxt_py_asgi_add_field_ctx_t add_field_ctx; + + status = PyDict_GetItem(dict, nxt_py_status_str); + if (nxt_slow_path(status == NULL || !PyLong_Check(status))) { + nxt_unit_req_error(http->req, "asgi_http_response_start: " + "'status' is not an integer"); + return PyErr_Format(PyExc_TypeError, "'status' is not an integer"); + } + + calc_size_ctx.fields_size = 0; + calc_size_ctx.fields_count = 0; + + headers = PyDict_GetItem(dict, nxt_py_headers_str); + if (headers != NULL) { + res = nxt_py_asgi_enum_headers(headers, nxt_py_asgi_calc_size, + &calc_size_ctx); + if (nxt_slow_path(res == NULL)) { + return NULL; + } + + Py_DECREF(res); + } + + rc = nxt_unit_response_init(http->req, PyLong_AsLong(status), + calc_size_ctx.fields_count, + calc_size_ctx.fields_size); + if (nxt_slow_path(rc != NXT_UNIT_OK)) { + return PyErr_Format(PyExc_RuntimeError, + "failed to allocate response object"); + } + + add_field_ctx.req = http->req; + add_field_ctx.content_length = -1; + + if (headers != NULL) { + res = nxt_py_asgi_enum_headers(headers, nxt_py_asgi_add_field, + &add_field_ctx); + if (nxt_slow_path(res == NULL)) { + return NULL; + } + + Py_DECREF(res); + } + + http->content_length = add_field_ctx.content_length; + + Py_INCREF(http); + return (PyObject *) http; +} + + +static PyObject * +nxt_py_asgi_http_response_body(nxt_py_asgi_http_t *http, PyObject *dict) +{ + int rc; + char *body_str; + ssize_t sent; + PyObject *body, *more_body, *future; + Py_ssize_t body_len, body_off; + + body = PyDict_GetItem(dict, nxt_py_body_str); + if (nxt_slow_path(body != NULL && !PyBytes_Check(body))) { + return PyErr_Format(PyExc_TypeError, "'body' is not a byte string"); + } + + more_body = PyDict_GetItem(dict, nxt_py_more_body_str); + if (nxt_slow_path(more_body != NULL && !PyBool_Check(more_body))) { + return PyErr_Format(PyExc_TypeError, "'more_body' is not a bool"); + } + + if (nxt_slow_path(http->complete)) { + return PyErr_Format(PyExc_RuntimeError, + "Unexpected ASGI message 'http.response.body' " + "sent, after response already completed"); + } + + if (nxt_slow_path(http->send_future != NULL)) { + return PyErr_Format(PyExc_RuntimeError, "Concurrent send"); + } + + if (body != NULL) { + body_str = PyBytes_AS_STRING(body); + body_len = PyBytes_GET_SIZE(body); + + nxt_unit_req_debug(http->req, "asgi_http_response_body: %d, %d", + (int) body_len, (more_body == Py_True) ); + + if (nxt_slow_path(http->bytes_sent + body_len + > http->content_length)) + { + return PyErr_Format(PyExc_RuntimeError, + "Response content longer than Content-Length"); + } + + body_off = 0; + + while (body_len > 0) { + sent = nxt_unit_response_write_nb(http->req, body_str, body_len, 0); + if (nxt_slow_path(sent < 0)) { + return PyErr_Format(PyExc_RuntimeError, "failed to send body"); + } + + if (nxt_slow_path(sent == 0)) { + nxt_unit_req_debug(http->req, "asgi_http_response_body: " + "out of shared memory, %d", + (int) body_len); + + future = PyObject_CallObject(nxt_py_loop_create_future, NULL); + if (nxt_slow_path(future == NULL)) { + nxt_unit_req_alert(http->req, + "Python failed to create Future object"); + nxt_python_print_exception(); + + return PyErr_Format(PyExc_RuntimeError, + "failed to create Future object"); + } + + http->send_body = body; + Py_INCREF(http->send_body); + http->send_body_off = body_off; + + nxt_queue_insert_tail(&nxt_py_asgi_drain_queue, &http->link); + + http->send_future = future; + Py_INCREF(http->send_future); + + return future; + } + + body_str += sent; + body_len -= sent; + body_off += sent; + http->bytes_sent += sent; + } + + } else { + nxt_unit_req_debug(http->req, "asgi_http_response_body: 0, %d", + (more_body == Py_True) ); + + if (!nxt_unit_response_is_sent(http->req)) { + rc = nxt_unit_response_send(http->req); + if (nxt_slow_path(rc != NXT_UNIT_OK)) { + return PyErr_Format(PyExc_RuntimeError, + "failed to send response"); + } + } + } + + if (more_body == NULL || more_body == Py_False) { + http->complete = 1; + } + + Py_INCREF(http); + return (PyObject *) http; +} + + +void +nxt_py_asgi_http_data_handler(nxt_unit_request_info_t *req) +{ + PyObject *msg, *future, *res; + nxt_py_asgi_http_t *http; + + http = req->data; + + nxt_unit_req_debug(req, "asgi_http_data_handler"); + + if (http->receive_future == NULL) { + return; + } + + msg = nxt_py_asgi_http_read_msg(http); + if (nxt_slow_path(msg == NULL)) { + return; + } + + if (msg == Py_None) { + Py_DECREF(msg); + return; + } + + future = http->receive_future; + http->receive_future = NULL; + + res = PyObject_CallMethodObjArgs(future, nxt_py_set_result_str, msg, NULL); + if (nxt_slow_path(res == NULL)) { + nxt_unit_req_alert(req, "'set_result' call failed"); + nxt_python_print_exception(); + } + + Py_XDECREF(res); + Py_DECREF(future); + + Py_DECREF(msg); +} + + +int +nxt_py_asgi_http_drain(nxt_queue_link_t *lnk) +{ + char *body_str; + ssize_t sent; + PyObject *future, *exc, *res; + Py_ssize_t body_len; + nxt_py_asgi_http_t *http; + + http = nxt_container_of(lnk, nxt_py_asgi_http_t, link); + + body_str = PyBytes_AS_STRING(http->send_body) + http->send_body_off; + body_len = PyBytes_GET_SIZE(http->send_body) - http->send_body_off; + + nxt_unit_req_debug(http->req, "asgi_http_drain: %d", (int) body_len); + + while (body_len > 0) { + sent = nxt_unit_response_write_nb(http->req, body_str, body_len, 0); + if (nxt_slow_path(sent < 0)) { + goto fail; + } + + if (nxt_slow_path(sent == 0)) { + return NXT_UNIT_AGAIN; + } + + body_str += sent; + body_len -= sent; + + http->send_body_off += sent; + http->bytes_sent += sent; + } + + Py_CLEAR(http->send_body); + + future = http->send_future; + http->send_future = NULL; + + res = PyObject_CallMethodObjArgs(future, nxt_py_set_result_str, Py_None, + NULL); + if (nxt_slow_path(res == NULL)) { + nxt_unit_req_alert(http->req, "'set_result' call failed"); + nxt_python_print_exception(); + } + + Py_XDECREF(res); + Py_DECREF(future); + + return NXT_UNIT_OK; + +fail: + + exc = PyObject_CallFunctionObjArgs(PyExc_RuntimeError, + nxt_py_failed_to_send_body_str, + NULL); + if (nxt_slow_path(exc == NULL)) { + nxt_unit_req_alert(http->req, "RuntimeError create failed"); + nxt_python_print_exception(); + + exc = Py_None; + Py_INCREF(exc); + } + + future = http->send_future; + http->send_future = NULL; + + res = PyObject_CallMethodObjArgs(future, nxt_py_set_exception_str, exc, + NULL); + if (nxt_slow_path(res == NULL)) { + nxt_unit_req_alert(http->req, "'set_exception' call failed"); + nxt_python_print_exception(); + } + + Py_XDECREF(res); + Py_DECREF(future); + Py_DECREF(exc); + + return NXT_UNIT_ERROR; +} + + +static PyObject * +nxt_py_asgi_http_done(PyObject *self, PyObject *future) +{ + int rc; + PyObject *res; + nxt_py_asgi_http_t *http; + + http = (nxt_py_asgi_http_t *) self; + + nxt_unit_req_debug(http->req, "asgi_http_done"); + + /* + * Get Future.result() and it raises an exception, if coroutine exited + * with exception. + */ + res = PyObject_CallMethodObjArgs(future, nxt_py_result_str, NULL); + if (nxt_slow_path(res == NULL)) { + nxt_unit_req_error(http->req, + "Python failed to call 'future.result()'"); + nxt_python_print_exception(); + + rc = NXT_UNIT_ERROR; + + } else { + Py_DECREF(res); + + rc = NXT_UNIT_OK; + } + + nxt_unit_request_done(http->req, rc); + + Py_RETURN_NONE; +} + + +#endif /* NXT_HAVE_ASGI */ diff --git a/src/python/nxt_python_asgi_lifespan.c b/src/python/nxt_python_asgi_lifespan.c new file mode 100644 index 00000000..14d0ee97 --- /dev/null +++ b/src/python/nxt_python_asgi_lifespan.c @@ -0,0 +1,505 @@ + +/* + * Copyright (C) NGINX, Inc. + */ + + +#include <python/nxt_python.h> + +#if (NXT_HAVE_ASGI) + +#include <nxt_main.h> +#include <python/nxt_python_asgi.h> +#include <python/nxt_python_asgi_str.h> + + +typedef struct { + PyObject_HEAD + int disabled; + int startup_received; + int startup_sent; + int shutdown_received; + int shutdown_sent; + int shutdown_called; + PyObject *startup_future; + PyObject *shutdown_future; + PyObject *receive_future; +} nxt_py_asgi_lifespan_t; + + +static PyObject *nxt_py_asgi_lifespan_receive(PyObject *self, PyObject *none); +static PyObject *nxt_py_asgi_lifespan_send(PyObject *self, PyObject *dict); +static PyObject *nxt_py_asgi_lifespan_send_startup( + nxt_py_asgi_lifespan_t *lifespan, int v, PyObject *dict); +static PyObject *nxt_py_asgi_lifespan_send_(nxt_py_asgi_lifespan_t *lifespan, + int v, int *sent, PyObject **future); +static PyObject *nxt_py_asgi_lifespan_send_shutdown( + nxt_py_asgi_lifespan_t *lifespan, int v, PyObject *dict); +static PyObject *nxt_py_asgi_lifespan_disable(nxt_py_asgi_lifespan_t *lifespan); +static PyObject *nxt_py_asgi_lifespan_done(PyObject *self, PyObject *future); + + +static nxt_py_asgi_lifespan_t *nxt_py_lifespan; + +static PyMethodDef nxt_py_asgi_lifespan_methods[] = { + { "receive", nxt_py_asgi_lifespan_receive, METH_NOARGS, 0 }, + { "send", nxt_py_asgi_lifespan_send, METH_O, 0 }, + { "_done", nxt_py_asgi_lifespan_done, METH_O, 0 }, + { NULL, NULL, 0, 0 } +}; + +static PyAsyncMethods nxt_py_asgi_async_methods = { + .am_await = nxt_py_asgi_await, +}; + +static PyTypeObject nxt_py_asgi_lifespan_type = { + PyVarObject_HEAD_INIT(NULL, 0) + + .tp_name = "unit._asgi_lifespan", + .tp_basicsize = sizeof(nxt_py_asgi_lifespan_t), + .tp_dealloc = nxt_py_asgi_dealloc, + .tp_as_async = &nxt_py_asgi_async_methods, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_doc = "unit ASGI Lifespan object", + .tp_iter = nxt_py_asgi_iter, + .tp_iternext = nxt_py_asgi_next, + .tp_methods = nxt_py_asgi_lifespan_methods, +}; + + +nxt_int_t +nxt_py_asgi_lifespan_startup(nxt_task_t *task) +{ + PyObject *scope, *res, *py_task, *receive, *send, *done; + nxt_int_t rc; + nxt_py_asgi_lifespan_t *lifespan; + + if (nxt_slow_path(PyType_Ready(&nxt_py_asgi_lifespan_type) != 0)) { + nxt_alert(task, + "Python failed to initialize the 'asgi_lifespan' type object"); + return NXT_ERROR; + } + + lifespan = PyObject_New(nxt_py_asgi_lifespan_t, &nxt_py_asgi_lifespan_type); + if (nxt_slow_path(lifespan == NULL)) { + nxt_alert(task, "Python failed to create lifespan object"); + return NXT_ERROR; + } + + rc = NXT_ERROR; + + receive = PyObject_GetAttrString((PyObject *) lifespan, "receive"); + if (nxt_slow_path(receive == NULL)) { + nxt_alert(task, "Python failed to get 'receive' method"); + goto release_lifespan; + } + + send = PyObject_GetAttrString((PyObject *) lifespan, "send"); + if (nxt_slow_path(receive == NULL)) { + nxt_alert(task, "Python failed to get 'send' method"); + goto release_receive; + } + + done = PyObject_GetAttrString((PyObject *) lifespan, "_done"); + if (nxt_slow_path(receive == NULL)) { + nxt_alert(task, "Python failed to get '_done' method"); + goto release_send; + } + + lifespan->startup_future = PyObject_CallObject(nxt_py_loop_create_future, + NULL); + if (nxt_slow_path(lifespan->startup_future == NULL)) { + nxt_unit_alert(NULL, "Python failed to create Future object"); + nxt_python_print_exception(); + + goto release_done; + } + + lifespan->disabled = 0; + lifespan->startup_received = 0; + lifespan->startup_sent = 0; + lifespan->shutdown_received = 0; + lifespan->shutdown_sent = 0; + lifespan->shutdown_called = 0; + lifespan->shutdown_future = NULL; + lifespan->receive_future = NULL; + + scope = nxt_py_asgi_new_scope(NULL, nxt_py_lifespan_str, nxt_py_2_0_str); + if (nxt_slow_path(scope == NULL)) { + goto release_future; + } + + res = PyObject_CallFunctionObjArgs(nxt_py_application, + scope, receive, send, NULL); + if (nxt_slow_path(res == NULL)) { + nxt_log(task, NXT_LOG_ERR, "Python failed to call the application"); + nxt_python_print_exception(); + goto release_scope; + } + + if (nxt_slow_path(!PyCoro_CheckExact(res))) { + nxt_log(task, NXT_LOG_ERR, + "Application result type is not a coroutine"); + Py_DECREF(res); + goto release_scope; + } + + py_task = PyObject_CallFunctionObjArgs(nxt_py_loop_create_task, res, NULL); + if (nxt_slow_path(py_task == NULL)) { + nxt_log(task, NXT_LOG_ERR, "Python failed to call the create_task"); + nxt_python_print_exception(); + Py_DECREF(res); + goto release_scope; + } + + Py_DECREF(res); + + res = PyObject_CallMethodObjArgs(py_task, nxt_py_add_done_callback_str, + done, NULL); + if (nxt_slow_path(res == NULL)) { + nxt_log(task, NXT_LOG_ERR, + "Python failed to call 'task.add_done_callback'"); + nxt_python_print_exception(); + goto release_task; + } + + Py_DECREF(res); + + res = PyObject_CallFunctionObjArgs(nxt_py_loop_run_until_complete, + lifespan->startup_future, NULL); + if (nxt_slow_path(res == NULL)) { + nxt_alert(task, "Python failed to call loop.run_until_complete"); + nxt_python_print_exception(); + goto release_task; + } + + Py_DECREF(res); + + if (lifespan->startup_sent == 1 || lifespan->disabled) { + nxt_py_lifespan = lifespan; + Py_INCREF(nxt_py_lifespan); + + rc = NXT_OK; + } + +release_task: + Py_DECREF(py_task); +release_scope: + Py_DECREF(scope); +release_future: + Py_CLEAR(lifespan->startup_future); +release_done: + Py_DECREF(done); +release_send: + Py_DECREF(send); +release_receive: + Py_DECREF(receive); +release_lifespan: + Py_DECREF(lifespan); + + return rc; +} + + +nxt_int_t +nxt_py_asgi_lifespan_shutdown(void) +{ + PyObject *msg, *future, *res; + nxt_py_asgi_lifespan_t *lifespan; + + if (nxt_slow_path(nxt_py_lifespan == NULL || nxt_py_lifespan->disabled)) { + return NXT_OK; + } + + lifespan = nxt_py_lifespan; + lifespan->shutdown_called = 1; + + if (lifespan->receive_future != NULL) { + future = lifespan->receive_future; + lifespan->receive_future = NULL; + + msg = nxt_py_asgi_new_msg(NULL, nxt_py_lifespan_shutdown_str); + + if (nxt_fast_path(msg != NULL)) { + res = PyObject_CallMethodObjArgs(future, nxt_py_set_result_str, + msg, NULL); + Py_XDECREF(res); + Py_DECREF(msg); + } + + Py_DECREF(future); + } + + if (lifespan->shutdown_sent) { + return NXT_OK; + } + + lifespan->shutdown_future = PyObject_CallObject(nxt_py_loop_create_future, + NULL); + if (nxt_slow_path(lifespan->shutdown_future == NULL)) { + nxt_unit_alert(NULL, "Python failed to create Future object"); + nxt_python_print_exception(); + return NXT_ERROR; + } + + res = PyObject_CallFunctionObjArgs(nxt_py_loop_run_until_complete, + lifespan->shutdown_future, NULL); + if (nxt_slow_path(res == NULL)) { + nxt_unit_alert(NULL, "Python failed to call loop.run_until_complete"); + nxt_python_print_exception(); + return NXT_ERROR; + } + + Py_DECREF(res); + Py_CLEAR(lifespan->shutdown_future); + + return NXT_OK; +} + + +static PyObject * +nxt_py_asgi_lifespan_receive(PyObject *self, PyObject *none) +{ + PyObject *msg, *future; + nxt_py_asgi_lifespan_t *lifespan; + + lifespan = (nxt_py_asgi_lifespan_t *) self; + + nxt_unit_debug(NULL, "asgi_lifespan_receive"); + + future = PyObject_CallObject(nxt_py_loop_create_future, NULL); + if (nxt_slow_path(future == NULL)) { + nxt_unit_alert(NULL, "Python failed to create Future object"); + nxt_python_print_exception(); + + return PyErr_Format(PyExc_RuntimeError, + "failed to create Future object"); + } + + if (!lifespan->startup_received) { + lifespan->startup_received = 1; + + msg = nxt_py_asgi_new_msg(NULL, nxt_py_lifespan_startup_str); + + return nxt_py_asgi_set_result_soon(NULL, future, msg); + } + + if (lifespan->shutdown_called && !lifespan->shutdown_received) { + lifespan->shutdown_received = 1; + + msg = nxt_py_asgi_new_msg(NULL, nxt_py_lifespan_shutdown_str); + + return nxt_py_asgi_set_result_soon(NULL, future, msg); + } + + Py_INCREF(future); + lifespan->receive_future = future; + + return future; +} + + +static PyObject * +nxt_py_asgi_lifespan_send(PyObject *self, PyObject *dict) +{ + PyObject *type, *msg; + const char *type_str; + Py_ssize_t type_len; + nxt_py_asgi_lifespan_t *lifespan; + + static const nxt_str_t startup_complete + = nxt_string("lifespan.startup.complete"); + static const nxt_str_t startup_failed + = nxt_string("lifespan.startup.failed"); + static const nxt_str_t shutdown_complete + = nxt_string("lifespan.shutdown.complete"); + static const nxt_str_t shutdown_failed + = nxt_string("lifespan.shutdown.failed"); + + lifespan = (nxt_py_asgi_lifespan_t *) self; + + type = PyDict_GetItem(dict, nxt_py_type_str); + if (nxt_slow_path(type == NULL || !PyUnicode_Check(type))) { + nxt_unit_error(NULL, + "asgi_lifespan_send: 'type' is not a unicode string"); + return PyErr_Format(PyExc_TypeError, + "'type' is not a unicode string"); + } + + type_str = PyUnicode_AsUTF8AndSize(type, &type_len); + + nxt_unit_debug(NULL, "asgi_lifespan_send type is '%.*s'", + (int) type_len, type_str); + + if (type_len == (Py_ssize_t) startup_complete.length + && memcmp(type_str, startup_complete.start, type_len) == 0) + { + return nxt_py_asgi_lifespan_send_startup(lifespan, 0, NULL); + } + + if (type_len == (Py_ssize_t) startup_failed.length + && memcmp(type_str, startup_failed.start, type_len) == 0) + { + msg = PyDict_GetItem(dict, nxt_py_message_str); + return nxt_py_asgi_lifespan_send_startup(lifespan, 1, msg); + } + + if (type_len == (Py_ssize_t) shutdown_complete.length + && memcmp(type_str, shutdown_complete.start, type_len) == 0) + { + return nxt_py_asgi_lifespan_send_shutdown(lifespan, 0, NULL); + } + + if (type_len == (Py_ssize_t) shutdown_failed.length + && memcmp(type_str, shutdown_failed.start, type_len) == 0) + { + msg = PyDict_GetItem(dict, nxt_py_message_str); + return nxt_py_asgi_lifespan_send_shutdown(lifespan, 1, msg); + } + + return nxt_py_asgi_lifespan_disable(lifespan); +} + + +static PyObject * +nxt_py_asgi_lifespan_send_startup(nxt_py_asgi_lifespan_t *lifespan, int v, + PyObject *message) +{ + const char *message_str; + Py_ssize_t message_len; + + if (nxt_slow_path(v != 0)) { + nxt_unit_error(NULL, "Application startup failed"); + + if (nxt_fast_path(message != NULL && PyUnicode_Check(message))) { + message_str = PyUnicode_AsUTF8AndSize(message, &message_len); + + nxt_unit_error(NULL, "%.*s", (int) message_len, message_str); + } + } + + return nxt_py_asgi_lifespan_send_(lifespan, v, + &lifespan->startup_sent, + &lifespan->startup_future); +} + + +static PyObject * +nxt_py_asgi_lifespan_send_(nxt_py_asgi_lifespan_t *lifespan, int v, int *sent, + PyObject **pfuture) +{ + PyObject *future, *res; + + if (*sent) { + return nxt_py_asgi_lifespan_disable(lifespan); + } + + *sent = 1 + v; + + if (*pfuture != NULL) { + future = *pfuture; + *pfuture = NULL; + + res = PyObject_CallMethodObjArgs(future, nxt_py_set_result_str, + Py_None, NULL); + if (nxt_slow_path(res == NULL)) { + nxt_unit_alert(NULL, "Failed to call 'future.set_result'"); + nxt_python_print_exception(); + + return nxt_py_asgi_lifespan_disable(lifespan); + } + + Py_DECREF(res); + Py_DECREF(future); + } + + Py_INCREF(lifespan); + + return (PyObject *) lifespan; +} + + +static PyObject * +nxt_py_asgi_lifespan_disable(nxt_py_asgi_lifespan_t *lifespan) +{ + nxt_unit_warn(NULL, "Got invalid state transition on lifespan protocol"); + + lifespan->disabled = 1; + + return PyErr_Format(PyExc_AssertionError, + "Got invalid state transition on lifespan protocol"); +} + + +static PyObject * +nxt_py_asgi_lifespan_send_shutdown(nxt_py_asgi_lifespan_t *lifespan, int v, + PyObject *message) +{ + return nxt_py_asgi_lifespan_send_(lifespan, v, + &lifespan->shutdown_sent, + &lifespan->shutdown_future); +} + + +static PyObject * +nxt_py_asgi_lifespan_done(PyObject *self, PyObject *future) +{ + PyObject *res; + nxt_py_asgi_lifespan_t *lifespan; + + nxt_unit_debug(NULL, "asgi_lifespan_done"); + + lifespan = (nxt_py_asgi_lifespan_t *) self; + + if (lifespan->startup_sent == 0) { + lifespan->disabled = 1; + } + + /* + * Get Future.result() and it raises an exception, if coroutine exited + * with exception. + */ + res = PyObject_CallMethodObjArgs(future, nxt_py_result_str, NULL); + if (nxt_slow_path(res == NULL)) { + nxt_unit_log(NULL, NXT_UNIT_LOG_INFO, + "ASGI Lifespan processing exception"); + nxt_python_print_exception(); + } + + Py_XDECREF(res); + + if (lifespan->startup_future != NULL) { + future = lifespan->startup_future; + lifespan->startup_future = NULL; + + res = PyObject_CallMethodObjArgs(future, nxt_py_set_result_str, + Py_None, NULL); + if (nxt_slow_path(res == NULL)) { + nxt_unit_alert(NULL, "Failed to call 'future.set_result'"); + nxt_python_print_exception(); + } + + Py_XDECREF(res); + Py_DECREF(future); + } + + if (lifespan->shutdown_future != NULL) { + future = lifespan->shutdown_future; + lifespan->shutdown_future = NULL; + + res = PyObject_CallMethodObjArgs(future, nxt_py_set_result_str, + Py_None, NULL); + if (nxt_slow_path(res == NULL)) { + nxt_unit_alert(NULL, "Failed to call 'future.set_result'"); + nxt_python_print_exception(); + } + + Py_XDECREF(res); + Py_DECREF(future); + } + + Py_RETURN_NONE; +} + + +#endif /* NXT_HAVE_ASGI */ diff --git a/src/python/nxt_python_asgi_str.c b/src/python/nxt_python_asgi_str.c new file mode 100644 index 00000000..37fa7f04 --- /dev/null +++ b/src/python/nxt_python_asgi_str.c @@ -0,0 +1,141 @@ + +/* + * Copyright (C) NGINX, Inc. + */ + + +#include <python/nxt_python.h> + +#if (NXT_HAVE_ASGI) + +#include <nxt_main.h> +#include <python/nxt_python_asgi_str.h> + + +PyObject *nxt_py_1_0_str; +PyObject *nxt_py_1_1_str; +PyObject *nxt_py_2_0_str; +PyObject *nxt_py_2_1_str; +PyObject *nxt_py_3_0_str; +PyObject *nxt_py_add_done_callback_str; +PyObject *nxt_py_asgi_str; +PyObject *nxt_py_bad_state_str; +PyObject *nxt_py_body_str; +PyObject *nxt_py_bytes_str; +PyObject *nxt_py_client_str; +PyObject *nxt_py_code_str; +PyObject *nxt_py_done_str; +PyObject *nxt_py_exception_str; +PyObject *nxt_py_failed_to_send_body_str; +PyObject *nxt_py_headers_str; +PyObject *nxt_py_http_str; +PyObject *nxt_py_http_disconnect_str; +PyObject *nxt_py_http_request_str; +PyObject *nxt_py_http_version_str; +PyObject *nxt_py_https_str; +PyObject *nxt_py_lifespan_str; +PyObject *nxt_py_lifespan_shutdown_str; +PyObject *nxt_py_lifespan_startup_str; +PyObject *nxt_py_method_str; +PyObject *nxt_py_message_str; +PyObject *nxt_py_message_too_big_str; +PyObject *nxt_py_more_body_str; +PyObject *nxt_py_path_str; +PyObject *nxt_py_query_string_str; +PyObject *nxt_py_raw_path_str; +PyObject *nxt_py_result_str; +PyObject *nxt_py_root_path_str; +PyObject *nxt_py_scheme_str; +PyObject *nxt_py_server_str; +PyObject *nxt_py_set_exception_str; +PyObject *nxt_py_set_result_str; +PyObject *nxt_py_spec_version_str; +PyObject *nxt_py_status_str; +PyObject *nxt_py_subprotocol_str; +PyObject *nxt_py_subprotocols_str; +PyObject *nxt_py_text_str; +PyObject *nxt_py_type_str; +PyObject *nxt_py_version_str; +PyObject *nxt_py_websocket_str; +PyObject *nxt_py_websocket_accept_str; +PyObject *nxt_py_websocket_close_str; +PyObject *nxt_py_websocket_connect_str; +PyObject *nxt_py_websocket_disconnect_str; +PyObject *nxt_py_websocket_receive_str; +PyObject *nxt_py_websocket_send_str; +PyObject *nxt_py_ws_str; +PyObject *nxt_py_wss_str; + +static nxt_python_string_t nxt_py_asgi_strings[] = { + { nxt_string("1.0"), &nxt_py_1_0_str }, + { nxt_string("1.1"), &nxt_py_1_1_str }, + { nxt_string("2.0"), &nxt_py_2_0_str }, + { nxt_string("2.1"), &nxt_py_2_1_str }, + { nxt_string("3.0"), &nxt_py_3_0_str }, + { nxt_string("add_done_callback"), &nxt_py_add_done_callback_str }, + { nxt_string("asgi"), &nxt_py_asgi_str }, + { nxt_string("bad state"), &nxt_py_bad_state_str }, + { nxt_string("body"), &nxt_py_body_str }, + { nxt_string("bytes"), &nxt_py_bytes_str }, + { nxt_string("client"), &nxt_py_client_str }, + { nxt_string("code"), &nxt_py_code_str }, + { nxt_string("done"), &nxt_py_done_str }, + { nxt_string("exception"), &nxt_py_exception_str }, + { nxt_string("failed to send body"), &nxt_py_failed_to_send_body_str }, + { nxt_string("headers"), &nxt_py_headers_str }, + { nxt_string("http"), &nxt_py_http_str }, + { nxt_string("http.disconnect"), &nxt_py_http_disconnect_str }, + { nxt_string("http.request"), &nxt_py_http_request_str }, + { nxt_string("http_version"), &nxt_py_http_version_str }, + { nxt_string("https"), &nxt_py_https_str }, + { nxt_string("lifespan"), &nxt_py_lifespan_str }, + { nxt_string("lifespan.shutdown"), &nxt_py_lifespan_shutdown_str }, + { nxt_string("lifespan.startup"), &nxt_py_lifespan_startup_str }, + { nxt_string("message"), &nxt_py_message_str }, + { nxt_string("message too big"), &nxt_py_message_too_big_str }, + { nxt_string("method"), &nxt_py_method_str }, + { nxt_string("more_body"), &nxt_py_more_body_str }, + { nxt_string("path"), &nxt_py_path_str }, + { nxt_string("query_string"), &nxt_py_query_string_str }, + { nxt_string("raw_path"), &nxt_py_raw_path_str }, + { nxt_string("result"), &nxt_py_result_str }, + { nxt_string("root_path"), &nxt_py_root_path_str }, // not used + { nxt_string("scheme"), &nxt_py_scheme_str }, + { nxt_string("server"), &nxt_py_server_str }, + { nxt_string("set_exception"), &nxt_py_set_exception_str }, + { nxt_string("set_result"), &nxt_py_set_result_str }, + { nxt_string("spec_version"), &nxt_py_spec_version_str }, + { nxt_string("status"), &nxt_py_status_str }, + { nxt_string("subprotocol"), &nxt_py_subprotocol_str }, + { nxt_string("subprotocols"), &nxt_py_subprotocols_str }, + { nxt_string("text"), &nxt_py_text_str }, + { nxt_string("type"), &nxt_py_type_str }, + { nxt_string("version"), &nxt_py_version_str }, + { nxt_string("websocket"), &nxt_py_websocket_str }, + { nxt_string("websocket.accept"), &nxt_py_websocket_accept_str }, + { nxt_string("websocket.close"), &nxt_py_websocket_close_str }, + { nxt_string("websocket.connect"), &nxt_py_websocket_connect_str }, + { nxt_string("websocket.disconnect"), &nxt_py_websocket_disconnect_str }, + { nxt_string("websocket.receive"), &nxt_py_websocket_receive_str }, + { nxt_string("websocket.send"), &nxt_py_websocket_send_str }, + { nxt_string("ws"), &nxt_py_ws_str }, + { nxt_string("wss"), &nxt_py_wss_str }, + { nxt_null_string, NULL }, +}; + + +nxt_int_t +nxt_py_asgi_str_init(void) +{ + return nxt_python_init_strings(nxt_py_asgi_strings); +} + + +void +nxt_py_asgi_str_done(void) +{ + nxt_python_done_strings(nxt_py_asgi_strings); +} + + +#endif /* NXT_HAVE_ASGI */ diff --git a/src/python/nxt_python_asgi_str.h b/src/python/nxt_python_asgi_str.h new file mode 100644 index 00000000..3f389c62 --- /dev/null +++ b/src/python/nxt_python_asgi_str.h @@ -0,0 +1,69 @@ + +/* + * Copyright (C) NGINX, Inc. + */ + +#ifndef _NXT_PYTHON_ASGI_STR_H_INCLUDED_ +#define _NXT_PYTHON_ASGI_STR_H_INCLUDED_ + + +extern PyObject *nxt_py_1_0_str; +extern PyObject *nxt_py_1_1_str; +extern PyObject *nxt_py_2_0_str; +extern PyObject *nxt_py_2_1_str; +extern PyObject *nxt_py_3_0_str; +extern PyObject *nxt_py_add_done_callback_str; +extern PyObject *nxt_py_asgi_str; +extern PyObject *nxt_py_bad_state_str; +extern PyObject *nxt_py_body_str; +extern PyObject *nxt_py_bytes_str; +extern PyObject *nxt_py_client_str; +extern PyObject *nxt_py_code_str; +extern PyObject *nxt_py_done_str; +extern PyObject *nxt_py_exception_str; +extern PyObject *nxt_py_failed_to_send_body_str; +extern PyObject *nxt_py_headers_str; +extern PyObject *nxt_py_http_str; +extern PyObject *nxt_py_http_disconnect_str; +extern PyObject *nxt_py_http_request_str; +extern PyObject *nxt_py_http_version_str; +extern PyObject *nxt_py_https_str; +extern PyObject *nxt_py_lifespan_str; +extern PyObject *nxt_py_lifespan_shutdown_str; +extern PyObject *nxt_py_lifespan_startup_str; +extern PyObject *nxt_py_method_str; +extern PyObject *nxt_py_message_str; +extern PyObject *nxt_py_message_too_big_str; +extern PyObject *nxt_py_more_body_str; +extern PyObject *nxt_py_path_str; +extern PyObject *nxt_py_query_string_str; +extern PyObject *nxt_py_result_str; +extern PyObject *nxt_py_raw_path_str; +extern PyObject *nxt_py_root_path_str; +extern PyObject *nxt_py_scheme_str; +extern PyObject *nxt_py_server_str; +extern PyObject *nxt_py_set_exception_str; +extern PyObject *nxt_py_set_result_str; +extern PyObject *nxt_py_spec_version_str; +extern PyObject *nxt_py_status_str; +extern PyObject *nxt_py_subprotocol_str; +extern PyObject *nxt_py_subprotocols_str; +extern PyObject *nxt_py_text_str; +extern PyObject *nxt_py_type_str; +extern PyObject *nxt_py_version_str; +extern PyObject *nxt_py_websocket_str; +extern PyObject *nxt_py_websocket_accept_str; +extern PyObject *nxt_py_websocket_close_str; +extern PyObject *nxt_py_websocket_connect_str; +extern PyObject *nxt_py_websocket_disconnect_str; +extern PyObject *nxt_py_websocket_receive_str; +extern PyObject *nxt_py_websocket_send_str; +extern PyObject *nxt_py_ws_str; +extern PyObject *nxt_py_wss_str; + + +nxt_int_t nxt_py_asgi_str_init(void); +void nxt_py_asgi_str_done(void); + + +#endif /* _NXT_PYTHON_ASGI_STR_H_INCLUDED_ */ diff --git a/src/python/nxt_python_asgi_websocket.c b/src/python/nxt_python_asgi_websocket.c new file mode 100644 index 00000000..5a27b588 --- /dev/null +++ b/src/python/nxt_python_asgi_websocket.c @@ -0,0 +1,1084 @@ + +/* + * Copyright (C) NGINX, Inc. + */ + + +#include <python/nxt_python.h> + +#if (NXT_HAVE_ASGI) + +#include <nxt_main.h> +#include <nxt_unit.h> +#include <nxt_unit_request.h> +#include <nxt_unit_websocket.h> +#include <nxt_websocket_header.h> +#include <python/nxt_python_asgi.h> +#include <python/nxt_python_asgi_str.h> + + +enum { + NXT_WS_INIT, + NXT_WS_CONNECT, + NXT_WS_ACCEPTED, + NXT_WS_DISCONNECTED, + NXT_WS_CLOSED, +}; + + +typedef struct { + nxt_queue_link_t link; + nxt_unit_websocket_frame_t *frame; +} nxt_py_asgi_penging_frame_t; + + +typedef struct { + PyObject_HEAD + nxt_unit_request_info_t *req; + PyObject *receive_future; + PyObject *receive_exc_str; + int state; + nxt_queue_t pending_frames; + uint64_t pending_payload_len; + uint64_t pending_frame_len; + int pending_fins; +} nxt_py_asgi_websocket_t; + + +static PyObject *nxt_py_asgi_websocket_receive(PyObject *self, PyObject *none); +static PyObject *nxt_py_asgi_websocket_send(PyObject *self, PyObject *dict); +static PyObject *nxt_py_asgi_websocket_accept(nxt_py_asgi_websocket_t *ws, + PyObject *dict); +static PyObject *nxt_py_asgi_websocket_close(nxt_py_asgi_websocket_t *ws, + PyObject *dict); +static PyObject *nxt_py_asgi_websocket_send_frame(nxt_py_asgi_websocket_t *ws, + PyObject *dict); +static void nxt_py_asgi_websocket_receive_done(nxt_py_asgi_websocket_t *ws, + PyObject *msg); +static void nxt_py_asgi_websocket_receive_fail(nxt_py_asgi_websocket_t *ws, + PyObject *exc); +static void nxt_py_asgi_websocket_suspend_frame(nxt_unit_websocket_frame_t *f); +static PyObject *nxt_py_asgi_websocket_pop_msg(nxt_py_asgi_websocket_t *ws, + nxt_unit_websocket_frame_t *frame); +static uint64_t nxt_py_asgi_websocket_pending_len( + nxt_py_asgi_websocket_t *ws); +static nxt_unit_websocket_frame_t *nxt_py_asgi_websocket_pop_frame( + nxt_py_asgi_websocket_t *ws); +static PyObject *nxt_py_asgi_websocket_disconnect_msg( + nxt_py_asgi_websocket_t *ws); +static PyObject *nxt_py_asgi_websocket_done(PyObject *self, PyObject *future); + + +static PyMethodDef nxt_py_asgi_websocket_methods[] = { + { "receive", nxt_py_asgi_websocket_receive, METH_NOARGS, 0 }, + { "send", nxt_py_asgi_websocket_send, METH_O, 0 }, + { "_done", nxt_py_asgi_websocket_done, METH_O, 0 }, + { NULL, NULL, 0, 0 } +}; + +static PyAsyncMethods nxt_py_asgi_async_methods = { + .am_await = nxt_py_asgi_await, +}; + +static PyTypeObject nxt_py_asgi_websocket_type = { + PyVarObject_HEAD_INIT(NULL, 0) + + .tp_name = "unit._asgi_websocket", + .tp_basicsize = sizeof(nxt_py_asgi_websocket_t), + .tp_dealloc = nxt_py_asgi_dealloc, + .tp_as_async = &nxt_py_asgi_async_methods, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_doc = "unit ASGI WebSocket connection object", + .tp_iter = nxt_py_asgi_iter, + .tp_iternext = nxt_py_asgi_next, + .tp_methods = nxt_py_asgi_websocket_methods, +}; + +static uint64_t nxt_py_asgi_ws_max_frame_size = 1024 * 1024; +static uint64_t nxt_py_asgi_ws_max_buffer_size = 10 * 1024 * 1024; + + +nxt_int_t +nxt_py_asgi_websocket_init(nxt_task_t *task) +{ + if (nxt_slow_path(PyType_Ready(&nxt_py_asgi_websocket_type) != 0)) { + nxt_alert(task, + "Python failed to initialize the \"asgi_websocket\" type object"); + return NXT_ERROR; + } + + return NXT_OK; +} + + +PyObject * +nxt_py_asgi_websocket_create(nxt_unit_request_info_t *req) +{ + nxt_py_asgi_websocket_t *ws; + + ws = PyObject_New(nxt_py_asgi_websocket_t, &nxt_py_asgi_websocket_type); + + if (nxt_fast_path(ws != NULL)) { + ws->req = req; + ws->receive_future = NULL; + ws->receive_exc_str = NULL; + ws->state = NXT_WS_INIT; + nxt_queue_init(&ws->pending_frames); + ws->pending_payload_len = 0; + ws->pending_frame_len = 0; + ws->pending_fins = 0; + } + + return (PyObject *) ws; +} + + +static PyObject * +nxt_py_asgi_websocket_receive(PyObject *self, PyObject *none) +{ + PyObject *future, *msg; + nxt_py_asgi_websocket_t *ws; + + ws = (nxt_py_asgi_websocket_t *) self; + + nxt_unit_req_debug(ws->req, "asgi_websocket_receive"); + + /* If exception happened out of receive() call, raise it now. */ + if (nxt_slow_path(ws->receive_exc_str != NULL)) { + PyErr_SetObject(PyExc_RuntimeError, ws->receive_exc_str); + + ws->receive_exc_str = NULL; + + return NULL; + } + + if (nxt_slow_path(ws->state == NXT_WS_CLOSED)) { + nxt_unit_req_error(ws->req, + "receive() called for closed WebSocket"); + + return PyErr_Format(PyExc_RuntimeError, + "WebSocket already closed"); + } + + future = PyObject_CallObject(nxt_py_loop_create_future, NULL); + if (nxt_slow_path(future == NULL)) { + nxt_unit_req_alert(ws->req, "Python failed to create Future object"); + nxt_python_print_exception(); + + return PyErr_Format(PyExc_RuntimeError, + "failed to create Future object"); + } + + if (nxt_slow_path(ws->state == NXT_WS_INIT)) { + ws->state = NXT_WS_CONNECT; + + msg = nxt_py_asgi_new_msg(ws->req, nxt_py_websocket_connect_str); + + return nxt_py_asgi_set_result_soon(ws->req, future, msg); + } + + if (ws->pending_fins > 0) { + msg = nxt_py_asgi_websocket_pop_msg(ws, NULL); + + return nxt_py_asgi_set_result_soon(ws->req, future, msg); + } + + if (nxt_slow_path(ws->state == NXT_WS_DISCONNECTED)) { + msg = nxt_py_asgi_websocket_disconnect_msg(ws); + + return nxt_py_asgi_set_result_soon(ws->req, future, msg); + } + + ws->receive_future = future; + Py_INCREF(ws->receive_future); + + return future; +} + + +static PyObject * +nxt_py_asgi_websocket_send(PyObject *self, PyObject *dict) +{ + PyObject *type; + const char *type_str; + Py_ssize_t type_len; + nxt_py_asgi_websocket_t *ws; + + static const nxt_str_t websocket_accept = nxt_string("websocket.accept"); + static const nxt_str_t websocket_close = nxt_string("websocket.close"); + static const nxt_str_t websocket_send = nxt_string("websocket.send"); + + ws = (nxt_py_asgi_websocket_t *) self; + + type = PyDict_GetItem(dict, nxt_py_type_str); + if (nxt_slow_path(type == NULL || !PyUnicode_Check(type))) { + nxt_unit_req_error(ws->req, "asgi_websocket_send: " + "'type' is not a unicode string"); + return PyErr_Format(PyExc_TypeError, + "'type' is not a unicode string"); + } + + type_str = PyUnicode_AsUTF8AndSize(type, &type_len); + + nxt_unit_req_debug(ws->req, "asgi_websocket_send type is '%.*s'", + (int) type_len, type_str); + + if (type_len == (Py_ssize_t) websocket_accept.length + && memcmp(type_str, websocket_accept.start, type_len) == 0) + { + return nxt_py_asgi_websocket_accept(ws, dict); + } + + if (type_len == (Py_ssize_t) websocket_close.length + && memcmp(type_str, websocket_close.start, type_len) == 0) + { + return nxt_py_asgi_websocket_close(ws, dict); + } + + if (type_len == (Py_ssize_t) websocket_send.length + && memcmp(type_str, websocket_send.start, type_len) == 0) + { + return nxt_py_asgi_websocket_send_frame(ws, dict); + } + + nxt_unit_req_error(ws->req, "asgi_websocket_send: " + "unexpected 'type': '%.*s'", (int) type_len, type_str); + return PyErr_Format(PyExc_AssertionError, "unexpected 'type': '%U'", type); +} + + +static PyObject * +nxt_py_asgi_websocket_accept(nxt_py_asgi_websocket_t *ws, PyObject *dict) +{ + int rc; + char *subprotocol_str; + PyObject *res, *headers, *subprotocol; + Py_ssize_t subprotocol_len; + nxt_py_asgi_calc_size_ctx_t calc_size_ctx; + nxt_py_asgi_add_field_ctx_t add_field_ctx; + + static const nxt_str_t ws_protocol = nxt_string("sec-websocket-protocol"); + + switch(ws->state) { + case NXT_WS_INIT: + return PyErr_Format(PyExc_RuntimeError, + "WebSocket connect not received"); + case NXT_WS_CONNECT: + break; + + case NXT_WS_ACCEPTED: + return PyErr_Format(PyExc_RuntimeError, "WebSocket already accepted"); + + case NXT_WS_DISCONNECTED: + return PyErr_Format(PyExc_RuntimeError, "WebSocket disconnected"); + + case NXT_WS_CLOSED: + return PyErr_Format(PyExc_RuntimeError, "WebSocket already closed"); + } + + if (nxt_slow_path(nxt_unit_response_is_websocket(ws->req))) { + return PyErr_Format(PyExc_RuntimeError, "WebSocket already accepted"); + } + + if (nxt_slow_path(nxt_unit_response_is_sent(ws->req))) { + return PyErr_Format(PyExc_RuntimeError, "response already sent"); + } + + calc_size_ctx.fields_size = 0; + calc_size_ctx.fields_count = 0; + + headers = PyDict_GetItem(dict, nxt_py_headers_str); + if (headers != NULL) { + res = nxt_py_asgi_enum_headers(headers, nxt_py_asgi_calc_size, + &calc_size_ctx); + if (nxt_slow_path(res == NULL)) { + return NULL; + } + } + + subprotocol = PyDict_GetItem(dict, nxt_py_subprotocol_str); + if (subprotocol != NULL && PyUnicode_Check(subprotocol)) { + subprotocol_str = PyUnicode_DATA(subprotocol); + subprotocol_len = PyUnicode_GET_LENGTH(subprotocol); + + calc_size_ctx.fields_size += ws_protocol.length + subprotocol_len; + calc_size_ctx.fields_count++; + + } else { + subprotocol_str = NULL; + subprotocol_len = 0; + } + + rc = nxt_unit_response_init(ws->req, 101, + calc_size_ctx.fields_count, + calc_size_ctx.fields_size); + if (nxt_slow_path(rc != NXT_UNIT_OK)) { + return PyErr_Format(PyExc_RuntimeError, + "failed to allocate response object"); + } + + add_field_ctx.req = ws->req; + add_field_ctx.content_length = -1; + + if (headers != NULL) { + res = nxt_py_asgi_enum_headers(headers, nxt_py_asgi_add_field, + &add_field_ctx); + if (nxt_slow_path(res == NULL)) { + return NULL; + } + } + + if (subprotocol_len > 0) { + rc = nxt_unit_response_add_field(ws->req, + (const char *) ws_protocol.start, + ws_protocol.length, + subprotocol_str, subprotocol_len); + if (nxt_slow_path(rc != NXT_UNIT_OK)) { + return PyErr_Format(PyExc_RuntimeError, + "failed to add header"); + } + } + + rc = nxt_unit_response_send(ws->req); + if (nxt_slow_path(rc != NXT_UNIT_OK)) { + return PyErr_Format(PyExc_RuntimeError, "failed to send response"); + } + + ws->state = NXT_WS_ACCEPTED; + + Py_INCREF(ws); + + return (PyObject *) ws; +} + + +static PyObject * +nxt_py_asgi_websocket_close(nxt_py_asgi_websocket_t *ws, PyObject *dict) +{ + int rc; + uint16_t status_code; + PyObject *code; + + if (nxt_slow_path(ws->state == NXT_WS_INIT)) { + return PyErr_Format(PyExc_RuntimeError, + "WebSocket connect not received"); + } + + if (nxt_slow_path(ws->state == NXT_WS_DISCONNECTED)) { + return PyErr_Format(PyExc_RuntimeError, "WebSocket disconnected"); + } + + if (nxt_slow_path(ws->state == NXT_WS_CLOSED)) { + return PyErr_Format(PyExc_RuntimeError, "WebSocket already closed"); + } + + if (nxt_unit_response_is_websocket(ws->req)) { + code = PyDict_GetItem(dict, nxt_py_code_str); + if (nxt_slow_path(code != NULL && !PyLong_Check(code))) { + return PyErr_Format(PyExc_TypeError, "'code' is not integer"); + } + + status_code = (code != NULL) ? htons(PyLong_AsLong(code)) + : htons(NXT_WEBSOCKET_CR_NORMAL); + + rc = nxt_unit_websocket_send(ws->req, NXT_WEBSOCKET_OP_CLOSE, + 1, &status_code, 2); + if (nxt_slow_path(rc != NXT_UNIT_OK)) { + return PyErr_Format(PyExc_RuntimeError, + "failed to send close frame"); + } + + } else { + rc = nxt_unit_response_init(ws->req, 403, 0, 0); + if (nxt_slow_path(rc != NXT_UNIT_OK)) { + return PyErr_Format(PyExc_RuntimeError, + "failed to allocate response object"); + } + + rc = nxt_unit_response_send(ws->req); + if (nxt_slow_path(rc != NXT_UNIT_OK)) { + return PyErr_Format(PyExc_RuntimeError, + "failed to send response"); + } + } + + ws->state = NXT_WS_CLOSED; + + Py_INCREF(ws); + + return (PyObject *) ws; +} + + +static PyObject * +nxt_py_asgi_websocket_send_frame(nxt_py_asgi_websocket_t *ws, PyObject *dict) +{ + int rc; + uint8_t opcode; + PyObject *bytes, *text; + const void *buf; + Py_ssize_t buf_size; + + if (nxt_slow_path(ws->state == NXT_WS_INIT)) { + return PyErr_Format(PyExc_RuntimeError, + "WebSocket connect not received"); + } + + if (nxt_slow_path(ws->state == NXT_WS_CONNECT)) { + return PyErr_Format(PyExc_RuntimeError, + "WebSocket not accepted yet"); + } + + if (nxt_slow_path(ws->state == NXT_WS_DISCONNECTED)) { + return PyErr_Format(PyExc_RuntimeError, "WebSocket disconnected"); + } + + if (nxt_slow_path(ws->state == NXT_WS_CLOSED)) { + return PyErr_Format(PyExc_RuntimeError, "WebSocket already closed"); + } + + bytes = PyDict_GetItem(dict, nxt_py_bytes_str); + if (bytes == Py_None) { + bytes = NULL; + } + + if (nxt_slow_path(bytes != NULL && !PyBytes_Check(bytes))) { + return PyErr_Format(PyExc_TypeError, + "'bytes' is not a byte string"); + } + + text = PyDict_GetItem(dict, nxt_py_text_str); + if (text == Py_None) { + text = NULL; + } + + if (nxt_slow_path(text != NULL && !PyUnicode_Check(text))) { + return PyErr_Format(PyExc_TypeError, + "'text' is not a unicode string"); + } + + if (nxt_slow_path(((bytes != NULL) ^ (text != NULL)) == 0)) { + return PyErr_Format(PyExc_ValueError, + "Exactly one of 'bytes' or 'text' must be non-None"); + } + + if (bytes != NULL) { + buf = PyBytes_AS_STRING(bytes); + buf_size = PyBytes_GET_SIZE(bytes); + opcode = NXT_WEBSOCKET_OP_BINARY; + + } else { + buf = PyUnicode_AsUTF8AndSize(text, &buf_size); + opcode = NXT_WEBSOCKET_OP_TEXT; + } + + rc = nxt_unit_websocket_send(ws->req, opcode, 1, buf, buf_size); + if (nxt_slow_path(rc != NXT_UNIT_OK)) { + return PyErr_Format(PyExc_RuntimeError, "failed to send close frame"); + } + + Py_INCREF(ws); + return (PyObject *) ws; +} + + +void +nxt_py_asgi_websocket_handler(nxt_unit_websocket_frame_t *frame) +{ + uint8_t opcode; + uint16_t status_code; + uint64_t rest; + PyObject *msg, *exc; + nxt_py_asgi_websocket_t *ws; + + ws = frame->req->data; + + nxt_unit_req_debug(ws->req, "asgi_websocket_handler"); + + opcode = frame->header->opcode; + if (nxt_slow_path(opcode != NXT_WEBSOCKET_OP_CONT + && opcode != NXT_WEBSOCKET_OP_TEXT + && opcode != NXT_WEBSOCKET_OP_BINARY + && opcode != NXT_WEBSOCKET_OP_CLOSE)) + { + nxt_unit_websocket_done(frame); + + nxt_unit_req_debug(ws->req, + "asgi_websocket_handler: ignore frame with opcode %d", + opcode); + + return; + } + + if (nxt_slow_path(ws->state != NXT_WS_ACCEPTED)) { + nxt_unit_websocket_done(frame); + + goto bad_state; + } + + rest = nxt_py_asgi_ws_max_frame_size - ws->pending_frame_len; + + if (nxt_slow_path(frame->payload_len > rest)) { + nxt_unit_websocket_done(frame); + + goto too_big; + } + + rest = nxt_py_asgi_ws_max_buffer_size - ws->pending_payload_len; + + if (nxt_slow_path(frame->payload_len > rest)) { + nxt_unit_websocket_done(frame); + + goto too_big; + } + + if (ws->receive_future == NULL || frame->header->fin == 0) { + nxt_py_asgi_websocket_suspend_frame(frame); + + return; + } + + if (!nxt_queue_is_empty(&ws->pending_frames)) { + if (nxt_slow_path(opcode == NXT_WEBSOCKET_OP_TEXT + || opcode == NXT_WEBSOCKET_OP_BINARY)) + { + nxt_unit_req_alert(ws->req, + "Invalid state: pending frames with active receiver. " + "CONT frame expected. (%d)", opcode); + + PyErr_SetString(PyExc_AssertionError, + "Invalid state: pending frames with active receiver. " + "CONT frame expected."); + + nxt_unit_websocket_done(frame); + + return; + } + } + + msg = nxt_py_asgi_websocket_pop_msg(ws, frame); + if (nxt_slow_path(msg == NULL)) { + exc = PyErr_Occurred(); + Py_INCREF(exc); + + goto raise; + } + + nxt_py_asgi_websocket_receive_done(ws, msg); + + return; + +bad_state: + + if (ws->receive_future == NULL) { + ws->receive_exc_str = nxt_py_bad_state_str; + + return; + } + + exc = PyObject_CallFunctionObjArgs(PyExc_RuntimeError, + nxt_py_bad_state_str, + NULL); + if (nxt_slow_path(exc == NULL)) { + nxt_unit_req_alert(ws->req, "RuntimeError create failed"); + nxt_python_print_exception(); + + exc = Py_None; + Py_INCREF(exc); + } + + goto raise; + +too_big: + + status_code = htons(NXT_WEBSOCKET_CR_MESSAGE_TOO_BIG); + + (void) nxt_unit_websocket_send(ws->req, NXT_WEBSOCKET_OP_CLOSE, + 1, &status_code, 2); + + ws->state = NXT_WS_CLOSED; + + if (ws->receive_future == NULL) { + ws->receive_exc_str = nxt_py_message_too_big_str; + + return; + } + + exc = PyObject_CallFunctionObjArgs(PyExc_RuntimeError, + nxt_py_message_too_big_str, + NULL); + if (nxt_slow_path(exc == NULL)) { + nxt_unit_req_alert(ws->req, "RuntimeError create failed"); + nxt_python_print_exception(); + + exc = Py_None; + Py_INCREF(exc); + } + +raise: + + nxt_py_asgi_websocket_receive_fail(ws, exc); +} + + +static void +nxt_py_asgi_websocket_receive_done(nxt_py_asgi_websocket_t *ws, PyObject *msg) +{ + PyObject *future, *res; + + future = ws->receive_future; + ws->receive_future = NULL; + + res = PyObject_CallMethodObjArgs(future, nxt_py_set_result_str, msg, NULL); + if (nxt_slow_path(res == NULL)) { + nxt_unit_req_alert(ws->req, "'set_result' call failed"); + nxt_python_print_exception(); + } + + Py_XDECREF(res); + Py_DECREF(future); + + Py_DECREF(msg); +} + + +static void +nxt_py_asgi_websocket_receive_fail(nxt_py_asgi_websocket_t *ws, PyObject *exc) +{ + PyObject *future, *res; + + future = ws->receive_future; + ws->receive_future = NULL; + + res = PyObject_CallMethodObjArgs(future, nxt_py_set_exception_str, exc, + NULL); + if (nxt_slow_path(res == NULL)) { + nxt_unit_req_alert(ws->req, "'set_exception' call failed"); + nxt_python_print_exception(); + } + + Py_XDECREF(res); + Py_DECREF(future); + + Py_DECREF(exc); +} + + +static void +nxt_py_asgi_websocket_suspend_frame(nxt_unit_websocket_frame_t *frame) +{ + int rc; + nxt_py_asgi_websocket_t *ws; + nxt_py_asgi_penging_frame_t *p; + + nxt_unit_req_debug(frame->req, "asgi_websocket_suspend_frame: " + "%d, %"PRIu64", %d", + frame->header->opcode, frame->payload_len, + frame->header->fin); + + ws = frame->req->data; + + rc = nxt_unit_websocket_retain(frame); + if (nxt_slow_path(rc == NXT_UNIT_ERROR)) { + nxt_unit_req_alert(ws->req, "Failed to retain frame for suspension."); + + nxt_unit_websocket_done(frame); + + PyErr_SetString(PyExc_RuntimeError, + "Failed to retain frame for suspension."); + + return; + } + + p = nxt_unit_malloc(frame->req->ctx, sizeof(nxt_py_asgi_penging_frame_t)); + if (nxt_slow_path(p == NULL)) { + nxt_unit_req_alert(ws->req, + "Failed to allocate buffer to suspend frame."); + + nxt_unit_websocket_done(frame); + + PyErr_SetString(PyExc_RuntimeError, + "Failed to allocate buffer to suspend frame."); + + return; + } + + p->frame = frame; + nxt_queue_insert_tail(&ws->pending_frames, &p->link); + + ws->pending_payload_len += frame->payload_len; + ws->pending_fins += frame->header->fin; + + if (frame->header->fin) { + ws->pending_frame_len = 0; + + } else { + if (frame->header->opcode == NXT_WEBSOCKET_OP_CONT) { + ws->pending_frame_len += frame->payload_len; + + } else { + ws->pending_frame_len = frame->payload_len; + } + } +} + + +static PyObject * +nxt_py_asgi_websocket_pop_msg(nxt_py_asgi_websocket_t *ws, + nxt_unit_websocket_frame_t *frame) +{ + int fin; + char *buf; + uint8_t code_buf[2], opcode; + uint16_t code; + PyObject *msg, *data, *type, *data_key; + uint64_t payload_len; + nxt_unit_websocket_frame_t *fin_frame; + + nxt_unit_req_debug(ws->req, "asgi_websocket_pop_msg"); + + fin_frame = NULL; + + if (nxt_queue_is_empty(&ws->pending_frames) + || (frame != NULL + && frame->header->opcode == NXT_WEBSOCKET_OP_CLOSE)) + { + payload_len = frame->payload_len; + + } else { + if (frame != NULL) { + payload_len = ws->pending_payload_len + frame->payload_len; + fin_frame = frame; + + } else { + payload_len = nxt_py_asgi_websocket_pending_len(ws); + } + + frame = nxt_py_asgi_websocket_pop_frame(ws); + } + + opcode = frame->header->opcode; + + if (nxt_slow_path(opcode == NXT_WEBSOCKET_OP_CONT)) { + nxt_unit_req_alert(ws->req, + "Invalid state: attempt to process CONT frame."); + + nxt_unit_websocket_done(frame); + + return PyErr_Format(PyExc_AssertionError, + "Invalid state: attempt to process CONT frame."); + } + + type = nxt_py_websocket_receive_str; + + switch (opcode) { + case NXT_WEBSOCKET_OP_TEXT: + buf = nxt_unit_malloc(frame->req->ctx, payload_len); + if (nxt_slow_path(buf == NULL)) { + nxt_unit_req_alert(ws->req, + "Failed to allocate buffer for payload (%d).", + (int) payload_len); + + nxt_unit_websocket_done(frame); + + return PyErr_Format(PyExc_RuntimeError, + "Failed to allocate buffer for payload (%d).", + (int) payload_len); + } + + data = NULL; + data_key = nxt_py_text_str; + + break; + + case NXT_WEBSOCKET_OP_BINARY: + data = PyBytes_FromStringAndSize(NULL, payload_len); + if (nxt_slow_path(data == NULL)) { + nxt_unit_req_alert(ws->req, + "Failed to create Bytes for payload (%d).", + (int) payload_len); + nxt_python_print_exception(); + + nxt_unit_websocket_done(frame); + + return PyErr_Format(PyExc_RuntimeError, + "Failed to create Bytes for payload."); + } + + buf = (char *) PyBytes_AS_STRING(data); + data_key = nxt_py_bytes_str; + + break; + + case NXT_WEBSOCKET_OP_CLOSE: + if (frame->payload_len >= 2) { + nxt_unit_websocket_read(frame, code_buf, 2); + code = ((uint16_t) code_buf[0]) << 8 | code_buf[1]; + + } else { + code = NXT_WEBSOCKET_CR_NORMAL; + } + + nxt_unit_websocket_done(frame); + + data = PyLong_FromLong(code); + if (nxt_slow_path(data == NULL)) { + nxt_unit_req_alert(ws->req, + "Failed to create Long from code %d.", + (int) code); + nxt_python_print_exception(); + + return PyErr_Format(PyExc_RuntimeError, + "Failed to create Long from code %d.", + (int) code); + } + + buf = NULL; + type = nxt_py_websocket_disconnect_str; + data_key = nxt_py_code_str; + + break; + + default: + nxt_unit_req_alert(ws->req, "Unexpected opcode %d", opcode); + + nxt_unit_websocket_done(frame); + + return PyErr_Format(PyExc_AssertionError, "Unexpected opcode %d", + opcode); + } + + if (buf != NULL) { + fin = frame->header->fin; + buf += nxt_unit_websocket_read(frame, buf, frame->payload_len); + + nxt_unit_websocket_done(frame); + + if (!fin) { + while (!nxt_queue_is_empty(&ws->pending_frames)) { + frame = nxt_py_asgi_websocket_pop_frame(ws); + fin = frame->header->fin; + + buf += nxt_unit_websocket_read(frame, buf, frame->payload_len); + + nxt_unit_websocket_done(frame); + + if (fin) { + break; + } + } + + if (fin_frame != NULL) { + buf += nxt_unit_websocket_read(fin_frame, buf, + fin_frame->payload_len); + nxt_unit_websocket_done(fin_frame); + } + } + + if (opcode == NXT_WEBSOCKET_OP_TEXT) { + buf -= payload_len; + + data = PyUnicode_DecodeUTF8(buf, payload_len, NULL); + + nxt_unit_free(ws->req->ctx, buf); + + if (nxt_slow_path(data == NULL)) { + nxt_unit_req_alert(ws->req, + "Failed to create Unicode for payload (%d).", + (int) payload_len); + nxt_python_print_exception(); + + return PyErr_Format(PyExc_RuntimeError, + "Failed to create Unicode."); + } + } + } + + msg = nxt_py_asgi_new_msg(ws->req, type); + if (nxt_slow_path(msg == NULL)) { + Py_DECREF(data); + return NULL; + } + + if (nxt_slow_path(PyDict_SetItem(msg, data_key, data) == -1)) { + nxt_unit_req_alert(ws->req, "Python failed to set 'msg.data' item"); + + Py_DECREF(msg); + Py_DECREF(data); + + return PyErr_Format(PyExc_RuntimeError, + "Python failed to set 'msg.data' item"); + } + + Py_DECREF(data); + + return msg; +} + + +static uint64_t +nxt_py_asgi_websocket_pending_len(nxt_py_asgi_websocket_t *ws) +{ + uint64_t res; + nxt_py_asgi_penging_frame_t *p; + + res = 0; + + nxt_queue_each(p, &ws->pending_frames, nxt_py_asgi_penging_frame_t, link) { + res += p->frame->payload_len; + + if (p->frame->header->fin) { + nxt_unit_req_debug(ws->req, "asgi_websocket_pending_len: %d", + (int) res); + return res; + } + } nxt_queue_loop; + + nxt_unit_req_debug(ws->req, "asgi_websocket_pending_len: %d (all)", + (int) res); + return res; +} + + +static nxt_unit_websocket_frame_t * +nxt_py_asgi_websocket_pop_frame(nxt_py_asgi_websocket_t *ws) +{ + nxt_queue_link_t *lnk; + nxt_unit_websocket_frame_t *frame; + nxt_py_asgi_penging_frame_t *p; + + lnk = nxt_queue_first(&ws->pending_frames); + nxt_queue_remove(lnk); + + p = nxt_queue_link_data(lnk, nxt_py_asgi_penging_frame_t, link); + + frame = p->frame; + ws->pending_payload_len -= frame->payload_len; + ws->pending_fins -= frame->header->fin; + + nxt_unit_free(frame->req->ctx, p); + + nxt_unit_req_debug(frame->req, "asgi_websocket_pop_frame: " + "%d, %"PRIu64", %d", + frame->header->opcode, frame->payload_len, + frame->header->fin); + + return frame; +} + + +void +nxt_py_asgi_websocket_close_handler(nxt_unit_request_info_t *req) +{ + PyObject *msg, *exc; + nxt_py_asgi_websocket_t *ws; + + ws = req->data; + + nxt_unit_req_debug(req, "asgi_websocket_close_handler"); + + if (ws->receive_future == NULL) { + ws->state = NXT_WS_DISCONNECTED; + + return; + } + + msg = nxt_py_asgi_websocket_disconnect_msg(ws); + if (nxt_slow_path(msg == NULL)) { + exc = PyErr_Occurred(); + Py_INCREF(exc); + + nxt_py_asgi_websocket_receive_fail(ws, exc); + + } else { + nxt_py_asgi_websocket_receive_done(ws, msg); + } +} + + +static PyObject * +nxt_py_asgi_websocket_disconnect_msg(nxt_py_asgi_websocket_t *ws) +{ + PyObject *msg, *code; + + msg = nxt_py_asgi_new_msg(ws->req, nxt_py_websocket_disconnect_str); + if (nxt_slow_path(msg == NULL)) { + return NULL; + } + + code = PyLong_FromLong(NXT_WEBSOCKET_CR_GOING_AWAY); + if (nxt_slow_path(code == NULL)) { + nxt_unit_req_alert(ws->req, "Python failed to create long"); + nxt_python_print_exception(); + + Py_DECREF(msg); + + return PyErr_Format(PyExc_RuntimeError, "failed to create long"); + } + + if (nxt_slow_path(PyDict_SetItem(msg, nxt_py_code_str, code) == -1)) { + nxt_unit_req_alert(ws->req, "Python failed to set 'msg.code' item"); + + Py_DECREF(msg); + Py_DECREF(code); + + return PyErr_Format(PyExc_RuntimeError, + "Python failed to set 'msg.code' item"); + } + + Py_DECREF(code); + + return msg; +} + + +static PyObject * +nxt_py_asgi_websocket_done(PyObject *self, PyObject *future) +{ + int rc; + uint16_t status_code; + PyObject *res; + nxt_py_asgi_websocket_t *ws; + + ws = (nxt_py_asgi_websocket_t *) self; + + nxt_unit_req_debug(ws->req, "asgi_websocket_done: %p", self); + + /* + * Get Future.result() and it raises an exception, if coroutine exited + * with exception. + */ + res = PyObject_CallMethodObjArgs(future, nxt_py_result_str, NULL); + if (nxt_slow_path(res == NULL)) { + nxt_unit_req_error(ws->req, + "Python failed to call 'future.result()'"); + nxt_python_print_exception(); + + rc = NXT_UNIT_ERROR; + + } else { + Py_DECREF(res); + + rc = NXT_UNIT_OK; + } + + if (ws->state == NXT_WS_ACCEPTED) { + status_code = (rc == NXT_UNIT_OK) + ? htons(NXT_WEBSOCKET_CR_NORMAL) + : htons(NXT_WEBSOCKET_CR_INTERNAL_SERVER_ERROR); + + rc = nxt_unit_websocket_send(ws->req, NXT_WEBSOCKET_OP_CLOSE, + 1, &status_code, 2); + } + + while (!nxt_queue_is_empty(&ws->pending_frames)) { + nxt_unit_websocket_done(nxt_py_asgi_websocket_pop_frame(ws)); + } + + nxt_unit_request_done(ws->req, rc); + + Py_RETURN_NONE; +} + + +#endif /* NXT_HAVE_ASGI */ diff --git a/src/nxt_python_wsgi.c b/src/python/nxt_python_wsgi.c index c4b7702e..97030cd3 100644 --- a/src/nxt_python_wsgi.c +++ b/src/python/nxt_python_wsgi.c @@ -8,17 +8,15 @@ #include <Python.h> -#include <compile.h> -#include <node.h> - #include <nxt_main.h> -#include <nxt_runtime.h> #include <nxt_router.h> #include <nxt_unit.h> #include <nxt_unit_field.h> #include <nxt_unit_request.h> #include <nxt_unit_response.h> +#include <python/nxt_python.h> + #include NXT_PYTHON_MOUNTS_H /* @@ -40,23 +38,6 @@ */ -#if PY_MAJOR_VERSION == 3 -#define NXT_PYTHON_BYTES_TYPE "bytestring" - -#define PyString_FromStringAndSize(str, size) \ - PyUnicode_DecodeLatin1((str), (size), "strict") - -#else -#define NXT_PYTHON_BYTES_TYPE "string" - -#define PyBytes_FromStringAndSize PyString_FromStringAndSize -#define PyBytes_Check PyString_Check -#define PyBytes_GET_SIZE PyString_GET_SIZE -#define PyBytes_AS_STRING PyString_AS_STRING -#define PyUnicode_InternInPlace PyString_InternInPlace -#define PyUnicode_AsUTF8 PyString_AS_STRING -#endif - typedef struct nxt_python_run_ctx_s nxt_python_run_ctx_t; typedef struct { @@ -68,18 +49,17 @@ typedef struct { PyObject_HEAD } nxt_py_error_t; -static nxt_int_t nxt_python_start(nxt_task_t *task, - nxt_process_data_t *data); -static nxt_int_t nxt_python_init_strings(void); static void nxt_python_request_handler(nxt_unit_request_info_t *req); -static void nxt_python_atexit(void); static PyObject *nxt_python_create_environ(nxt_task_t *task); static PyObject *nxt_python_get_environ(nxt_python_run_ctx_t *ctx); static int nxt_python_add_sptr(nxt_python_run_ctx_t *ctx, PyObject *name, nxt_unit_sptr_t *sptr, uint32_t size); static int nxt_python_add_field(nxt_python_run_ctx_t *ctx, - nxt_unit_field_t *field); + nxt_unit_field_t *field, int n, uint32_t vl); +static PyObject *nxt_python_field_name(const char *name, uint8_t len); +static PyObject *nxt_python_field_value(nxt_unit_field_t *f, int n, + uint32_t vl); static int nxt_python_add_obj(nxt_python_run_ctx_t *ctx, PyObject *name, PyObject *value); @@ -99,7 +79,6 @@ static PyObject *nxt_py_input_readlines(nxt_py_input_t *self, PyObject *args); static PyObject *nxt_py_input_iter(PyObject *self); static PyObject *nxt_py_input_next(PyObject *self); -static void nxt_python_print_exception(void); static int nxt_python_write(nxt_python_run_ctx_t *ctx, PyObject *bytes); struct nxt_python_run_ctx_s { @@ -109,22 +88,6 @@ struct nxt_python_run_ctx_s { nxt_unit_request_info_t *req; }; -static uint32_t compat[] = { - NXT_VERNUM, NXT_DEBUG, -}; - - -NXT_EXPORT nxt_app_module_t nxt_app_module = { - sizeof(compat), - compat, - nxt_string("python"), - PY_VERSION, - nxt_python_mounts, - nxt_nitems(nxt_python_mounts), - NULL, - nxt_python_start, -}; - static PyMethodDef nxt_py_start_resp_method[] = { {"unit_start_response", nxt_py_start_resp, METH_VARARGS, ""} @@ -158,22 +121,13 @@ static PyTypeObject nxt_py_input_type = { }; -static PyObject *nxt_py_stderr_flush; -static PyObject *nxt_py_application; static PyObject *nxt_py_start_resp_obj; static PyObject *nxt_py_write_obj; static PyObject *nxt_py_environ_ptyp; -#if PY_MAJOR_VERSION == 3 -static wchar_t *nxt_py_home; -#else -static char *nxt_py_home; -#endif - static PyThreadState *nxt_python_thread_state; static nxt_python_run_ctx_t *nxt_python_run_ctx; - static PyObject *nxt_py_80_str; static PyObject *nxt_py_close_str; static PyObject *nxt_py_content_length_str; @@ -191,11 +145,6 @@ static PyObject *nxt_py_server_port_str; static PyObject *nxt_py_server_protocol_str; static PyObject *nxt_py_wsgi_uri_scheme_str; -typedef struct { - nxt_str_t string; - PyObject **object_p; -} nxt_python_string_t; - static nxt_python_string_t nxt_python_strings[] = { { nxt_string("80"), &nxt_py_80_str }, { nxt_string("close"), &nxt_py_close_str }, @@ -213,136 +162,22 @@ static nxt_python_string_t nxt_python_strings[] = { { nxt_string("SERVER_PORT"), &nxt_py_server_port_str }, { nxt_string("SERVER_PROTOCOL"), &nxt_py_server_protocol_str }, { nxt_string("wsgi.url_scheme"), &nxt_py_wsgi_uri_scheme_str }, + { nxt_null_string, NULL }, }; -static nxt_int_t -nxt_python_start(nxt_task_t *task, nxt_process_data_t *data) +nxt_int_t +nxt_python_wsgi_init(nxt_task_t *task, nxt_unit_init_t *init) { - int rc; - char *nxt_py_module; - size_t len; - PyObject *obj, *pypath, *module; - nxt_unit_ctx_t *unit_ctx; - nxt_unit_init_t python_init; - nxt_common_app_conf_t *app_conf; - nxt_python_app_conf_t *c; -#if PY_MAJOR_VERSION == 3 - char *path; - size_t size; - nxt_int_t pep405; - - static const char pyvenv[] = "/pyvenv.cfg"; - static const char bin_python[] = "/bin/python"; -#endif - - app_conf = data->app; - c = &app_conf->u.python; - - if (c->home != NULL) { - len = nxt_strlen(c->home); + PyObject *obj; -#if PY_MAJOR_VERSION == 3 - - path = nxt_malloc(len + sizeof(pyvenv)); - if (nxt_slow_path(path == NULL)) { - nxt_alert(task, "Failed to allocate memory"); - return NXT_ERROR; - } - - nxt_memcpy(path, c->home, len); - nxt_memcpy(path + len, pyvenv, sizeof(pyvenv)); - - pep405 = (access(path, R_OK) == 0); - - nxt_free(path); - - if (pep405) { - size = (len + sizeof(bin_python)) * sizeof(wchar_t); - - } else { - size = (len + 1) * sizeof(wchar_t); - } - - nxt_py_home = nxt_malloc(size); - if (nxt_slow_path(nxt_py_home == NULL)) { - nxt_alert(task, "Failed to allocate memory"); - return NXT_ERROR; - } - - if (pep405) { - mbstowcs(nxt_py_home, c->home, len); - mbstowcs(nxt_py_home + len, bin_python, sizeof(bin_python)); - Py_SetProgramName(nxt_py_home); - - } else { - mbstowcs(nxt_py_home, c->home, len + 1); - Py_SetPythonHome(nxt_py_home); - } - -#else - nxt_py_home = nxt_malloc(len + 1); - if (nxt_slow_path(nxt_py_home == NULL)) { - nxt_alert(task, "Failed to allocate memory"); - return NXT_ERROR; - } - - nxt_memcpy(nxt_py_home, c->home, len + 1); - Py_SetPythonHome(nxt_py_home); -#endif - } - - Py_InitializeEx(0); - - module = NULL; obj = NULL; - if (nxt_slow_path(nxt_python_init_strings() != NXT_OK)) { + if (nxt_slow_path(nxt_python_init_strings(nxt_python_strings) != NXT_OK)) { nxt_alert(task, "Python failed to init string objects"); goto fail; } - obj = PySys_GetObject((char *) "stderr"); - if (nxt_slow_path(obj == NULL)) { - nxt_alert(task, "Python failed to get \"sys.stderr\" object"); - goto fail; - } - - nxt_py_stderr_flush = PyObject_GetAttrString(obj, "flush"); - if (nxt_slow_path(nxt_py_stderr_flush == NULL)) { - nxt_alert(task, "Python failed to get \"flush\" attribute of " - "\"sys.stderr\" object"); - goto fail; - } - - Py_DECREF(obj); - - if (c->path.length > 0) { - obj = PyString_FromStringAndSize((char *) c->path.start, - c->path.length); - - if (nxt_slow_path(obj == NULL)) { - nxt_alert(task, "Python failed to create string object \"%V\"", - &c->path); - goto fail; - } - - pypath = PySys_GetObject((char *) "path"); - - if (nxt_slow_path(pypath == NULL)) { - nxt_alert(task, "Python failed to get \"sys.path\" list"); - goto fail; - } - - if (nxt_slow_path(PyList_Insert(pypath, 0, obj) != 0)) { - nxt_alert(task, "Python failed to insert \"%V\" into \"sys.path\"", - &c->path); - goto fail; - } - - Py_DECREF(obj); - } - obj = PyCFunction_New(nxt_py_start_resp_method, NULL); if (nxt_slow_path(obj == NULL)) { nxt_alert(task, @@ -366,107 +201,43 @@ nxt_python_start(nxt_task_t *task, nxt_process_data_t *data) } nxt_py_environ_ptyp = obj; - - obj = Py_BuildValue("[s]", "unit"); - if (nxt_slow_path(obj == NULL)) { - nxt_alert(task, "Python failed to create the \"sys.argv\" list"); - goto fail; - } - - if (nxt_slow_path(PySys_SetObject((char *) "argv", obj) != 0)) { - nxt_alert(task, "Python failed to set the \"sys.argv\" list"); - goto fail; - } - - Py_CLEAR(obj); - - nxt_py_module = nxt_alloca(c->module.length + 1); - nxt_memcpy(nxt_py_module, c->module.start, c->module.length); - nxt_py_module[c->module.length] = '\0'; - - module = PyImport_ImportModule(nxt_py_module); - if (nxt_slow_path(module == NULL)) { - nxt_alert(task, "Python failed to import module \"%s\"", nxt_py_module); - nxt_python_print_exception(); - goto fail; - } - - obj = PyDict_GetItemString(PyModule_GetDict(module), "application"); - if (nxt_slow_path(obj == NULL)) { - nxt_alert(task, "Python failed to get \"application\" " - "from module \"%s\"", nxt_py_module); - goto fail; - } - - if (nxt_slow_path(PyCallable_Check(obj) == 0)) { - nxt_alert(task, "\"application\" in module \"%s\" " - "is not a callable object", nxt_py_module); - goto fail; - } - - Py_INCREF(obj); - Py_CLEAR(module); - - nxt_py_application = obj; obj = NULL; - nxt_unit_default_init(task, &python_init); - - python_init.callbacks.request_handler = nxt_python_request_handler; - python_init.shm_limit = data->app->shm_limit; - - unit_ctx = nxt_unit_init(&python_init); - if (nxt_slow_path(unit_ctx == NULL)) { - goto fail; - } - - nxt_python_thread_state = PyEval_SaveThread(); - - rc = nxt_unit_run(unit_ctx); - - nxt_unit_done(unit_ctx); - - PyEval_RestoreThread(nxt_python_thread_state); - - nxt_python_atexit(); - - exit(rc); + init->callbacks.request_handler = nxt_python_request_handler; return NXT_OK; fail: Py_XDECREF(obj); - Py_XDECREF(module); - - nxt_python_atexit(); return NXT_ERROR; } -static nxt_int_t -nxt_python_init_strings(void) +int +nxt_python_wsgi_run(nxt_unit_ctx_t *ctx) { - PyObject *obj; - nxt_uint_t i; - nxt_python_string_t *pstr; + int rc; - for (i = 0; i < nxt_nitems(nxt_python_strings); i++) { - pstr = &nxt_python_strings[i]; + nxt_python_thread_state = PyEval_SaveThread(); - obj = PyString_FromStringAndSize((char *) pstr->string.start, - pstr->string.length); - if (nxt_slow_path(obj == NULL)) { - return NXT_ERROR; - } + rc = nxt_unit_run(ctx); - PyUnicode_InternInPlace(&obj); + PyEval_RestoreThread(nxt_python_thread_state); - *pstr->object_p = obj; - } + return rc; +} - return NXT_OK; + +void +nxt_python_wsgi_done(void) +{ + nxt_python_done_strings(nxt_python_strings); + + Py_XDECREF(nxt_py_start_resp_obj); + Py_XDECREF(nxt_py_write_obj); + Py_XDECREF(nxt_py_environ_ptyp); } @@ -597,29 +368,6 @@ done: } -static void -nxt_python_atexit(void) -{ - nxt_uint_t i; - - for (i = 0; i < nxt_nitems(nxt_python_strings); i++) { - Py_XDECREF(*nxt_python_strings[i].object_p); - } - - Py_XDECREF(nxt_py_stderr_flush); - Py_XDECREF(nxt_py_application); - Py_XDECREF(nxt_py_start_resp_obj); - Py_XDECREF(nxt_py_write_obj); - Py_XDECREF(nxt_py_environ_ptyp); - - Py_Finalize(); - - if (nxt_py_home != NULL) { - nxt_free(nxt_py_home); - } -} - - static PyObject * nxt_python_create_environ(nxt_task_t *task) { @@ -749,9 +497,9 @@ static PyObject * nxt_python_get_environ(nxt_python_run_ctx_t *ctx) { int rc; - uint32_t i; + uint32_t i, j, vl; PyObject *environ; - nxt_unit_field_t *f; + nxt_unit_field_t *f, *f2; nxt_unit_request_t *r; environ = PyDict_Copy(nxt_py_environ_ptyp); @@ -803,10 +551,27 @@ nxt_python_get_environ(nxt_python_run_ctx_t *ctx) r->server_name_length)); RC(nxt_python_add_obj(ctx, nxt_py_server_port_str, nxt_py_80_str)); - for (i = 0; i < r->fields_count; i++) { + nxt_unit_request_group_dup_fields(ctx->req); + + for (i = 0; i < r->fields_count;) { f = r->fields + i; + vl = f->value_length; + + for (j = i + 1; j < r->fields_count; j++) { + f2 = r->fields + j; + + if (f2->hash != f->hash + || nxt_unit_sptr_get(&f2->name) != nxt_unit_sptr_get(&f->name)) + { + break; + } - RC(nxt_python_add_field(ctx, f)); + vl += 2 + f2->value_length; + } + + RC(nxt_python_add_field(ctx, f, j - i, vl)); + + i = j; } if (r->content_length_field != NXT_UNIT_NONE_FIELD) { @@ -870,14 +635,15 @@ nxt_python_add_sptr(nxt_python_run_ctx_t *ctx, PyObject *name, static int -nxt_python_add_field(nxt_python_run_ctx_t *ctx, nxt_unit_field_t *field) +nxt_python_add_field(nxt_python_run_ctx_t *ctx, nxt_unit_field_t *field, int n, + uint32_t vl) { char *src; PyObject *name, *value; src = nxt_unit_sptr_get(&field->name); - name = PyString_FromStringAndSize(src, field->name_length); + name = nxt_python_field_name(src, field->name_length); if (nxt_slow_path(name == NULL)) { nxt_unit_req_error(ctx->req, "Python failed to create name string \"%.*s\"", @@ -887,13 +653,13 @@ nxt_python_add_field(nxt_python_run_ctx_t *ctx, nxt_unit_field_t *field) return NXT_UNIT_ERROR; } - src = nxt_unit_sptr_get(&field->value); + value = nxt_python_field_value(field, n, vl); - value = PyString_FromStringAndSize(src, field->value_length); if (nxt_slow_path(value == NULL)) { nxt_unit_req_error(ctx->req, "Python failed to create value string \"%.*s\"", - (int) field->value_length, src); + (int) field->value_length, + (char *) nxt_unit_sptr_get(&field->value)); nxt_python_print_exception(); goto fail; @@ -920,6 +686,80 @@ fail: } +static PyObject * +nxt_python_field_name(const char *name, uint8_t len) +{ + char *p, c; + uint8_t i; + PyObject *res; + +#if PY_MAJOR_VERSION == 3 + res = PyUnicode_New(len + 5, 255); +#else + res = PyString_FromStringAndSize(NULL, len + 5); +#endif + + if (nxt_slow_path(res == NULL)) { + return NULL; + } + + p = PyString_AS_STRING(res); + + p = nxt_cpymem(p, "HTTP_", 5); + + for (i = 0; i < len; i++) { + c = name[i]; + + if (c >= 'a' && c <= 'z') { + *p++ = (c & ~0x20); + continue; + } + + if (c == '-') { + *p++ = '_'; + continue; + } + + *p++ = c; + } + + return res; +} + + +static PyObject * +nxt_python_field_value(nxt_unit_field_t *f, int n, uint32_t vl) +{ + int i; + char *p, *src; + PyObject *res; + +#if PY_MAJOR_VERSION == 3 + res = PyUnicode_New(vl, 255); +#else + res = PyString_FromStringAndSize(NULL, vl); +#endif + + if (nxt_slow_path(res == NULL)) { + return NULL; + } + + p = PyString_AS_STRING(res); + + src = nxt_unit_sptr_get(&f->value); + p = nxt_cpymem(p, src, f->value_length); + + for (i = 1; i < n; i++) { + p = nxt_cpymem(p, ", ", 2); + + src = nxt_unit_sptr_get(&f[i].value); + p = nxt_cpymem(p, src, f[i].value_length); + } + + return res; +} + + static int nxt_python_add_obj(nxt_python_run_ctx_t *ctx, PyObject *name, PyObject *value) { @@ -1386,28 +1226,6 @@ nxt_py_input_next(PyObject *self) } -static void -nxt_python_print_exception(void) -{ - PyErr_Print(); - -#if PY_MAJOR_VERSION == 3 - /* The backtrace may be buffered in sys.stderr file object. */ - { - PyObject *result; - - result = PyObject_CallFunction(nxt_py_stderr_flush, NULL); - if (nxt_slow_path(result == NULL)) { - PyErr_Clear(); - return; - } - - Py_DECREF(result); - } -#endif -} - - static int nxt_python_write(nxt_python_run_ctx_t *ctx, PyObject *bytes) { diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 00000000..b62264ca --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,329 @@ +import fcntl +import os +import platform +import re +import shutil +import signal +import stat +import subprocess +import sys +import tempfile +import time + +import pytest + +from unit.check.go import check_go +from unit.check.node import check_node +from unit.check.tls import check_openssl + + +def pytest_addoption(parser): + parser.addoption( + "--detailed", + default=False, + action="store_true", + help="Detailed output for tests", + ) + parser.addoption( + "--print_log", + default=False, + action="store_true", + help="Print unit.log to stdout in case of errors", + ) + parser.addoption( + "--save_log", + default=False, + action="store_true", + help="Save unit.log after the test execution", + ) + parser.addoption( + "--unsafe", + default=False, + action="store_true", + help="Run unsafe tests", + ) + + +unit_instance = {} +option = None + + +def pytest_configure(config): + global option + option = config.option + + option.generated_tests = {} + option.current_dir = os.path.abspath( + os.path.join(os.path.dirname(__file__), os.pardir) + ) + option.test_dir = option.current_dir + '/test' + option.architecture = platform.architecture()[0] + option.system = platform.system() + + # set stdout to non-blocking + + if option.detailed or option.print_log: + fcntl.fcntl(sys.stdout.fileno(), fcntl.F_SETFL, 0) + + +def pytest_generate_tests(metafunc): + cls = metafunc.cls + if not hasattr(cls, 'application_type'): + return + + type = cls.application_type + + def generate_tests(versions): + metafunc.fixturenames.append('tmp_ct') + metafunc.parametrize('tmp_ct', versions) + + for version in versions: + option.generated_tests[ + metafunc.function.__name__ + '[{}]'.format(version) + ] = (type + ' ' + version) + + # take available module from option and generate tests for each version + + for module, prereq_version in cls.prerequisites['modules'].items(): + if module in option.available['modules']: + available_versions = option.available['modules'][module] + + if prereq_version == 'all': + generate_tests(available_versions) + + elif prereq_version == 'any': + option.generated_tests[metafunc.function.__name__] = ( + type + ' ' + available_versions[0] + ) + elif callable(prereq_version): + generate_tests( + list(filter(prereq_version, available_versions)) + ) + + else: + raise ValueError( + """ +Unexpected prerequisite version "%s" for module "%s" in %s. +'all', 'any' or callable expected.""" + % (str(prereq_version), module, str(cls)) + ) + + +def pytest_sessionstart(session): + option.available = {'modules': {}, 'features': {}} + + unit = unit_run() + + # read unit.log + + for i in range(50): + with open(unit['temp_dir'] + '/unit.log', 'r') as f: + log = f.read() + m = re.search('controller started', log) + + if m is None: + time.sleep(0.1) + else: + break + + if m is None: + _print_log() + exit("Unit is writing log too long") + + # discover available modules from unit.log + + for module in re.findall(r'module: ([a-zA-Z]+) (.*) ".*"$', log, re.M): + if module[0] not in option.available['modules']: + option.available['modules'][module[0]] = [module[1]] + else: + option.available['modules'][module[0]].append(module[1]) + + # discover modules from check + + option.available['modules']['openssl'] = check_openssl(unit['unitd']) + option.available['modules']['go'] = check_go( + option.current_dir, unit['temp_dir'], option.test_dir + ) + option.available['modules']['node'] = check_node(option.current_dir) + + # remove None values + + option.available['modules'] = { + k: v for k, v in option.available['modules'].items() if v is not None + } + + unit_stop() + + +def setup_method(self): + option.skip_alerts = [ + r'read signalfd\(4\) failed', + r'sendmsg.+failed', + r'recvmsg.+failed', + ] + option.skip_sanitizer = False + +def unit_run(): + global unit_instance + build_dir = option.current_dir + '/build' + unitd = build_dir + '/unitd' + + if not os.path.isfile(unitd): + exit('Could not find unit') + + temp_dir = tempfile.mkdtemp(prefix='unit-test-') + public_dir(temp_dir) + + if oct(stat.S_IMODE(os.stat(build_dir).st_mode)) != '0o777': + public_dir(build_dir) + + os.mkdir(temp_dir + '/state') + + with open(temp_dir + '/unit.log', 'w') as log: + unit_instance['process'] = subprocess.Popen( + [ + unitd, + '--no-daemon', + '--modules', + build_dir, + '--state', + temp_dir + '/state', + '--pid', + temp_dir + '/unit.pid', + '--log', + temp_dir + '/unit.log', + '--control', + 'unix:' + temp_dir + '/control.unit.sock', + '--tmp', + temp_dir, + ], + stderr=log, + ) + + if not waitforfiles(temp_dir + '/control.unit.sock'): + _print_log() + exit('Could not start unit') + + # dumb (TODO: remove) + option.skip_alerts = [ + r'read signalfd\(4\) failed', + r'sendmsg.+failed', + r'recvmsg.+failed', + ] + option.skip_sanitizer = False + + unit_instance['temp_dir'] = temp_dir + unit_instance['log'] = temp_dir + '/unit.log' + unit_instance['control_sock'] = temp_dir + '/control.unit.sock' + unit_instance['unitd'] = unitd + + return unit_instance + + +def unit_stop(): + p = unit_instance['process'] + + if p.poll() is not None: + return + + p.send_signal(signal.SIGQUIT) + + try: + retcode = p.wait(15) + if retcode: + return 'Child process terminated with code ' + str(retcode) + except: + p.kill() + return 'Could not terminate unit' + + shutil.rmtree(unit_instance['temp_dir']) + +def public_dir(path): + os.chmod(path, 0o777) + + for root, dirs, files in os.walk(path): + for d in dirs: + os.chmod(os.path.join(root, d), 0o777) + for f in files: + os.chmod(os.path.join(root, f), 0o777) + +def waitforfiles(*files): + for i in range(50): + wait = False + ret = False + + for f in files: + if not os.path.exists(f): + wait = True + break + + if wait: + time.sleep(0.1) + + else: + ret = True + break + + return ret + + +def skip_alert(*alerts): + option.skip_alerts.extend(alerts) + + +def _check_alerts(log): + found = False + + alerts = re.findall(r'.+\[alert\].+', log) + + if alerts: + print('All alerts/sanitizer errors found in log:') + [print(alert) for alert in alerts] + found = True + + if option.skip_alerts: + for skip in option.skip_alerts: + alerts = [al for al in alerts if re.search(skip, al) is None] + + if alerts: + _print_log(data=log) + assert not alerts, 'alert(s)' + + if not option.skip_sanitizer: + sanitizer_errors = re.findall('.+Sanitizer.+', log) + + if sanitizer_errors: + _print_log(data=log) + assert not sanitizer_errors, 'sanitizer error(s)' + + if found: + print('skipped.') + + +def _print_log(path=None, data=None): + if path is None: + path = unit_instance['log'] + + print('Path to unit.log:\n' + path + '\n') + + if option.print_log: + os.set_blocking(sys.stdout.fileno(), True) + sys.stdout.flush() + + if data is None: + with open(path, 'r', encoding='utf-8', errors='ignore') as f: + shutil.copyfileobj(f, sys.stdout) + else: + sys.stdout.write(data) + + +@pytest.fixture +def is_unsafe(request): + return request.config.getoption("--unsafe") + +@pytest.fixture +def is_su(request): + return os.geteuid() == 0 + +def pytest_sessionfinish(session): + unit_stop() diff --git a/test/php/get_variables/index.php b/test/php/get_variables/index.php index dd7ef985..d6eb7d6b 100644 --- a/test/php/get_variables/index.php +++ b/test/php/get_variables/index.php @@ -1,7 +1,7 @@ <?php header('Content-Length: 0'); header('X-Var-1: ' . $_GET['var1']); -header('X-Var-2: ' . $_GET['var2'] . isset($_GET['var2'])); -header('X-Var-3: ' . $_GET['var3'] . isset($_GET['var3'])); -header('X-Var-4: ' . $_GET['var4'] . isset($_GET['var4'])); +header('X-Var-2: ' . (isset($_GET['var2']) ? $_GET['var2'] : 'not set')); +header('X-Var-3: ' . (isset($_GET['var3']) ? $_GET['var3'] : 'not set')); +header('X-Var-4: ' . (isset($_GET['var4']) ? $_GET['var4'] : 'not set')); ?> diff --git a/test/php/list-extensions/index.php b/test/php/list-extensions/index.php new file mode 100644 index 00000000..d6eb40d0 --- /dev/null +++ b/test/php/list-extensions/index.php @@ -0,0 +1,11 @@ +<?php + +function quote($str) { + return '"' . $str . '"'; +} + +header('Content-Type: application/json'); + +print "[" . join(",", array_map('quote', get_loaded_extensions())) . "]"; + +?> diff --git a/test/php/list-extensions/php.ini b/test/php/list-extensions/php.ini new file mode 100644 index 00000000..5848a0f2 --- /dev/null +++ b/test/php/list-extensions/php.ini @@ -0,0 +1 @@ +extension=json.so diff --git a/test/php/post_variables/index.php b/test/php/post_variables/index.php index 5ea17324..8981d54d 100644 --- a/test/php/post_variables/index.php +++ b/test/php/post_variables/index.php @@ -1,6 +1,6 @@ <?php header('Content-Length: 0'); header('X-Var-1: ' . $_POST['var1']); -header('X-Var-2: ' . $_POST['var2'] . isset($_POST['var2'])); -header('X-Var-3: ' . $_POST['var3'] . isset($_POST['var3'])); +header('X-Var-2: ' . (isset($_POST['var2']) ? $_POST['var2'] : 'not set')); +header('X-Var-3: ' . (isset($_POST['var3']) ? $_POST['var3'] : 'not set')); ?> diff --git a/test/pytest.ini b/test/pytest.ini new file mode 100644 index 00000000..c672788a --- /dev/null +++ b/test/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +addopts = -rs -vvv +python_functions = test_* diff --git a/test/python/204_no_content/asgi.py b/test/python/204_no_content/asgi.py new file mode 100644 index 00000000..634facc2 --- /dev/null +++ b/test/python/204_no_content/asgi.py @@ -0,0 +1,8 @@ +async def application(scope, receive, send): + assert scope['type'] == 'http' + + await send({ + 'type': 'http.response.start', + 'status': 204, + 'headers': [], + }) diff --git a/test/python/callable/wsgi.py b/test/python/callable/wsgi.py new file mode 100644 index 00000000..365f82fa --- /dev/null +++ b/test/python/callable/wsgi.py @@ -0,0 +1,7 @@ +def application(env, start_response): + start_response('204', [('Content-Length', '0')]) + return [] + +def app(env, start_response): + start_response('200', [('Content-Length', '0')]) + return [] diff --git a/test/python/delayed/asgi.py b/test/python/delayed/asgi.py new file mode 100644 index 00000000..d5cad929 --- /dev/null +++ b/test/python/delayed/asgi.py @@ -0,0 +1,51 @@ +import asyncio + +async def application(scope, receive, send): + assert scope['type'] == 'http' + + body = b'' + while True: + m = await receive() + body += m.get('body', b'') + if not m.get('more_body', False): + break + + headers = scope.get('headers', []) + + def get_header(n, v=None): + for h in headers: + if h[0] == n: + return h[1] + return v + + parts = int(get_header(b'x-parts', 1)) + delay = int(get_header(b'x-delay', 0)) + + loop = asyncio.get_event_loop() + + async def sleep(n): + future = loop.create_future() + loop.call_later(n, future.set_result, None) + await future + + await send({ + 'type': 'http.response.start', + 'status': 200, + 'headers': [ + (b'content-length', str(len(body)).encode()), + ] + }) + + if not body: + await sleep(delay) + return + + step = int(len(body) / parts) + for i in range(0, len(body), step): + await send({ + 'type': 'http.response.body', + 'body': body[i : i + step], + 'more_body': True, + }) + + await sleep(delay) diff --git a/test/python/empty/asgi.py b/test/python/empty/asgi.py new file mode 100644 index 00000000..58b7c1f2 --- /dev/null +++ b/test/python/empty/asgi.py @@ -0,0 +1,10 @@ +async def application(scope, receive, send): + assert scope['type'] == 'http' + + await send({ + 'type': 'http.response.start', + 'status': 200, + 'headers': [ + (b'content-length', b'0'), + ] + }) diff --git a/test/python/lifespan/empty/asgi.py b/test/python/lifespan/empty/asgi.py new file mode 100644 index 00000000..ea43af13 --- /dev/null +++ b/test/python/lifespan/empty/asgi.py @@ -0,0 +1,27 @@ +import os + + +async def application(scope, receive, send): + if scope['type'] == 'lifespan': + with open('version', 'w+') as f: + f.write( + scope['asgi']['version'] + ' ' + scope['asgi']['spec_version'] + ) + while True: + message = await receive() + if message['type'] == 'lifespan.startup': + os.remove('startup') + await send({'type': 'lifespan.startup.complete'}) + elif message['type'] == 'lifespan.shutdown': + os.remove('shutdown') + await send({'type': 'lifespan.shutdown.complete'}) + return + + if scope['type'] == 'http': + await send( + { + 'type': 'http.response.start', + 'status': 204, + 'headers': [(b'content-length', b'0'),], + } + ) diff --git a/test/python/lifespan/error/asgi.py b/test/python/lifespan/error/asgi.py new file mode 100644 index 00000000..509cb3ee --- /dev/null +++ b/test/python/lifespan/error/asgi.py @@ -0,0 +1,3 @@ +async def application(scope, receive, send): + if scope['type'] != 'http': + raise Exception('Exception blah') diff --git a/test/python/lifespan/error_auto/asgi.py b/test/python/lifespan/error_auto/asgi.py new file mode 100644 index 00000000..90d506a1 --- /dev/null +++ b/test/python/lifespan/error_auto/asgi.py @@ -0,0 +1,2 @@ +async def application(scope, receive, send): + assert scope['type'] == 'http' diff --git a/test/python/lifespan/failed/asgi.py b/test/python/lifespan/failed/asgi.py new file mode 100644 index 00000000..8f315f70 --- /dev/null +++ b/test/python/lifespan/failed/asgi.py @@ -0,0 +1,11 @@ +async def application(scope, receive, send): + if scope['type'] == 'lifespan': + while True: + message = await receive() + if message['type'] == 'lifespan.startup': + await send({"type": "lifespan.startup.failed"}) + raise Exception('Exception blah') + + elif message['type'] == 'lifespan.shutdown': + await send({'type': 'lifespan.shutdown.complete'}) + return diff --git a/test/python/mirror/asgi.py b/test/python/mirror/asgi.py new file mode 100644 index 00000000..7088e893 --- /dev/null +++ b/test/python/mirror/asgi.py @@ -0,0 +1,22 @@ +async def application(scope, receive, send): + assert scope['type'] == 'http' + + body = b'' + while True: + m = await receive() + body += m.get('body', b'') + if not m.get('more_body', False): + break + + await send({ + 'type': 'http.response.start', + 'status': 200, + 'headers': [ + (b'content-length', str(len(body)).encode()), + ] + }) + + await send({ + 'type': 'http.response.body', + 'body': body, + }) diff --git a/test/python/query_string/asgi.py b/test/python/query_string/asgi.py new file mode 100644 index 00000000..28f4d107 --- /dev/null +++ b/test/python/query_string/asgi.py @@ -0,0 +1,11 @@ +async def application(scope, receive, send): + assert scope['type'] == 'http' + + await send({ + 'type': 'http.response.start', + 'status': 200, + 'headers': [ + (b'content-length', b'0'), + (b'query-string', scope['query_string']), + ] + }) diff --git a/test/python/server_port/asgi.py b/test/python/server_port/asgi.py new file mode 100644 index 00000000..e79ced00 --- /dev/null +++ b/test/python/server_port/asgi.py @@ -0,0 +1,11 @@ +async def application(scope, receive, send): + assert scope['type'] == 'http' + + await send({ + 'type': 'http.response.start', + 'status': 200, + 'headers': [ + (b'content-length', b'0'), + (b'server-port', str(scope['server'][1]).encode()), + ] + }) diff --git a/test/python/threading/asgi.py b/test/python/threading/asgi.py new file mode 100644 index 00000000..3c978e50 --- /dev/null +++ b/test/python/threading/asgi.py @@ -0,0 +1,42 @@ +import asyncio +import sys +import time +import threading + + +class Foo(threading.Thread): + num = 10 + + def __init__(self, x): + self.__x = x + threading.Thread.__init__(self) + + def log_index(self, index): + sys.stderr.write( + "(" + str(index) + ") Thread: " + str(self.__x) + "\n" + ) + sys.stderr.flush() + + def run(self): + i = 0 + for _ in range(3): + self.log_index(i) + i += 1 + time.sleep(1) + self.log_index(i) + i += 1 + + +async def application(scope, receive, send): + assert scope['type'] == 'http' + + Foo(Foo.num).start() + Foo.num += 10 + + await send({ + 'type': 'http.response.start', + 'status': 200, + 'headers': [ + (b'content-length', b'0'), + ] + }) diff --git a/test/python/variables/asgi.py b/test/python/variables/asgi.py new file mode 100644 index 00000000..dd1cca72 --- /dev/null +++ b/test/python/variables/asgi.py @@ -0,0 +1,40 @@ +async def application(scope, receive, send): + assert scope['type'] == 'http' + + body = b'' + while True: + m = await receive() + body += m.get('body', b'') + if not m.get('more_body', False): + break + + headers = scope.get('headers', []) + + def get_header(n): + res = [] + for h in headers: + if h[0] == n: + res.append(h[1]) + return b', '.join(res) + + await send({ + 'type': 'http.response.start', + 'status': 200, + 'headers': [ + (b'content-type', get_header(b'content-type')), + (b'content-length', str(len(body)).encode()), + (b'request-method', scope['method'].encode()), + (b'request-uri', scope['path'].encode()), + (b'http-host', get_header(b'host')), + (b'http-version', scope['http_version'].encode()), + (b'asgi-version', scope['asgi']['version'].encode()), + (b'asgi-spec-version', scope['asgi']['spec_version'].encode()), + (b'scheme', scope['scheme'].encode()), + (b'custom-header', get_header(b'custom-header')), + ] + }) + + await send({ + 'type': 'http.response.body', + 'body': body, + }) diff --git a/test/python/websockets/mirror/asgi.py b/test/python/websockets/mirror/asgi.py new file mode 100644 index 00000000..0f1d9953 --- /dev/null +++ b/test/python/websockets/mirror/asgi.py @@ -0,0 +1,18 @@ +async def application(scope, receive, send): + if scope['type'] == 'websocket': + while True: + m = await receive() + if m['type'] == 'websocket.connect': + await send({ + 'type': 'websocket.accept', + }) + + if m['type'] == 'websocket.receive': + await send({ + 'type': 'websocket.send', + 'bytes': m.get('bytes', None), + 'text': m.get('text', None), + }) + + if m['type'] == 'websocket.disconnect': + break; diff --git a/test/python/websockets/subprotocol/asgi.py b/test/python/websockets/subprotocol/asgi.py new file mode 100644 index 00000000..92263dd7 --- /dev/null +++ b/test/python/websockets/subprotocol/asgi.py @@ -0,0 +1,25 @@ +async def application(scope, receive, send): + assert scope['type'] == 'websocket' + + while True: + m = await receive() + if m['type'] == 'websocket.connect': + subprotocols = scope['subprotocols'] + + await send({ + 'type': 'websocket.accept', + 'headers': [ + (b'x-subprotocols', str(subprotocols).encode()), + ], + 'subprotocol': subprotocols[0], + }) + + if m['type'] == 'websocket.receive': + await send({ + 'type': 'websocket.send', + 'bytes': m.get('bytes', None), + 'text': m.get('text', None), + }) + + if m['type'] == 'websocket.disconnect': + break; diff --git a/test/run.py b/test/run.py deleted file mode 100755 index 384663f9..00000000 --- a/test/run.py +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env python3 -import os -import sys -import unittest - -if __name__ == '__main__': - loader = unittest.TestLoader() - suite = unittest.TestSuite() - - this_dir = os.path.dirname(__file__) - tests = loader.discover(start_dir=this_dir) - suite.addTests(tests) - - runner = unittest.TextTestRunner(stream=sys.stdout, verbosity=3) - result = runner.run(suite) - - ret = not (len(result.failures) == len(result.errors) == 0) - - sys.exit(ret) diff --git a/test/test_access_log.py b/test/test_access_log.py index 3ef8f7a0..eaba82ab 100644 --- a/test/test_access_log.py +++ b/test/test_access_log.py @@ -1,5 +1,6 @@ import time -import unittest + +import pytest from unit.applications.lang.python import TestApplicationPython @@ -10,11 +11,9 @@ class TestAccessLog(TestApplicationPython): def load(self, script): super().load(script) - self.assertIn( - 'success', - self.conf('"' + self.testdir + '/access.log"', 'access_log'), - 'access_log configure', - ) + assert 'success' in self.conf( + '"' + self.temp_dir + '/access.log"', 'access_log' + ), 'access_log configure' def wait_for_record(self, pattern, name='access.log'): return super().wait_for_record(pattern, name) @@ -22,7 +21,7 @@ class TestAccessLog(TestApplicationPython): def test_access_log_keepalive(self): self.load('mirror') - self.assertEqual(self.get()['status'], 200, 'init') + assert self.get()['status'] == 200, 'init' (resp, sock) = self.post( headers={ @@ -35,9 +34,9 @@ class TestAccessLog(TestApplicationPython): read_timeout=1, ) - self.assertIsNotNone( - self.wait_for_record(r'"POST / HTTP/1.1" 200 5'), 'keepalive 1' - ) + assert ( + self.wait_for_record(r'"POST / HTTP/1.1" 200 5') is not None + ), 'keepalive 1' resp = self.post( headers={ @@ -51,9 +50,9 @@ class TestAccessLog(TestApplicationPython): self.stop() - self.assertIsNotNone( - self.wait_for_record(r'"POST / HTTP/1.1" 200 10'), 'keepalive 2' - ) + assert ( + self.wait_for_record(r'"POST / HTTP/1.1" 200 10') is not None + ), 'keepalive 2' def test_access_log_pipeline(self): self.load('empty') @@ -79,18 +78,18 @@ Connection: close self.stop() - self.assertIsNotNone( - self.wait_for_record(r'"GET / HTTP/1.1" 200 0 "Referer-1" "-"'), - 'pipeline 1', - ) - self.assertIsNotNone( - self.wait_for_record(r'"GET / HTTP/1.1" 200 0 "Referer-2" "-"'), - 'pipeline 2', - ) - self.assertIsNotNone( - self.wait_for_record(r'"GET / HTTP/1.1" 200 0 "Referer-3" "-"'), - 'pipeline 3', - ) + assert ( + self.wait_for_record(r'"GET / HTTP/1.1" 200 0 "Referer-1" "-"') + is not None + ), 'pipeline 1' + assert ( + self.wait_for_record(r'"GET / HTTP/1.1" 200 0 "Referer-2" "-"') + is not None + ), 'pipeline 2' + assert ( + self.wait_for_record(r'"GET / HTTP/1.1" 200 0 "Referer-3" "-"') + is not None + ), 'pipeline 3' def test_access_log_ipv6(self): self.load('empty') @@ -101,17 +100,17 @@ Connection: close self.stop() - self.assertIsNotNone( + assert ( self.wait_for_record( r'::1 - - \[.+\] "GET / HTTP/1.1" 200 0 "-" "-"' - ), - 'ipv6', - ) + ) + is not None + ), 'ipv6' def test_access_log_unix(self): self.load('empty') - addr = self.testdir + '/sock' + addr = self.temp_dir + '/sock' self.conf( {"unix:" + addr: {"pass": "applications/empty"}}, 'listeners' @@ -121,12 +120,12 @@ Connection: close self.stop() - self.assertIsNotNone( + assert ( self.wait_for_record( r'unix: - - \[.+\] "GET / HTTP/1.1" 200 0 "-" "-"' - ), - 'unix', - ) + ) + is not None + ), 'unix' def test_access_log_referer(self): self.load('empty') @@ -141,12 +140,10 @@ Connection: close self.stop() - self.assertIsNotNone( - self.wait_for_record( - r'"GET / HTTP/1.1" 200 0 "referer-value" "-"' - ), - 'referer', - ) + assert ( + self.wait_for_record(r'"GET / HTTP/1.1" 200 0 "referer-value" "-"') + is not None + ), 'referer' def test_access_log_user_agent(self): self.load('empty') @@ -161,12 +158,12 @@ Connection: close self.stop() - self.assertIsNotNone( + assert ( self.wait_for_record( r'"GET / HTTP/1.1" 200 0 "-" "user-agent-value"' - ), - 'user agent', - ) + ) + is not None + ), 'user agent' def test_access_log_http10(self): self.load('empty') @@ -175,14 +172,14 @@ Connection: close self.stop() - self.assertIsNotNone( - self.wait_for_record(r'"GET / HTTP/1.0" 200 0 "-" "-"'), 'http 1.0' - ) + assert ( + self.wait_for_record(r'"GET / HTTP/1.0" 200 0 "-" "-"') is not None + ), 'http 1.0' def test_access_log_partial(self): self.load('empty') - self.assertEqual(self.post()['status'], 200, 'init') + assert self.post()['status'] == 200, 'init' resp = self.http(b"""GE""", raw=True, read_timeout=1) @@ -190,27 +187,27 @@ Connection: close self.stop() - self.assertIsNotNone( - self.wait_for_record(r'"GE" 400 0 "-" "-"'), 'partial' - ) + assert ( + self.wait_for_record(r'"GE" 400 0 "-" "-"') is not None + ), 'partial' def test_access_log_partial_2(self): self.load('empty') - self.assertEqual(self.post()['status'], 200, 'init') + assert self.post()['status'] == 200, 'init' self.http(b"""GET /\n""", raw=True) self.stop() - self.assertIsNotNone( - self.wait_for_record(r'"GET /" 400 \d+ "-" "-"'), 'partial 2' - ) + assert ( + self.wait_for_record(r'"GET /" 400 \d+ "-" "-"') is not None + ), 'partial 2' def test_access_log_partial_3(self): self.load('empty') - self.assertEqual(self.post()['status'], 200, 'init') + assert self.post()['status'] == 200, 'init' resp = self.http(b"""GET / HTTP/1.1""", raw=True, read_timeout=1) @@ -218,14 +215,14 @@ Connection: close self.stop() - self.assertIsNotNone( - self.wait_for_record(r'"GET /" 400 0 "-" "-"'), 'partial 3' - ) + assert ( + self.wait_for_record(r'"GET /" 400 0 "-" "-"') is not None + ), 'partial 3' def test_access_log_partial_4(self): self.load('empty') - self.assertEqual(self.post()['status'], 200, 'init') + assert self.post()['status'] == 200, 'init' resp = self.http(b"""GET / HTTP/1.1\n""", raw=True, read_timeout=1) @@ -233,25 +230,24 @@ Connection: close self.stop() - self.assertIsNotNone( - self.wait_for_record(r'"GET / HTTP/1.1" 400 0 "-" "-"'), - 'partial 4', - ) + assert ( + self.wait_for_record(r'"GET / HTTP/1.1" 400 0 "-" "-"') is not None + ), 'partial 4' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_access_log_partial_5(self): self.load('empty') - self.assertEqual(self.post()['status'], 200, 'init') + assert self.post()['status'] == 200, 'init' self.get(headers={'Connection': 'close'}) self.stop() - self.assertIsNotNone( - self.wait_for_record(r'"GET / HTTP/1.1" 400 \d+ "-" "-"'), - 'partial 5', - ) + assert ( + self.wait_for_record(r'"GET / HTTP/1.1" 400 \d+ "-" "-"') + is not None + ), 'partial 5' def test_access_log_get_parameters(self): self.load('empty') @@ -260,12 +256,12 @@ Connection: close self.stop() - self.assertIsNotNone( + assert ( self.wait_for_record( r'"GET /\?blah&var=val HTTP/1.1" 200 0 "-" "-"' - ), - 'get parameters', - ) + ) + is not None + ), 'get parameters' def test_access_log_delete(self): self.load('empty') @@ -276,25 +272,20 @@ Connection: close self.stop() - self.assertIsNone( - self.search_in_log(r'/delete', 'access.log'), 'delete' - ) + assert self.search_in_log(r'/delete', 'access.log') is None, 'delete' def test_access_log_change(self): self.load('empty') self.get() - self.conf('"' + self.testdir + '/new.log"', 'access_log') + self.conf('"' + self.temp_dir + '/new.log"', 'access_log') self.get() self.stop() - self.assertIsNotNone( - self.wait_for_record(r'"GET / HTTP/1.1" 200 0 "-" "-"', 'new.log'), - 'change', - ) - -if __name__ == '__main__': - TestAccessLog.main() + assert ( + self.wait_for_record(r'"GET / HTTP/1.1" 200 0 "-" "-"', 'new.log') + is not None + ), 'change' diff --git a/test/test_asgi_application.py b/test/test_asgi_application.py new file mode 100644 index 00000000..948d9823 --- /dev/null +++ b/test/test_asgi_application.py @@ -0,0 +1,402 @@ +import re +import time +from distutils.version import LooseVersion + +import pytest + +from conftest import skip_alert +from unit.applications.lang.python import TestApplicationPython + + +class TestASGIApplication(TestApplicationPython): + prerequisites = {'modules': {'python': + lambda v: LooseVersion(v) >= LooseVersion('3.5')}} + load_module = 'asgi' + + def findall(self, pattern): + with open(self.temp_dir + '/unit.log', 'r', errors='ignore') as f: + return re.findall(pattern, f.read()) + + def test_asgi_application_variables(self): + self.load('variables') + + body = 'Test body string.' + + resp = self.http( + b"""POST / HTTP/1.1 +Host: localhost +Content-Length: %d +Custom-Header: blah +Custom-hEader: Blah +Content-Type: text/html +Connection: close +custom-header: BLAH + +%s""" % (len(body), body.encode()), + raw=True, + ) + + assert resp['status'] == 200, 'status' + headers = resp['headers'] + header_server = headers.pop('Server') + assert re.search(r'Unit/[\d\.]+', header_server), 'server header' + + date = headers.pop('Date') + assert date[-4:] == ' GMT', 'date header timezone' + assert ( + abs(self.date_to_sec_epoch(date) - self.sec_epoch()) < 5 + ), 'date header' + + assert headers == { + 'Connection': 'close', + 'content-length': str(len(body)), + 'content-type': 'text/html', + 'request-method': 'POST', + 'request-uri': '/', + 'http-host': 'localhost', + 'http-version': '1.1', + 'custom-header': 'blah, Blah, BLAH', + 'asgi-version': '3.0', + 'asgi-spec-version': '2.1', + 'scheme': 'http', + }, 'headers' + assert resp['body'] == body, 'body' + + def test_asgi_application_query_string(self): + self.load('query_string') + + resp = self.get(url='/?var1=val1&var2=val2') + + assert ( + resp['headers']['query-string'] == 'var1=val1&var2=val2' + ), 'query-string header' + + def test_asgi_application_query_string_space(self): + self.load('query_string') + + resp = self.get(url='/ ?var1=val1&var2=val2') + assert ( + resp['headers']['query-string'] == 'var1=val1&var2=val2' + ), 'query-string space' + + resp = self.get(url='/ %20?var1=val1&var2=val2') + assert ( + resp['headers']['query-string'] == 'var1=val1&var2=val2' + ), 'query-string space 2' + + resp = self.get(url='/ %20 ?var1=val1&var2=val2') + assert ( + resp['headers']['query-string'] == 'var1=val1&var2=val2' + ), 'query-string space 3' + + resp = self.get(url='/blah %20 blah? var1= val1 & var2=val2') + assert ( + resp['headers']['query-string'] == ' var1= val1 & var2=val2' + ), 'query-string space 4' + + def test_asgi_application_query_string_empty(self): + self.load('query_string') + + resp = self.get(url='/?') + + assert resp['status'] == 200, 'query string empty status' + assert resp['headers']['query-string'] == '', 'query string empty' + + def test_asgi_application_query_string_absent(self): + self.load('query_string') + + resp = self.get() + + assert resp['status'] == 200, 'query string absent status' + assert resp['headers']['query-string'] == '', 'query string absent' + + @pytest.mark.skip('not yet') + def test_asgi_application_server_port(self): + self.load('server_port') + + assert ( + self.get()['headers']['Server-Port'] == '7080' + ), 'Server-Port header' + + @pytest.mark.skip('not yet') + def test_asgi_application_working_directory_invalid(self): + self.load('empty') + + assert 'success' in self.conf( + '"/blah"', 'applications/empty/working_directory' + ), 'configure invalid working_directory' + + assert self.get()['status'] == 500, 'status' + + def test_asgi_application_204_transfer_encoding(self): + self.load('204_no_content') + + assert ( + 'Transfer-Encoding' not in self.get()['headers'] + ), '204 header transfer encoding' + + def test_asgi_application_shm_ack_handle(self): + self.load('mirror') + + # Minimum possible limit + shm_limit = 10 * 1024 * 1024 + + assert ( + 'success' in self.conf('{"shm": ' + str(shm_limit) + '}', + 'applications/mirror/limits') + ) + + # Should exceed shm_limit + max_body_size = 12 * 1024 * 1024 + + assert ( + 'success' in self.conf('{"http":{"max_body_size": ' + + str(max_body_size) + ' }}', + 'settings') + ) + + assert self.get()['status'] == 200, 'init' + + body = '0123456789AB' * 1024 * 1024 # 12 Mb + resp = self.post( + headers={ + 'Host': 'localhost', + 'Connection': 'close', + 'Content-Type': 'text/html', + }, + body=body, + read_buffer_size=1024 * 1024, + ) + + assert resp['body'] == body, 'keep-alive 1' + + def test_asgi_keepalive_body(self): + self.load('mirror') + + assert self.get()['status'] == 200, 'init' + + body = '0123456789' * 500 + (resp, sock) = self.post( + headers={ + 'Host': 'localhost', + 'Connection': 'keep-alive', + 'Content-Type': 'text/html', + }, + start=True, + body=body, + read_timeout=1, + ) + + assert resp['body'] == body, 'keep-alive 1' + + body = '0123456789' + resp = self.post( + headers={ + 'Host': 'localhost', + 'Connection': 'close', + 'Content-Type': 'text/html', + }, + sock=sock, + body=body, + ) + + assert resp['body'] == body, 'keep-alive 2' + + def test_asgi_keepalive_reconfigure(self): + skip_alert( + r'pthread_mutex.+failed', + r'failed to apply', + r'process \d+ exited on signal', + ) + self.load('mirror') + + assert self.get()['status'] == 200, 'init' + + body = '0123456789' + conns = 3 + socks = [] + + for i in range(conns): + (resp, sock) = self.post( + headers={ + 'Host': 'localhost', + 'Connection': 'keep-alive', + 'Content-Type': 'text/html', + }, + start=True, + body=body, + read_timeout=1, + ) + + assert resp['body'] == body, 'keep-alive open' + assert 'success' in self.conf( + str(i + 1), 'applications/mirror/processes' + ), 'reconfigure' + + socks.append(sock) + + for i in range(conns): + (resp, sock) = self.post( + headers={ + 'Host': 'localhost', + 'Connection': 'keep-alive', + 'Content-Type': 'text/html', + }, + start=True, + sock=socks[i], + body=body, + read_timeout=1, + ) + + assert resp['body'] == body, 'keep-alive request' + assert 'success' in self.conf( + str(i + 1), 'applications/mirror/processes' + ), 'reconfigure 2' + + for i in range(conns): + resp = self.post( + headers={ + 'Host': 'localhost', + 'Connection': 'close', + 'Content-Type': 'text/html', + }, + sock=socks[i], + body=body, + ) + + assert resp['body'] == body, 'keep-alive close' + assert 'success' in self.conf( + str(i + 1), 'applications/mirror/processes' + ), 'reconfigure 3' + + def test_asgi_keepalive_reconfigure_2(self): + self.load('mirror') + + assert self.get()['status'] == 200, 'init' + + body = '0123456789' + + (resp, sock) = self.post( + headers={ + 'Host': 'localhost', + 'Connection': 'keep-alive', + 'Content-Type': 'text/html', + }, + start=True, + body=body, + read_timeout=1, + ) + + assert resp['body'] == body, 'reconfigure 2 keep-alive 1' + + self.load('empty') + + assert self.get()['status'] == 200, 'init' + + (resp, sock) = self.post( + headers={ + 'Host': 'localhost', + 'Connection': 'close', + 'Content-Type': 'text/html', + }, + start=True, + sock=sock, + body=body, + ) + + assert resp['status'] == 200, 'reconfigure 2 keep-alive 2' + assert resp['body'] == '', 'reconfigure 2 keep-alive 2 body' + + assert 'success' in self.conf( + {"listeners": {}, "applications": {}} + ), 'reconfigure 2 clear configuration' + + resp = self.get(sock=sock) + + assert resp == {}, 'reconfigure 2 keep-alive 3' + + def test_asgi_keepalive_reconfigure_3(self): + self.load('empty') + + assert self.get()['status'] == 200, 'init' + + (_, sock) = self.http( + b"""GET / HTTP/1.1 +""", + start=True, + raw=True, + no_recv=True, + ) + + assert self.get()['status'] == 200 + + assert 'success' in self.conf( + {"listeners": {}, "applications": {}} + ), 'reconfigure 3 clear configuration' + + resp = self.http( + b"""Host: localhost +Connection: close + +""", + sock=sock, + raw=True, + ) + + assert resp['status'] == 200, 'reconfigure 3' + + def test_asgi_process_switch(self): + self.load('delayed') + + assert 'success' in self.conf( + '2', 'applications/delayed/processes' + ), 'configure 2 processes' + + self.get( + headers={ + 'Host': 'localhost', + 'Content-Length': '0', + 'X-Delay': '5', + 'Connection': 'close', + }, + no_recv=True, + ) + + headers_delay_1 = { + 'Connection': 'close', + 'Host': 'localhost', + 'Content-Length': '0', + 'X-Delay': '1', + } + + self.get(headers=headers_delay_1, no_recv=True) + + time.sleep(0.5) + + for _ in range(10): + self.get(headers=headers_delay_1, no_recv=True) + + self.get(headers=headers_delay_1) + + def test_asgi_application_loading_error(self): + skip_alert(r'Python failed to import module "blah"') + + self.load('empty') + + assert 'success' in self.conf('"blah"', 'applications/empty/module') + + assert self.get()['status'] == 503, 'loading error' + + def test_asgi_application_threading(self): + """wait_for_record() timeouts after 5s while every thread works at + least 3s. So without releasing GIL test should fail. + """ + + self.load('threading') + + for _ in range(10): + self.get(no_recv=True) + + assert ( + self.wait_for_record(r'\(5\) Thread: 100') is not None + ), 'last thread finished' diff --git a/test/test_asgi_lifespan.py b/test/test_asgi_lifespan.py new file mode 100644 index 00000000..c37a1aae --- /dev/null +++ b/test/test_asgi_lifespan.py @@ -0,0 +1,80 @@ +import os +from distutils.version import LooseVersion + +import pytest + +from conftest import option +from conftest import public_dir +from unit.applications.lang.python import TestApplicationPython + + +class TestASGILifespan(TestApplicationPython): + prerequisites = { + 'modules': {'python': lambda v: LooseVersion(v) >= LooseVersion('3.5')} + } + load_module = 'asgi' + + def test_asgi_lifespan(self): + self.load('lifespan/empty') + + startup_path = option.test_dir + '/python/lifespan/empty/startup' + shutdown_path = option.test_dir + '/python/lifespan/empty/shutdown' + version_path = option.test_dir + '/python/lifespan/empty/version' + + os.chmod(option.test_dir + '/python/lifespan/empty', 0o777) + + open(startup_path, 'a').close() + os.chmod(startup_path, 0o777) + + open(shutdown_path, 'a').close() + os.chmod(shutdown_path, 0o777) + + open(version_path, 'a').close() + os.chmod(version_path, 0o777) + + assert self.get()['status'] == 204 + + self.stop() + + is_startup = os.path.isfile(startup_path) + is_shutdown = os.path.isfile(shutdown_path) + + if is_startup: + os.remove(startup_path) + + if is_shutdown: + os.remove(shutdown_path) + + with open(version_path, 'r') as f: + version = f.read() + + os.remove(version_path) + + assert not is_startup, 'startup' + assert not is_shutdown, 'shutdown' + assert version == '3.0 2.0', 'version' + + def test_asgi_lifespan_failed(self): + self.load('lifespan/failed') + + assert self.get()['status'] == 503 + + assert ( + self.wait_for_record(r'\[error\].*Application startup failed') + is not None + ), 'error message' + assert self.wait_for_record(r'Exception blah') is not None, 'exception' + + def test_asgi_lifespan_error(self): + self.load('lifespan/error') + + self.get() + + assert self.wait_for_record(r'Exception blah') is not None, 'exception' + + def test_asgi_lifespan_error_auto(self): + self.load('lifespan/error_auto') + + self.get() + + assert self.wait_for_record(r'AssertionError') is not None, 'assertion' diff --git a/test/test_asgi_websockets.py b/test/test_asgi_websockets.py new file mode 100644 index 00000000..ab49b130 --- /dev/null +++ b/test/test_asgi_websockets.py @@ -0,0 +1,1446 @@ +import struct +import time +from distutils.version import LooseVersion + +import pytest + +from conftest import option +from conftest import skip_alert +from unit.applications.lang.python import TestApplicationPython +from unit.applications.websockets import TestApplicationWebsocket + + +class TestASGIWebsockets(TestApplicationPython): + prerequisites = {'modules': {'python': + lambda v: LooseVersion(v) >= LooseVersion('3.5')}} + load_module = 'asgi' + + ws = TestApplicationWebsocket() + + def setup_method(self): + super().setup_method() + + assert 'success' in self.conf( + {'http': {'websocket': {'keepalive_interval': 0}}}, 'settings' + ), 'clear keepalive_interval' + + skip_alert(r'socket close\(\d+\) failed') + + def close_connection(self, sock): + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty soc' + + self.ws.frame_write(sock, self.ws.OP_CLOSE, self.ws.serialize_close()) + + self.check_close(sock) + + def check_close(self, sock, code=1000, no_close=False): + frame = self.ws.frame_read(sock) + + assert frame['fin'] == True, 'close fin' + assert frame['opcode'] == self.ws.OP_CLOSE, 'close opcode' + assert frame['code'] == code, 'close code' + + if not no_close: + sock.close() + + def check_frame(self, frame, fin, opcode, payload, decode=True): + if opcode == self.ws.OP_BINARY or not decode: + data = frame['data'] + else: + data = frame['data'].decode('utf-8') + + assert frame['fin'] == fin, 'fin' + assert frame['opcode'] == opcode, 'opcode' + assert data == payload, 'payload' + + def test_asgi_websockets_handshake(self): + self.load('websockets/mirror') + + resp, sock, key = self.ws.upgrade() + sock.close() + + assert resp['status'] == 101, 'status' + assert resp['headers']['Upgrade'] == 'websocket', 'upgrade' + assert resp['headers']['Connection'] == 'Upgrade', 'connection' + assert resp['headers']['Sec-WebSocket-Accept'] == self.ws.accept( + key + ), 'key' + + def test_asgi_websockets_subprotocol(self): + self.load('websockets/subprotocol') + + resp, sock, key = self.ws.upgrade() + sock.close() + + assert resp['status'] == 101, 'status' + assert resp['headers']['x-subprotocols'] == "('chat', 'phone', 'video')", 'subprotocols' + assert resp['headers']['sec-websocket-protocol'] == 'chat', 'key' + + def test_asgi_websockets_mirror(self): + self.load('websockets/mirror') + + message = 'blah' + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, message) + frame = self.ws.frame_read(sock) + + assert message == frame['data'].decode('utf-8'), 'mirror' + + self.ws.frame_write(sock, self.ws.OP_TEXT, message) + frame = self.ws.frame_read(sock) + + assert message == frame['data'].decode('utf-8'), 'mirror 2' + + sock.close() + + def test_asgi_websockets_no_mask(self): + self.load('websockets/mirror') + + message = 'blah' + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, message, mask=False) + + frame = self.ws.frame_read(sock) + + assert frame['opcode'] == self.ws.OP_CLOSE, 'no mask opcode' + assert frame['code'] == 1002, 'no mask close code' + + sock.close() + + def test_asgi_websockets_fragmentation(self): + self.load('websockets/mirror') + + message = 'blah' + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, message, fin=False) + self.ws.frame_write(sock, self.ws.OP_CONT, ' ', fin=False) + self.ws.frame_write(sock, self.ws.OP_CONT, message) + + frame = self.ws.frame_read(sock) + + assert message + ' ' + message == frame['data'].decode( + 'utf-8' + ), 'mirror framing' + + sock.close() + + def test_asgi_websockets_length_long(self): + self.load('websockets/mirror') + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment1', fin=False) + self.ws.frame_write( + sock, self.ws.OP_CONT, 'fragment2', length=2**64 - 1 + ) + + self.check_close(sock, 1009) # 1009 - CLOSE_TOO_LARGE + + def test_asgi_websockets_frame_fragmentation_invalid(self): + self.load('websockets/mirror') + + message = 'blah' + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_PING, message, fin=False) + + frame = self.ws.frame_read(sock) + + frame.pop('data') + assert frame == { + 'fin': True, + 'rsv1': False, + 'rsv2': False, + 'rsv3': False, + 'opcode': self.ws.OP_CLOSE, + 'mask': 0, + 'code': 1002, + 'reason': 'Fragmented control frame', + }, 'close frame' + + sock.close() + + def test_asgi_websockets_large(self): + self.load('websockets/mirror') + + message = '0123456789' * 300 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, message) + + frame = self.ws.frame_read(sock) + data = frame['data'].decode('utf-8') + + frame = self.ws.frame_read(sock) + data += frame['data'].decode('utf-8') + + assert message == data, 'large' + + sock.close() + + def test_asgi_websockets_two_clients(self): + self.load('websockets/mirror') + + message1 = 'blah1' + message2 = 'blah2' + + _, sock1, _ = self.ws.upgrade() + _, sock2, _ = self.ws.upgrade() + + self.ws.frame_write(sock1, self.ws.OP_TEXT, message1) + self.ws.frame_write(sock2, self.ws.OP_TEXT, message2) + + frame1 = self.ws.frame_read(sock1) + frame2 = self.ws.frame_read(sock2) + + assert message1 == frame1['data'].decode('utf-8'), 'client 1' + assert message2 == frame2['data'].decode('utf-8'), 'client 2' + + sock1.close() + sock2.close() + + @pytest.mark.skip('not yet') + def test_asgi_websockets_handshake_upgrade_absent( + self + ): # FAIL https://tools.ietf.org/html/rfc6455#section-4.2.1 + self.load('websockets/mirror') + + resp = self.get( + headers={ + 'Host': 'localhost', + 'Connection': 'Upgrade', + 'Sec-WebSocket-Key': self.ws.key(), + 'Sec-WebSocket-Protocol': 'chat', + 'Sec-WebSocket-Version': 13, + }, + ) + + assert resp['status'] == 400, 'upgrade absent' + + def test_asgi_websockets_handshake_case_insensitive(self): + self.load('websockets/mirror') + + resp, sock, _ = self.ws.upgrade( + headers={ + 'Host': 'localhost', + 'Upgrade': 'WEBSOCKET', + 'Connection': 'UPGRADE', + 'Sec-WebSocket-Key': self.ws.key(), + 'Sec-WebSocket-Protocol': 'chat', + 'Sec-WebSocket-Version': 13, + } + ) + sock.close() + + assert resp['status'] == 101, 'status' + + @pytest.mark.skip('not yet') + def test_asgi_websockets_handshake_connection_absent(self): # FAIL + self.load('websockets/mirror') + + resp = self.get( + headers={ + 'Host': 'localhost', + 'Upgrade': 'websocket', + 'Sec-WebSocket-Key': self.ws.key(), + 'Sec-WebSocket-Protocol': 'chat', + 'Sec-WebSocket-Version': 13, + }, + ) + + assert resp['status'] == 400, 'status' + + def test_asgi_websockets_handshake_version_absent(self): + self.load('websockets/mirror') + + resp = self.get( + headers={ + 'Host': 'localhost', + 'Upgrade': 'websocket', + 'Connection': 'Upgrade', + 'Sec-WebSocket-Key': self.ws.key(), + 'Sec-WebSocket-Protocol': 'chat', + }, + ) + + assert resp['status'] == 426, 'status' + + @pytest.mark.skip('not yet') + def test_asgi_websockets_handshake_key_invalid(self): + self.load('websockets/mirror') + + resp = self.get( + headers={ + 'Host': 'localhost', + 'Upgrade': 'websocket', + 'Connection': 'Upgrade', + 'Sec-WebSocket-Key': '!', + 'Sec-WebSocket-Protocol': 'chat', + 'Sec-WebSocket-Version': 13, + }, + ) + + assert resp['status'] == 400, 'key length' + + key = self.ws.key() + resp = self.get( + headers={ + 'Host': 'localhost', + 'Upgrade': 'websocket', + 'Connection': 'Upgrade', + 'Sec-WebSocket-Key': [key, key], + 'Sec-WebSocket-Protocol': 'chat', + 'Sec-WebSocket-Version': 13, + }, + ) + + assert resp['status'] == 400, 'key double' # FAIL https://tools.ietf.org/html/rfc6455#section-11.3.1 + + def test_asgi_websockets_handshake_method_invalid(self): + self.load('websockets/mirror') + + resp = self.post( + headers={ + 'Host': 'localhost', + 'Upgrade': 'websocket', + 'Connection': 'Upgrade', + 'Sec-WebSocket-Key': self.ws.key(), + 'Sec-WebSocket-Protocol': 'chat', + 'Sec-WebSocket-Version': 13, + }, + ) + + assert resp['status'] == 400, 'status' + + def test_asgi_websockets_handshake_http_10(self): + self.load('websockets/mirror') + + resp = self.get( + headers={ + 'Host': 'localhost', + 'Upgrade': 'websocket', + 'Connection': 'Upgrade', + 'Sec-WebSocket-Key': self.ws.key(), + 'Sec-WebSocket-Protocol': 'chat', + 'Sec-WebSocket-Version': 13, + }, + http_10=True, + ) + + assert resp['status'] == 400, 'status' + + def test_asgi_websockets_handshake_uri_invalid(self): + self.load('websockets/mirror') + + resp = self.get( + headers={ + 'Host': 'localhost', + 'Upgrade': 'websocket', + 'Connection': 'Upgrade', + 'Sec-WebSocket-Key': self.ws.key(), + 'Sec-WebSocket-Protocol': 'chat', + 'Sec-WebSocket-Version': 13, + }, + url='!', + ) + + assert resp['status'] == 400, 'status' + + def test_asgi_websockets_protocol_absent(self): + self.load('websockets/mirror') + + key = self.ws.key() + resp, sock, _ = self.ws.upgrade( + headers={ + 'Host': 'localhost', + 'Upgrade': 'websocket', + 'Connection': 'Upgrade', + 'Sec-WebSocket-Key': key, + 'Sec-WebSocket-Version': 13, + } + ) + sock.close() + + assert resp['status'] == 101, 'status' + assert resp['headers']['Upgrade'] == 'websocket', 'upgrade' + assert resp['headers']['Connection'] == 'Upgrade', 'connection' + assert resp['headers']['Sec-WebSocket-Accept'] == self.ws.accept( + key + ), 'key' + + # autobahn-testsuite + # + # Some following tests fail because of Unit does not support UTF-8 + # validation for websocket frames. It should be implemented + # by application, if necessary. + + def test_asgi_websockets_1_1_1__1_1_8(self): + self.load('websockets/mirror') + + opcode = self.ws.OP_TEXT + + _, sock, _ = self.ws.upgrade() + + def check_length(length, chopsize=None): + payload = '*' * length + + self.ws.frame_write(sock, opcode, payload, chopsize=chopsize) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, opcode, payload) + + check_length(0) # 1_1_1 + check_length(125) # 1_1_2 + check_length(126) # 1_1_3 + check_length(127) # 1_1_4 + check_length(128) # 1_1_5 + check_length(65535) # 1_1_6 + check_length(65536) # 1_1_7 + check_length(65536, chopsize = 997) # 1_1_8 + + self.close_connection(sock) + + def test_asgi_websockets_1_2_1__1_2_8(self): + self.load('websockets/mirror') + + opcode = self.ws.OP_BINARY + + _, sock, _ = self.ws.upgrade() + + def check_length(length, chopsize=None): + payload = b'\xfe' * length + + self.ws.frame_write(sock, opcode, payload, chopsize=chopsize) + frame = self.ws.frame_read(sock) + + self.check_frame(frame, True, opcode, payload) + + check_length(0) # 1_2_1 + check_length(125) # 1_2_2 + check_length(126) # 1_2_3 + check_length(127) # 1_2_4 + check_length(128) # 1_2_5 + check_length(65535) # 1_2_6 + check_length(65536) # 1_2_7 + check_length(65536, chopsize = 997) # 1_2_8 + + self.close_connection(sock) + + def test_asgi_websockets_2_1__2_6(self): + self.load('websockets/mirror') + + op_ping = self.ws.OP_PING + op_pong = self.ws.OP_PONG + + _, sock, _ = self.ws.upgrade() + + def check_ping(payload, chopsize=None, decode=True): + self.ws.frame_write(sock, op_ping, payload, chopsize=chopsize) + frame = self.ws.frame_read(sock) + + self.check_frame(frame, True, op_pong, payload, decode=decode) + + check_ping('') # 2_1 + check_ping('Hello, world!') # 2_2 + check_ping(b'\x00\xff\xfe\xfd\xfc\xfb\x00\xff', decode=False) # 2_3 + check_ping(b'\xfe' * 125, decode=False) # 2_4 + check_ping(b'\xfe' * 125, chopsize=1, decode=False) # 2_6 + + self.close_connection(sock) + + # 2_5 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_PING, b'\xfe' * 126) + self.check_close(sock, 1002) + + def test_asgi_websockets_2_7__2_9(self): + self.load('websockets/mirror') + + # 2_7 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_PONG, '') + assert self.recvall(sock, read_timeout=0.1) == b'', '2_7' + + # 2_8 + + self.ws.frame_write(sock, self.ws.OP_PONG, 'unsolicited pong payload') + assert self.recvall(sock, read_timeout=0.1) == b'', '2_8' + + # 2_9 + + payload = 'ping payload' + + self.ws.frame_write(sock, self.ws.OP_PONG, 'unsolicited pong payload') + self.ws.frame_write(sock, self.ws.OP_PING, payload) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_PONG, payload) + + self.close_connection(sock) + + def test_asgi_websockets_2_10__2_11(self): + self.load('websockets/mirror') + + # 2_10 + + _, sock, _ = self.ws.upgrade() + + for i in range(0, 10): + self.ws.frame_write(sock, self.ws.OP_PING, 'payload-%d' % i) + + for i in range(0, 10): + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_PONG, 'payload-%d' % i) + + # 2_11 + + for i in range(0, 10): + opcode = self.ws.OP_PING + self.ws.frame_write(sock, opcode, 'payload-%d' % i, chopsize=1) + + for i in range(0, 10): + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_PONG, 'payload-%d' % i) + + self.close_connection(sock) + + @pytest.mark.skip('not yet') + def test_asgi_websockets_3_1__3_7(self): + self.load('websockets/mirror') + + payload = 'Hello, world!' + + # 3_1 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, payload, rsv1=True) + self.check_close(sock, 1002) + + # 3_2 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, payload) + self.ws.frame_write(sock, self.ws.OP_TEXT, payload, rsv2=True) + self.ws.frame_write(sock, self.ws.OP_PING, '') + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, payload) + + self.check_close(sock, 1002, no_close=True) + + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty 3_2' + sock.close() + + # 3_3 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, payload) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, payload) + + self.ws.frame_write( + sock, self.ws.OP_TEXT, payload, rsv1=True, rsv2=True + ) + + self.check_close(sock, 1002, no_close=True) + + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty 3_3' + sock.close() + + # 3_4 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, payload, chopsize=1) + self.ws.frame_write( + sock, self.ws.OP_TEXT, payload, rsv3=True, chopsize=1 + ) + self.ws.frame_write(sock, self.ws.OP_PING, '') + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, payload) + + self.check_close(sock, 1002, no_close=True) + + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty 3_4' + sock.close() + + # 3_5 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write( + sock, + self.ws.OP_BINARY, + b'\x00\xff\xfe\xfd\xfc\xfb\x00\xff', + rsv1=True, + rsv3=True, + ) + + self.check_close(sock, 1002) + + # 3_6 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write( + sock, self.ws.OP_PING, payload, rsv2=True, rsv3=True + ) + + self.check_close(sock, 1002) + + # 3_7 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write( + sock, self.ws.OP_CLOSE, payload, rsv1=True, rsv2=True, rsv3=True + ) + + self.check_close(sock, 1002) + + def test_asgi_websockets_4_1_1__4_2_5(self): + self.load('websockets/mirror') + + payload = 'Hello, world!' + + # 4_1_1 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, 0x03, '') + self.check_close(sock, 1002) + + # 4_1_2 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, 0x04, 'reserved opcode payload') + self.check_close(sock, 1002) + + # 4_1_3 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, payload) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, payload) + + self.ws.frame_write(sock, 0x05, '') + self.ws.frame_write(sock, self.ws.OP_PING, '') + + self.check_close(sock, 1002) + + # 4_1_4 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, payload) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, payload) + + self.ws.frame_write(sock, 0x06, payload) + self.ws.frame_write(sock, self.ws.OP_PING, '') + + self.check_close(sock, 1002) + + # 4_1_5 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, payload, chopsize=1) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, payload) + + self.ws.frame_write(sock, 0x07, payload, chopsize=1) + self.ws.frame_write(sock, self.ws.OP_PING, '') + + self.check_close(sock, 1002) + + # 4_2_1 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, 0x0B, '') + self.check_close(sock, 1002) + + # 4_2_2 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, 0x0C, 'reserved opcode payload') + self.check_close(sock, 1002) + + # 4_2_3 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, payload) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, payload) + + self.ws.frame_write(sock, 0x0D, '') + self.ws.frame_write(sock, self.ws.OP_PING, '') + + self.check_close(sock, 1002) + + # 4_2_4 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, payload) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, payload) + + self.ws.frame_write(sock, 0x0E, payload) + self.ws.frame_write(sock, self.ws.OP_PING, '') + + self.check_close(sock, 1002) + + # 4_2_5 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, payload, chopsize=1) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, payload) + + self.ws.frame_write(sock, 0x0F, payload, chopsize=1) + self.ws.frame_write(sock, self.ws.OP_PING, '') + + self.check_close(sock, 1002) + + def test_asgi_websockets_5_1__5_20(self): + self.load('websockets/mirror') + + # 5_1 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_PING, 'fragment1', fin=False) + self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment2', fin=True) + self.check_close(sock, 1002) + + # 5_2 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_PONG, 'fragment1', fin=False) + self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment2', fin=True) + self.check_close(sock, 1002) + + # 5_3 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment1', fin=False) + self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment2', fin=True) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, 'fragment1fragment2') + + # 5_4 + + self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment1', fin=False) + assert self.recvall(sock, read_timeout=0.1) == b'', '5_4' + self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment2', fin=True) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, 'fragment1fragment2') + + # 5_5 + + self.ws.frame_write( + sock, self.ws.OP_TEXT, 'fragment1', fin=False, chopsize=1 + ) + self.ws.frame_write( + sock, self.ws.OP_CONT, 'fragment2', fin=True, chopsize=1 + ) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, 'fragment1fragment2') + + # 5_6 + + ping_payload = 'ping payload' + + self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment1', fin=False) + self.ws.frame_write(sock, self.ws.OP_PING, ping_payload) + self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment2', fin=True) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_PONG, ping_payload) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, 'fragment1fragment2') + + # 5_7 + + ping_payload = 'ping payload' + + self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment1', fin=False) + assert self.recvall(sock, read_timeout=0.1) == b'', '5_7' + + self.ws.frame_write(sock, self.ws.OP_PING, ping_payload) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_PONG, ping_payload) + + self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment2', fin=True) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, 'fragment1fragment2') + + # 5_8 + + ping_payload = 'ping payload' + + self.ws.frame_write( + sock, self.ws.OP_TEXT, 'fragment1', fin=False, chopsize=1 + ) + self.ws.frame_write(sock, self.ws.OP_PING, ping_payload, chopsize=1) + self.ws.frame_write( + sock, self.ws.OP_CONT, 'fragment2', fin=True, chopsize=1 + ) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_PONG, ping_payload) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, 'fragment1fragment2') + + # 5_9 + + self.ws.frame_write( + sock, self.ws.OP_CONT, 'non-continuation payload', fin=True + ) + self.ws.frame_write(sock, self.ws.OP_TEXT, 'Hello, world!', fin=True) + self.check_close(sock, 1002) + + # 5_10 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write( + sock, self.ws.OP_CONT, 'non-continuation payload', fin=True + ) + self.ws.frame_write(sock, self.ws.OP_TEXT, 'Hello, world!', fin=True) + self.check_close(sock, 1002) + + # 5_11 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write( + sock, + self.ws.OP_CONT, + 'non-continuation payload', + fin=True, + chopsize=1, + ) + self.ws.frame_write( + sock, self.ws.OP_TEXT, 'Hello, world!', fin=True, chopsize=1 + ) + self.check_close(sock, 1002) + + # 5_12 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write( + sock, self.ws.OP_CONT, 'non-continuation payload', fin=False + ) + self.ws.frame_write(sock, self.ws.OP_TEXT, 'Hello, world!', fin=True) + self.check_close(sock, 1002) + + # 5_13 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write( + sock, self.ws.OP_CONT, 'non-continuation payload', fin=False + ) + self.ws.frame_write(sock, self.ws.OP_TEXT, 'Hello, world!', fin=True) + self.check_close(sock, 1002) + + # 5_14 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write( + sock, + self.ws.OP_CONT, + 'non-continuation payload', + fin=False, + chopsize=1, + ) + self.ws.frame_write( + sock, self.ws.OP_TEXT, 'Hello, world!', fin=True, chopsize=1 + ) + self.check_close(sock, 1002) + + # 5_15 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment1', fin=False) + self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment2', fin=True) + self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment3', fin=False) + self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment4', fin=True) + self.check_close(sock, 1002) + + # 5_16 + + _, sock, _ = self.ws.upgrade() + + for i in range(0, 2): + self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment1', fin=False) + self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment2', fin=False) + self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment3', fin=True) + self.check_close(sock, 1002) + + # 5_17 + + _, sock, _ = self.ws.upgrade() + + for i in range(0, 2): + self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment1', fin=True) + self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment2', fin=False) + self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment3', fin=True) + self.check_close(sock, 1002) + + # 5_18 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment1', fin=False) + self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment2') + self.check_close(sock, 1002) + + # 5_19 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment1', fin=False) + self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment2', fin=False) + self.ws.frame_write(sock, self.ws.OP_PING, 'pongme 1!') + + time.sleep(1) + + self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment3', fin=False) + self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment4', fin=False) + self.ws.frame_write(sock, self.ws.OP_PING, 'pongme 2!') + self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment5') + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_PONG, 'pongme 1!') + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_PONG, 'pongme 2!') + + self.check_frame( + self.ws.frame_read(sock), + True, + self.ws.OP_TEXT, + 'fragment1fragment2fragment3fragment4fragment5', + ) + + # 5_20 + + self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment1', fin=False) + self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment2', fin=False) + self.ws.frame_write(sock, self.ws.OP_PING, 'pongme 1!') + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_PONG, 'pongme 1!') + + time.sleep(1) + + self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment3', fin=False) + self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment4', fin=False) + self.ws.frame_write(sock, self.ws.OP_PING, 'pongme 2!') + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_PONG, 'pongme 2!') + + assert self.recvall(sock, read_timeout=0.1) == b'', '5_20' + self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment5') + + self.check_frame( + self.ws.frame_read(sock), + True, + self.ws.OP_TEXT, + 'fragment1fragment2fragment3fragment4fragment5', + ) + + self.close_connection(sock) + + def test_asgi_websockets_6_1_1__6_4_4(self): + self.load('websockets/mirror') + + # 6_1_1 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, '') + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, '') + + # 6_1_2 + + self.ws.frame_write(sock, self.ws.OP_TEXT, '', fin=False) + self.ws.frame_write(sock, self.ws.OP_CONT, '', fin=False) + self.ws.frame_write(sock, self.ws.OP_CONT, '') + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, '') + + # 6_1_3 + + payload = 'middle frame payload' + + self.ws.frame_write(sock, self.ws.OP_TEXT, '', fin=False) + self.ws.frame_write(sock, self.ws.OP_CONT, payload, fin=False) + self.ws.frame_write(sock, self.ws.OP_CONT, '') + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, payload) + + # 6_2_1 + + payload = 'Hello-µ@ßöäüàá-UTF-8!!' + + self.ws.frame_write(sock, self.ws.OP_TEXT, payload) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, payload) + + # 6_2_2 + + self.ws.frame_write(sock, self.ws.OP_TEXT, payload[:12], fin=False) + self.ws.frame_write(sock, self.ws.OP_CONT, payload[12:]) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, payload) + + # 6_2_3 + + self.ws.message(sock, self.ws.OP_TEXT, payload, fragmention_size=1) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, payload) + + # 6_2_4 + + payload = '\xce\xba\xe1\xbd\xb9\xcf\x83\xce\xbc\xce\xb5' + + self.ws.message(sock, self.ws.OP_TEXT, payload, fragmention_size=1) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, payload) + + self.close_connection(sock) + +# Unit does not support UTF-8 validation +# +# # 6_3_1 FAIL +# +# payload_1 = '\xce\xba\xe1\xbd\xb9\xcf\x83\xce\xbc\xce\xb5' +# payload_2 = '\xed\xa0\x80' +# payload_3 = '\x65\x64\x69\x74\x65\x64' +# +# payload = payload_1 + payload_2 + payload_3 +# +# self.ws.message(sock, self.ws.OP_TEXT, payload) +# self.check_close(sock, 1007) +# +# # 6_3_2 FAIL +# +# _, sock, _ = self.ws.upgrade() +# +# self.ws.message(sock, self.ws.OP_TEXT, payload, fragmention_size=1) +# self.check_close(sock, 1007) +# +# # 6_4_1 ... 6_4_4 FAIL + + def test_asgi_websockets_7_1_1__7_5_1(self): + self.load('websockets/mirror') + + # 7_1_1 + + _, sock, _ = self.ws.upgrade() + + payload = "Hello World!" + + self.ws.frame_write(sock, self.ws.OP_TEXT, payload) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, payload) + + self.close_connection(sock) + + # 7_1_2 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_CLOSE, self.ws.serialize_close()) + self.ws.frame_write(sock, self.ws.OP_CLOSE, self.ws.serialize_close()) + + self.check_close(sock) + + # 7_1_3 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_CLOSE, self.ws.serialize_close()) + self.check_close(sock, no_close=True) + + self.ws.frame_write(sock, self.ws.OP_PING, '') + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty soc' + + sock.close() + + # 7_1_4 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_CLOSE, self.ws.serialize_close()) + self.check_close(sock, no_close=True) + + self.ws.frame_write(sock, self.ws.OP_TEXT, payload) + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty soc' + + sock.close() + + # 7_1_5 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment1', fin=False) + self.ws.frame_write(sock, self.ws.OP_CLOSE, self.ws.serialize_close()) + self.check_close(sock, no_close=True) + + self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment2') + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty soc' + + sock.close() + + # 7_1_6 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, 'BAsd7&jh23' * 26 * 2 ** 10) + self.ws.frame_write(sock, self.ws.OP_TEXT, payload) + self.ws.frame_write(sock, self.ws.OP_CLOSE, self.ws.serialize_close()) + + self.recvall(sock, read_timeout=1) + + self.ws.frame_write(sock, self.ws.OP_PING, '') + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty soc' + + sock.close() + + # 7_3_1 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_CLOSE, '') + self.check_close(sock) + + # 7_3_2 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_CLOSE, 'a') + self.check_close(sock, 1002) + + # 7_3_3 + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_CLOSE, self.ws.serialize_close()) + self.check_close(sock) + + # 7_3_4 + + _, sock, _ = self.ws.upgrade() + + payload = self.ws.serialize_close(reason='Hello World!') + + self.ws.frame_write(sock, self.ws.OP_CLOSE, payload) + self.check_close(sock) + + # 7_3_5 + + _, sock, _ = self.ws.upgrade() + + payload = self.ws.serialize_close(reason='*' * 123) + + self.ws.frame_write(sock, self.ws.OP_CLOSE, payload) + self.check_close(sock) + + # 7_3_6 + + _, sock, _ = self.ws.upgrade() + + payload = self.ws.serialize_close(reason='*' * 124) + + self.ws.frame_write(sock, self.ws.OP_CLOSE, payload) + self.check_close(sock, 1002) + +# # 7_5_1 FAIL Unit does not support UTF-8 validation +# +# _, sock, _ = self.ws.upgrade() +# +# payload = self.ws.serialize_close(reason = '\xce\xba\xe1\xbd\xb9\xcf' \ +# '\x83\xce\xbc\xce\xb5\xed\xa0\x80\x65\x64\x69\x74\x65\x64') +# +# self.ws.frame_write(sock, self.ws.OP_CLOSE, payload) +# self.check_close(sock, 1007) + + def test_asgi_websockets_7_7_X__7_9_X(self): + self.load('websockets/mirror') + + valid_codes = [ + 1000, + 1001, + 1002, + 1003, + 1007, + 1008, + 1009, + 1010, + 1011, + 3000, + 3999, + 4000, + 4999, + ] + + invalid_codes = [0, 999, 1004, 1005, 1006, 1016, 1100, 2000, 2999] + + for code in valid_codes: + _, sock, _ = self.ws.upgrade() + + payload = self.ws.serialize_close(code=code) + + self.ws.frame_write(sock, self.ws.OP_CLOSE, payload) + self.check_close(sock) + + for code in invalid_codes: + _, sock, _ = self.ws.upgrade() + + payload = self.ws.serialize_close(code=code) + + self.ws.frame_write(sock, self.ws.OP_CLOSE, payload) + self.check_close(sock, 1002) + + def test_asgi_websockets_7_13_1__7_13_2(self): + self.load('websockets/mirror') + + # 7_13_1 + + _, sock, _ = self.ws.upgrade() + + payload = self.ws.serialize_close(code=5000) + + self.ws.frame_write(sock, self.ws.OP_CLOSE, payload) + self.check_close(sock, 1002) + + # 7_13_2 + + _, sock, _ = self.ws.upgrade() + + payload = struct.pack('!I', 65536) + ''.encode('utf-8') + + self.ws.frame_write(sock, self.ws.OP_CLOSE, payload) + self.check_close(sock, 1002) + + def test_asgi_websockets_9_1_1__9_6_6(self, is_unsafe): + if not is_unsafe: + pytest.skip('unsafe, long run') + + self.load('websockets/mirror') + + assert 'success' in self.conf( + { + 'http': { + 'websocket': { + 'max_frame_size': 33554432, + 'keepalive_interval': 0, + } + } + }, + 'settings', + ), 'increase max_frame_size and keepalive_interval' + + _, sock, _ = self.ws.upgrade() + + op_text = self.ws.OP_TEXT + op_binary = self.ws.OP_BINARY + + def check_payload(opcode, length, chopsize=None): + if opcode == self.ws.OP_TEXT: + payload = '*' * length + else: + payload = b'*' * length + + self.ws.frame_write(sock, opcode, payload, chopsize=chopsize) + frame = self.ws.frame_read(sock, read_timeout=5) + self.check_frame(frame, True, opcode, payload) + + def check_message(opcode, f_size): + if opcode == self.ws.OP_TEXT: + payload = '*' * 4 * 2 ** 20 + else: + payload = b'*' * 4 * 2 ** 20 + + self.ws.message(sock, opcode, payload, fragmention_size=f_size) + frame = self.ws.frame_read(sock, read_timeout=5) + self.check_frame(frame, True, opcode, payload) + + check_payload(op_text, 64 * 2 ** 10) # 9_1_1 + check_payload(op_text, 256 * 2 ** 10) # 9_1_2 + check_payload(op_text, 2 ** 20) # 9_1_3 + check_payload(op_text, 4 * 2 ** 20) # 9_1_4 + check_payload(op_text, 8 * 2 ** 20) # 9_1_5 + check_payload(op_text, 16 * 2 ** 20) # 9_1_6 + + check_payload(op_binary, 64 * 2 ** 10) # 9_2_1 + check_payload(op_binary, 256 * 2 ** 10) # 9_2_2 + check_payload(op_binary, 2 ** 20) # 9_2_3 + check_payload(op_binary, 4 * 2 ** 20) # 9_2_4 + check_payload(op_binary, 8 * 2 ** 20) # 9_2_5 + check_payload(op_binary, 16 * 2 ** 20) # 9_2_6 + + if option.system != 'Darwin' and option.system != 'FreeBSD': + check_message(op_text, 64) # 9_3_1 + check_message(op_text, 256) # 9_3_2 + check_message(op_text, 2 ** 10) # 9_3_3 + check_message(op_text, 4 * 2 ** 10) # 9_3_4 + check_message(op_text, 16 * 2 ** 10) # 9_3_5 + check_message(op_text, 64 * 2 ** 10) # 9_3_6 + check_message(op_text, 256 * 2 ** 10) # 9_3_7 + check_message(op_text, 2 ** 20) # 9_3_8 + check_message(op_text, 4 * 2 ** 20) # 9_3_9 + + check_message(op_binary, 64) # 9_4_1 + check_message(op_binary, 256) # 9_4_2 + check_message(op_binary, 2 ** 10) # 9_4_3 + check_message(op_binary, 4 * 2 ** 10) # 9_4_4 + check_message(op_binary, 16 * 2 ** 10) # 9_4_5 + check_message(op_binary, 64 * 2 ** 10) # 9_4_6 + check_message(op_binary, 256 * 2 ** 10) # 9_4_7 + check_message(op_binary, 2 ** 20) # 9_4_8 + check_message(op_binary, 4 * 2 ** 20) # 9_4_9 + + check_payload(op_text, 2 ** 20, chopsize=64) # 9_5_1 + check_payload(op_text, 2 ** 20, chopsize=128) # 9_5_2 + check_payload(op_text, 2 ** 20, chopsize=256) # 9_5_3 + check_payload(op_text, 2 ** 20, chopsize=512) # 9_5_4 + check_payload(op_text, 2 ** 20, chopsize=1024) # 9_5_5 + check_payload(op_text, 2 ** 20, chopsize=2048) # 9_5_6 + + check_payload(op_binary, 2 ** 20, chopsize=64) # 9_6_1 + check_payload(op_binary, 2 ** 20, chopsize=128) # 9_6_2 + check_payload(op_binary, 2 ** 20, chopsize=256) # 9_6_3 + check_payload(op_binary, 2 ** 20, chopsize=512) # 9_6_4 + check_payload(op_binary, 2 ** 20, chopsize=1024) # 9_6_5 + check_payload(op_binary, 2 ** 20, chopsize=2048) # 9_6_6 + + self.close_connection(sock) + + def test_asgi_websockets_10_1_1(self): + self.load('websockets/mirror') + + _, sock, _ = self.ws.upgrade() + + payload = '*' * 65536 + + self.ws.message(sock, self.ws.OP_TEXT, payload, fragmention_size=1300) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_TEXT, payload) + + self.close_connection(sock) + + # settings + + def test_asgi_websockets_max_frame_size(self): + self.load('websockets/mirror') + + assert 'success' in self.conf( + {'http': {'websocket': {'max_frame_size': 100}}}, 'settings' + ), 'configure max_frame_size' + + _, sock, _ = self.ws.upgrade() + + payload = '*' * 94 + opcode = self.ws.OP_TEXT + + self.ws.frame_write(sock, opcode, payload) # frame length is 100 + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, opcode, payload) + + payload = '*' * 95 + + self.ws.frame_write(sock, opcode, payload) # frame length is 101 + self.check_close(sock, 1009) # 1009 - CLOSE_TOO_LARGE + + def test_asgi_websockets_read_timeout(self): + self.load('websockets/mirror') + + assert 'success' in self.conf( + {'http': {'websocket': {'read_timeout': 5}}}, 'settings' + ), 'configure read_timeout' + + _, sock, _ = self.ws.upgrade() + + frame = self.ws.frame_to_send(self.ws.OP_TEXT, 'blah') + sock.sendall(frame[:2]) + + time.sleep(2) + + self.check_close(sock, 1001) # 1001 - CLOSE_GOING_AWAY + + def test_asgi_websockets_keepalive_interval(self): + self.load('websockets/mirror') + + assert 'success' in self.conf( + {'http': {'websocket': {'keepalive_interval': 5}}}, 'settings' + ), 'configure keepalive_interval' + + _, sock, _ = self.ws.upgrade() + + frame = self.ws.frame_to_send(self.ws.OP_TEXT, 'blah') + sock.sendall(frame[:2]) + + time.sleep(2) + + frame = self.ws.frame_read(sock) + self.check_frame(frame, True, self.ws.OP_PING, '') # PING frame + + sock.close() diff --git a/test/test_configuration.py b/test/test_configuration.py index 0b0c9c78..d1e6f000 100644 --- a/test/test_configuration.py +++ b/test/test_configuration.py @@ -1,5 +1,6 @@ -import unittest +import pytest +from conftest import skip_alert from unit.control import TestControl @@ -7,16 +8,14 @@ class TestConfiguration(TestControl): prerequisites = {'modules': {'python': 'any'}} def test_json_empty(self): - self.assertIn('error', self.conf(''), 'empty') + assert 'error' in self.conf(''), 'empty' def test_json_leading_zero(self): - self.assertIn('error', self.conf('00'), 'leading zero') + assert 'error' in self.conf('00'), 'leading zero' def test_json_unicode(self): - self.assertIn( - 'success', - self.conf( - b""" + assert 'success' in self.conf( + u""" { "ap\u0070": { "type": "\u0070ython", @@ -26,50 +25,36 @@ class TestConfiguration(TestControl): } } """, - 'applications', - ), - 'unicode', - ) + 'applications', + ), 'unicode' + + assert self.conf_get('applications') == { + "app": { + "type": "python", + "processes": {"spare": 0}, + "path": "/app", + "module": "wsgi", + } + }, 'unicode get' - self.assertDictEqual( - self.conf_get('applications'), + def test_json_unicode_2(self): + assert 'success' in self.conf( { - "app": { + "приложение": { "type": "python", "processes": {"spare": 0}, "path": "/app", "module": "wsgi", } }, - 'unicode get', - ) + 'applications', + ), 'unicode 2' - def test_json_unicode_2(self): - self.assertIn( - 'success', - self.conf( - { - "приложение": { - "type": "python", - "processes": {"spare": 0}, - "path": "/app", - "module": "wsgi", - } - }, - 'applications', - ), - 'unicode 2', - ) - - self.assertIn( - 'приложение', self.conf_get('applications'), 'unicode 2 get' - ) + assert 'приложение' in self.conf_get('applications'), 'unicode 2 get' def test_json_unicode_number(self): - self.assertIn( - 'error', - self.conf( - b""" + assert 'success' in self.conf( + u""" { "app": { "type": "python", @@ -79,16 +64,12 @@ class TestConfiguration(TestControl): } } """, - 'applications', - ), - 'unicode number', - ) + 'applications', + ), 'unicode number' def test_json_utf8_bom(self): - self.assertIn( - 'success', - self.conf( - b"""\xEF\xBB\xBF + assert 'success' in self.conf( + b"""\xEF\xBB\xBF { "app": { "type": "python", @@ -98,16 +79,12 @@ class TestConfiguration(TestControl): } } """, - 'applications', - ), - 'UTF-8 BOM', - ) + 'applications', + ), 'UTF-8 BOM' def test_json_comment_single_line(self): - self.assertIn( - 'success', - self.conf( - b""" + assert 'success' in self.conf( + b""" // this is bridge { "//app": { @@ -121,16 +98,12 @@ class TestConfiguration(TestControl): } // end of json \xEF\t """, - 'applications', - ), - 'single line comments', - ) + 'applications', + ), 'single line comments' def test_json_comment_multi_line(self): - self.assertIn( - 'success', - self.conf( - b""" + assert 'success' in self.conf( + b""" /* this is bridge */ { "/*app": { @@ -148,41 +121,31 @@ class TestConfiguration(TestControl): } /* end of json \xEF\t\b */ """, - 'applications', - ), - 'multi line comments', - ) + 'applications', + ), 'multi line comments' def test_json_comment_invalid(self): - self.assertIn('error', self.conf(b'/{}', 'applications'), 'slash') - self.assertIn('error', self.conf(b'//{}', 'applications'), 'comment') - self.assertIn('error', self.conf(b'{} /', 'applications'), 'slash end') - self.assertIn( - 'error', self.conf(b'/*{}', 'applications'), 'slash star' - ) - self.assertIn( - 'error', self.conf(b'{} /*', 'applications'), 'slash star end' - ) + assert 'error' in self.conf(b'/{}', 'applications'), 'slash' + assert 'error' in self.conf(b'//{}', 'applications'), 'comment' + assert 'error' in self.conf(b'{} /', 'applications'), 'slash end' + assert 'error' in self.conf(b'/*{}', 'applications'), 'slash star' + assert 'error' in self.conf(b'{} /*', 'applications'), 'slash star end' def test_applications_open_brace(self): - self.assertIn('error', self.conf('{', 'applications'), 'open brace') + assert 'error' in self.conf('{', 'applications'), 'open brace' def test_applications_string(self): - self.assertIn('error', self.conf('"{}"', 'applications'), 'string') + assert 'error' in self.conf('"{}"', 'applications'), 'string' - @unittest.skip('not yet, unsafe') + @pytest.mark.skip('not yet, unsafe') def test_applications_type_only(self): - self.assertIn( - 'error', - self.conf({"app": {"type": "python"}}, 'applications'), - 'type only', - ) + assert 'error' in self.conf( + {"app": {"type": "python"}}, 'applications' + ), 'type only' def test_applications_miss_quote(self): - self.assertIn( - 'error', - self.conf( - """ + assert 'error' in self.conf( + """ { app": { "type": "python", @@ -192,16 +155,12 @@ class TestConfiguration(TestControl): } } """, - 'applications', - ), - 'miss quote', - ) + 'applications', + ), 'miss quote' def test_applications_miss_colon(self): - self.assertIn( - 'error', - self.conf( - """ + assert 'error' in self.conf( + """ { "app" { "type": "python", @@ -211,16 +170,12 @@ class TestConfiguration(TestControl): } } """, - 'applications', - ), - 'miss colon', - ) + 'applications', + ), 'miss colon' def test_applications_miss_comma(self): - self.assertIn( - 'error', - self.conf( - """ + assert 'error' in self.conf( + """ { "app": { "type": "python" @@ -230,144 +185,117 @@ class TestConfiguration(TestControl): } } """, - 'applications', - ), - 'miss comma', - ) + 'applications', + ), 'miss comma' def test_applications_skip_spaces(self): - self.assertIn( - 'success', self.conf(b'{ \n\r\t}', 'applications'), 'skip spaces' - ) + assert 'success' in self.conf( + b'{ \n\r\t}', 'applications' + ), 'skip spaces' def test_applications_relative_path(self): - self.assertIn( - 'success', - self.conf( - { - "app": { - "type": "python", - "processes": {"spare": 0}, - "path": "../app", - "module": "wsgi", - } - }, - 'applications', - ), - 'relative path', - ) + assert 'success' in self.conf( + { + "app": { + "type": "python", + "processes": {"spare": 0}, + "path": "../app", + "module": "wsgi", + } + }, + 'applications', + ), 'relative path' - @unittest.skip('not yet, unsafe') + @pytest.mark.skip('not yet, unsafe') def test_listeners_empty(self): - self.assertIn( - 'error', self.conf({"*:7080": {}}, 'listeners'), 'listener empty' - ) + assert 'error' in self.conf( + {"*:7080": {}}, 'listeners' + ), 'listener empty' def test_listeners_no_app(self): - self.assertIn( - 'error', - self.conf({"*:7080": {"pass": "applications/app"}}, 'listeners'), - 'listeners no app', - ) + assert 'error' in self.conf( + {"*:7080": {"pass": "applications/app"}}, 'listeners' + ), 'listeners no app' def test_listeners_wildcard(self): - self.assertIn( - 'success', - self.conf( - { - "listeners": {"*:7080": {"pass": "applications/app"}}, - "applications": { - "app": { - "type": "python", - "processes": {"spare": 0}, - "path": "/app", - "module": "wsgi", - } - }, - } - ), - 'listeners wildcard', - ) + assert 'success' in self.conf( + { + "listeners": {"*:7080": {"pass": "applications/app"}}, + "applications": { + "app": { + "type": "python", + "processes": {"spare": 0}, + "path": "/app", + "module": "wsgi", + } + }, + } + ), 'listeners wildcard' def test_listeners_explicit(self): - self.assertIn( - 'success', - self.conf( - { - "listeners": {"127.0.0.1:7080": {"pass": "applications/app"}}, - "applications": { - "app": { - "type": "python", - "processes": {"spare": 0}, - "path": "/app", - "module": "wsgi", - } - }, - } - ), - 'explicit', - ) + assert 'success' in self.conf( + { + "listeners": {"127.0.0.1:7080": {"pass": "applications/app"}}, + "applications": { + "app": { + "type": "python", + "processes": {"spare": 0}, + "path": "/app", + "module": "wsgi", + } + }, + } + ), 'explicit' def test_listeners_explicit_ipv6(self): - self.assertIn( - 'success', - self.conf( - { - "listeners": {"[::1]:7080": {"pass": "applications/app"}}, - "applications": { - "app": { - "type": "python", - "processes": {"spare": 0}, - "path": "/app", - "module": "wsgi", - } - }, - } - ), - 'explicit ipv6', - ) + assert 'success' in self.conf( + { + "listeners": {"[::1]:7080": {"pass": "applications/app"}}, + "applications": { + "app": { + "type": "python", + "processes": {"spare": 0}, + "path": "/app", + "module": "wsgi", + } + }, + } + ), 'explicit ipv6' - @unittest.skip('not yet, unsafe') + @pytest.mark.skip('not yet, unsafe') def test_listeners_no_port(self): - self.assertIn( - 'error', - self.conf( - { - "listeners": {"127.0.0.1": {"pass": "applications/app"}}, - "applications": { - "app": { - "type": "python", - "processes": {"spare": 0}, - "path": "/app", - "module": "wsgi", - } - }, - } - ), - 'no port', - ) + assert 'error' in self.conf( + { + "listeners": {"127.0.0.1": {"pass": "applications/app"}}, + "applications": { + "app": { + "type": "python", + "processes": {"spare": 0}, + "path": "/app", + "module": "wsgi", + } + }, + } + ), 'no port' def test_json_application_name_large(self): name = "X" * 1024 * 1024 - self.assertIn( - 'success', - self.conf( - { - "listeners": {"*:7080": {"pass": "applications/" + name}}, - "applications": { - name: { - "type": "python", - "processes": {"spare": 0}, - "path": "/app", - "module": "wsgi", - } - }, - } - ), + assert 'success' in self.conf( + { + "listeners": {"*:7080": {"pass": "applications/" + name}}, + "applications": { + name: { + "type": "python", + "processes": {"spare": 0}, + "path": "/app", + "module": "wsgi", + } + }, + } ) - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_json_application_many(self): apps = 999 @@ -388,7 +316,7 @@ class TestConfiguration(TestControl): }, } - self.assertIn('success', self.conf(conf)) + assert 'success' in self.conf(conf) def test_json_application_many2(self): conf = { @@ -407,35 +335,21 @@ class TestConfiguration(TestControl): "listeners": {"*:7080": {"pass": "applications/app-1"}}, } - self.assertIn('success', self.conf(conf)) - - def test_unprivileged_user_error(self): - self.skip_alerts.extend( - [ - r'cannot set user "root"', - r'failed to apply new conf', - ] - ) - if self.is_su: - print('unprivileged tests, skip this') - raise unittest.SkipTest() - - self.assertIn( - 'error', - self.conf( - { - "app": { - "type": "external", - "processes": 1, - "executable": "/app", - "user": "root", - } - }, - 'applications', - ), - 'setting user', - ) + assert 'success' in self.conf(conf) + def test_unprivileged_user_error(self, is_su): + skip_alert(r'cannot set user "root"', r'failed to apply new conf') + if is_su: + pytest.skip('unprivileged tests') -if __name__ == '__main__': - TestConfiguration.main() + assert 'error' in self.conf( + { + "app": { + "type": "external", + "processes": 1, + "executable": "/app", + "user": "root", + } + }, + 'applications', + ), 'setting user' diff --git a/test/test_go_application.py b/test/test_go_application.py index b9b78e2b..8c77dfc5 100644 --- a/test/test_go_application.py +++ b/test/test_go_application.py @@ -1,3 +1,5 @@ +import re + from unit.applications.lang.go import TestApplicationGo @@ -19,44 +21,38 @@ class TestGoApplication(TestApplicationGo): body=body, ) - self.assertEqual(resp['status'], 200, 'status') + assert resp['status'] == 200, 'status' headers = resp['headers'] header_server = headers.pop('Server') - self.assertRegex(header_server, r'Unit/[\d\.]+', 'server header') + assert re.search(r'Unit/[\d\.]+', header_server), 'server header' date = headers.pop('Date') - self.assertEqual(date[-4:], ' GMT', 'date header timezone') - self.assertLess( - abs(self.date_to_sec_epoch(date) - self.sec_epoch()), - 5, - 'date header', - ) - - self.assertDictEqual( - headers, - { - 'Content-Length': str(len(body)), - 'Content-Type': 'text/html', - 'Request-Method': 'POST', - 'Request-Uri': '/', - 'Http-Host': 'localhost', - 'Server-Protocol': 'HTTP/1.1', - 'Server-Protocol-Major': '1', - 'Server-Protocol-Minor': '1', - 'Custom-Header': 'blah', - 'Connection': 'close', - }, - 'headers', - ) - self.assertEqual(resp['body'], body, 'body') + assert date[-4:] == ' GMT', 'date header timezone' + assert ( + abs(self.date_to_sec_epoch(date) - self.sec_epoch()) < 5 + ), 'date header' + + assert headers == { + 'Content-Length': str(len(body)), + 'Content-Type': 'text/html', + 'Request-Method': 'POST', + 'Request-Uri': '/', + 'Http-Host': 'localhost', + 'Server-Protocol': 'HTTP/1.1', + 'Server-Protocol-Major': '1', + 'Server-Protocol-Minor': '1', + 'Custom-Header': 'blah', + 'Connection': 'close', + }, 'headers' + assert resp['body'] == body, 'body' def test_go_application_get_variables(self): self.load('get_variables') resp = self.get(url='/?var1=val1&var2=&var3') - self.assertEqual(resp['headers']['X-Var-1'], 'val1', 'GET variables') - self.assertEqual(resp['headers']['X-Var-2'], '', 'GET variables 2') - self.assertEqual(resp['headers']['X-Var-3'], '', 'GET variables 3') + assert resp['headers']['X-Var-1'] == 'val1', 'GET variables' + assert resp['headers']['X-Var-2'] == '', 'GET variables 2' + assert resp['headers']['X-Var-3'] == '', 'GET variables 3' def test_go_application_post_variables(self): self.load('post_variables') @@ -70,24 +66,24 @@ class TestGoApplication(TestApplicationGo): body='var1=val1&var2=&var3', ) - self.assertEqual(resp['headers']['X-Var-1'], 'val1', 'POST variables') - self.assertEqual(resp['headers']['X-Var-2'], '', 'POST variables 2') - self.assertEqual(resp['headers']['X-Var-3'], '', 'POST variables 3') + assert resp['headers']['X-Var-1'] == 'val1', 'POST variables' + assert resp['headers']['X-Var-2'] == '', 'POST variables 2' + assert resp['headers']['X-Var-3'] == '', 'POST variables 3' def test_go_application_404(self): self.load('404') resp = self.get() - self.assertEqual(resp['status'], 404, '404 status') - self.assertRegex( - resp['body'], r'<title>404 Not Found</title>', '404 body' - ) + assert resp['status'] == 404, '404 status' + assert re.search( + r'<title>404 Not Found</title>', resp['body'] + ), '404 body' def test_go_keepalive_body(self): self.load('mirror') - self.assertEqual(self.get()['status'], 200, 'init') + assert self.get()['status'] == 200, 'init' body = '0123456789' * 500 (resp, sock) = self.post( @@ -101,7 +97,7 @@ class TestGoApplication(TestApplicationGo): read_timeout=1, ) - self.assertEqual(resp['body'], body, 'keep-alive 1') + assert resp['body'] == body, 'keep-alive 1' body = '0123456789' resp = self.post( @@ -114,7 +110,7 @@ class TestGoApplication(TestApplicationGo): body=body, ) - self.assertEqual(resp['body'], body, 'keep-alive 2') + assert resp['body'] == body, 'keep-alive 2' def test_go_application_cookies(self): self.load('cookies') @@ -127,28 +123,24 @@ class TestGoApplication(TestApplicationGo): } ) - self.assertEqual(resp['headers']['X-Cookie-1'], 'val1', 'cookie 1') - self.assertEqual(resp['headers']['X-Cookie-2'], 'val2', 'cookie 2') + assert resp['headers']['X-Cookie-1'] == 'val1', 'cookie 1' + assert resp['headers']['X-Cookie-2'] == 'val2', 'cookie 2' def test_go_application_command_line_arguments_type(self): self.load('command_line_arguments') - self.assertIn( - 'error', + assert 'error' in \ self.conf( '' "a b c", 'applications/command_line_arguments/arguments' - ), - 'arguments type', - ) + ), \ + 'arguments type' def test_go_application_command_line_arguments_0(self): self.load('command_line_arguments') - self.assertEqual( - self.get()['headers']['X-Arg-0'], - self.conf_get('applications/command_line_arguments/executable'), - 'argument 0', - ) + assert self.get()['headers']['X-Arg-0'] == self.conf_get( + 'applications/command_line_arguments/executable' + ), 'argument 0' def test_go_application_command_line_arguments(self): self.load('command_line_arguments') @@ -162,9 +154,9 @@ class TestGoApplication(TestApplicationGo): 'applications/command_line_arguments/arguments', ) - self.assertEqual( - self.get()['body'], arg1 + ',' + arg2 + ',' + arg3, 'arguments' - ) + assert ( + self.get()['body'] == arg1 + ',' + arg2 + ',' + arg3 + ), 'arguments' def test_go_application_command_line_arguments_change(self): self.load('command_line_arguments') @@ -173,18 +165,14 @@ class TestGoApplication(TestApplicationGo): self.conf('["0", "a", "$", ""]', args_path) - self.assertEqual(self.get()['body'], '0,a,$,', 'arguments') + assert self.get()['body'] == '0,a,$,', 'arguments' self.conf('["-1", "b", "%"]', args_path) - self.assertEqual(self.get()['body'], '-1,b,%', 'arguments change') + assert self.get()['body'] == '-1,b,%', 'arguments change' self.conf('[]', args_path) - self.assertEqual( - self.get()['headers']['Content-Length'], '0', 'arguments empty' - ) - - -if __name__ == '__main__': - TestGoApplication.main() + assert ( + self.get()['headers']['Content-Length'] == '0' + ), 'arguments empty' diff --git a/test/test_go_isolation.py b/test/test_go_isolation.py index 61d39617..1e7243f6 100644 --- a/test/test_go_isolation.py +++ b/test/test_go_isolation.py @@ -1,21 +1,22 @@ import grp +import os import pwd -import unittest + +import pytest from unit.applications.lang.go import TestApplicationGo from unit.feature.isolation import TestFeatureIsolation - class TestGoIsolation(TestApplicationGo): prerequisites = {'modules': {'go': 'any'}, 'features': ['isolation']} isolation = TestFeatureIsolation() @classmethod - def setUpClass(cls, complete_check=True): - unit = super().setUpClass(complete_check=False) + def setup_class(cls, complete_check=True): + unit = super().setup_class(complete_check=False) - TestFeatureIsolation().check(cls.available, unit.testdir) + TestFeatureIsolation().check(cls.available, unit.temp_dir) return unit if not complete_check else unit.complete() @@ -41,24 +42,20 @@ class TestGoIsolation(TestApplicationGo): for ns, ns_value in self.available['features']['isolation'].items(): if ns.upper() in obj['NS']: - self.assertEqual( - obj['NS'][ns.upper()], ns_value, '%s match' % ns - ) + assert obj['NS'][ns.upper()] == ns_value, '%s match' % ns - def test_isolation_unpriv_user(self): + def test_isolation_unpriv_user(self, is_su): if not self.isolation_key('unprivileged_userns_clone'): - print('unprivileged clone is not available') - raise unittest.SkipTest() + pytest.skip('unprivileged clone is not available') - if self.is_su: - print('privileged tests, skip this') - raise unittest.SkipTest() + if is_su: + pytest.skip('privileged tests, skip this') self.load('ns_inspect') obj = self.getjson()['body'] - self.assertEqual(obj['UID'], self.uid, 'uid match') - self.assertEqual(obj['GID'], self.gid, 'gid match') + assert obj['UID'] == os.geteuid(), 'uid match' + assert obj['GID'] == os.getegid(), 'gid match' self.load('ns_inspect', isolation={'namespaces': {'credential': True}}) @@ -67,8 +64,8 @@ class TestGoIsolation(TestApplicationGo): nobody_uid, nogroup_gid, nogroup = self.unpriv_creds() # unprivileged unit map itself to nobody in the container by default - self.assertEqual(obj['UID'], nobody_uid, 'uid of nobody') - self.assertEqual(obj['GID'], nogroup_gid, 'gid of %s' % nogroup) + assert obj['UID'] == nobody_uid, 'uid of nobody' + assert obj['GID'] == nogroup_gid, 'gid of %s' % nogroup self.load( 'ns_inspect', @@ -78,8 +75,8 @@ class TestGoIsolation(TestApplicationGo): obj = self.getjson()['body'] - self.assertEqual(obj['UID'], 0, 'uid match user=root') - self.assertEqual(obj['GID'], 0, 'gid match user=root') + assert obj['UID'] == 0, 'uid match user=root' + assert obj['GID'] == 0, 'gid match user=root' self.load( 'ns_inspect', @@ -90,10 +87,8 @@ class TestGoIsolation(TestApplicationGo): obj = self.getjson()['body'] - self.assertEqual(obj['UID'], 0, 'uid match user=root group=nogroup') - self.assertEqual( - obj['GID'], nogroup_gid, 'gid match user=root group=nogroup' - ) + assert obj['UID'] == 0, 'uid match user=root group=nogroup' + assert obj['GID'] == nogroup_gid, 'gid match user=root group=nogroup' self.load( 'ns_inspect', @@ -101,20 +96,19 @@ class TestGoIsolation(TestApplicationGo): group='root', isolation={ 'namespaces': {'credential': True}, - 'uidmap': [{'container': 0, 'host': self.uid, 'size': 1}], - 'gidmap': [{'container': 0, 'host': self.gid, 'size': 1}], + 'uidmap': [{'container': 0, 'host': os.geteuid(), 'size': 1}], + 'gidmap': [{'container': 0, 'host': os.getegid(), 'size': 1}], }, ) obj = self.getjson()['body'] - self.assertEqual(obj['UID'], 0, 'uid match uidmap') - self.assertEqual(obj['GID'], 0, 'gid match gidmap') + assert obj['UID'] == 0, 'uid match uidmap' + assert obj['GID'] == 0, 'gid match gidmap' - def test_isolation_priv_user(self): - if not self.is_su: - print('unprivileged tests, skip this') - raise unittest.SkipTest() + def test_isolation_priv_user(self, is_su): + if not is_su: + pytest.skip('unprivileged tests, skip this') self.load('ns_inspect') @@ -122,16 +116,16 @@ class TestGoIsolation(TestApplicationGo): obj = self.getjson()['body'] - self.assertEqual(obj['UID'], nobody_uid, 'uid match') - self.assertEqual(obj['GID'], nogroup_gid, 'gid match') + assert obj['UID'] == nobody_uid, 'uid match' + assert obj['GID'] == nogroup_gid, 'gid match' self.load('ns_inspect', isolation={'namespaces': {'credential': True}}) obj = self.getjson()['body'] # privileged unit map app creds in the container by default - self.assertEqual(obj['UID'], nobody_uid, 'uid nobody') - self.assertEqual(obj['GID'], nogroup_gid, 'gid nobody') + assert obj['UID'] == nobody_uid, 'uid nobody' + assert obj['GID'] == nogroup_gid, 'gid nobody' self.load( 'ns_inspect', @@ -141,8 +135,8 @@ class TestGoIsolation(TestApplicationGo): obj = self.getjson()['body'] - self.assertEqual(obj['UID'], 0, 'uid nobody user=root') - self.assertEqual(obj['GID'], 0, 'gid nobody user=root') + assert obj['UID'] == 0, 'uid nobody user=root' + assert obj['GID'] == 0, 'gid nobody user=root' self.load( 'ns_inspect', @@ -153,10 +147,8 @@ class TestGoIsolation(TestApplicationGo): obj = self.getjson()['body'] - self.assertEqual(obj['UID'], 0, 'uid match user=root group=nogroup') - self.assertEqual( - obj['GID'], nogroup_gid, 'gid match user=root group=nogroup' - ) + assert obj['UID'] == 0, 'uid match user=root group=nogroup' + assert obj['GID'] == nogroup_gid, 'gid match user=root group=nogroup' self.load( 'ns_inspect', @@ -171,8 +163,8 @@ class TestGoIsolation(TestApplicationGo): obj = self.getjson()['body'] - self.assertEqual(obj['UID'], 0, 'uid match uidmap user=root') - self.assertEqual(obj['GID'], 0, 'gid match gidmap user=root') + assert obj['UID'] == 0, 'uid match uidmap user=root' + assert obj['GID'] == 0, 'gid match gidmap user=root' # map 65535 uids self.load( @@ -188,21 +180,15 @@ class TestGoIsolation(TestApplicationGo): obj = self.getjson()['body'] - self.assertEqual( - obj['UID'], nobody_uid, 'uid match uidmap user=nobody' - ) - self.assertEqual( - obj['GID'], nogroup_gid, 'gid match uidmap user=nobody' - ) + assert obj['UID'] == nobody_uid, 'uid match uidmap user=nobody' + assert obj['GID'] == nogroup_gid, 'gid match uidmap user=nobody' def test_isolation_mnt(self): if not self.isolation_key('mnt'): - print('mnt namespace is not supported') - raise unittest.SkipTest() + pytest.skip('mnt namespace is not supported') if not self.isolation_key('unprivileged_userns_clone'): - print('unprivileged clone is not available') - raise unittest.SkipTest() + pytest.skip('unprivileged clone is not available') self.load( 'ns_inspect', @@ -218,27 +204,20 @@ class TestGoIsolation(TestApplicationGo): for ns in allns: if ns.upper() in obj['NS']: - self.assertEqual( - obj['NS'][ns.upper()], - self.available['features']['isolation'][ns], - '%s match' % ns, - ) - - self.assertNotEqual( - obj['NS']['MNT'], self.isolation.getns('mnt'), 'mnt set' - ) - self.assertNotEqual( - obj['NS']['USER'], self.isolation.getns('user'), 'user set' - ) + assert ( + obj['NS'][ns.upper()] + == self.available['features']['isolation'][ns] + ), ('%s match' % ns) - def test_isolation_pid(self): + assert obj['NS']['MNT'] != self.isolation.getns('mnt'), 'mnt set' + assert obj['NS']['USER'] != self.isolation.getns('user'), 'user set' + + def test_isolation_pid(self, is_su): if not self.isolation_key('pid'): - print('pid namespace is not supported') - raise unittest.SkipTest() + pytest.skip('pid namespace is not supported') - if not (self.is_su or self.isolation_key('unprivileged_userns_clone')): - print('requires root or unprivileged_userns_clone') - raise unittest.SkipTest() + if not (is_su or self.isolation_key('unprivileged_userns_clone')): + pytest.skip('requires root or unprivileged_userns_clone') self.load( 'ns_inspect', @@ -247,7 +226,7 @@ class TestGoIsolation(TestApplicationGo): obj = self.getjson()['body'] - self.assertEqual(obj['PID'], 1, 'pid of container is 1') + assert obj['PID'] == 1, 'pid of container is 1' def test_isolation_namespace_false(self): self.load('ns_inspect') @@ -275,58 +254,67 @@ class TestGoIsolation(TestApplicationGo): for ns in allns: if ns.upper() in obj['NS']: - self.assertEqual( - obj['NS'][ns.upper()], - self.available['features']['isolation'][ns], - '%s match' % ns, - ) + assert ( + obj['NS'][ns.upper()] + == self.available['features']['isolation'][ns] + ), ('%s match' % ns) def test_go_isolation_rootfs_container(self): if not self.isolation_key('unprivileged_userns_clone'): - print('unprivileged clone is not available') - raise unittest.SkipTest() + pytest.skip('unprivileged clone is not available') if not self.isolation_key('mnt'): - print('mnt namespace is not supported') - raise unittest.SkipTest() + pytest.skip('mnt namespace is not supported') isolation = { 'namespaces': {'mount': True, 'credential': True}, - 'rootfs': self.testdir, + 'rootfs': self.temp_dir, } self.load('ns_inspect', isolation=isolation) obj = self.getjson(url='/?file=/go/app')['body'] - self.assertEqual(obj['FileExists'], True, 'app relative to rootfs') + assert obj['FileExists'] == True, 'app relative to rootfs' obj = self.getjson(url='/?file=/bin/sh')['body'] - self.assertEqual(obj['FileExists'], False, 'file should not exists') + assert obj['FileExists'] == False, 'file should not exists' - def test_go_isolation_rootfs_container_priv(self): - if not self.is_su: - print("requires root") - raise unittest.SkipTest() + def test_go_isolation_rootfs_container_priv(self, is_su): + if not is_su: + pytest.skip('requires root') if not self.isolation_key('mnt'): - print('mnt namespace is not supported') - raise unittest.SkipTest() + pytest.skip('mnt namespace is not supported') isolation = { 'namespaces': {'mount': True}, - 'rootfs': self.testdir, + 'rootfs': self.temp_dir, } self.load('ns_inspect', isolation=isolation) obj = self.getjson(url='/?file=/go/app')['body'] - self.assertEqual(obj['FileExists'], True, 'app relative to rootfs') + assert obj['FileExists'] == True, 'app relative to rootfs' obj = self.getjson(url='/?file=/bin/sh')['body'] - self.assertEqual(obj['FileExists'], False, 'file should not exists') + assert obj['FileExists'] == False, 'file should not exists' + + def test_go_isolation_rootfs_default_tmpfs(self): + if not self.isolation_key('unprivileged_userns_clone'): + pytest.skip('unprivileged clone is not available') + + if not self.isolation_key('mnt'): + pytest.skip('mnt namespace is not supported') + + isolation = { + 'namespaces': {'mount': True, 'credential': True}, + 'rootfs': self.temp_dir, + } + + self.load('ns_inspect', isolation=isolation) + obj = self.getjson(url='/?file=/tmp')['body'] -if __name__ == '__main__': - TestGoIsolation.main() + assert obj['FileExists'] == True, 'app has /tmp' diff --git a/test/test_go_isolation_rootfs.py b/test/test_go_isolation_rootfs.py index 0039ff87..d8e177b1 100644 --- a/test/test_go_isolation_rootfs.py +++ b/test/test_go_isolation_rootfs.py @@ -1,5 +1,6 @@ import os -import unittest + +import pytest from unit.applications.lang.go import TestApplicationGo @@ -7,28 +8,22 @@ from unit.applications.lang.go import TestApplicationGo class TestGoIsolationRootfs(TestApplicationGo): prerequisites = {'modules': {'go': 'all'}} - def test_go_isolation_rootfs_chroot(self): - if not self.is_su: - print("requires root") - raise unittest.SkipTest() + def test_go_isolation_rootfs_chroot(self, is_su): + if not is_su: + pytest.skip('requires root') if os.uname().sysname == 'Darwin': - print('chroot tests not supported on OSX') - raise unittest.SkipTest() + pytest.skip('chroot tests not supported on OSX') isolation = { - 'rootfs': self.testdir, + 'rootfs': self.temp_dir, } self.load('ns_inspect', isolation=isolation) obj = self.getjson(url='/?file=/go/app')['body'] - self.assertEqual(obj['FileExists'], True, 'app relative to rootfs') + assert obj['FileExists'] == True, 'app relative to rootfs' obj = self.getjson(url='/?file=/bin/sh')['body'] - self.assertEqual(obj['FileExists'], False, 'file should not exists') - - -if __name__ == '__main__': - TestGoIsolationRootfs.main() + assert obj['FileExists'] == False, 'file should not exists' diff --git a/test/test_http_header.py b/test/test_http_header.py index ea4520c1..8381a0d9 100644 --- a/test/test_http_header.py +++ b/test/test_http_header.py @@ -1,4 +1,4 @@ -import unittest +import pytest from unit.applications.lang.python import TestApplicationPython @@ -17,12 +17,10 @@ class TestHTTPHeader(TestApplicationPython): } ) - self.assertEqual(resp['status'], 200, 'value leading sp status') - self.assertEqual( - resp['headers']['Custom-Header'], - ',', - 'value leading sp custom header', - ) + assert resp['status'] == 200, 'value leading sp status' + assert ( + resp['headers']['Custom-Header'] == ',' + ), 'value leading sp custom header' def test_http_header_value_leading_htab(self): self.load('custom_header') @@ -35,12 +33,10 @@ class TestHTTPHeader(TestApplicationPython): } ) - self.assertEqual(resp['status'], 200, 'value leading htab status') - self.assertEqual( - resp['headers']['Custom-Header'], - ',', - 'value leading htab custom header', - ) + assert resp['status'] == 200, 'value leading htab status' + assert ( + resp['headers']['Custom-Header'] == ',' + ), 'value leading htab custom header' def test_http_header_value_trailing_sp(self): self.load('custom_header') @@ -53,12 +49,10 @@ class TestHTTPHeader(TestApplicationPython): } ) - self.assertEqual(resp['status'], 200, 'value trailing sp status') - self.assertEqual( - resp['headers']['Custom-Header'], - ',', - 'value trailing sp custom header', - ) + assert resp['status'] == 200, 'value trailing sp status' + assert ( + resp['headers']['Custom-Header'] == ',' + ), 'value trailing sp custom header' def test_http_header_value_trailing_htab(self): self.load('custom_header') @@ -71,12 +65,10 @@ class TestHTTPHeader(TestApplicationPython): } ) - self.assertEqual(resp['status'], 200, 'value trailing htab status') - self.assertEqual( - resp['headers']['Custom-Header'], - ',', - 'value trailing htab custom header', - ) + assert resp['status'] == 200, 'value trailing htab status' + assert ( + resp['headers']['Custom-Header'] == ',' + ), 'value trailing htab custom header' def test_http_header_value_both_sp(self): self.load('custom_header') @@ -89,12 +81,10 @@ class TestHTTPHeader(TestApplicationPython): } ) - self.assertEqual(resp['status'], 200, 'value both sp status') - self.assertEqual( - resp['headers']['Custom-Header'], - ',', - 'value both sp custom header', - ) + assert resp['status'] == 200, 'value both sp status' + assert ( + resp['headers']['Custom-Header'] == ',' + ), 'value both sp custom header' def test_http_header_value_both_htab(self): self.load('custom_header') @@ -107,12 +97,10 @@ class TestHTTPHeader(TestApplicationPython): } ) - self.assertEqual(resp['status'], 200, 'value both htab status') - self.assertEqual( - resp['headers']['Custom-Header'], - ',', - 'value both htab custom header', - ) + assert resp['status'] == 200, 'value both htab status' + assert ( + resp['headers']['Custom-Header'] == ',' + ), 'value both htab custom header' def test_http_header_value_chars(self): self.load('custom_header') @@ -120,17 +108,16 @@ class TestHTTPHeader(TestApplicationPython): resp = self.get( headers={ 'Host': 'localhost', - 'Custom-Header': '(),/:;<=>?@[\]{}\t !#$%&\'*+-.^_`|~', + 'Custom-Header': r'(),/:;<=>?@[\]{}\t !#$%&\'*+-.^_`|~', 'Connection': 'close', } ) - self.assertEqual(resp['status'], 200, 'value chars status') - self.assertEqual( - resp['headers']['Custom-Header'], - '(),/:;<=>?@[\]{}\t !#$%&\'*+-.^_`|~', - 'value chars custom header', - ) + assert resp['status'] == 200, 'value chars status' + assert ( + resp['headers']['Custom-Header'] + == r'(),/:;<=>?@[\]{}\t !#$%&\'*+-.^_`|~' + ), 'value chars custom header' def test_http_header_value_chars_edge(self): self.load('custom_header') @@ -146,10 +133,8 @@ Connection: close encoding='latin1', ) - self.assertEqual(resp['status'], 200, 'value chars edge status') - self.assertEqual( - resp['headers']['Custom-Header'], '\xFF', 'value chars edge' - ) + assert resp['status'] == 200, 'value chars edge status' + assert resp['headers']['Custom-Header'] == '\xFF', 'value chars edge' def test_http_header_value_chars_below(self): self.load('custom_header') @@ -164,7 +149,7 @@ Connection: close raw=True, ) - self.assertEqual(resp['status'], 400, 'value chars below') + assert resp['status'] == 400, 'value chars below' def test_http_header_field_leading_sp(self): self.load('empty') @@ -177,7 +162,7 @@ Connection: close } ) - self.assertEqual(resp['status'], 400, 'field leading sp') + assert resp['status'] == 400, 'field leading sp' def test_http_header_field_leading_htab(self): self.load('empty') @@ -190,7 +175,7 @@ Connection: close } ) - self.assertEqual(resp['status'], 400, 'field leading htab') + assert resp['status'] == 400, 'field leading htab' def test_http_header_field_trailing_sp(self): self.load('empty') @@ -203,7 +188,7 @@ Connection: close } ) - self.assertEqual(resp['status'], 400, 'field trailing sp') + assert resp['status'] == 400, 'field trailing sp' def test_http_header_field_trailing_htab(self): self.load('empty') @@ -216,12 +201,12 @@ Connection: close } ) - self.assertEqual(resp['status'], 400, 'field trailing htab') + assert resp['status'] == 400, 'field trailing htab' def test_http_header_content_length_big(self): self.load('empty') - self.assertEqual( + assert ( self.post( headers={ 'Host': 'localhost', @@ -229,15 +214,14 @@ Connection: close 'Connection': 'close', }, body='X' * 1000, - )['status'], - 400, - 'Content-Length big', - ) + )['status'] + == 400 + ), 'Content-Length big' def test_http_header_content_length_negative(self): self.load('empty') - self.assertEqual( + assert ( self.post( headers={ 'Host': 'localhost', @@ -245,15 +229,14 @@ Connection: close 'Connection': 'close', }, body='X' * 1000, - )['status'], - 400, - 'Content-Length negative', - ) + )['status'] + == 400 + ), 'Content-Length negative' def test_http_header_content_length_text(self): self.load('empty') - self.assertEqual( + assert ( self.post( headers={ 'Host': 'localhost', @@ -261,15 +244,14 @@ Connection: close 'Connection': 'close', }, body='X' * 1000, - )['status'], - 400, - 'Content-Length text', - ) + )['status'] + == 400 + ), 'Content-Length text' def test_http_header_content_length_multiple_values(self): self.load('empty') - self.assertEqual( + assert ( self.post( headers={ 'Host': 'localhost', @@ -277,15 +259,14 @@ Connection: close 'Connection': 'close', }, body='X' * 1000, - )['status'], - 400, - 'Content-Length multiple value', - ) + )['status'] + == 400 + ), 'Content-Length multiple value' def test_http_header_content_length_multiple_fields(self): self.load('empty') - self.assertEqual( + assert ( self.post( headers={ 'Host': 'localhost', @@ -293,39 +274,35 @@ Connection: close 'Connection': 'close', }, body='X' * 1000, - )['status'], - 400, - 'Content-Length multiple fields', - ) + )['status'] + == 400 + ), 'Content-Length multiple fields' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_http_header_host_absent(self): self.load('host') resp = self.get(headers={'Connection': 'close'}) - self.assertEqual(resp['status'], 400, 'Host absent status') + assert resp['status'] == 400, 'Host absent status' def test_http_header_host_empty(self): self.load('host') resp = self.get(headers={'Host': '', 'Connection': 'close'}) - self.assertEqual(resp['status'], 200, 'Host empty status') - self.assertNotEqual( - resp['headers']['X-Server-Name'], '', 'Host empty SERVER_NAME' - ) + assert resp['status'] == 200, 'Host empty status' + assert resp['headers']['X-Server-Name'] != '', 'Host empty SERVER_NAME' def test_http_header_host_big(self): self.load('empty') - self.assertEqual( + assert ( self.get(headers={'Host': 'X' * 10000, 'Connection': 'close'})[ 'status' - ], - 431, - 'Host big', - ) + ] + == 431 + ), 'Host big' def test_http_header_host_port(self): self.load('host') @@ -334,17 +311,13 @@ Connection: close headers={'Host': 'exmaple.com:7080', 'Connection': 'close'} ) - self.assertEqual(resp['status'], 200, 'Host port status') - self.assertEqual( - resp['headers']['X-Server-Name'], - 'exmaple.com', - 'Host port SERVER_NAME', - ) - self.assertEqual( - resp['headers']['X-Http-Host'], - 'exmaple.com:7080', - 'Host port HTTP_HOST', - ) + assert resp['status'] == 200, 'Host port status' + assert ( + resp['headers']['X-Server-Name'] == 'exmaple.com' + ), 'Host port SERVER_NAME' + assert ( + resp['headers']['X-Http-Host'] == 'exmaple.com:7080' + ), 'Host port HTTP_HOST' def test_http_header_host_port_empty(self): self.load('host') @@ -353,63 +326,49 @@ Connection: close headers={'Host': 'exmaple.com:', 'Connection': 'close'} ) - self.assertEqual(resp['status'], 200, 'Host port empty status') - self.assertEqual( - resp['headers']['X-Server-Name'], - 'exmaple.com', - 'Host port empty SERVER_NAME', - ) - self.assertEqual( - resp['headers']['X-Http-Host'], - 'exmaple.com:', - 'Host port empty HTTP_HOST', - ) + assert resp['status'] == 200, 'Host port empty status' + assert ( + resp['headers']['X-Server-Name'] == 'exmaple.com' + ), 'Host port empty SERVER_NAME' + assert ( + resp['headers']['X-Http-Host'] == 'exmaple.com:' + ), 'Host port empty HTTP_HOST' def test_http_header_host_literal(self): self.load('host') resp = self.get(headers={'Host': '127.0.0.1', 'Connection': 'close'}) - self.assertEqual(resp['status'], 200, 'Host literal status') - self.assertEqual( - resp['headers']['X-Server-Name'], - '127.0.0.1', - 'Host literal SERVER_NAME', - ) + assert resp['status'] == 200, 'Host literal status' + assert ( + resp['headers']['X-Server-Name'] == '127.0.0.1' + ), 'Host literal SERVER_NAME' def test_http_header_host_literal_ipv6(self): self.load('host') resp = self.get(headers={'Host': '[::1]:7080', 'Connection': 'close'}) - self.assertEqual(resp['status'], 200, 'Host literal ipv6 status') - self.assertEqual( - resp['headers']['X-Server-Name'], - '[::1]', - 'Host literal ipv6 SERVER_NAME', - ) - self.assertEqual( - resp['headers']['X-Http-Host'], - '[::1]:7080', - 'Host literal ipv6 HTTP_HOST', - ) + assert resp['status'] == 200, 'Host literal ipv6 status' + assert ( + resp['headers']['X-Server-Name'] == '[::1]' + ), 'Host literal ipv6 SERVER_NAME' + assert ( + resp['headers']['X-Http-Host'] == '[::1]:7080' + ), 'Host literal ipv6 HTTP_HOST' def test_http_header_host_trailing_period(self): self.load('host') resp = self.get(headers={'Host': '127.0.0.1.', 'Connection': 'close'}) - self.assertEqual(resp['status'], 200, 'Host trailing period status') - self.assertEqual( - resp['headers']['X-Server-Name'], - '127.0.0.1', - 'Host trailing period SERVER_NAME', - ) - self.assertEqual( - resp['headers']['X-Http-Host'], - '127.0.0.1.', - 'Host trailing period HTTP_HOST', - ) + assert resp['status'] == 200, 'Host trailing period status' + assert ( + resp['headers']['X-Server-Name'] == '127.0.0.1' + ), 'Host trailing period SERVER_NAME' + assert ( + resp['headers']['X-Http-Host'] == '127.0.0.1.' + ), 'Host trailing period HTTP_HOST' def test_http_header_host_trailing_period_2(self): self.load('host') @@ -418,66 +377,53 @@ Connection: close headers={'Host': 'EXAMPLE.COM.', 'Connection': 'close'} ) - self.assertEqual(resp['status'], 200, 'Host trailing period 2 status') - self.assertEqual( - resp['headers']['X-Server-Name'], - 'example.com', - 'Host trailing period 2 SERVER_NAME', - ) - self.assertEqual( - resp['headers']['X-Http-Host'], - 'EXAMPLE.COM.', - 'Host trailing period 2 HTTP_HOST', - ) + assert resp['status'] == 200, 'Host trailing period 2 status' + assert ( + resp['headers']['X-Server-Name'] == 'example.com' + ), 'Host trailing period 2 SERVER_NAME' + assert ( + resp['headers']['X-Http-Host'] == 'EXAMPLE.COM.' + ), 'Host trailing period 2 HTTP_HOST' def test_http_header_host_case_insensitive(self): self.load('host') resp = self.get(headers={'Host': 'EXAMPLE.COM', 'Connection': 'close'}) - self.assertEqual(resp['status'], 200, 'Host case insensitive') - self.assertEqual( - resp['headers']['X-Server-Name'], - 'example.com', - 'Host case insensitive SERVER_NAME', - ) + assert resp['status'] == 200, 'Host case insensitive' + assert ( + resp['headers']['X-Server-Name'] == 'example.com' + ), 'Host case insensitive SERVER_NAME' def test_http_header_host_double_dot(self): self.load('empty') - self.assertEqual( + assert ( self.get(headers={'Host': '127.0.0..1', 'Connection': 'close'})[ 'status' - ], - 400, - 'Host double dot', - ) + ] + == 400 + ), 'Host double dot' def test_http_header_host_slash(self): self.load('empty') - self.assertEqual( + assert ( self.get(headers={'Host': '/localhost', 'Connection': 'close'})[ 'status' - ], - 400, - 'Host slash', - ) + ] + == 400 + ), 'Host slash' def test_http_header_host_multiple_fields(self): self.load('empty') - self.assertEqual( + assert ( self.get( headers={ 'Host': ['localhost', 'example.com'], 'Connection': 'close', } - )['status'], - 400, - 'Host multiple fields', - ) - - -if __name__ == '__main__': - TestHTTPHeader.main() + )['status'] + == 400 + ), 'Host multiple fields' diff --git a/test/test_java_application.py b/test/test_java_application.py index 0cb18c25..afcdf651 100644 --- a/test/test_java_application.py +++ b/test/test_java_application.py @@ -1,54 +1,46 @@ import io import os +import re import time +from conftest import option +from conftest import public_dir +from conftest import skip_alert from unit.applications.lang.java import TestApplicationJava - class TestJavaApplication(TestApplicationJava): prerequisites = {'modules': {'java': 'all'}} def test_java_conf_error(self): - self.skip_alerts.extend( - [ - r'realpath.*failed', - r'failed to apply new conf', - r'application setup failed', - ] - ) - self.assertIn( - 'error', - self.conf( - { - "listeners": {"*:7080": {"pass": "applications/app"}}, - "applications": { - "app": { - "type": "java", - "processes": 1, - "working_directory": self.current_dir - + "/java/empty", - "webapp": self.testdir + "/java", - "unit_jars": self.testdir + "/no_such_dir", - } - }, - } - ), - 'conf error', - ) + skip_alert( + r'realpath.*failed', + r'failed to apply new conf', + r'application setup failed', + ) + assert 'error' in self.conf( + { + "listeners": {"*:7080": {"pass": "applications/app"}}, + "applications": { + "app": { + "type": "java", + "processes": 1, + "working_directory": option.test_dir + "/java/empty", + "webapp": self.temp_dir + "/java", + "unit_jars": self.temp_dir + "/no_such_dir", + } + }, + } + ), 'conf error' def test_java_war(self): self.load('empty_war') - self.assertIn( - 'success', - self.conf( - '"' + self.testdir + '/java/empty.war"', - '/config/applications/empty_war/webapp', - ), - 'configure war', - ) + assert 'success' in self.conf( + '"' + self.temp_dir + '/java/empty.war"', + '/config/applications/empty_war/webapp', + ), 'configure war' - self.assertEqual(self.get()['status'], 200, 'war') + assert self.get()['status'] == 200, 'war' def test_java_application_cookies(self): self.load('cookies') @@ -61,22 +53,20 @@ class TestJavaApplication(TestApplicationJava): } )['headers'] - self.assertEqual(headers['X-Cookie-1'], 'val1', 'cookie 1') - self.assertEqual(headers['X-Cookie-2'], 'val2', 'cookie 2') + assert headers['X-Cookie-1'] == 'val1', 'cookie 1' + assert headers['X-Cookie-2'] == 'val2', 'cookie 2' def test_java_application_filter(self): self.load('filter') headers = self.get()['headers'] - self.assertEqual(headers['X-Filter-Before'], '1', 'filter before') - self.assertEqual(headers['X-Filter-After'], '1', 'filter after') + assert headers['X-Filter-Before'] == '1', 'filter before' + assert headers['X-Filter-After'] == '1', 'filter after' - self.assertEqual( - self.get(url='/test')['headers']['X-Filter-After'], - '0', - 'filter after 2', - ) + assert ( + self.get(url='/test')['headers']['X-Filter-After'] == '0' + ), 'filter after 2' def test_java_application_get_variables(self): self.load('get_params') @@ -85,21 +75,17 @@ class TestJavaApplication(TestApplicationJava): 'headers' ] - self.assertEqual(headers['X-Var-1'], 'val1', 'GET variables') - self.assertEqual(headers['X-Var-2'], 'true', 'GET variables 2') - self.assertEqual(headers['X-Var-3'], 'false', 'GET variables 3') + assert headers['X-Var-1'] == 'val1', 'GET variables' + assert headers['X-Var-2'] == 'true', 'GET variables 2' + assert headers['X-Var-3'] == 'false', 'GET variables 3' - self.assertEqual( - headers['X-Param-Names'], 'var4 var2 var1 ', 'getParameterNames' - ) - self.assertEqual( - headers['X-Param-Values'], 'val4 foo ', 'getParameterValues' - ) - self.assertEqual( - headers['X-Param-Map'], - 'var2= var1=val1 var4=val4,foo ', - 'getParameterMap', - ) + assert ( + headers['X-Param-Names'] == 'var4 var2 var1 ' + ), 'getParameterNames' + assert headers['X-Param-Values'] == 'val4 foo ', 'getParameterValues' + assert ( + headers['X-Param-Map'] == 'var2= var1=val1 var4=val4,foo ' + ), 'getParameterMap' def test_java_application_post_variables(self): self.load('post_params') @@ -113,9 +99,9 @@ class TestJavaApplication(TestApplicationJava): body='var1=val1&var2=', )['headers'] - self.assertEqual(headers['X-Var-1'], 'val1', 'POST variables') - self.assertEqual(headers['X-Var-2'], 'true', 'POST variables 2') - self.assertEqual(headers['X-Var-3'], 'false', 'POST variables 3') + assert headers['X-Var-1'] == 'val1', 'POST variables' + assert headers['X-Var-2'] == 'true', 'POST variables 2' + assert headers['X-Var-3'] == 'false', 'POST variables 3' def test_java_application_session(self): self.load('session') @@ -123,8 +109,8 @@ class TestJavaApplication(TestApplicationJava): headers = self.get(url='/?var1=val1')['headers'] session_id = headers['X-Session-Id'] - self.assertEqual(headers['X-Var-1'], 'null', 'variable empty') - self.assertEqual(headers['X-Session-New'], 'true', 'session create') + assert headers['X-Var-1'] == 'null', 'variable empty' + assert headers['X-Session-New'] == 'true', 'session create' headers = self.get( headers={ @@ -135,36 +121,33 @@ class TestJavaApplication(TestApplicationJava): url='/?var1=val2', )['headers'] - self.assertEqual(headers['X-Var-1'], 'val1', 'variable') - self.assertEqual(headers['X-Session-New'], 'false', 'session resume') - self.assertEqual( - session_id, headers['X-Session-Id'], 'session same id' - ) + assert headers['X-Var-1'] == 'val1', 'variable' + assert headers['X-Session-New'] == 'false', 'session resume' + assert session_id == headers['X-Session-Id'], 'session same id' def test_java_application_session_active(self): self.load('session_inactive') - resp = self.get(headers={ - 'X-Interval': '4', - 'Host': 'localhost', - 'Connection': 'close', - }) + resp = self.get( + headers={ + 'X-Interval': '4', + 'Host': 'localhost', + 'Connection': 'close', + } + ) session_id = resp['headers']['X-Session-Id'] - self.assertEqual(resp['status'], 200, 'session init') - self.assertEqual( - resp['headers']['X-Session-Interval'], '4', 'session interval' - ) - self.assertLess( + assert resp['status'] == 200, 'session init' + assert resp['headers']['X-Session-Interval'] == '4', 'session interval' + assert ( abs( self.date_to_sec_epoch( resp['headers']['X-Session-Last-Access-Time'] ) - self.sec_epoch() - ), - 5, - 'session last access time', - ) + ) + < 5 + ), 'session last access time' time.sleep(1) @@ -176,9 +159,7 @@ class TestJavaApplication(TestApplicationJava): } ) - self.assertEqual( - resp['headers']['X-Session-Id'], session_id, 'session active' - ) + assert resp['headers']['X-Session-Id'] == session_id, 'session active' session_id = resp['headers']['X-Session-Id'] @@ -192,9 +173,9 @@ class TestJavaApplication(TestApplicationJava): } ) - self.assertEqual( - resp['headers']['X-Session-Id'], session_id, 'session active 2' - ) + assert ( + resp['headers']['X-Session-Id'] == session_id + ), 'session active 2' time.sleep(2) @@ -206,18 +187,20 @@ class TestJavaApplication(TestApplicationJava): } ) - self.assertEqual( - resp['headers']['X-Session-Id'], session_id, 'session active 3' - ) + assert ( + resp['headers']['X-Session-Id'] == session_id + ), 'session active 3' def test_java_application_session_inactive(self): self.load('session_inactive') - resp = self.get(headers={ - 'X-Interval': '1', - 'Host': 'localhost', - 'Connection': 'close', - }) + resp = self.get( + headers={ + 'X-Interval': '1', + 'Host': 'localhost', + 'Connection': 'close', + } + ) session_id = resp['headers']['X-Session-Id'] time.sleep(3) @@ -230,9 +213,9 @@ class TestJavaApplication(TestApplicationJava): } ) - self.assertNotEqual( - resp['headers']['X-Session-Id'], session_id, 'session inactive' - ) + assert ( + resp['headers']['X-Session-Id'] != session_id + ), 'session inactive' def test_java_application_session_invalidate(self): self.load('session_invalidate') @@ -248,9 +231,9 @@ class TestJavaApplication(TestApplicationJava): } ) - self.assertNotEqual( - resp['headers']['X-Session-Id'], session_id, 'session invalidate' - ) + assert ( + resp['headers']['X-Session-Id'] != session_id + ), 'session invalidate' def test_java_application_session_listeners(self): self.load('session_listeners') @@ -258,10 +241,8 @@ class TestJavaApplication(TestApplicationJava): headers = self.get(url='/test?var1=val1')['headers'] session_id = headers['X-Session-Id'] - self.assertEqual( - headers['X-Session-Created'], session_id, 'session create' - ) - self.assertEqual(headers['X-Attr-Added'], 'var1=val1', 'attribute add') + assert headers['X-Session-Created'] == session_id, 'session create' + assert headers['X-Attr-Added'] == 'var1=val1', 'attribute add' headers = self.get( headers={ @@ -272,12 +253,8 @@ class TestJavaApplication(TestApplicationJava): url='/?var1=val2', )['headers'] - self.assertEqual( - session_id, headers['X-Session-Id'], 'session same id' - ) - self.assertEqual( - headers['X-Attr-Replaced'], 'var1=val1', 'attribute replace' - ) + assert session_id == headers['X-Session-Id'], 'session same id' + assert headers['X-Attr-Replaced'] == 'var1=val1', 'attribute replace' headers = self.get( headers={ @@ -288,289 +265,219 @@ class TestJavaApplication(TestApplicationJava): url='/', )['headers'] - self.assertEqual( - session_id, headers['X-Session-Id'], 'session same id' - ) - self.assertEqual( - headers['X-Attr-Removed'], 'var1=val2', 'attribute remove' - ) + assert session_id == headers['X-Session-Id'], 'session same id' + assert headers['X-Attr-Removed'] == 'var1=val2', 'attribute remove' def test_java_application_jsp(self): self.load('jsp') headers = self.get(url='/index.jsp')['headers'] - self.assertEqual(headers['X-Unit-JSP'], 'ok', 'JSP Ok header') + assert headers['X-Unit-JSP'] == 'ok', 'JSP Ok header' def test_java_application_url_pattern(self): self.load('url_pattern') headers = self.get(url='/foo/bar/index.html')['headers'] - self.assertEqual(headers['X-Id'], 'servlet1', '#1 Servlet1 request') - self.assertEqual( - headers['X-Request-URI'], '/foo/bar/index.html', '#1 request URI' - ) - self.assertEqual( - headers['X-Servlet-Path'], '/foo/bar', '#1 servlet path' - ) - self.assertEqual(headers['X-Path-Info'], '/index.html', '#1 path info') + assert headers['X-Id'] == 'servlet1', '#1 Servlet1 request' + assert ( + headers['X-Request-URI'] == '/foo/bar/index.html' + ), '#1 request URI' + assert headers['X-Servlet-Path'] == '/foo/bar', '#1 servlet path' + assert headers['X-Path-Info'] == '/index.html', '#1 path info' headers = self.get(url='/foo/bar/index.bop')['headers'] - self.assertEqual(headers['X-Id'], 'servlet1', '#2 Servlet1 request') - self.assertEqual( - headers['X-Request-URI'], '/foo/bar/index.bop', '#2 request URI' - ) - self.assertEqual( - headers['X-Servlet-Path'], '/foo/bar', '#2 servlet path' - ) - self.assertEqual(headers['X-Path-Info'], '/index.bop', '#2 path info') + assert headers['X-Id'] == 'servlet1', '#2 Servlet1 request' + assert ( + headers['X-Request-URI'] == '/foo/bar/index.bop' + ), '#2 request URI' + assert headers['X-Servlet-Path'] == '/foo/bar', '#2 servlet path' + assert headers['X-Path-Info'] == '/index.bop', '#2 path info' headers = self.get(url='/baz')['headers'] - self.assertEqual(headers['X-Id'], 'servlet2', '#3 Servlet2 request') - self.assertEqual(headers['X-Request-URI'], '/baz', '#3 request URI') - self.assertEqual(headers['X-Servlet-Path'], '/baz', '#3 servlet path') - self.assertEqual(headers['X-Path-Info'], 'null', '#3 path info') + assert headers['X-Id'] == 'servlet2', '#3 Servlet2 request' + assert headers['X-Request-URI'] == '/baz', '#3 request URI' + assert headers['X-Servlet-Path'] == '/baz', '#3 servlet path' + assert headers['X-Path-Info'] == 'null', '#3 path info' headers = self.get(url='/baz/index.html')['headers'] - self.assertEqual(headers['X-Id'], 'servlet2', '#4 Servlet2 request') - self.assertEqual( - headers['X-Request-URI'], '/baz/index.html', '#4 request URI' - ) - self.assertEqual(headers['X-Servlet-Path'], '/baz', '#4 servlet path') - self.assertEqual(headers['X-Path-Info'], '/index.html', '#4 path info') + assert headers['X-Id'] == 'servlet2', '#4 Servlet2 request' + assert headers['X-Request-URI'] == '/baz/index.html', '#4 request URI' + assert headers['X-Servlet-Path'] == '/baz', '#4 servlet path' + assert headers['X-Path-Info'] == '/index.html', '#4 path info' headers = self.get(url='/catalog')['headers'] - self.assertEqual(headers['X-Id'], 'servlet3', '#5 Servlet3 request') - self.assertEqual( - headers['X-Request-URI'], '/catalog', '#5 request URI' - ) - self.assertEqual( - headers['X-Servlet-Path'], '/catalog', '#5 servlet path' - ) - self.assertEqual(headers['X-Path-Info'], 'null', '#5 path info') + assert headers['X-Id'] == 'servlet3', '#5 Servlet3 request' + assert headers['X-Request-URI'] == '/catalog', '#5 request URI' + assert headers['X-Servlet-Path'] == '/catalog', '#5 servlet path' + assert headers['X-Path-Info'] == 'null', '#5 path info' headers = self.get(url='/catalog/index.html')['headers'] - self.assertEqual(headers['X-Id'], 'default', '#6 default request') - self.assertEqual( - headers['X-Request-URI'], '/catalog/index.html', '#6 request URI' - ) - self.assertEqual( - headers['X-Servlet-Path'], '/catalog/index.html', '#6 servlet path' - ) - self.assertEqual(headers['X-Path-Info'], 'null', '#6 path info') + assert headers['X-Id'] == 'default', '#6 default request' + assert ( + headers['X-Request-URI'] == '/catalog/index.html' + ), '#6 request URI' + assert ( + headers['X-Servlet-Path'] == '/catalog/index.html' + ), '#6 servlet path' + assert headers['X-Path-Info'] == 'null', '#6 path info' headers = self.get(url='/catalog/racecar.bop')['headers'] - self.assertEqual(headers['X-Id'], 'servlet4', '#7 servlet4 request') - self.assertEqual( - headers['X-Request-URI'], '/catalog/racecar.bop', '#7 request URI' - ) - self.assertEqual( - headers['X-Servlet-Path'], - '/catalog/racecar.bop', - '#7 servlet path', - ) - self.assertEqual(headers['X-Path-Info'], 'null', '#7 path info') + assert headers['X-Id'] == 'servlet4', '#7 servlet4 request' + assert ( + headers['X-Request-URI'] == '/catalog/racecar.bop' + ), '#7 request URI' + assert ( + headers['X-Servlet-Path'] == '/catalog/racecar.bop' + ), '#7 servlet path' + assert headers['X-Path-Info'] == 'null', '#7 path info' headers = self.get(url='/index.bop')['headers'] - self.assertEqual(headers['X-Id'], 'servlet4', '#8 servlet4 request') - self.assertEqual( - headers['X-Request-URI'], '/index.bop', '#8 request URI' - ) - self.assertEqual( - headers['X-Servlet-Path'], '/index.bop', '#8 servlet path' - ) - self.assertEqual(headers['X-Path-Info'], 'null', '#8 path info') + assert headers['X-Id'] == 'servlet4', '#8 servlet4 request' + assert headers['X-Request-URI'] == '/index.bop', '#8 request URI' + assert headers['X-Servlet-Path'] == '/index.bop', '#8 servlet path' + assert headers['X-Path-Info'] == 'null', '#8 path info' headers = self.get(url='/foo/baz')['headers'] - self.assertEqual(headers['X-Id'], 'servlet0', '#9 servlet0 request') - self.assertEqual( - headers['X-Request-URI'], '/foo/baz', '#9 request URI' - ) - self.assertEqual(headers['X-Servlet-Path'], '/foo', '#9 servlet path') - self.assertEqual(headers['X-Path-Info'], '/baz', '#9 path info') + assert headers['X-Id'] == 'servlet0', '#9 servlet0 request' + assert headers['X-Request-URI'] == '/foo/baz', '#9 request URI' + assert headers['X-Servlet-Path'] == '/foo', '#9 servlet path' + assert headers['X-Path-Info'] == '/baz', '#9 path info' headers = self.get()['headers'] - self.assertEqual(headers['X-Id'], 'default', '#10 default request') - self.assertEqual(headers['X-Request-URI'], '/', '#10 request URI') - self.assertEqual(headers['X-Servlet-Path'], '/', '#10 servlet path') - self.assertEqual(headers['X-Path-Info'], 'null', '#10 path info') + assert headers['X-Id'] == 'default', '#10 default request' + assert headers['X-Request-URI'] == '/', '#10 request URI' + assert headers['X-Servlet-Path'] == '/', '#10 servlet path' + assert headers['X-Path-Info'] == 'null', '#10 path info' headers = self.get(url='/index.bop/')['headers'] - self.assertEqual(headers['X-Id'], 'default', '#11 default request') - self.assertEqual( - headers['X-Request-URI'], '/index.bop/', '#11 request URI' - ) - self.assertEqual( - headers['X-Servlet-Path'], '/index.bop/', '#11 servlet path' - ) - self.assertEqual(headers['X-Path-Info'], 'null', '#11 path info') + assert headers['X-Id'] == 'default', '#11 default request' + assert headers['X-Request-URI'] == '/index.bop/', '#11 request URI' + assert headers['X-Servlet-Path'] == '/index.bop/', '#11 servlet path' + assert headers['X-Path-Info'] == 'null', '#11 path info' def test_java_application_header(self): self.load('header') headers = self.get()['headers'] - self.assertEqual( - headers['X-Set-Utf8-Value'], '????', 'set Utf8 header value' - ) - self.assertEqual( - headers['X-Set-Utf8-Name-???'], 'x', 'set Utf8 header name' - ) - self.assertEqual( - headers['X-Add-Utf8-Value'], '????', 'add Utf8 header value' - ) - self.assertEqual( - headers['X-Add-Utf8-Name-???'], 'y', 'add Utf8 header name' - ) - self.assertEqual(headers['X-Add-Test'], 'v1', 'add null header') - self.assertEqual('X-Set-Test1' in headers, False, 'set null header') - self.assertEqual(headers['X-Set-Test2'], '', 'set empty header') + assert headers['X-Set-Utf8-Value'] == '????', 'set Utf8 header value' + assert headers['X-Set-Utf8-Name-???'] == 'x', 'set Utf8 header name' + assert headers['X-Add-Utf8-Value'] == '????', 'add Utf8 header value' + assert headers['X-Add-Utf8-Name-???'] == 'y', 'add Utf8 header name' + assert headers['X-Add-Test'] == 'v1', 'add null header' + assert ('X-Set-Test1' in headers) == False, 'set null header' + assert headers['X-Set-Test2'] == '', 'set empty header' def test_java_application_content_type(self): self.load('content_type') headers = self.get(url='/1')['headers'] - self.assertEqual( - headers['Content-Type'], - 'text/plain;charset=utf-8', - '#1 Content-Type header', - ) - self.assertEqual( - headers['X-Content-Type'], - 'text/plain;charset=utf-8', - '#1 response Content-Type', - ) - self.assertEqual( - headers['X-Character-Encoding'], 'utf-8', '#1 response charset' - ) + assert ( + headers['Content-Type'] == 'text/plain;charset=utf-8' + ), '#1 Content-Type header' + assert ( + headers['X-Content-Type'] == 'text/plain;charset=utf-8' + ), '#1 response Content-Type' + assert ( + headers['X-Character-Encoding'] == 'utf-8' + ), '#1 response charset' headers = self.get(url='/2')['headers'] - self.assertEqual( - headers['Content-Type'], - 'text/plain;charset=iso-8859-1', - '#2 Content-Type header', - ) - self.assertEqual( - headers['X-Content-Type'], - 'text/plain;charset=iso-8859-1', - '#2 response Content-Type', - ) - self.assertEqual( - headers['X-Character-Encoding'], - 'iso-8859-1', - '#2 response charset', - ) + assert ( + headers['Content-Type'] == 'text/plain;charset=iso-8859-1' + ), '#2 Content-Type header' + assert ( + headers['X-Content-Type'] == 'text/plain;charset=iso-8859-1' + ), '#2 response Content-Type' + assert ( + headers['X-Character-Encoding'] == 'iso-8859-1' + ), '#2 response charset' headers = self.get(url='/3')['headers'] - self.assertEqual( - headers['Content-Type'], - 'text/plain;charset=windows-1251', - '#3 Content-Type header', - ) - self.assertEqual( - headers['X-Content-Type'], - 'text/plain;charset=windows-1251', - '#3 response Content-Type', - ) - self.assertEqual( - headers['X-Character-Encoding'], - 'windows-1251', - '#3 response charset', - ) + assert ( + headers['Content-Type'] == 'text/plain;charset=windows-1251' + ), '#3 Content-Type header' + assert ( + headers['X-Content-Type'] == 'text/plain;charset=windows-1251' + ), '#3 response Content-Type' + assert ( + headers['X-Character-Encoding'] == 'windows-1251' + ), '#3 response charset' headers = self.get(url='/4')['headers'] - self.assertEqual( - headers['Content-Type'], - 'text/plain;charset=windows-1251', - '#4 Content-Type header', - ) - self.assertEqual( - headers['X-Content-Type'], - 'text/plain;charset=windows-1251', - '#4 response Content-Type', - ) - self.assertEqual( - headers['X-Character-Encoding'], - 'windows-1251', - '#4 response charset', - ) + assert ( + headers['Content-Type'] == 'text/plain;charset=windows-1251' + ), '#4 Content-Type header' + assert ( + headers['X-Content-Type'] == 'text/plain;charset=windows-1251' + ), '#4 response Content-Type' + assert ( + headers['X-Character-Encoding'] == 'windows-1251' + ), '#4 response charset' headers = self.get(url='/5')['headers'] - self.assertEqual( - headers['Content-Type'], - 'text/plain;charset=iso-8859-1', - '#5 Content-Type header', - ) - self.assertEqual( - headers['X-Content-Type'], - 'text/plain;charset=iso-8859-1', - '#5 response Content-Type', - ) - self.assertEqual( - headers['X-Character-Encoding'], - 'iso-8859-1', - '#5 response charset', - ) + assert ( + headers['Content-Type'] == 'text/plain;charset=iso-8859-1' + ), '#5 Content-Type header' + assert ( + headers['X-Content-Type'] == 'text/plain;charset=iso-8859-1' + ), '#5 response Content-Type' + assert ( + headers['X-Character-Encoding'] == 'iso-8859-1' + ), '#5 response charset' headers = self.get(url='/6')['headers'] - self.assertEqual( - 'Content-Type' in headers, False, '#6 no Content-Type header' - ) - self.assertEqual( - 'X-Content-Type' in headers, False, '#6 no response Content-Type' - ) - self.assertEqual( - headers['X-Character-Encoding'], 'utf-8', '#6 response charset' - ) + assert ( + 'Content-Type' in headers + ) == False, '#6 no Content-Type header' + assert ( + 'X-Content-Type' in headers + ) == False, '#6 no response Content-Type' + assert ( + headers['X-Character-Encoding'] == 'utf-8' + ), '#6 response charset' headers = self.get(url='/7')['headers'] - self.assertEqual( - headers['Content-Type'], - 'text/plain;charset=utf-8', - '#7 Content-Type header', - ) - self.assertEqual( - headers['X-Content-Type'], - 'text/plain;charset=utf-8', - '#7 response Content-Type', - ) - self.assertEqual( - headers['X-Character-Encoding'], 'utf-8', '#7 response charset' - ) + assert ( + headers['Content-Type'] == 'text/plain;charset=utf-8' + ), '#7 Content-Type header' + assert ( + headers['X-Content-Type'] == 'text/plain;charset=utf-8' + ), '#7 response Content-Type' + assert ( + headers['X-Character-Encoding'] == 'utf-8' + ), '#7 response charset' headers = self.get(url='/8')['headers'] - self.assertEqual( - headers['Content-Type'], - 'text/html;charset=utf-8', - '#8 Content-Type header', - ) - self.assertEqual( - headers['X-Content-Type'], - 'text/html;charset=utf-8', - '#8 response Content-Type', - ) - self.assertEqual( - headers['X-Character-Encoding'], 'utf-8', '#8 response charset' - ) + assert ( + headers['Content-Type'] == 'text/html;charset=utf-8' + ), '#8 Content-Type header' + assert ( + headers['X-Content-Type'] == 'text/html;charset=utf-8' + ), '#8 response Content-Type' + assert ( + headers['X-Character-Encoding'] == 'utf-8' + ), '#8 response charset' def test_java_application_welcome_files(self): self.load('welcome_files') @@ -579,126 +486,90 @@ class TestJavaApplication(TestApplicationJava): resp = self.get(url='/dir1') - self.assertEqual(resp['status'], 302, 'dir redirect expected') + assert resp['status'] == 302, 'dir redirect expected' resp = self.get(url='/dir1/') - self.assertEqual( - 'This is index.txt.' in resp['body'], True, 'dir1 index body' - ) - self.assertEqual( - resp['headers']['X-TXT-Filter'], '1', 'TXT Filter header' - ) + assert ( + 'This is index.txt.' in resp['body'] + ) == True, 'dir1 index body' + assert resp['headers']['X-TXT-Filter'] == '1', 'TXT Filter header' headers = self.get(url='/dir2/')['headers'] - self.assertEqual(headers['X-Unit-JSP'], 'ok', 'JSP Ok header') - self.assertEqual(headers['X-JSP-Filter'], '1', 'JSP Filter header') + assert headers['X-Unit-JSP'] == 'ok', 'JSP Ok header' + assert headers['X-JSP-Filter'] == '1', 'JSP Filter header' headers = self.get(url='/dir3/')['headers'] - self.assertEqual( - headers['X-App-Servlet'], '1', 'URL pattern overrides welcome file' - ) + assert ( + headers['X-App-Servlet'] == '1' + ), 'URL pattern overrides welcome file' headers = self.get(url='/dir4/')['headers'] - self.assertEqual( - 'X-App-Servlet' in headers, - False, - 'Static welcome file served first', - ) + assert ( + 'X-App-Servlet' in headers + ) == False, 'Static welcome file served first' headers = self.get(url='/dir5/')['headers'] - self.assertEqual( - headers['X-App-Servlet'], - '1', - 'Servlet for welcome file served when no static file found', - ) + assert ( + headers['X-App-Servlet'] == '1' + ), 'Servlet for welcome file served when no static file found' def test_java_application_request_listeners(self): self.load('request_listeners') headers = self.get(url='/test1')['headers'] - self.assertEqual( - headers['X-Request-Initialized'], - '/test1', - 'request initialized event', - ) - self.assertEqual( - headers['X-Request-Destroyed'], '', 'request destroyed event' - ) - self.assertEqual(headers['X-Attr-Added'], '', 'attribute added event') - self.assertEqual( - headers['X-Attr-Removed'], '', 'attribute removed event' - ) - self.assertEqual( - headers['X-Attr-Replaced'], '', 'attribute replaced event' - ) + assert ( + headers['X-Request-Initialized'] == '/test1' + ), 'request initialized event' + assert headers['X-Request-Destroyed'] == '', 'request destroyed event' + assert headers['X-Attr-Added'] == '', 'attribute added event' + assert headers['X-Attr-Removed'] == '', 'attribute removed event' + assert headers['X-Attr-Replaced'] == '', 'attribute replaced event' headers = self.get(url='/test2?var1=1')['headers'] - self.assertEqual( - headers['X-Request-Initialized'], - '/test2', - 'request initialized event', - ) - self.assertEqual( - headers['X-Request-Destroyed'], '/test1', 'request destroyed event' - ) - self.assertEqual( - headers['X-Attr-Added'], 'var=1;', 'attribute added event' - ) - self.assertEqual( - headers['X-Attr-Removed'], 'var=1;', 'attribute removed event' - ) - self.assertEqual( - headers['X-Attr-Replaced'], '', 'attribute replaced event' - ) + assert ( + headers['X-Request-Initialized'] == '/test2' + ), 'request initialized event' + assert ( + headers['X-Request-Destroyed'] == '/test1' + ), 'request destroyed event' + assert headers['X-Attr-Added'] == 'var=1;', 'attribute added event' + assert headers['X-Attr-Removed'] == 'var=1;', 'attribute removed event' + assert headers['X-Attr-Replaced'] == '', 'attribute replaced event' headers = self.get(url='/test3?var1=1&var2=2')['headers'] - self.assertEqual( - headers['X-Request-Initialized'], - '/test3', - 'request initialized event', - ) - self.assertEqual( - headers['X-Request-Destroyed'], '/test2', 'request destroyed event' - ) - self.assertEqual( - headers['X-Attr-Added'], 'var=1;', 'attribute added event' - ) - self.assertEqual( - headers['X-Attr-Removed'], 'var=2;', 'attribute removed event' - ) - self.assertEqual( - headers['X-Attr-Replaced'], 'var=1;', 'attribute replaced event' - ) + assert ( + headers['X-Request-Initialized'] == '/test3' + ), 'request initialized event' + assert ( + headers['X-Request-Destroyed'] == '/test2' + ), 'request destroyed event' + assert headers['X-Attr-Added'] == 'var=1;', 'attribute added event' + assert headers['X-Attr-Removed'] == 'var=2;', 'attribute removed event' + assert ( + headers['X-Attr-Replaced'] == 'var=1;' + ), 'attribute replaced event' headers = self.get(url='/test4?var1=1&var2=2&var3=3')['headers'] - self.assertEqual( - headers['X-Request-Initialized'], - '/test4', - 'request initialized event', - ) - self.assertEqual( - headers['X-Request-Destroyed'], '/test3', 'request destroyed event' - ) - self.assertEqual( - headers['X-Attr-Added'], 'var=1;', 'attribute added event' - ) - self.assertEqual( - headers['X-Attr-Removed'], '', 'attribute removed event' - ) - self.assertEqual( - headers['X-Attr-Replaced'], - 'var=1;var=2;', - 'attribute replaced event', - ) + assert ( + headers['X-Request-Initialized'] == '/test4' + ), 'request initialized event' + assert ( + headers['X-Request-Destroyed'] == '/test3' + ), 'request destroyed event' + assert headers['X-Attr-Added'] == 'var=1;', 'attribute added event' + assert headers['X-Attr-Removed'] == '', 'attribute removed event' + assert ( + headers['X-Attr-Replaced'] == 'var=1;var=2;' + ), 'attribute replaced event' def test_java_application_request_uri_forward(self): self.load('forward') @@ -708,105 +579,67 @@ class TestJavaApplication(TestApplicationJava): ) headers = resp['headers'] - self.assertEqual( - headers['X-REQUEST-Id'], 'fwd', 'initial request servlet mapping' - ) - self.assertEqual( - headers['X-Forward-To'], - '/data/test?uri=new_uri&a=2&b=3', - 'forwarding triggered', - ) - self.assertEqual( - headers['X-REQUEST-Param-uri'], - '/data/test?uri=new_uri&a=2&b=3', - 'original uri parameter', - ) - self.assertEqual( - headers['X-REQUEST-Param-a'], '1', 'original a parameter' - ) - self.assertEqual( - headers['X-REQUEST-Param-c'], '4', 'original c parameter' - ) - - self.assertEqual( - headers['X-FORWARD-Id'], 'data', 'forward request servlet mapping' - ) - self.assertEqual( - headers['X-FORWARD-Request-URI'], - '/data/test', - 'forward request uri', - ) - self.assertEqual( - headers['X-FORWARD-Servlet-Path'], - '/data', - 'forward request servlet path', - ) - self.assertEqual( - headers['X-FORWARD-Path-Info'], - '/test', - 'forward request path info', - ) - self.assertEqual( - headers['X-FORWARD-Query-String'], - 'uri=new_uri&a=2&b=3', - 'forward request query string', - ) - self.assertEqual( - headers['X-FORWARD-Param-uri'], - 'new_uri,/data/test?uri=new_uri&a=2&b=3', - 'forward uri parameter', - ) - self.assertEqual( - headers['X-FORWARD-Param-a'], '2,1', 'forward a parameter' - ) - self.assertEqual( - headers['X-FORWARD-Param-b'], '3', 'forward b parameter' - ) - self.assertEqual( - headers['X-FORWARD-Param-c'], '4', 'forward c parameter' - ) - - self.assertEqual( - headers['X-javax.servlet.forward.request_uri'], - '/fwd', - 'original request uri', - ) - self.assertEqual( - headers['X-javax.servlet.forward.context_path'], - '', - 'original request context path', - ) - self.assertEqual( - headers['X-javax.servlet.forward.servlet_path'], - '/fwd', - 'original request servlet path', - ) - self.assertEqual( - headers['X-javax.servlet.forward.path_info'], - 'null', - 'original request path info', - ) - self.assertEqual( - headers['X-javax.servlet.forward.query_string'], - 'uri=%2Fdata%2Ftest%3Furi%3Dnew_uri%26a%3D2%26b%3D3&a=1&c=4', - 'original request query', - ) - - self.assertEqual( - 'Before forwarding' in resp['body'], - False, - 'discarded data added before forward() call', - ) - self.assertEqual( - 'X-After-Forwarding' in headers, - False, - 'cannot add headers after forward() call', - ) - self.assertEqual( - 'After forwarding' in resp['body'], - False, - 'cannot add data after forward() call', - ) + assert ( + headers['X-REQUEST-Id'] == 'fwd' + ), 'initial request servlet mapping' + assert ( + headers['X-Forward-To'] == '/data/test?uri=new_uri&a=2&b=3' + ), 'forwarding triggered' + assert ( + headers['X-REQUEST-Param-uri'] == '/data/test?uri=new_uri&a=2&b=3' + ), 'original uri parameter' + assert headers['X-REQUEST-Param-a'] == '1', 'original a parameter' + assert headers['X-REQUEST-Param-c'] == '4', 'original c parameter' + + assert ( + headers['X-FORWARD-Id'] == 'data' + ), 'forward request servlet mapping' + assert ( + headers['X-FORWARD-Request-URI'] == '/data/test' + ), 'forward request uri' + assert ( + headers['X-FORWARD-Servlet-Path'] == '/data' + ), 'forward request servlet path' + assert ( + headers['X-FORWARD-Path-Info'] == '/test' + ), 'forward request path info' + assert ( + headers['X-FORWARD-Query-String'] == 'uri=new_uri&a=2&b=3' + ), 'forward request query string' + assert ( + headers['X-FORWARD-Param-uri'] + == 'new_uri,/data/test?uri=new_uri&a=2&b=3' + ), 'forward uri parameter' + assert headers['X-FORWARD-Param-a'] == '2,1', 'forward a parameter' + assert headers['X-FORWARD-Param-b'] == '3', 'forward b parameter' + assert headers['X-FORWARD-Param-c'] == '4', 'forward c parameter' + + assert ( + headers['X-javax.servlet.forward.request_uri'] == '/fwd' + ), 'original request uri' + assert ( + headers['X-javax.servlet.forward.context_path'] == '' + ), 'original request context path' + assert ( + headers['X-javax.servlet.forward.servlet_path'] == '/fwd' + ), 'original request servlet path' + assert ( + headers['X-javax.servlet.forward.path_info'] == 'null' + ), 'original request path info' + assert ( + headers['X-javax.servlet.forward.query_string'] + == 'uri=%2Fdata%2Ftest%3Furi%3Dnew_uri%26a%3D2%26b%3D3&a=1&c=4' + ), 'original request query' + + assert ( + 'Before forwarding' in resp['body'] + ) == False, 'discarded data added before forward() call' + assert ( + 'X-After-Forwarding' in headers + ) == False, 'cannot add headers after forward() call' + assert ( + 'After forwarding' in resp['body'] + ) == False, 'cannot add data after forward() call' def test_java_application_named_dispatcher_forward(self): self.load('forward') @@ -814,74 +647,52 @@ class TestJavaApplication(TestApplicationJava): resp = self.get(url='/fwd?disp=name&uri=data') headers = resp['headers'] - self.assertEqual( - headers['X-REQUEST-Id'], 'fwd', 'initial request servlet mapping' - ) - self.assertEqual( - headers['X-Forward-To'], 'data', 'forwarding triggered' - ) - - self.assertEqual( - headers['X-FORWARD-Id'], 'data', 'forward request servlet mapping' - ) - self.assertEqual( - headers['X-FORWARD-Request-URI'], '/fwd', 'forward request uri' - ) - self.assertEqual( - headers['X-FORWARD-Servlet-Path'], - '/fwd', - 'forward request servlet path', - ) - self.assertEqual( - headers['X-FORWARD-Path-Info'], 'null', 'forward request path info' - ) - self.assertEqual( - headers['X-FORWARD-Query-String'], - 'disp=name&uri=data', - 'forward request query string', - ) - - self.assertEqual( - headers['X-javax.servlet.forward.request_uri'], - 'null', - 'original request uri', - ) - self.assertEqual( - headers['X-javax.servlet.forward.context_path'], - 'null', - 'original request context path', - ) - self.assertEqual( - headers['X-javax.servlet.forward.servlet_path'], - 'null', - 'original request servlet path', - ) - self.assertEqual( - headers['X-javax.servlet.forward.path_info'], - 'null', - 'original request path info', - ) - self.assertEqual( - headers['X-javax.servlet.forward.query_string'], - 'null', - 'original request query', - ) - - self.assertEqual( - 'Before forwarding' in resp['body'], - False, - 'discarded data added before forward() call', - ) - self.assertEqual( - 'X-After-Forwarding' in headers, - False, - 'cannot add headers after forward() call', - ) - self.assertEqual( - 'After forwarding' in resp['body'], - False, - 'cannot add data after forward() call', - ) + assert ( + headers['X-REQUEST-Id'] == 'fwd' + ), 'initial request servlet mapping' + assert headers['X-Forward-To'] == 'data', 'forwarding triggered' + + assert ( + headers['X-FORWARD-Id'] == 'data' + ), 'forward request servlet mapping' + assert ( + headers['X-FORWARD-Request-URI'] == '/fwd' + ), 'forward request uri' + assert ( + headers['X-FORWARD-Servlet-Path'] == '/fwd' + ), 'forward request servlet path' + assert ( + headers['X-FORWARD-Path-Info'] == 'null' + ), 'forward request path info' + assert ( + headers['X-FORWARD-Query-String'] == 'disp=name&uri=data' + ), 'forward request query string' + + assert ( + headers['X-javax.servlet.forward.request_uri'] == 'null' + ), 'original request uri' + assert ( + headers['X-javax.servlet.forward.context_path'] == 'null' + ), 'original request context path' + assert ( + headers['X-javax.servlet.forward.servlet_path'] == 'null' + ), 'original request servlet path' + assert ( + headers['X-javax.servlet.forward.path_info'] == 'null' + ), 'original request path info' + assert ( + headers['X-javax.servlet.forward.query_string'] == 'null' + ), 'original request query' + + assert ( + 'Before forwarding' in resp['body'] + ) == False, 'discarded data added before forward() call' + assert ( + 'X-After-Forwarding' in headers + ) == False, 'cannot add headers after forward() call' + assert ( + 'After forwarding' in resp['body'] + ) == False, 'cannot add data after forward() call' def test_java_application_request_uri_include(self): self.load('include') @@ -890,55 +701,40 @@ class TestJavaApplication(TestApplicationJava): headers = resp['headers'] body = resp['body'] - self.assertEqual( - headers['X-REQUEST-Id'], 'inc', 'initial request servlet mapping' - ) - self.assertEqual( - headers['X-Include'], '/data/test', 'including triggered' - ) - - self.assertEqual( - 'X-INCLUDE-Id' in headers, - False, - 'unable to add headers in include request', - ) - - self.assertEqual( - 'javax.servlet.include.request_uri: /data/test' in body, - True, - 'include request uri', - ) - # self.assertEqual('javax.servlet.include.context_path: ' in body, - # 'include request context path') - self.assertEqual( - 'javax.servlet.include.servlet_path: /data' in body, - True, - 'include request servlet path', - ) - self.assertEqual( - 'javax.servlet.include.path_info: /test' in body, - True, - 'include request path info', - ) - self.assertEqual( - 'javax.servlet.include.query_string: null' in body, - True, - 'include request query', - ) - - self.assertEqual( - 'Before include' in body, - True, - 'preserve data added before include() call', - ) - self.assertEqual( - headers['X-After-Include'], - 'you-should-see-this', - 'add headers after include() call', - ) - self.assertEqual( - 'After include' in body, True, 'add data after include() call' - ) + assert ( + headers['X-REQUEST-Id'] == 'inc' + ), 'initial request servlet mapping' + assert headers['X-Include'] == '/data/test', 'including triggered' + + assert ( + 'X-INCLUDE-Id' in headers + ) == False, 'unable to add headers in include request' + + assert ( + 'javax.servlet.include.request_uri: /data/test' in body + ) == True, 'include request uri' + #assert ( + # 'javax.servlet.include.context_path: ' in body + #) == True, 'include request context path' + assert ( + 'javax.servlet.include.servlet_path: /data' in body + ) == True, 'include request servlet path' + assert ( + 'javax.servlet.include.path_info: /test' in body + ) == True, 'include request path info' + assert ( + 'javax.servlet.include.query_string: null' in body + ) == True, 'include request query' + + assert ( + 'Before include' in body + ) == True, 'preserve data added before include() call' + assert ( + headers['X-After-Include'] == 'you-should-see-this' + ), 'add headers after include() call' + assert ( + 'After include' in body + ) == True, 'add data after include() call' def test_java_application_named_dispatcher_include(self): self.load('include') @@ -947,144 +743,105 @@ class TestJavaApplication(TestApplicationJava): headers = resp['headers'] body = resp['body'] - self.assertEqual( - headers['X-REQUEST-Id'], 'inc', 'initial request servlet mapping' - ) - self.assertEqual(headers['X-Include'], 'data', 'including triggered') - - self.assertEqual( - 'X-INCLUDE-Id' in headers, - False, - 'unable to add headers in include request', - ) - - self.assertEqual( - 'javax.servlet.include.request_uri: null' in body, - True, - 'include request uri', - ) - # self.assertEqual('javax.servlet.include.context_path: null' in body, - # 'include request context path') - self.assertEqual( - 'javax.servlet.include.servlet_path: null' in body, - True, - 'include request servlet path', - ) - self.assertEqual( - 'javax.servlet.include.path_info: null' in body, - True, - 'include request path info', - ) - self.assertEqual( - 'javax.servlet.include.query_string: null' in body, - True, - 'include request query', - ) - - self.assertEqual( - 'Before include' in body, - True, - 'preserve data added before include() call', - ) - self.assertEqual( - headers['X-After-Include'], - 'you-should-see-this', - 'add headers after include() call', - ) - self.assertEqual( - 'After include' in body, True, 'add data after include() call' - ) + assert ( + headers['X-REQUEST-Id'] == 'inc' + ), 'initial request servlet mapping' + assert headers['X-Include'] == 'data', 'including triggered' + + assert ( + 'X-INCLUDE-Id' in headers + ) == False, 'unable to add headers in include request' + + assert ( + 'javax.servlet.include.request_uri: null' in body + ) == True, 'include request uri' + #assert ( + # 'javax.servlet.include.context_path: null' in body + #) == True, 'include request context path' + assert ( + 'javax.servlet.include.servlet_path: null' in body + ) == True, 'include request servlet path' + assert ( + 'javax.servlet.include.path_info: null' in body + ) == True, 'include request path info' + assert ( + 'javax.servlet.include.query_string: null' in body + ) == True, 'include request query' + + assert ( + 'Before include' in body + ) == True, 'preserve data added before include() call' + assert ( + headers['X-After-Include'] == 'you-should-see-this' + ), 'add headers after include() call' + assert ( + 'After include' in body + ) == True, 'add data after include() call' def test_java_application_path_translation(self): self.load('path_translation') headers = self.get(url='/pt/test?path=/')['headers'] - self.assertEqual( - headers['X-Servlet-Path'], '/pt', 'matched servlet path' - ) - self.assertEqual( - headers['X-Path-Info'], '/test', 'the rest of the path' - ) - self.assertEqual( - headers['X-Path-Translated'], - headers['X-Real-Path'] + headers['X-Path-Info'], - 'translated path is the app root + path info', - ) - self.assertEqual( - headers['X-Resource-Paths'].endswith('/WEB-INF/, /index.html]'), - True, - 'app root directory content', - ) - self.assertEqual( - headers['X-Resource-As-Stream'], - 'null', - 'no resource stream for root path', - ) + assert headers['X-Servlet-Path'] == '/pt', 'matched servlet path' + assert headers['X-Path-Info'] == '/test', 'the rest of the path' + assert ( + headers['X-Path-Translated'] + == headers['X-Real-Path'] + headers['X-Path-Info'] + ), 'translated path is the app root + path info' + assert ( + headers['X-Resource-Paths'].endswith('/WEB-INF/, /index.html]') + == True + ), 'app root directory content' + assert ( + headers['X-Resource-As-Stream'] == 'null' + ), 'no resource stream for root path' headers = self.get(url='/test?path=/none')['headers'] - self.assertEqual( - headers['X-Servlet-Path'], '/test', 'matched whole path' - ) - self.assertEqual( - headers['X-Path-Info'], - 'null', - 'the rest of the path is null, whole path matched', - ) - self.assertEqual( - headers['X-Path-Translated'], - 'null', - 'translated path is null because path info is null', - ) - self.assertEqual( - headers['X-Real-Path'].endswith('/none'), - True, - 'read path is not null', - ) - self.assertEqual( - headers['X-Resource-Paths'], 'null', 'no resource found' - ) - self.assertEqual( - headers['X-Resource-As-Stream'], 'null', 'no resource stream' - ) + assert headers['X-Servlet-Path'] == '/test', 'matched whole path' + assert ( + headers['X-Path-Info'] == 'null' + ), 'the rest of the path is null, whole path matched' + assert ( + headers['X-Path-Translated'] == 'null' + ), 'translated path is null because path info is null' + assert ( + headers['X-Real-Path'].endswith('/none') == True + ), 'read path is not null' + assert headers['X-Resource-Paths'] == 'null', 'no resource found' + assert headers['X-Resource-As-Stream'] == 'null', 'no resource stream' def test_java_application_query_string(self): self.load('query_string') - self.assertEqual( - self.get(url='/?a=b')['headers']['X-Query-String'], - 'a=b', - 'query string', - ) + assert ( + self.get(url='/?a=b')['headers']['X-Query-String'] == 'a=b' + ), 'query string' def test_java_application_query_empty(self): self.load('query_string') - self.assertEqual( - self.get(url='/?')['headers']['X-Query-String'], - '', - 'query string empty', - ) + assert ( + self.get(url='/?')['headers']['X-Query-String'] == '' + ), 'query string empty' def test_java_application_query_absent(self): self.load('query_string') - self.assertEqual( - self.get()['headers']['X-Query-String'], - 'null', - 'query string absent', - ) + assert ( + self.get()['headers']['X-Query-String'] == 'null' + ), 'query string absent' def test_java_application_empty(self): self.load('empty') - self.assertEqual(self.get()['status'], 200, 'empty') + assert self.get()['status'] == 200, 'empty' def test_java_application_keepalive_body(self): self.load('mirror') - self.assertEqual(self.post()['status'], 200, 'init') + assert self.post()['status'] == 200, 'init' body = '0123456789' * 500 (resp, sock) = self.post( @@ -1098,7 +855,7 @@ class TestJavaApplication(TestApplicationJava): read_timeout=1, ) - self.assertEqual(resp['body'], body, 'keep-alive 1') + assert resp['body'] == body, 'keep-alive 1' body = '0123456789' resp = self.post( @@ -1111,22 +868,22 @@ class TestJavaApplication(TestApplicationJava): body=body, ) - self.assertEqual(resp['body'], body, 'keep-alive 2') + assert resp['body'] == body, 'keep-alive 2' def test_java_application_http_10(self): self.load('empty') - self.assertEqual(self.get(http_10=True)['status'], 200, 'HTTP 1.0') + assert self.get(http_10=True)['status'] == 200, 'HTTP 1.0' def test_java_application_no_method(self): self.load('empty') - self.assertEqual(self.post()['status'], 405, 'no method') + assert self.post()['status'] == 405, 'no method' def test_java_application_get_header(self): self.load('get_header') - self.assertEqual( + assert ( self.get( headers={ 'X-Header': 'blah', @@ -1134,15 +891,14 @@ class TestJavaApplication(TestApplicationJava): 'Host': 'localhost', 'Connection': 'close', } - )['headers']['X-Reply'], - 'blah', - 'get header', - ) + )['headers']['X-Reply'] + == 'blah' + ), 'get header' def test_java_application_get_header_empty(self): self.load('get_header') - self.assertNotIn('X-Reply', self.get()['headers'], 'get header empty') + assert 'X-Reply' not in self.get()['headers'], 'get header empty' def test_java_application_get_headers(self): self.load('get_headers') @@ -1156,32 +912,28 @@ class TestJavaApplication(TestApplicationJava): } )['headers'] - self.assertEqual(headers['X-Reply-0'], 'blah', 'get headers') - self.assertEqual(headers['X-Reply-1'], 'blah', 'get headers 2') + assert headers['X-Reply-0'] == 'blah', 'get headers' + assert headers['X-Reply-1'] == 'blah', 'get headers 2' def test_java_application_get_headers_empty(self): self.load('get_headers') - self.assertNotIn( - 'X-Reply-0', self.get()['headers'], 'get headers empty' - ) + assert 'X-Reply-0' not in self.get()['headers'], 'get headers empty' def test_java_application_get_header_names(self): self.load('get_header_names') headers = self.get()['headers'] - self.assertRegex( - headers['X-Reply-0'], r'(?:Host|Connection)', 'get header names' - ) - self.assertRegex( - headers['X-Reply-1'], r'(?:Host|Connection)', 'get header names 2' - ) - self.assertNotEqual( - headers['X-Reply-0'], - headers['X-Reply-1'], - 'get header names not equal', - ) + assert re.search( + r'(?:Host|Connection)', headers['X-Reply-0'] + ), 'get header names' + assert re.search( + r'(?:Host|Connection)', headers['X-Reply-1'] + ), 'get header names 2' + assert ( + headers['X-Reply-0'] != headers['X-Reply-1'] + ), 'get header names not equal' def test_java_application_header_int(self): self.load('header_int') @@ -1195,8 +947,8 @@ class TestJavaApplication(TestApplicationJava): } )['headers'] - self.assertEqual(headers['X-Set-Int'], '1', 'set int header') - self.assertEqual(headers['X-Get-Int'], '2', 'get int header') + assert headers['X-Set-Int'] == '1', 'set int header' + assert headers['X-Get-Int'] == '2', 'get int header' def test_java_application_header_date(self): self.load('header_date') @@ -1212,20 +964,18 @@ class TestJavaApplication(TestApplicationJava): } )['headers'] - self.assertEqual( - headers['X-Set-Date'], - 'Thu, 01 Jan 1970 00:00:01 GMT', - 'set date header', - ) - self.assertEqual(headers['X-Get-Date'], date, 'get date header') + assert ( + headers['X-Set-Date'] == 'Thu, 01 Jan 1970 00:00:01 GMT' + ), 'set date header' + assert headers['X-Get-Date'] == date, 'get date header' def test_java_application_multipart(self): self.load('multipart') reldst = '/uploads' - fulldst = self.testdir + reldst + fulldst = self.temp_dir + reldst os.mkdir(fulldst) - self.public_dir(fulldst) + public_dir(fulldst) fields = { 'file': { @@ -1252,16 +1002,13 @@ class TestJavaApplication(TestApplicationJava): body=body, ) - self.assertEqual(resp['status'], 200, 'multipart status') - self.assertRegex( - resp['body'], r'sample\.txt created', 'multipart body' - ) - self.assertIsNotNone( + assert resp['status'] == 200, 'multipart status' + assert re.search( + r'sample\.txt created', resp['body'] + ), 'multipart body' + assert ( self.search_in_log( r'^Data from sample file$', name=reldst + '/sample.txt' - ), - 'file created', - ) - -if __name__ == '__main__': - TestJavaApplication.main() + ) + is not None + ), 'file created' diff --git a/test/test_java_isolation_rootfs.py b/test/test_java_isolation_rootfs.py index 4d39bdc3..f0f04df1 100644 --- a/test/test_java_isolation_rootfs.py +++ b/test/test_java_isolation_rootfs.py @@ -1,30 +1,32 @@ import os import subprocess -import unittest +import pytest + +from conftest import option from unit.applications.lang.java import TestApplicationJava class TestJavaIsolationRootfs(TestApplicationJava): prerequisites = {'modules': {'java': 'all'}} - def setUp(self): - if not self.is_su: - return + def setup_method(self, is_su): + super().setup_method() - super().setUp() + if not is_su: + return - os.makedirs(self.testdir + '/jars') - os.makedirs(self.testdir + '/tmp') - os.chmod(self.testdir + '/tmp', 0o777) + os.makedirs(self.temp_dir + '/jars') + os.makedirs(self.temp_dir + '/tmp') + os.chmod(self.temp_dir + '/tmp', 0o777) try: process = subprocess.Popen( [ "mount", "--bind", - self.pardir + "/build", - self.testdir + "/jars", + option.current_dir + "/build", + self.temp_dir + "/jars", ], stderr=subprocess.STDOUT, ) @@ -32,54 +34,45 @@ class TestJavaIsolationRootfs(TestApplicationJava): process.communicate() except: - self.fail('Cann\'t run mount process.') + pytest.fail('Cann\'t run mount process.') - def tearDown(self): - if not self.is_su: + def teardown_method(self, is_su): + if not is_su: return try: process = subprocess.Popen( - ["umount", "--lazy", self.testdir + "/jars"], + ["umount", "--lazy", self.temp_dir + "/jars"], stderr=subprocess.STDOUT, ) process.communicate() except: - self.fail('Cann\'t run mount process.') + pytest.fail('Cann\'t run mount process.') # super teardown must happen after unmount to avoid deletion of /build - super().tearDown() + super().teardown_method() - def test_java_isolation_rootfs_chroot_war(self): - if not self.is_su: - print('require root') - raise unittest.SkipTest() + def test_java_isolation_rootfs_chroot_war(self, is_su): + if not is_su: + pytest.skip('require root') isolation = { - 'rootfs': self.testdir, + 'rootfs': self.temp_dir, } self.load('empty_war', isolation=isolation) - self.assertIn( - 'success', - self.conf( - '"/"', '/config/applications/empty_war/working_directory', - ), + assert 'success' in self.conf( + '"/"', '/config/applications/empty_war/working_directory', ) - self.assertIn( - 'success', self.conf('"/jars"', 'applications/empty_war/unit_jars') + assert 'success' in self.conf( + '"/jars"', 'applications/empty_war/unit_jars' ) - self.assertIn( - 'success', - self.conf('"/java/empty.war"', 'applications/empty_war/webapp'), + assert 'success' in self.conf( + '"/java/empty.war"', 'applications/empty_war/webapp' ) - self.assertEqual(self.get()['status'], 200, 'war') - - -if __name__ == '__main__': - TestJavaIsolationRootfs.main() + assert self.get()['status'] == 200, 'war' diff --git a/test/test_java_websockets.py b/test/test_java_websockets.py index d78f7263..7e6d82e8 100644 --- a/test/test_java_websockets.py +++ b/test/test_java_websockets.py @@ -1,7 +1,10 @@ import struct import time -import unittest +import pytest + +from conftest import option +from conftest import skip_alert from unit.applications.lang.java import TestApplicationJava from unit.applications.websockets import TestApplicationWebsocket @@ -11,23 +14,17 @@ class TestJavaWebsockets(TestApplicationJava): ws = TestApplicationWebsocket() - def setUp(self): - super().setUp() + def setup_method(self): + super().setup_method() - self.assertIn( - 'success', - self.conf( - {'http': {'websocket': {'keepalive_interval': 0}}}, 'settings' - ), - 'clear keepalive_interval', - ) + assert 'success' in self.conf( + {'http': {'websocket': {'keepalive_interval': 0}}}, 'settings' + ), 'clear keepalive_interval' - self.skip_alerts.extend( - [r'socket close\(\d+\) failed'] - ) + skip_alert(r'socket close\(\d+\) failed') def close_connection(self, sock): - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', 'empty soc') + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty soc' self.ws.frame_write(sock, self.ws.OP_CLOSE, self.ws.serialize_close()) @@ -36,9 +33,9 @@ class TestJavaWebsockets(TestApplicationJava): def check_close(self, sock, code=1000, no_close=False): frame = self.ws.frame_read(sock) - self.assertEqual(frame['fin'], True, 'close fin') - self.assertEqual(frame['opcode'], self.ws.OP_CLOSE, 'close opcode') - self.assertEqual(frame['code'], code, 'close code') + assert frame['fin'] == True, 'close fin' + assert frame['opcode'] == self.ws.OP_CLOSE, 'close opcode' + assert frame['code'] == code, 'close code' if not no_close: sock.close() @@ -49,9 +46,9 @@ class TestJavaWebsockets(TestApplicationJava): else: data = frame['data'].decode('utf-8') - self.assertEqual(frame['fin'], fin, 'fin') - self.assertEqual(frame['opcode'], opcode, 'opcode') - self.assertEqual(data, payload, 'payload') + assert frame['fin'] == fin, 'fin' + assert frame['opcode'] == opcode, 'opcode' + assert data == payload, 'payload' def test_java_websockets_handshake(self): self.load('websockets_mirror') @@ -59,14 +56,12 @@ class TestJavaWebsockets(TestApplicationJava): resp, sock, key = self.ws.upgrade() sock.close() - self.assertEqual(resp['status'], 101, 'status') - self.assertEqual(resp['headers']['Upgrade'], 'websocket', 'upgrade') - self.assertEqual( - resp['headers']['Connection'], 'Upgrade', 'connection' - ) - self.assertEqual( - resp['headers']['Sec-WebSocket-Accept'], self.ws.accept(key), 'key' - ) + assert resp['status'] == 101, 'status' + assert resp['headers']['Upgrade'] == 'websocket', 'upgrade' + assert resp['headers']['Connection'] == 'Upgrade', 'connection' + assert resp['headers']['Sec-WebSocket-Accept'] == self.ws.accept( + key + ), 'key' def test_java_websockets_mirror(self): self.load('websockets_mirror') @@ -78,12 +73,12 @@ class TestJavaWebsockets(TestApplicationJava): self.ws.frame_write(sock, self.ws.OP_TEXT, message) frame = self.ws.frame_read(sock) - self.assertEqual(message, frame['data'].decode('utf-8'), 'mirror') + assert message == frame['data'].decode('utf-8'), 'mirror' self.ws.frame_write(sock, self.ws.OP_TEXT, message) frame = self.ws.frame_read(sock) - self.assertEqual(message, frame['data'].decode('utf-8'), 'mirror 2') + assert message == frame['data'].decode('utf-8'), 'mirror 2' sock.close() @@ -98,8 +93,8 @@ class TestJavaWebsockets(TestApplicationJava): frame = self.ws.frame_read(sock) - self.assertEqual(frame['opcode'], self.ws.OP_CLOSE, 'no mask opcode') - self.assertEqual(frame['code'], 1002, 'no mask close code') + assert frame['opcode'] == self.ws.OP_CLOSE, 'no mask opcode' + assert frame['code'] == 1002, 'no mask close code' sock.close() @@ -116,11 +111,9 @@ class TestJavaWebsockets(TestApplicationJava): frame = self.ws.frame_read(sock) - self.assertEqual( - message + ' ' + message, - frame['data'].decode('utf-8'), - 'mirror framing', - ) + assert message + ' ' + message == frame['data'].decode( + 'utf-8' + ), 'mirror framing' sock.close() @@ -136,20 +129,16 @@ class TestJavaWebsockets(TestApplicationJava): frame = self.ws.frame_read(sock) frame.pop('data') - self.assertDictEqual( - frame, - { - 'fin': True, - 'rsv1': False, - 'rsv2': False, - 'rsv3': False, - 'opcode': self.ws.OP_CLOSE, - 'mask': 0, - 'code': 1002, - 'reason': 'Fragmented control frame', - }, - 'close frame', - ) + assert frame == { + 'fin': True, + 'rsv1': False, + 'rsv2': False, + 'rsv3': False, + 'opcode': self.ws.OP_CLOSE, + 'mask': 0, + 'code': 1002, + 'reason': 'Fragmented control frame', + }, 'close frame' sock.close() @@ -168,13 +157,13 @@ class TestJavaWebsockets(TestApplicationJava): frame1 = self.ws.frame_read(sock1) frame2 = self.ws.frame_read(sock2) - self.assertEqual(message1, frame1['data'].decode('utf-8'), 'client 1') - self.assertEqual(message2, frame2['data'].decode('utf-8'), 'client 2') + assert message1 == frame1['data'].decode('utf-8'), 'client 1' + assert message2 == frame2['data'].decode('utf-8'), 'client 2' sock1.close() sock2.close() - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_java_websockets_handshake_upgrade_absent( self ): # FAIL https://tools.ietf.org/html/rfc6455#section-4.2.1 @@ -190,7 +179,7 @@ class TestJavaWebsockets(TestApplicationJava): }, ) - self.assertEqual(resp['status'], 400, 'upgrade absent') + assert resp['status'] == 400, 'upgrade absent' def test_java_websockets_handshake_case_insensitive(self): self.load('websockets_mirror') @@ -207,9 +196,9 @@ class TestJavaWebsockets(TestApplicationJava): ) sock.close() - self.assertEqual(resp['status'], 101, 'status') + assert resp['status'] == 101, 'status' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_java_websockets_handshake_connection_absent(self): # FAIL self.load('websockets_mirror') @@ -223,7 +212,7 @@ class TestJavaWebsockets(TestApplicationJava): }, ) - self.assertEqual(resp['status'], 400, 'status') + assert resp['status'] == 400, 'status' def test_java_websockets_handshake_version_absent(self): self.load('websockets_mirror') @@ -238,9 +227,9 @@ class TestJavaWebsockets(TestApplicationJava): }, ) - self.assertEqual(resp['status'], 426, 'status') + assert resp['status'] == 426, 'status' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_java_websockets_handshake_key_invalid(self): self.load('websockets_mirror') @@ -255,7 +244,7 @@ class TestJavaWebsockets(TestApplicationJava): }, ) - self.assertEqual(resp['status'], 400, 'key length') + assert resp['status'] == 400, 'key length' key = self.ws.key() resp = self.get( @@ -269,9 +258,7 @@ class TestJavaWebsockets(TestApplicationJava): }, ) - self.assertEqual( - resp['status'], 400, 'key double' - ) # FAIL https://tools.ietf.org/html/rfc6455#section-11.3.1 + assert resp['status'] == 400, 'key double' # FAIL https://tools.ietf.org/html/rfc6455#section-11.3.1 def test_java_websockets_handshake_method_invalid(self): self.load('websockets_mirror') @@ -287,7 +274,7 @@ class TestJavaWebsockets(TestApplicationJava): }, ) - self.assertEqual(resp['status'], 400, 'status') + assert resp['status'] == 400, 'status' def test_java_websockets_handshake_http_10(self): self.load('websockets_mirror') @@ -304,7 +291,7 @@ class TestJavaWebsockets(TestApplicationJava): http_10=True, ) - self.assertEqual(resp['status'], 400, 'status') + assert resp['status'] == 400, 'status' def test_java_websockets_handshake_uri_invalid(self): self.load('websockets_mirror') @@ -321,7 +308,7 @@ class TestJavaWebsockets(TestApplicationJava): url='!', ) - self.assertEqual(resp['status'], 400, 'status') + assert resp['status'] == 400, 'status' def test_java_websockets_protocol_absent(self): self.load('websockets_mirror') @@ -338,14 +325,12 @@ class TestJavaWebsockets(TestApplicationJava): ) sock.close() - self.assertEqual(resp['status'], 101, 'status') - self.assertEqual(resp['headers']['Upgrade'], 'websocket', 'upgrade') - self.assertEqual( - resp['headers']['Connection'], 'Upgrade', 'connection' - ) - self.assertEqual( - resp['headers']['Sec-WebSocket-Accept'], self.ws.accept(key), 'key' - ) + assert resp['status'] == 101, 'status' + assert resp['headers']['Upgrade'] == 'websocket', 'upgrade' + assert resp['headers']['Connection'] == 'Upgrade', 'connection' + assert resp['headers']['Sec-WebSocket-Accept'] == self.ws.accept( + key + ), 'key' # autobahn-testsuite # @@ -442,12 +427,12 @@ class TestJavaWebsockets(TestApplicationJava): _, sock, _ = self.ws.upgrade() self.ws.frame_write(sock, self.ws.OP_PONG, '') - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', '2_7') + assert self.recvall(sock, read_timeout=0.1) == b'', '2_7' # 2_8 self.ws.frame_write(sock, self.ws.OP_PONG, 'unsolicited pong payload') - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', '2_8') + assert self.recvall(sock, read_timeout=0.1) == b'', '2_8' # 2_9 @@ -487,7 +472,7 @@ class TestJavaWebsockets(TestApplicationJava): self.close_connection(sock) - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_java_websockets_3_1__3_7(self): self.load('websockets_mirror') @@ -513,7 +498,7 @@ class TestJavaWebsockets(TestApplicationJava): self.check_close(sock, 1002, no_close=True) - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', 'empty 3_2') + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty 3_2' sock.close() # 3_3 @@ -531,7 +516,7 @@ class TestJavaWebsockets(TestApplicationJava): self.check_close(sock, 1002, no_close=True) - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', 'empty 3_3') + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty 3_3' sock.close() # 3_4 @@ -549,7 +534,7 @@ class TestJavaWebsockets(TestApplicationJava): self.check_close(sock, 1002, no_close=True) - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', 'empty 3_4') + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty 3_4' sock.close() # 3_5 @@ -735,7 +720,7 @@ class TestJavaWebsockets(TestApplicationJava): # 5_4 self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment1', fin=False) - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', '5_4') + assert self.recvall(sock, read_timeout=0.1) == b'', '5_4' self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment2', fin=True) frame = self.ws.frame_read(sock) @@ -772,7 +757,7 @@ class TestJavaWebsockets(TestApplicationJava): ping_payload = 'ping payload' self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment1', fin=False) - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', '5_7') + assert self.recvall(sock, read_timeout=0.1) == b'', '5_7' self.ws.frame_write(sock, self.ws.OP_PING, ping_payload) @@ -956,7 +941,7 @@ class TestJavaWebsockets(TestApplicationJava): frame = self.ws.frame_read(sock) self.check_frame(frame, True, self.ws.OP_PONG, 'pongme 2!') - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', '5_20') + assert self.recvall(sock, read_timeout=0.1) == b'', '5_20' self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment5') self.check_frame( @@ -1089,7 +1074,7 @@ class TestJavaWebsockets(TestApplicationJava): self.check_close(sock, no_close=True) self.ws.frame_write(sock, self.ws.OP_PING, '') - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', 'empty soc') + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty soc' sock.close() @@ -1101,7 +1086,7 @@ class TestJavaWebsockets(TestApplicationJava): self.check_close(sock, no_close=True) self.ws.frame_write(sock, self.ws.OP_TEXT, payload) - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', 'empty soc') + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty soc' sock.close() @@ -1114,7 +1099,7 @@ class TestJavaWebsockets(TestApplicationJava): self.check_close(sock, no_close=True) self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment2') - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', 'empty soc') + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty soc' sock.close() @@ -1129,7 +1114,7 @@ class TestJavaWebsockets(TestApplicationJava): self.recvall(sock, read_timeout=1) self.ws.frame_write(sock, self.ws.OP_PING, '') - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', 'empty soc') + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty soc' sock.close() @@ -1249,27 +1234,23 @@ class TestJavaWebsockets(TestApplicationJava): self.ws.frame_write(sock, self.ws.OP_CLOSE, payload) self.check_close(sock, 1002) - def test_java_websockets_9_1_1__9_6_6(self): - if not self.unsafe: - self.skipTest("unsafe, long run") + def test_java_websockets_9_1_1__9_6_6(self, is_unsafe): + if not is_unsafe: + pytest.skip('unsafe, long run') self.load('websockets_mirror') - self.assertIn( - 'success', - self.conf( - { - 'http': { - 'websocket': { - 'max_frame_size': 33554432, - 'keepalive_interval': 0, - } + assert 'success' in self.conf( + { + 'http': { + 'websocket': { + 'max_frame_size': 33554432, + 'keepalive_interval': 0, } - }, - 'settings', - ), - 'increase max_frame_size and keepalive_interval', - ) + } + }, + 'settings', + ), 'increase max_frame_size and keepalive_interval' _, sock, _ = self.ws.upgrade() @@ -1310,7 +1291,7 @@ class TestJavaWebsockets(TestApplicationJava): check_payload(op_binary, 8 * 2 ** 20) # 9_2_5 check_payload(op_binary, 16 * 2 ** 20) # 9_2_6 - if self.system != 'Darwin' and self.system != 'FreeBSD': + if option.system != 'Darwin' and option.system != 'FreeBSD': check_message(op_text, 64) # 9_3_1 check_message(op_text, 256) # 9_3_2 check_message(op_text, 2 ** 10) # 9_3_3 @@ -1366,13 +1347,9 @@ class TestJavaWebsockets(TestApplicationJava): def test_java_websockets_max_frame_size(self): self.load('websockets_mirror') - self.assertIn( - 'success', - self.conf( - {'http': {'websocket': {'max_frame_size': 100}}}, 'settings' - ), - 'configure max_frame_size', - ) + assert 'success' in self.conf( + {'http': {'websocket': {'max_frame_size': 100}}}, 'settings' + ), 'configure max_frame_size' _, sock, _ = self.ws.upgrade() @@ -1392,13 +1369,9 @@ class TestJavaWebsockets(TestApplicationJava): def test_java_websockets_read_timeout(self): self.load('websockets_mirror') - self.assertIn( - 'success', - self.conf( - {'http': {'websocket': {'read_timeout': 5}}}, 'settings' - ), - 'configure read_timeout', - ) + assert 'success' in self.conf( + {'http': {'websocket': {'read_timeout': 5}}}, 'settings' + ), 'configure read_timeout' _, sock, _ = self.ws.upgrade() @@ -1412,13 +1385,9 @@ class TestJavaWebsockets(TestApplicationJava): def test_java_websockets_keepalive_interval(self): self.load('websockets_mirror') - self.assertIn( - 'success', - self.conf( - {'http': {'websocket': {'keepalive_interval': 5}}}, 'settings' - ), - 'configure keepalive_interval', - ) + assert 'success' in self.conf( + {'http': {'websocket': {'keepalive_interval': 5}}}, 'settings' + ), 'configure keepalive_interval' _, sock, _ = self.ws.upgrade() @@ -1431,7 +1400,3 @@ class TestJavaWebsockets(TestApplicationJava): self.check_frame(frame, True, self.ws.OP_PING, '') # PING frame sock.close() - - -if __name__ == '__main__': - TestJavaWebsockets.main() diff --git a/test/test_node_application.py b/test/test_node_application.py index e46cc6a1..a0b882f3 100644 --- a/test/test_node_application.py +++ b/test/test_node_application.py @@ -1,5 +1,8 @@ -import unittest +import re +import pytest + +from conftest import waitforfiles from unit.applications.lang.node import TestApplicationNode @@ -10,16 +13,14 @@ class TestNodeApplication(TestApplicationNode): self.load('basic') resp = self.get() - self.assertEqual( - resp['headers']['Content-Type'], 'text/plain', 'basic header' - ) - self.assertEqual(resp['body'], 'Hello World\n', 'basic body') + assert resp['headers']['Content-Type'] == 'text/plain', 'basic header' + assert resp['body'] == 'Hello World\n', 'basic body' def test_node_application_seq(self): self.load('basic') - self.assertEqual(self.get()['status'], 200, 'seq') - self.assertEqual(self.get()['status'], 200, 'seq 2') + assert self.get()['status'] == 200, 'seq' + assert self.get()['status'] == 200, 'seq 2' def test_node_application_variables(self): self.load('variables') @@ -36,51 +37,44 @@ class TestNodeApplication(TestApplicationNode): body=body, ) - self.assertEqual(resp['status'], 200, 'status') + assert resp['status'] == 200, 'status' headers = resp['headers'] header_server = headers.pop('Server') - self.assertRegex(header_server, r'Unit/[\d\.]+', 'server header') + assert re.search(r'Unit/[\d\.]+', header_server), 'server header' date = headers.pop('Date') - self.assertEqual(date[-4:], ' GMT', 'date header timezone') - self.assertLess( - abs(self.date_to_sec_epoch(date) - self.sec_epoch()), - 5, - 'date header', - ) + assert date[-4:] == ' GMT', 'date header timezone' + assert ( + abs(self.date_to_sec_epoch(date) - self.sec_epoch()) < 5 + ), 'date header' raw_headers = headers.pop('Request-Raw-Headers') - self.assertRegex( - raw_headers, + assert re.search( r'^(?:Host|localhost|Content-Type|' - 'text\/html|Custom-Header|blah|Content-Length|17|Connection|' - 'close|,)+$', - 'raw headers', - ) - - self.assertDictEqual( - headers, - { - 'Connection': 'close', - 'Content-Length': str(len(body)), - 'Content-Type': 'text/html', - 'Request-Method': 'POST', - 'Request-Uri': '/', - 'Http-Host': 'localhost', - 'Server-Protocol': 'HTTP/1.1', - 'Custom-Header': 'blah', - }, - 'headers', - ) - self.assertEqual(resp['body'], body, 'body') + r'text\/html|Custom-Header|blah|Content-Length|17|Connection|' + r'close|,)+$', + raw_headers, + ), 'raw headers' + + assert headers == { + 'Connection': 'close', + 'Content-Length': str(len(body)), + 'Content-Type': 'text/html', + 'Request-Method': 'POST', + 'Request-Uri': '/', + 'Http-Host': 'localhost', + 'Server-Protocol': 'HTTP/1.1', + 'Custom-Header': 'blah', + }, 'headers' + assert resp['body'] == body, 'body' def test_node_application_get_variables(self): self.load('get_variables') resp = self.get(url='/?var1=val1&var2=&var3') - self.assertEqual(resp['headers']['X-Var-1'], 'val1', 'GET variables') - self.assertEqual(resp['headers']['X-Var-2'], '', 'GET variables 2') - self.assertEqual(resp['headers']['X-Var-3'], '', 'GET variables 3') + assert resp['headers']['X-Var-1'] == 'val1', 'GET variables' + assert resp['headers']['X-Var-2'] == '', 'GET variables 2' + assert resp['headers']['X-Var-3'] == '', 'GET variables 3' def test_node_application_post_variables(self): self.load('post_variables') @@ -94,24 +88,24 @@ class TestNodeApplication(TestApplicationNode): body='var1=val1&var2=&var3', ) - self.assertEqual(resp['headers']['X-Var-1'], 'val1', 'POST variables') - self.assertEqual(resp['headers']['X-Var-2'], '', 'POST variables 2') - self.assertEqual(resp['headers']['X-Var-3'], '', 'POST variables 3') + assert resp['headers']['X-Var-1'] == 'val1', 'POST variables' + assert resp['headers']['X-Var-2'] == '', 'POST variables 2' + assert resp['headers']['X-Var-3'] == '', 'POST variables 3' def test_node_application_404(self): self.load('404') resp = self.get() - self.assertEqual(resp['status'], 404, '404 status') - self.assertRegex( - resp['body'], r'<title>404 Not Found</title>', '404 body' - ) + assert resp['status'] == 404, '404 status' + assert re.search( + r'<title>404 Not Found</title>', resp['body'] + ), '404 body' def test_node_keepalive_body(self): self.load('mirror') - self.assertEqual(self.get()['status'], 200, 'init') + assert self.get()['status'] == 200, 'init' body = '0123456789' * 500 (resp, sock) = self.post( @@ -125,7 +119,7 @@ class TestNodeApplication(TestApplicationNode): read_timeout=1, ) - self.assertEqual(resp['body'], '0123456789' * 500, 'keep-alive 1') + assert resp['body'] == '0123456789' * 500, 'keep-alive 1' body = '0123456789' resp = self.post( @@ -138,47 +132,34 @@ class TestNodeApplication(TestApplicationNode): body=body, ) - self.assertEqual(resp['body'], body, 'keep-alive 2') + assert resp['body'] == body, 'keep-alive 2' def test_node_application_write_buffer(self): self.load('write_buffer') - self.assertEqual( - self.get()['body'], 'buffer', 'write buffer' - ) + assert self.get()['body'] == 'buffer', 'write buffer' def test_node_application_write_callback(self): self.load('write_callback') - self.assertEqual( - self.get()['body'], - 'helloworld', - 'write callback order', - ) - self.assertTrue( - self.waitforfiles(self.testdir + '/node/callback'), - 'write callback', - ) + assert self.get()['body'] == 'helloworld', 'write callback order' + assert waitforfiles(self.temp_dir + '/node/callback'), 'write callback' def test_node_application_write_before_write_head(self): self.load('write_before_write_head') - self.assertEqual(self.get()['status'], 200, 'write before writeHead') + assert self.get()['status'] == 200, 'write before writeHead' def test_node_application_double_end(self): self.load('double_end') - self.assertEqual(self.get()['status'], 200, 'double end') - self.assertEqual(self.get()['status'], 200, 'double end 2') + assert self.get()['status'] == 200, 'double end' + assert self.get()['status'] == 200, 'double end 2' def test_node_application_write_return(self): self.load('write_return') - self.assertEqual( - self.get()['body'], - 'bodytrue', - 'write return', - ) + assert self.get()['body'] == 'bodytrue', 'write return' def test_node_application_remove_header(self): self.load('remove_header') @@ -190,69 +171,61 @@ class TestNodeApplication(TestApplicationNode): 'Connection': 'close', } ) - self.assertEqual(resp['headers']['Was-Header'], 'true', 'was header') - self.assertEqual(resp['headers']['Has-Header'], 'false', 'has header') - self.assertFalse('X-Header' in resp['headers'], 'remove header') + assert resp['headers']['Was-Header'] == 'true', 'was header' + assert resp['headers']['Has-Header'] == 'false', 'has header' + assert not ('X-Header' in resp['headers']), 'remove header' def test_node_application_remove_header_nonexisting(self): self.load('remove_header') - self.assertEqual( + assert ( self.get( headers={ 'Host': 'localhost', 'X-Remove': 'blah', 'Connection': 'close', } - )['headers']['Has-Header'], - 'true', - 'remove header nonexisting', - ) + )['headers']['Has-Header'] + == 'true' + ), 'remove header nonexisting' def test_node_application_update_header(self): self.load('update_header') - self.assertEqual( - self.get()['headers']['X-Header'], 'new', 'update header' - ) + assert self.get()['headers']['X-Header'] == 'new', 'update header' def test_node_application_set_header_array(self): self.load('set_header_array') - self.assertListEqual( - self.get()['headers']['Set-Cookie'], - ['tc=one,two,three', 'tc=four,five,six'], - 'set header array', - ) + assert self.get()['headers']['Set-Cookie'] == [ + 'tc=one,two,three', + 'tc=four,five,six', + ], 'set header array' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_node_application_status_message(self): self.load('status_message') - self.assertRegex( - self.get(raw_resp=True), r'200 blah', 'status message' - ) + assert re.search(r'200 blah', self.get(raw_resp=True)), 'status message' def test_node_application_get_header_type(self): self.load('get_header_type') - self.assertEqual( - self.get()['headers']['X-Type'], 'number', 'get header type' - ) + assert self.get()['headers']['X-Type'] == 'number', 'get header type' def test_node_application_header_name_case(self): self.load('header_name_case') headers = self.get()['headers'] - self.assertEqual(headers['X-HEADER'], '3', 'header value') - self.assertNotIn('X-Header', headers, 'insensitive') - self.assertNotIn('X-header', headers, 'insensitive 2') + assert headers['X-HEADER'] == '3', 'header value' + assert 'X-Header' not in headers, 'insensitive' + assert 'X-header' not in headers, 'insensitive 2' def test_node_application_promise_handler(self): self.load('promise_handler') - self.assertEqual( + assert ( self.post( headers={ 'Host': 'localhost', @@ -260,19 +233,15 @@ class TestNodeApplication(TestApplicationNode): 'Connection': 'close', }, body='callback', - )['status'], - 200, - 'promise handler request', - ) - self.assertTrue( - self.waitforfiles(self.testdir + '/node/callback'), - 'promise handler', - ) + )['status'] + == 200 + ), 'promise handler request' + assert waitforfiles(self.temp_dir + '/node/callback'), 'promise handler' def test_node_application_promise_handler_write_after_end(self): self.load('promise_handler') - self.assertEqual( + assert ( self.post( headers={ 'Host': 'localhost', @@ -281,15 +250,14 @@ class TestNodeApplication(TestApplicationNode): 'Connection': 'close', }, body='callback', - )['status'], - 200, - 'promise handler request write after end', - ) + )['status'] + == 200 + ), 'promise handler request write after end' def test_node_application_promise_end(self): self.load('promise_end') - self.assertEqual( + assert ( self.post( headers={ 'Host': 'localhost', @@ -297,13 +265,10 @@ class TestNodeApplication(TestApplicationNode): 'Connection': 'close', }, body='end', - )['status'], - 200, - 'promise end request', - ) - self.assertTrue( - self.waitforfiles(self.testdir + '/node/callback'), 'promise end' - ) + )['status'] + == 200 + ), 'promise end request' + assert waitforfiles(self.temp_dir + '/node/callback'), 'promise end' def test_node_application_promise_multiple_calls(self): self.load('promise_handler') @@ -317,10 +282,9 @@ class TestNodeApplication(TestApplicationNode): body='callback1', ) - self.assertTrue( - self.waitforfiles(self.testdir + '/node/callback1'), - 'promise first call', - ) + assert waitforfiles( + self.temp_dir + '/node/callback1' + ), 'promise first call' self.post( headers={ @@ -331,65 +295,55 @@ class TestNodeApplication(TestApplicationNode): body='callback2', ) - self.assertTrue( - self.waitforfiles(self.testdir + '/node/callback2'), - 'promise second call', - ) + assert waitforfiles( + self.temp_dir + '/node/callback2' + ), 'promise second call' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_node_application_header_name_valid(self): self.load('header_name_valid') - self.assertNotIn('status', self.get(), 'header name valid') + assert 'status' not in self.get(), 'header name valid' def test_node_application_header_value_object(self): self.load('header_value_object') - self.assertIn('X-Header', self.get()['headers'], 'header value object') + assert 'X-Header' in self.get()['headers'], 'header value object' def test_node_application_get_header_names(self): self.load('get_header_names') - self.assertListEqual( - self.get()['headers']['X-Names'], - ['date', 'x-header'], - 'get header names', - ) + assert self.get()['headers']['X-Names'] == [ + 'date', + 'x-header', + ], 'get header names' def test_node_application_has_header(self): self.load('has_header') - self.assertEqual( + assert ( self.get( headers={ 'Host': 'localhost', 'X-Header': 'length', 'Connection': 'close', } - )['headers']['X-Has-Header'], - 'false', - 'has header length', - ) + )['headers']['X-Has-Header'] + == 'false' + ), 'has header length' - self.assertEqual( + assert ( self.get( headers={ 'Host': 'localhost', 'X-Header': 'Date', 'Connection': 'close', } - )['headers']['X-Has-Header'], - 'false', - 'has header date', - ) + )['headers']['X-Has-Header'] + == 'false' + ), 'has header date' def test_node_application_write_multiple(self): self.load('write_multiple') - self.assertEqual( - self.get()['body'], 'writewrite2end', 'write multiple' - ) - - -if __name__ == '__main__': - TestNodeApplication.main() + assert self.get()['body'] == 'writewrite2end', 'write multiple' diff --git a/test/test_node_websockets.py b/test/test_node_websockets.py index 1928d8c9..6a6b7f2d 100644 --- a/test/test_node_websockets.py +++ b/test/test_node_websockets.py @@ -1,7 +1,10 @@ import struct import time -import unittest +import pytest + +from conftest import option +from conftest import skip_alert from unit.applications.lang.node import TestApplicationNode from unit.applications.websockets import TestApplicationWebsocket @@ -11,23 +14,17 @@ class TestNodeWebsockets(TestApplicationNode): ws = TestApplicationWebsocket() - def setUp(self): - super().setUp() + def setup_method(self): + super().setup_method() - self.assertIn( - 'success', - self.conf( - {'http': {'websocket': {'keepalive_interval': 0}}}, 'settings' - ), - 'clear keepalive_interval', - ) + assert 'success' in self.conf( + {'http': {'websocket': {'keepalive_interval': 0}}}, 'settings' + ), 'clear keepalive_interval' - self.skip_alerts.extend( - [r'socket close\(\d+\) failed'] - ) + skip_alert(r'socket close\(\d+\) failed') def close_connection(self, sock): - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', 'empty soc') + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty soc' self.ws.frame_write(sock, self.ws.OP_CLOSE, self.ws.serialize_close()) @@ -36,9 +33,9 @@ class TestNodeWebsockets(TestApplicationNode): def check_close(self, sock, code=1000, no_close=False): frame = self.ws.frame_read(sock) - self.assertEqual(frame['fin'], True, 'close fin') - self.assertEqual(frame['opcode'], self.ws.OP_CLOSE, 'close opcode') - self.assertEqual(frame['code'], code, 'close code') + assert frame['fin'] == True, 'close fin' + assert frame['opcode'] == self.ws.OP_CLOSE, 'close opcode' + assert frame['code'] == code, 'close code' if not no_close: sock.close() @@ -49,9 +46,9 @@ class TestNodeWebsockets(TestApplicationNode): else: data = frame['data'].decode('utf-8') - self.assertEqual(frame['fin'], fin, 'fin') - self.assertEqual(frame['opcode'], opcode, 'opcode') - self.assertEqual(data, payload, 'payload') + assert frame['fin'] == fin, 'fin' + assert frame['opcode'] == opcode, 'opcode' + assert data == payload, 'payload' def test_node_websockets_handshake(self): self.load('websockets/mirror') @@ -59,14 +56,12 @@ class TestNodeWebsockets(TestApplicationNode): resp, sock, key = self.ws.upgrade() sock.close() - self.assertEqual(resp['status'], 101, 'status') - self.assertEqual(resp['headers']['Upgrade'], 'websocket', 'upgrade') - self.assertEqual( - resp['headers']['Connection'], 'Upgrade', 'connection' - ) - self.assertEqual( - resp['headers']['Sec-WebSocket-Accept'], self.ws.accept(key), 'key' - ) + assert resp['status'] == 101, 'status' + assert resp['headers']['Upgrade'] == 'websocket', 'upgrade' + assert resp['headers']['Connection'] == 'Upgrade', 'connection' + assert resp['headers']['Sec-WebSocket-Accept'] == self.ws.accept( + key + ), 'key' def test_node_websockets_mirror(self): self.load('websockets/mirror') @@ -78,12 +73,12 @@ class TestNodeWebsockets(TestApplicationNode): self.ws.frame_write(sock, self.ws.OP_TEXT, message) frame = self.ws.frame_read(sock) - self.assertEqual(message, frame['data'].decode('utf-8'), 'mirror') + assert message == frame['data'].decode('utf-8'), 'mirror' self.ws.frame_write(sock, self.ws.OP_TEXT, message) frame = self.ws.frame_read(sock) - self.assertEqual(message, frame['data'].decode('utf-8'), 'mirror 2') + assert message == frame['data'].decode('utf-8'), 'mirror 2' sock.close() @@ -98,8 +93,8 @@ class TestNodeWebsockets(TestApplicationNode): frame = self.ws.frame_read(sock) - self.assertEqual(frame['opcode'], self.ws.OP_CLOSE, 'no mask opcode') - self.assertEqual(frame['code'], 1002, 'no mask close code') + assert frame['opcode'] == self.ws.OP_CLOSE, 'no mask opcode' + assert frame['code'] == 1002, 'no mask close code' sock.close() @@ -116,11 +111,9 @@ class TestNodeWebsockets(TestApplicationNode): frame = self.ws.frame_read(sock) - self.assertEqual( - message + ' ' + message, - frame['data'].decode('utf-8'), - 'mirror framing', - ) + assert message + ' ' + message == frame['data'].decode( + 'utf-8' + ), 'mirror framing' sock.close() @@ -136,20 +129,16 @@ class TestNodeWebsockets(TestApplicationNode): frame = self.ws.frame_read(sock) frame.pop('data') - self.assertDictEqual( - frame, - { - 'fin': True, - 'rsv1': False, - 'rsv2': False, - 'rsv3': False, - 'opcode': self.ws.OP_CLOSE, - 'mask': 0, - 'code': 1002, - 'reason': 'Fragmented control frame', - }, - 'close frame', - ) + assert frame == { + 'fin': True, + 'rsv1': False, + 'rsv2': False, + 'rsv3': False, + 'opcode': self.ws.OP_CLOSE, + 'mask': 0, + 'code': 1002, + 'reason': 'Fragmented control frame', + }, 'close frame' sock.close() @@ -168,7 +157,7 @@ class TestNodeWebsockets(TestApplicationNode): frame = self.ws.frame_read(sock) data += frame['data'].decode('utf-8') - self.assertEqual(message, data, 'large') + assert message == data, 'large' sock.close() @@ -187,13 +176,13 @@ class TestNodeWebsockets(TestApplicationNode): frame1 = self.ws.frame_read(sock1) frame2 = self.ws.frame_read(sock2) - self.assertEqual(message1, frame1['data'].decode('utf-8'), 'client 1') - self.assertEqual(message2, frame2['data'].decode('utf-8'), 'client 2') + assert message1 == frame1['data'].decode('utf-8'), 'client 1' + assert message2 == frame2['data'].decode('utf-8'), 'client 2' sock1.close() sock2.close() - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_node_websockets_handshake_upgrade_absent( self ): # FAIL https://tools.ietf.org/html/rfc6455#section-4.2.1 @@ -209,7 +198,7 @@ class TestNodeWebsockets(TestApplicationNode): }, ) - self.assertEqual(resp['status'], 400, 'upgrade absent') + assert resp['status'] == 400, 'upgrade absent' def test_node_websockets_handshake_case_insensitive(self): self.load('websockets/mirror') @@ -226,9 +215,9 @@ class TestNodeWebsockets(TestApplicationNode): ) sock.close() - self.assertEqual(resp['status'], 101, 'status') + assert resp['status'] == 101, 'status' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_node_websockets_handshake_connection_absent(self): # FAIL self.load('websockets/mirror') @@ -242,7 +231,7 @@ class TestNodeWebsockets(TestApplicationNode): }, ) - self.assertEqual(resp['status'], 400, 'status') + assert resp['status'] == 400, 'status' def test_node_websockets_handshake_version_absent(self): self.load('websockets/mirror') @@ -257,9 +246,9 @@ class TestNodeWebsockets(TestApplicationNode): }, ) - self.assertEqual(resp['status'], 426, 'status') + assert resp['status'] == 426, 'status' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_node_websockets_handshake_key_invalid(self): self.load('websockets/mirror') @@ -274,7 +263,7 @@ class TestNodeWebsockets(TestApplicationNode): }, ) - self.assertEqual(resp['status'], 400, 'key length') + assert resp['status'] == 400, 'key length' key = self.ws.key() resp = self.get( @@ -288,9 +277,7 @@ class TestNodeWebsockets(TestApplicationNode): }, ) - self.assertEqual( - resp['status'], 400, 'key double' - ) # FAIL https://tools.ietf.org/html/rfc6455#section-11.3.1 + assert resp['status'] == 400, 'key double' # FAIL https://tools.ietf.org/html/rfc6455#section-11.3.1 def test_node_websockets_handshake_method_invalid(self): self.load('websockets/mirror') @@ -306,7 +293,7 @@ class TestNodeWebsockets(TestApplicationNode): }, ) - self.assertEqual(resp['status'], 400, 'status') + assert resp['status'] == 400, 'status' def test_node_websockets_handshake_http_10(self): self.load('websockets/mirror') @@ -323,7 +310,7 @@ class TestNodeWebsockets(TestApplicationNode): http_10=True, ) - self.assertEqual(resp['status'], 400, 'status') + assert resp['status'] == 400, 'status' def test_node_websockets_handshake_uri_invalid(self): self.load('websockets/mirror') @@ -340,7 +327,7 @@ class TestNodeWebsockets(TestApplicationNode): url='!', ) - self.assertEqual(resp['status'], 400, 'status') + assert resp['status'] == 400, 'status' def test_node_websockets_protocol_absent(self): self.load('websockets/mirror') @@ -357,14 +344,12 @@ class TestNodeWebsockets(TestApplicationNode): ) sock.close() - self.assertEqual(resp['status'], 101, 'status') - self.assertEqual(resp['headers']['Upgrade'], 'websocket', 'upgrade') - self.assertEqual( - resp['headers']['Connection'], 'Upgrade', 'connection' - ) - self.assertEqual( - resp['headers']['Sec-WebSocket-Accept'], self.ws.accept(key), 'key' - ) + assert resp['status'] == 101, 'status' + assert resp['headers']['Upgrade'] == 'websocket', 'upgrade' + assert resp['headers']['Connection'] == 'Upgrade', 'connection' + assert resp['headers']['Sec-WebSocket-Accept'] == self.ws.accept( + key + ), 'key' # autobahn-testsuite # @@ -461,12 +446,12 @@ class TestNodeWebsockets(TestApplicationNode): _, sock, _ = self.ws.upgrade() self.ws.frame_write(sock, self.ws.OP_PONG, '') - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', '2_7') + assert self.recvall(sock, read_timeout=0.1) == b'', '2_7' # 2_8 self.ws.frame_write(sock, self.ws.OP_PONG, 'unsolicited pong payload') - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', '2_8') + assert self.recvall(sock, read_timeout=0.1) == b'', '2_8' # 2_9 @@ -506,7 +491,7 @@ class TestNodeWebsockets(TestApplicationNode): self.close_connection(sock) - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_node_websockets_3_1__3_7(self): self.load('websockets/mirror') @@ -532,7 +517,7 @@ class TestNodeWebsockets(TestApplicationNode): self.check_close(sock, 1002, no_close=True) - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', 'empty 3_2') + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty 3_2' sock.close() # 3_3 @@ -550,7 +535,7 @@ class TestNodeWebsockets(TestApplicationNode): self.check_close(sock, 1002, no_close=True) - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', 'empty 3_3') + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty 3_3' sock.close() # 3_4 @@ -568,7 +553,7 @@ class TestNodeWebsockets(TestApplicationNode): self.check_close(sock, 1002, no_close=True) - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', 'empty 3_4') + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty 3_4' sock.close() # 3_5 @@ -754,7 +739,7 @@ class TestNodeWebsockets(TestApplicationNode): # 5_4 self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment1', fin=False) - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', '5_4') + assert self.recvall(sock, read_timeout=0.1) == b'', '5_4' self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment2', fin=True) frame = self.ws.frame_read(sock) @@ -791,7 +776,7 @@ class TestNodeWebsockets(TestApplicationNode): ping_payload = 'ping payload' self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment1', fin=False) - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', '5_7') + assert self.recvall(sock, read_timeout=0.1) == b'', '5_7' self.ws.frame_write(sock, self.ws.OP_PING, ping_payload) @@ -975,7 +960,7 @@ class TestNodeWebsockets(TestApplicationNode): frame = self.ws.frame_read(sock) self.check_frame(frame, True, self.ws.OP_PONG, 'pongme 2!') - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', '5_20') + assert self.recvall(sock, read_timeout=0.1) == b'', '5_20' self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment5') self.check_frame( @@ -1108,7 +1093,7 @@ class TestNodeWebsockets(TestApplicationNode): self.check_close(sock, no_close=True) self.ws.frame_write(sock, self.ws.OP_PING, '') - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', 'empty soc') + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty soc' sock.close() @@ -1120,7 +1105,7 @@ class TestNodeWebsockets(TestApplicationNode): self.check_close(sock, no_close=True) self.ws.frame_write(sock, self.ws.OP_TEXT, payload) - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', 'empty soc') + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty soc' sock.close() @@ -1133,7 +1118,7 @@ class TestNodeWebsockets(TestApplicationNode): self.check_close(sock, no_close=True) self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment2') - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', 'empty soc') + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty soc' sock.close() @@ -1148,7 +1133,7 @@ class TestNodeWebsockets(TestApplicationNode): self.recvall(sock, read_timeout=1) self.ws.frame_write(sock, self.ws.OP_PING, '') - self.assertEqual(self.recvall(sock, read_timeout=0.1), b'', 'empty soc') + assert self.recvall(sock, read_timeout=0.1) == b'', 'empty soc' sock.close() @@ -1268,27 +1253,23 @@ class TestNodeWebsockets(TestApplicationNode): self.ws.frame_write(sock, self.ws.OP_CLOSE, payload) self.check_close(sock, 1002) - def test_node_websockets_9_1_1__9_6_6(self): - if not self.unsafe: - self.skipTest("unsafe, long run") + def test_node_websockets_9_1_1__9_6_6(self, is_unsafe): + if not is_unsafe: + pytest.skip('unsafe, long run') self.load('websockets/mirror') - self.assertIn( - 'success', - self.conf( - { - 'http': { - 'websocket': { - 'max_frame_size': 33554432, - 'keepalive_interval': 0, - } + assert 'success' in self.conf( + { + 'http': { + 'websocket': { + 'max_frame_size': 33554432, + 'keepalive_interval': 0, } - }, - 'settings', - ), - 'increase max_frame_size and keepalive_interval', - ) + } + }, + 'settings', + ), 'increase max_frame_size and keepalive_interval' _, sock, _ = self.ws.upgrade() @@ -1329,7 +1310,7 @@ class TestNodeWebsockets(TestApplicationNode): check_payload(op_binary, 8 * 2 ** 20) # 9_2_5 check_payload(op_binary, 16 * 2 ** 20) # 9_2_6 - if self.system != 'Darwin' and self.system != 'FreeBSD': + if option.system != 'Darwin' and option.system != 'FreeBSD': check_message(op_text, 64) # 9_3_1 check_message(op_text, 256) # 9_3_2 check_message(op_text, 2 ** 10) # 9_3_3 @@ -1385,13 +1366,9 @@ class TestNodeWebsockets(TestApplicationNode): def test_node_websockets_max_frame_size(self): self.load('websockets/mirror') - self.assertIn( - 'success', - self.conf( - {'http': {'websocket': {'max_frame_size': 100}}}, 'settings' - ), - 'configure max_frame_size', - ) + assert 'success' in self.conf( + {'http': {'websocket': {'max_frame_size': 100}}}, 'settings' + ), 'configure max_frame_size' _, sock, _ = self.ws.upgrade() @@ -1411,13 +1388,9 @@ class TestNodeWebsockets(TestApplicationNode): def test_node_websockets_read_timeout(self): self.load('websockets/mirror') - self.assertIn( - 'success', - self.conf( - {'http': {'websocket': {'read_timeout': 5}}}, 'settings' - ), - 'configure read_timeout', - ) + assert 'success' in self.conf( + {'http': {'websocket': {'read_timeout': 5}}}, 'settings' + ), 'configure read_timeout' _, sock, _ = self.ws.upgrade() @@ -1431,13 +1404,9 @@ class TestNodeWebsockets(TestApplicationNode): def test_node_websockets_keepalive_interval(self): self.load('websockets/mirror') - self.assertIn( - 'success', - self.conf( - {'http': {'websocket': {'keepalive_interval': 5}}}, 'settings' - ), - 'configure keepalive_interval', - ) + assert 'success' in self.conf( + {'http': {'websocket': {'keepalive_interval': 5}}}, 'settings' + ), 'configure keepalive_interval' _, sock, _ = self.ws.upgrade() @@ -1450,7 +1419,3 @@ class TestNodeWebsockets(TestApplicationNode): self.check_frame(frame, True, self.ws.OP_PING, '') # PING frame sock.close() - - -if __name__ == '__main__': - TestNodeWebsockets.main() diff --git a/test/test_perl_application.py b/test/test_perl_application.py index dbf6abf7..78e32a43 100644 --- a/test/test_perl_application.py +++ b/test/test_perl_application.py @@ -1,5 +1,8 @@ -import unittest +import re +import pytest + +from conftest import skip_alert from unit.applications.lang.perl import TestApplicationPerl @@ -21,149 +24,130 @@ class TestPerlApplication(TestApplicationPerl): body=body, ) - self.assertEqual(resp['status'], 200, 'status') + assert resp['status'] == 200, 'status' headers = resp['headers'] header_server = headers.pop('Server') - self.assertRegex(header_server, r'Unit/[\d\.]+', 'server header') - self.assertEqual( - headers.pop('Server-Software'), - header_server, - 'server software header', - ) + assert re.search(r'Unit/[\d\.]+', header_server), 'server header' + assert ( + headers.pop('Server-Software') == header_server + ), 'server software header' date = headers.pop('Date') - self.assertEqual(date[-4:], ' GMT', 'date header timezone') - self.assertLess( - abs(self.date_to_sec_epoch(date) - self.sec_epoch()), - 5, - 'date header', - ) - - self.assertDictEqual( - headers, - { - 'Connection': 'close', - 'Content-Length': str(len(body)), - 'Content-Type': 'text/html', - 'Request-Method': 'POST', - 'Request-Uri': '/', - 'Http-Host': 'localhost', - 'Server-Protocol': 'HTTP/1.1', - 'Custom-Header': 'blah', - 'Psgi-Version': '11', - 'Psgi-Url-Scheme': 'http', - 'Psgi-Multithread': '', - 'Psgi-Multiprocess': '1', - 'Psgi-Run-Once': '', - 'Psgi-Nonblocking': '', - 'Psgi-Streaming': '1', - }, - 'headers', - ) - self.assertEqual(resp['body'], body, 'body') + assert date[-4:] == ' GMT', 'date header timezone' + assert ( + abs(self.date_to_sec_epoch(date) - self.sec_epoch()) < 5 + ), 'date header' + + assert headers == { + 'Connection': 'close', + 'Content-Length': str(len(body)), + 'Content-Type': 'text/html', + 'Request-Method': 'POST', + 'Request-Uri': '/', + 'Http-Host': 'localhost', + 'Server-Protocol': 'HTTP/1.1', + 'Custom-Header': 'blah', + 'Psgi-Version': '11', + 'Psgi-Url-Scheme': 'http', + 'Psgi-Multithread': '', + 'Psgi-Multiprocess': '1', + 'Psgi-Run-Once': '', + 'Psgi-Nonblocking': '', + 'Psgi-Streaming': '1', + }, 'headers' + assert resp['body'] == body, 'body' def test_perl_application_query_string(self): self.load('query_string') resp = self.get(url='/?var1=val1&var2=val2') - self.assertEqual( - resp['headers']['Query-String'], - 'var1=val1&var2=val2', - 'Query-String header', - ) + assert ( + resp['headers']['Query-String'] == 'var1=val1&var2=val2' + ), 'Query-String header' def test_perl_application_query_string_empty(self): self.load('query_string') resp = self.get(url='/?') - self.assertEqual(resp['status'], 200, 'query string empty status') - self.assertEqual( - resp['headers']['Query-String'], '', 'query string empty' - ) + assert resp['status'] == 200, 'query string empty status' + assert resp['headers']['Query-String'] == '', 'query string empty' def test_perl_application_query_string_absent(self): self.load('query_string') resp = self.get() - self.assertEqual(resp['status'], 200, 'query string absent status') - self.assertEqual( - resp['headers']['Query-String'], '', 'query string absent' - ) + assert resp['status'] == 200, 'query string absent status' + assert resp['headers']['Query-String'] == '', 'query string absent' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_perl_application_server_port(self): self.load('server_port') - self.assertEqual( - self.get()['headers']['Server-Port'], '7080', 'Server-Port header' - ) + assert ( + self.get()['headers']['Server-Port'] == '7080' + ), 'Server-Port header' def test_perl_application_input_read_empty(self): self.load('input_read_empty') - self.assertEqual(self.get()['body'], '', 'read empty') + assert self.get()['body'] == '', 'read empty' def test_perl_application_input_read_parts(self): self.load('input_read_parts') - self.assertEqual( - self.post(body='0123456789')['body'], - '0123456789', - 'input read parts', - ) + assert ( + self.post(body='0123456789')['body'] == '0123456789' + ), 'input read parts' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_perl_application_input_read_offset(self): self.load('input_read_offset') - self.assertEqual( - self.post(body='0123456789')['body'], '4567', 'read offset' - ) + assert self.post(body='0123456789')['body'] == '4567', 'read offset' def test_perl_application_input_copy(self): self.load('input_copy') body = '0123456789' - self.assertEqual(self.post(body=body)['body'], body, 'input copy') + assert self.post(body=body)['body'] == body, 'input copy' def test_perl_application_errors_print(self): self.load('errors_print') - self.assertEqual(self.get()['body'], '1', 'errors result') + assert self.get()['body'] == '1', 'errors result' self.stop() - self.assertIsNotNone( - self.wait_for_record(r'\[error\].+Error in application'), - 'errors print', - ) + assert ( + self.wait_for_record(r'\[error\].+Error in application') + is not None + ), 'errors print' def test_perl_application_header_equal_names(self): self.load('header_equal_names') - self.assertListEqual( - self.get()['headers']['Set-Cookie'], - ['tc=one,two,three', 'tc=four,five,six'], - 'header equal names', - ) + assert self.get()['headers']['Set-Cookie'] == [ + 'tc=one,two,three', + 'tc=four,five,six', + ], 'header equal names' def test_perl_application_header_pairs(self): self.load('header_pairs') - self.assertEqual(self.get()['headers']['blah'], 'blah', 'header pairs') + assert self.get()['headers']['blah'] == 'blah', 'header pairs' def test_perl_application_body_empty(self): self.load('body_empty') - self.assertEqual(self.get()['body'], '', 'body empty') + assert self.get()['body'] == '', 'body empty' def test_perl_application_body_array(self): self.load('body_array') - self.assertEqual(self.get()['body'], '0123456789', 'body array') + assert self.get()['body'] == '0123456789', 'body array' def test_perl_application_body_large(self): self.load('variables') @@ -172,31 +156,29 @@ class TestPerlApplication(TestApplicationPerl): resp = self.post(body=body)['body'] - self.assertEqual(resp, body, 'body large') + assert resp == body, 'body large' def test_perl_application_body_io_empty(self): self.load('body_io_empty') - self.assertEqual(self.get()['status'], 200, 'body io empty') + assert self.get()['status'] == 200, 'body io empty' def test_perl_application_body_io_file(self): self.load('body_io_file') - self.assertEqual(self.get()['body'], 'body\n', 'body io file') + assert self.get()['body'] == 'body\n', 'body io file' - @unittest.skip('not yet, unsafe') + @pytest.mark.skip('not yet') def test_perl_application_syntax_error(self): - self.skip_alerts.extend( - [r'PSGI: Failed to parse script'] - ) + skip_alert(r'PSGI: Failed to parse script') self.load('syntax_error') - self.assertEqual(self.get()['status'], 500, 'syntax error') + assert self.get()['status'] == 500, 'syntax error' def test_perl_keepalive_body(self): self.load('variables') - self.assertEqual(self.get()['status'], 200, 'init') + assert self.get()['status'] == 200, 'init' body = '0123456789' * 500 (resp, sock) = self.post( @@ -210,7 +192,7 @@ class TestPerlApplication(TestApplicationPerl): read_timeout=1, ) - self.assertEqual(resp['body'], body, 'keep-alive 1') + assert resp['body'] == body, 'keep-alive 1' body = '0123456789' resp = self.post( @@ -223,39 +205,35 @@ class TestPerlApplication(TestApplicationPerl): body=body, ) - self.assertEqual(resp['body'], body, 'keep-alive 2') + assert resp['body'] == body, 'keep-alive 2' def test_perl_body_io_fake(self): self.load('body_io_fake') - self.assertEqual(self.get()['body'], '21', 'body io fake') + assert self.get()['body'] == '21', 'body io fake' - self.assertIsNotNone( - self.wait_for_record(r'\[error\].+IOFake getline\(\) \$\/ is \d+'), - 'body io fake $/ value', - ) + assert ( + self.wait_for_record(r'\[error\].+IOFake getline\(\) \$\/ is \d+') + is not None + ), 'body io fake $/ value' - self.assertIsNotNone( - self.wait_for_record(r'\[error\].+IOFake close\(\) called'), - 'body io fake close', - ) + assert ( + self.wait_for_record(r'\[error\].+IOFake close\(\) called') + is not None + ), 'body io fake close' def test_perl_delayed_response(self): self.load('delayed_response') resp = self.get() - self.assertEqual(resp['status'], 200, 'status') - self.assertEqual(resp['body'], 'Hello World!', 'body') + assert resp['status'] == 200, 'status' + assert resp['body'] == 'Hello World!', 'body' def test_perl_streaming_body(self): self.load('streaming_body') resp = self.get() - self.assertEqual(resp['status'], 200, 'status') - self.assertEqual(resp['body'], 'Hello World!', 'body') - - -if __name__ == '__main__': - TestPerlApplication.main() + assert resp['status'] == 200, 'status' + assert resp['body'] == 'Hello World!', 'body' diff --git a/test/test_php_application.py b/test/test_php_application.py index d8bfade2..063d3e0c 100644 --- a/test/test_php_application.py +++ b/test/test_php_application.py @@ -2,8 +2,10 @@ import os import re import shutil import time -import unittest +import pytest + +from conftest import option from unit.applications.lang.php import TestApplicationPHP class TestPHPApplication(TestApplicationPHP): @@ -12,30 +14,21 @@ class TestPHPApplication(TestApplicationPHP): def before_disable_functions(self): body = self.get()['body'] - self.assertRegex(body, r'time: \d+', 'disable_functions before time') - self.assertRegex(body, r'exec: \/\w+', 'disable_functions before exec') + assert re.search(r'time: \d+', body), 'disable_functions before time' + assert re.search(r'exec: \/\w+', body), 'disable_functions before exec' def set_opcache(self, app, val): - self.assertIn( - 'success', - self.conf( - { - "admin": { - "opcache.enable": val, - "opcache.enable_cli": val, - }, - }, - 'applications/' + app + '/options', - ), + assert 'success' in self.conf( + {"admin": {"opcache.enable": val, "opcache.enable_cli": val,},}, + 'applications/' + app + '/options', ) opcache = self.get()['headers']['X-OPcache'] if not opcache or opcache == '-1': - print('opcache is not supported') - raise unittest.SkipTest() + pytest.skip('opcache is not supported') - self.assertEqual(opcache, val, 'opcache value') + assert opcache == val, 'opcache value' def test_php_application_variables(self): self.load('variables') @@ -50,140 +43,122 @@ class TestPHPApplication(TestApplicationPHP): 'Connection': 'close', }, body=body, - url='/index.php/blah?var=val' + url='/index.php/blah?var=val', ) - self.assertEqual(resp['status'], 200, 'status') + assert resp['status'] == 200, 'status' headers = resp['headers'] header_server = headers.pop('Server') - self.assertRegex(header_server, r'Unit/[\d\.]+', 'server header') - self.assertEqual( - headers.pop('Server-Software'), - header_server, - 'server software header', - ) + assert re.search(r'Unit/[\d\.]+', header_server), 'server header' + assert ( + headers.pop('Server-Software') == header_server + ), 'server software header' date = headers.pop('Date') - self.assertEqual(date[-4:], ' GMT', 'date header timezone') - self.assertLess( - abs(self.date_to_sec_epoch(date) - self.sec_epoch()), - 5, - 'date header', - ) + assert date[-4:] == ' GMT', 'date header timezone' + assert ( + abs(self.date_to_sec_epoch(date) - self.sec_epoch()) < 5 + ), 'date header' if 'X-Powered-By' in headers: headers.pop('X-Powered-By') headers.pop('Content-type') - self.assertDictEqual( - headers, - { - 'Connection': 'close', - 'Content-Length': str(len(body)), - 'Request-Method': 'POST', - 'Path-Info': '/blah', - 'Request-Uri': '/index.php/blah?var=val', - 'Http-Host': 'localhost', - 'Server-Protocol': 'HTTP/1.1', - 'Custom-Header': 'blah', - }, - 'headers', - ) - self.assertEqual(resp['body'], body, 'body') + assert headers == { + 'Connection': 'close', + 'Content-Length': str(len(body)), + 'Request-Method': 'POST', + 'Path-Info': '/blah', + 'Request-Uri': '/index.php/blah?var=val', + 'Http-Host': 'localhost', + 'Server-Protocol': 'HTTP/1.1', + 'Custom-Header': 'blah', + }, 'headers' + assert resp['body'] == body, 'body' def test_php_application_query_string(self): self.load('query_string') resp = self.get(url='/?var1=val1&var2=val2') - self.assertEqual( - resp['headers']['Query-String'], - 'var1=val1&var2=val2', - 'query string', - ) + assert ( + resp['headers']['Query-String'] == 'var1=val1&var2=val2' + ), 'query string' def test_php_application_query_string_empty(self): self.load('query_string') resp = self.get(url='/?') - self.assertEqual(resp['status'], 200, 'query string empty status') - self.assertEqual( - resp['headers']['Query-String'], '', 'query string empty' - ) + assert resp['status'] == 200, 'query string empty status' + assert resp['headers']['Query-String'] == '', 'query string empty' def test_php_application_query_string_absent(self): self.load('query_string') resp = self.get() - self.assertEqual(resp['status'], 200, 'query string absent status') - self.assertEqual( - resp['headers']['Query-String'], '', 'query string absent' - ) + assert resp['status'] == 200, 'query string absent status' + assert resp['headers']['Query-String'] == '', 'query string absent' def test_php_application_phpinfo(self): self.load('phpinfo') resp = self.get() - self.assertEqual(resp['status'], 200, 'status') - self.assertNotEqual(resp['body'], '', 'body not empty') + assert resp['status'] == 200, 'status' + assert resp['body'] != '', 'body not empty' def test_php_application_header_status(self): self.load('header') - self.assertEqual( + assert ( self.get( headers={ 'Host': 'localhost', 'Connection': 'close', 'X-Header': 'HTTP/1.1 404 Not Found', } - )['status'], - 404, - 'status', - ) + )['status'] + == 404 + ), 'status' - self.assertEqual( + assert ( self.get( headers={ 'Host': 'localhost', 'Connection': 'close', 'X-Header': 'http/1.1 404 Not Found', } - )['status'], - 404, - 'status case insensitive', - ) + )['status'] + == 404 + ), 'status case insensitive' - self.assertEqual( + assert ( self.get( headers={ 'Host': 'localhost', 'Connection': 'close', 'X-Header': 'HTTP/ 404 Not Found', } - )['status'], - 404, - 'status version empty', - ) - + )['status'] + == 404 + ), 'status version empty' def test_php_application_404(self): self.load('404') resp = self.get() - self.assertEqual(resp['status'], 404, '404 status') - self.assertRegex( - resp['body'], r'<title>404 Not Found</title>', '404 body' - ) + assert resp['status'] == 404, '404 status' + assert re.search( + r'<title>404 Not Found</title>', resp['body'] + ), '404 body' def test_php_application_keepalive_body(self): self.load('mirror') - self.assertEqual(self.get()['status'], 200, 'init') + assert self.get()['status'] == 200, 'init' body = '0123456789' * 500 (resp, sock) = self.post( @@ -197,7 +172,7 @@ class TestPHPApplication(TestApplicationPHP): read_timeout=1, ) - self.assertEqual(resp['body'], body, 'keep-alive 1') + assert resp['body'] == body, 'keep-alive 1' body = '0123456789' resp = self.post( @@ -210,22 +185,22 @@ class TestPHPApplication(TestApplicationPHP): body=body, ) - self.assertEqual(resp['body'], body, 'keep-alive 2') + assert resp['body'] == body, 'keep-alive 2' def test_php_application_conditional(self): self.load('conditional') - self.assertRegex(self.get()['body'], r'True', 'conditional true') - self.assertRegex(self.post()['body'], r'False', 'conditional false') + assert re.search(r'True', self.get()['body']), 'conditional true' + assert re.search(r'False', self.post()['body']), 'conditional false' def test_php_application_get_variables(self): self.load('get_variables') resp = self.get(url='/?var1=val1&var2=&var3') - self.assertEqual(resp['headers']['X-Var-1'], 'val1', 'GET variables') - self.assertEqual(resp['headers']['X-Var-2'], '1', 'GET variables 2') - self.assertEqual(resp['headers']['X-Var-3'], '1', 'GET variables 3') - self.assertEqual(resp['headers']['X-Var-4'], '', 'GET variables 4') + assert resp['headers']['X-Var-1'] == 'val1', 'GET variables' + assert resp['headers']['X-Var-2'] == '', 'GET variables 2' + assert resp['headers']['X-Var-3'] == '', 'GET variables 3' + assert resp['headers']['X-Var-4'] == 'not set', 'GET variables 4' def test_php_application_post_variables(self): self.load('post_variables') @@ -238,9 +213,9 @@ class TestPHPApplication(TestApplicationPHP): }, body='var1=val1&var2=', ) - self.assertEqual(resp['headers']['X-Var-1'], 'val1', 'POST variables') - self.assertEqual(resp['headers']['X-Var-2'], '1', 'POST variables 2') - self.assertEqual(resp['headers']['X-Var-3'], '', 'POST variables 3') + assert resp['headers']['X-Var-1'] == 'val1', 'POST variables' + assert resp['headers']['X-Var-2'] == '', 'POST variables 2' + assert resp['headers']['X-Var-3'] == 'not set', 'POST variables 3' def test_php_application_cookies(self): self.load('cookies') @@ -253,41 +228,32 @@ class TestPHPApplication(TestApplicationPHP): } ) - self.assertEqual(resp['headers']['X-Cookie-1'], 'val', 'cookie') - self.assertEqual(resp['headers']['X-Cookie-2'], 'val2', 'cookie') + assert resp['headers']['X-Cookie-1'] == 'val', 'cookie' + assert resp['headers']['X-Cookie-2'] == 'val2', 'cookie' def test_php_application_ini_precision(self): self.load('ini_precision') - self.assertNotEqual( - self.get()['headers']['X-Precision'], '4', 'ini value default' - ) + assert self.get()['headers']['X-Precision'] != '4', 'ini value default' self.conf( {"file": "ini/php.ini"}, 'applications/ini_precision/options' ) - self.assertEqual( - self.get()['headers']['X-File'], - self.current_dir + '/php/ini_precision/ini/php.ini', - 'ini file', - ) - self.assertEqual( - self.get()['headers']['X-Precision'], '4', 'ini value' - ) + assert ( + self.get()['headers']['X-File'] + == option.test_dir + '/php/ini_precision/ini/php.ini' + ), 'ini file' + assert self.get()['headers']['X-Precision'] == '4', 'ini value' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_php_application_ini_admin_user(self): self.load('ini_precision') - self.assertIn( - 'error', - self.conf( - {"user": {"precision": "4"}, "admin": {"precision": "5"}}, - 'applications/ini_precision/options', - ), - 'ini admin user', - ) + assert 'error' in self.conf( + {"user": {"precision": "4"}, "admin": {"precision": "5"}}, + 'applications/ini_precision/options', + ), 'ini admin user' def test_php_application_ini_admin(self): self.load('ini_precision') @@ -297,9 +263,7 @@ class TestPHPApplication(TestApplicationPHP): 'applications/ini_precision/options', ) - self.assertEqual( - self.get()['headers']['X-Precision'], '5', 'ini value admin' - ) + assert self.get()['headers']['X-Precision'] == '5', 'ini value admin' def test_php_application_ini_user(self): self.load('ini_precision') @@ -309,9 +273,7 @@ class TestPHPApplication(TestApplicationPHP): 'applications/ini_precision/options', ) - self.assertEqual( - self.get()['headers']['X-Precision'], '5', 'ini value user' - ) + assert self.get()['headers']['X-Precision'] == '5', 'ini value user' def test_php_application_ini_user_2(self): self.load('ini_precision') @@ -320,17 +282,13 @@ class TestPHPApplication(TestApplicationPHP): {"file": "ini/php.ini"}, 'applications/ini_precision/options' ) - self.assertEqual( - self.get()['headers']['X-Precision'], '4', 'ini user file' - ) + assert self.get()['headers']['X-Precision'] == '4', 'ini user file' self.conf( {"precision": "5"}, 'applications/ini_precision/options/user' ) - self.assertEqual( - self.get()['headers']['X-Precision'], '5', 'ini value user' - ) + assert self.get()['headers']['X-Precision'] == '5', 'ini value user' def test_php_application_ini_set_admin(self): self.load('ini_precision') @@ -339,11 +297,9 @@ class TestPHPApplication(TestApplicationPHP): {"admin": {"precision": "5"}}, 'applications/ini_precision/options' ) - self.assertEqual( - self.get(url='/?precision=6')['headers']['X-Precision'], - '5', - 'ini set admin', - ) + assert ( + self.get(url='/?precision=6')['headers']['X-Precision'] == '5' + ), 'ini set admin' def test_php_application_ini_set_user(self): self.load('ini_precision') @@ -352,11 +308,9 @@ class TestPHPApplication(TestApplicationPHP): {"user": {"precision": "5"}}, 'applications/ini_precision/options' ) - self.assertEqual( - self.get(url='/?precision=6')['headers']['X-Precision'], - '6', - 'ini set user', - ) + assert ( + self.get(url='/?precision=6')['headers']['X-Precision'] == '6' + ), 'ini set user' def test_php_application_ini_repeat(self): self.load('ini_precision') @@ -365,13 +319,9 @@ class TestPHPApplication(TestApplicationPHP): {"user": {"precision": "5"}}, 'applications/ini_precision/options' ) - self.assertEqual( - self.get()['headers']['X-Precision'], '5', 'ini value' - ) + assert self.get()['headers']['X-Precision'] == '5', 'ini value' - self.assertEqual( - self.get()['headers']['X-Precision'], '5', 'ini value repeat' - ) + assert self.get()['headers']['X-Precision'] == '5', 'ini value repeat' def test_php_application_disable_functions_exec(self): self.load('time_exec') @@ -385,8 +335,8 @@ class TestPHPApplication(TestApplicationPHP): body = self.get()['body'] - self.assertRegex(body, r'time: \d+', 'disable_functions time') - self.assertNotRegex(body, r'exec: \/\w+', 'disable_functions exec') + assert re.search(r'time: \d+', body), 'disable_functions time' + assert not re.search(r'exec: \/\w+', body), 'disable_functions exec' def test_php_application_disable_functions_comma(self): self.load('time_exec') @@ -400,10 +350,12 @@ class TestPHPApplication(TestApplicationPHP): body = self.get()['body'] - self.assertNotRegex(body, r'time: \d+', 'disable_functions comma time') - self.assertNotRegex( - body, r'exec: \/\w+', 'disable_functions comma exec' - ) + assert not re.search( + r'time: \d+', body + ), 'disable_functions comma time' + assert not re.search( + r'exec: \/\w+', body + ), 'disable_functions comma exec' def test_php_application_disable_functions_space(self): self.load('time_exec') @@ -417,10 +369,12 @@ class TestPHPApplication(TestApplicationPHP): body = self.get()['body'] - self.assertNotRegex(body, r'time: \d+', 'disable_functions space time') - self.assertNotRegex( - body, r'exec: \/\w+', 'disable_functions space exec' - ) + assert not re.search( + r'time: \d+', body + ), 'disable_functions space time' + assert not re.search( + r'exec: \/\w+', body + ), 'disable_functions space exec' def test_php_application_disable_functions_user(self): self.load('time_exec') @@ -434,10 +388,10 @@ class TestPHPApplication(TestApplicationPHP): body = self.get()['body'] - self.assertRegex(body, r'time: \d+', 'disable_functions user time') - self.assertNotRegex( - body, r'exec: \/\w+', 'disable_functions user exec' - ) + assert re.search(r'time: \d+', body), 'disable_functions user time' + assert not re.search( + r'exec: \/\w+', body + ), 'disable_functions user exec' def test_php_application_disable_functions_nonexistent(self): self.load('time_exec') @@ -451,187 +405,165 @@ class TestPHPApplication(TestApplicationPHP): body = self.get()['body'] - self.assertRegex( - body, r'time: \d+', 'disable_functions nonexistent time' - ) - self.assertRegex( - body, r'exec: \/\w+', 'disable_functions nonexistent exec' - ) + assert re.search( + r'time: \d+', body + ), 'disable_functions nonexistent time' + assert re.search( + r'exec: \/\w+', body + ), 'disable_functions nonexistent exec' def test_php_application_disable_classes(self): self.load('date_time') - self.assertRegex( - self.get()['body'], r'012345', 'disable_classes before' - ) + assert re.search( + r'012345', self.get()['body'] + ), 'disable_classes before' self.conf( {"admin": {"disable_classes": "DateTime"}}, 'applications/date_time/options', ) - self.assertNotRegex( - self.get()['body'], r'012345', 'disable_classes before' - ) + assert not re.search( + r'012345', self.get()['body'] + ), 'disable_classes before' def test_php_application_disable_classes_user(self): self.load('date_time') - self.assertRegex( - self.get()['body'], r'012345', 'disable_classes before' - ) + assert re.search( + r'012345', self.get()['body'] + ), 'disable_classes before' self.conf( {"user": {"disable_classes": "DateTime"}}, 'applications/date_time/options', ) - self.assertNotRegex( - self.get()['body'], r'012345', 'disable_classes before' - ) + assert not re.search( + r'012345', self.get()['body'] + ), 'disable_classes before' def test_php_application_error_log(self): self.load('error_log') - self.assertEqual(self.get()['status'], 200, 'status') + assert self.get()['status'] == 200, 'status' time.sleep(1) - self.assertEqual(self.get()['status'], 200, 'status 2') + assert self.get()['status'] == 200, 'status 2' self.stop() pattern = r'\d{4}\/\d\d\/\d\d\s\d\d:.+\[notice\].+Error in application' - self.assertIsNotNone(self.wait_for_record(pattern), 'errors print') + assert self.wait_for_record(pattern) is not None, 'errors print' - with open(self.testdir + '/unit.log', 'r', errors='ignore') as f: + with open(self.temp_dir + '/unit.log', 'r', errors='ignore') as f: errs = re.findall(pattern, f.read()) - self.assertEqual(len(errs), 2, 'error_log count') + assert len(errs) == 2, 'error_log count' date = errs[0].split('[')[0] date2 = errs[1].split('[')[0] - self.assertNotEqual(date, date2, 'date diff') + assert date != date2, 'date diff' def test_php_application_script(self): - self.assertIn( - 'success', - self.conf( - { - "listeners": {"*:7080": {"pass": "applications/script"}}, - "applications": { - "script": { - "type": "php", - "processes": {"spare": 0}, - "root": self.current_dir + "/php/script", - "script": "phpinfo.php", - } - }, - } - ), - 'configure script', - ) + assert 'success' in self.conf( + { + "listeners": {"*:7080": {"pass": "applications/script"}}, + "applications": { + "script": { + "type": "php", + "processes": {"spare": 0}, + "root": option.test_dir + "/php/script", + "script": "phpinfo.php", + } + }, + } + ), 'configure script' resp = self.get() - self.assertEqual(resp['status'], 200, 'status') - self.assertNotEqual(resp['body'], '', 'body not empty') + assert resp['status'] == 200, 'status' + assert resp['body'] != '', 'body not empty' def test_php_application_index_default(self): - self.assertIn( - 'success', - self.conf( - { - "listeners": {"*:7080": {"pass": "applications/phpinfo"}}, - "applications": { - "phpinfo": { - "type": "php", - "processes": {"spare": 0}, - "root": self.current_dir + "/php/phpinfo", - } - }, - } - ), - 'configure index default', - ) + assert 'success' in self.conf( + { + "listeners": {"*:7080": {"pass": "applications/phpinfo"}}, + "applications": { + "phpinfo": { + "type": "php", + "processes": {"spare": 0}, + "root": option.test_dir + "/php/phpinfo", + } + }, + } + ), 'configure index default' resp = self.get() - self.assertEqual(resp['status'], 200, 'status') - self.assertNotEqual(resp['body'], '', 'body not empty') + assert resp['status'] == 200, 'status' + assert resp['body'] != '', 'body not empty' def test_php_application_extension_check(self): self.load('phpinfo') - self.assertNotEqual( - self.get(url='/index.wrong')['status'], 200, 'status' - ) + assert self.get(url='/index.wrong')['status'] != 200, 'status' - new_root = self.testdir + "/php" + new_root = self.temp_dir + "/php" os.mkdir(new_root) - shutil.copy(self.current_dir + '/php/phpinfo/index.wrong', new_root) - - self.assertIn( - 'success', - self.conf( - { - "listeners": {"*:7080": {"pass": "applications/phpinfo"}}, - "applications": { - "phpinfo": { - "type": "php", - "processes": {"spare": 0}, - "root": new_root, - "working_directory": new_root, - } - }, - } - ), - 'configure new root', - ) + shutil.copy(option.test_dir + '/php/phpinfo/index.wrong', new_root) + + assert 'success' in self.conf( + { + "listeners": {"*:7080": {"pass": "applications/phpinfo"}}, + "applications": { + "phpinfo": { + "type": "php", + "processes": {"spare": 0}, + "root": new_root, + "working_directory": new_root, + } + }, + } + ), 'configure new root' resp = self.get() - self.assertNotEqual( - str(resp['status']) + resp['body'], '200', 'status new root' - ) + assert str(resp['status']) + resp['body'] != '200', 'status new root' def run_php_application_cwd_root_tests(self): - self.assertIn( - 'success', self.conf_delete('applications/cwd/working_directory') + assert 'success' in self.conf_delete( + 'applications/cwd/working_directory' ) - script_cwd = self.current_dir + '/php/cwd' + script_cwd = option.test_dir + '/php/cwd' resp = self.get() - self.assertEqual(resp['status'], 200, 'status ok') - self.assertEqual(resp['body'], script_cwd, 'default cwd') - - self.assertIn( - 'success', - self.conf( - '"' + self.current_dir + '"', - 'applications/cwd/working_directory', - ), + assert resp['status'] == 200, 'status ok' + assert resp['body'] == script_cwd, 'default cwd' + + assert 'success' in self.conf( + '"' + option.test_dir + '"', 'applications/cwd/working_directory', ) resp = self.get() - self.assertEqual(resp['status'], 200, 'status ok') - self.assertEqual(resp['body'], script_cwd, 'wdir cwd') + assert resp['status'] == 200, 'status ok' + assert resp['body'] == script_cwd, 'wdir cwd' resp = self.get(url='/?chdir=/') - self.assertEqual(resp['status'], 200, 'status ok') - self.assertEqual(resp['body'], '/', 'cwd after chdir') + assert resp['status'] == 200, 'status ok' + assert resp['body'] == '/', 'cwd after chdir' # cwd must be restored resp = self.get() - self.assertEqual(resp['status'], 200, 'status ok') - self.assertEqual(resp['body'], script_cwd, 'cwd restored') + assert resp['status'] == 200, 'status ok' + assert resp['body'] == script_cwd, 'cwd restored' resp = self.get(url='/subdir/') - self.assertEqual( - resp['body'], script_cwd + '/subdir', 'cwd subdir', - ) + assert resp['body'] == script_cwd + '/subdir', 'cwd subdir' def test_php_application_cwd_root(self): self.load('cwd') @@ -650,26 +582,20 @@ class TestPHPApplication(TestApplicationPHP): def run_php_application_cwd_script_tests(self): self.load('cwd') - script_cwd = self.current_dir + '/php/cwd' + script_cwd = option.test_dir + '/php/cwd' - self.assertIn( - 'success', self.conf_delete('applications/cwd/working_directory') + assert 'success' in self.conf_delete( + 'applications/cwd/working_directory' ) - self.assertIn( - 'success', self.conf('"index.php"', 'applications/cwd/script') - ) + assert 'success' in self.conf('"index.php"', 'applications/cwd/script') - self.assertEqual( - self.get()['body'], script_cwd, 'default cwd', - ) + assert self.get()['body'] == script_cwd, 'default cwd' - self.assertEqual( - self.get(url='/?chdir=/')['body'], '/', 'cwd after chdir', - ) + assert self.get(url='/?chdir=/')['body'] == '/', 'cwd after chdir' # cwd must be restored - self.assertEqual(self.get()['body'], script_cwd, 'cwd restored') + assert self.get()['body'] == script_cwd, 'cwd restored' def test_php_application_cwd_script(self): self.load('cwd') @@ -688,14 +614,10 @@ class TestPHPApplication(TestApplicationPHP): def test_php_application_path_relative(self): self.load('open') - self.assertEqual(self.get()['body'], 'test', 'relative path') - - self.assertNotEqual( - self.get(url='/?chdir=/')['body'], 'test', 'relative path w/ chdir' - ) - - self.assertEqual(self.get()['body'], 'test', 'relative path 2') + assert self.get()['body'] == 'test', 'relative path' + assert ( + self.get(url='/?chdir=/')['body'] != 'test' + ), 'relative path w/ chdir' -if __name__ == '__main__': - TestPHPApplication.main() + assert self.get()['body'] == 'test', 'relative path 2' diff --git a/test/test_php_basic.py b/test/test_php_basic.py index 16483c4a..1420ec21 100644 --- a/test/test_php_basic.py +++ b/test/test_php_basic.py @@ -23,133 +23,97 @@ class TestPHPBasic(TestControl): conf = self.conf_get() - self.assertEqual(conf['listeners'], {}, 'listeners') - self.assertEqual( - conf['applications'], - { - "app": { - "type": "php", - "processes": {"spare": 0}, - "root": "/app", - "index": "index.php", - } - }, - 'applications', - ) - - self.assertEqual( - self.conf_get('applications'), - { - "app": { - "type": "php", - "processes": {"spare": 0}, - "root": "/app", - "index": "index.php", - } - }, - 'applications prefix', - ) - - self.assertEqual( - self.conf_get('applications/app'), - { + assert conf['listeners'] == {}, 'listeners' + assert conf['applications'] == { + "app": { "type": "php", "processes": {"spare": 0}, "root": "/app", "index": "index.php", - }, - 'applications prefix 2', - ) + } + }, 'applications' - self.assertEqual(self.conf_get('applications/app/type'), 'php', 'type') - self.assertEqual( - self.conf_get('applications/app/processes/spare'), - 0, - 'spare processes', - ) + assert self.conf_get('applications') == { + "app": { + "type": "php", + "processes": {"spare": 0}, + "root": "/app", + "index": "index.php", + } + }, 'applications prefix' + + assert self.conf_get('applications/app') == { + "type": "php", + "processes": {"spare": 0}, + "root": "/app", + "index": "index.php", + }, 'applications prefix 2' + + assert self.conf_get('applications/app/type') == 'php', 'type' + assert ( + self.conf_get('applications/app/processes/spare') == 0 + ), 'spare processes' def test_php_get_listeners(self): self.conf(self.conf_basic) - self.assertEqual( - self.conf_get()['listeners'], - {"*:7080": {"pass": "applications/app"}}, - 'listeners', - ) + assert self.conf_get()['listeners'] == { + "*:7080": {"pass": "applications/app"} + }, 'listeners' - self.assertEqual( - self.conf_get('listeners'), - {"*:7080": {"pass": "applications/app"}}, - 'listeners prefix', - ) + assert self.conf_get('listeners') == { + "*:7080": {"pass": "applications/app"} + }, 'listeners prefix' - self.assertEqual( - self.conf_get('listeners/*:7080'), - {"pass": "applications/app"}, - 'listeners prefix 2', - ) + assert self.conf_get('listeners/*:7080') == { + "pass": "applications/app" + }, 'listeners prefix 2' def test_php_change_listener(self): self.conf(self.conf_basic) self.conf({"*:7081": {"pass": "applications/app"}}, 'listeners') - self.assertEqual( - self.conf_get('listeners'), - {"*:7081": {"pass": "applications/app"}}, - 'change listener', - ) + assert self.conf_get('listeners') == { + "*:7081": {"pass": "applications/app"} + }, 'change listener' def test_php_add_listener(self): self.conf(self.conf_basic) self.conf({"pass": "applications/app"}, 'listeners/*:7082') - self.assertEqual( - self.conf_get('listeners'), - { - "*:7080": {"pass": "applications/app"}, - "*:7082": {"pass": "applications/app"}, - }, - 'add listener', - ) + assert self.conf_get('listeners') == { + "*:7080": {"pass": "applications/app"}, + "*:7082": {"pass": "applications/app"}, + }, 'add listener' def test_php_change_application(self): self.conf(self.conf_basic) self.conf('30', 'applications/app/processes/max') - self.assertEqual( - self.conf_get('applications/app/processes/max'), - 30, - 'change application max', - ) + assert ( + self.conf_get('applications/app/processes/max') == 30 + ), 'change application max' self.conf('"/www"', 'applications/app/root') - self.assertEqual( - self.conf_get('applications/app/root'), - '/www', - 'change application root', - ) + assert ( + self.conf_get('applications/app/root') == '/www' + ), 'change application root' def test_php_delete(self): self.conf(self.conf_basic) - self.assertIn('error', self.conf_delete('applications/app')) - self.assertIn('success', self.conf_delete('listeners/*:7080')) - self.assertIn('success', self.conf_delete('applications/app')) - self.assertIn('error', self.conf_delete('applications/app')) + assert 'error' in self.conf_delete('applications/app') + assert 'success' in self.conf_delete('listeners/*:7080') + assert 'success' in self.conf_delete('applications/app') + assert 'error' in self.conf_delete('applications/app') def test_php_delete_blocks(self): self.conf(self.conf_basic) - self.assertIn('success', self.conf_delete('listeners')) - self.assertIn('success', self.conf_delete('applications')) - - self.assertIn('success', self.conf(self.conf_app, 'applications')) - self.assertIn( - 'success', - self.conf({"*:7081": {"pass": "applications/app"}}, 'listeners'), - 'applications restore', - ) - + assert 'success' in self.conf_delete('listeners') + assert 'success' in self.conf_delete('applications') -if __name__ == '__main__': - TestPHPBasic.main() + assert 'success' in self.conf(self.conf_app, 'applications') + assert 'success' in self.conf( + {"*:7081": {"pass": "applications/app"}}, 'listeners' + ), 'applications restore' diff --git a/test/test_php_isolation.py b/test/test_php_isolation.py index 1b70ef02..8ab3419a 100644 --- a/test/test_php_isolation.py +++ b/test/test_php_isolation.py @@ -1,5 +1,6 @@ -import unittest +import pytest +from conftest import option from unit.applications.lang.php import TestApplicationPHP from unit.feature.isolation import TestFeatureIsolation @@ -10,48 +11,77 @@ class TestPHPIsolation(TestApplicationPHP): isolation = TestFeatureIsolation() @classmethod - def setUpClass(cls, complete_check=True): - unit = super().setUpClass(complete_check=False) + def setup_class(cls, complete_check=True): + unit = super().setup_class(complete_check=False) - TestFeatureIsolation().check(cls.available, unit.testdir) + TestFeatureIsolation().check(cls.available, unit.temp_dir) return unit if not complete_check else unit.complete() - def test_php_isolation_rootfs(self): + def test_php_isolation_rootfs(self, is_su): isolation_features = self.available['features']['isolation'].keys() if 'mnt' not in isolation_features: - print('requires mnt ns') - raise unittest.SkipTest() + pytest.skip('requires mnt ns') - if not self.is_su: + if not is_su: if 'user' not in isolation_features: - print('requires unprivileged userns or root') - raise unittest.SkipTest() + pytest.skip('requires unprivileged userns or root') if not 'unprivileged_userns_clone' in isolation_features: - print('requires unprivileged userns or root') - raise unittest.SkipTest() + pytest.skip('requires unprivileged userns or root') isolation = { - 'namespaces': {'credential': not self.is_su, 'mount': True}, - 'rootfs': self.current_dir, + 'namespaces': {'credential': not is_su, 'mount': True}, + 'rootfs': option.test_dir, } self.load('phpinfo', isolation=isolation) - self.assertIn( - 'success', self.conf('"/php/phpinfo"', 'applications/phpinfo/root') + assert 'success' in self.conf( + '"/php/phpinfo"', 'applications/phpinfo/root' ) - self.assertIn( - 'success', - self.conf( - '"/php/phpinfo"', 'applications/phpinfo/working_directory' - ), + assert 'success' in self.conf( + '"/php/phpinfo"', 'applications/phpinfo/working_directory' ) - self.assertEqual(self.get()['status'], 200, 'empty rootfs') + assert self.get()['status'] == 200, 'empty rootfs' + def test_php_isolation_rootfs_extensions(self, is_su): + isolation_features = self.available['features']['isolation'].keys() + + if not is_su: + if 'user' not in isolation_features: + pytest.skip('requires unprivileged userns or root') + + if not 'unprivileged_userns_clone' in isolation_features: + pytest.skip('requires unprivileged userns or root') + + if 'mnt' not in isolation_features: + pytest.skip('requires mnt ns') + + isolation = { + 'rootfs': option.test_dir, + 'namespaces': {'credential': not is_su, 'mount': not is_su}, + } + + self.load('list-extensions', isolation=isolation) + + assert 'success' in self.conf( + '"/php/list-extensions"', 'applications/list-extensions/root' + ) + + assert 'success' in self.conf( + {'file': '/php/list-extensions/php.ini'}, + 'applications/list-extensions/options', + ) + + assert 'success' in self.conf( + '"/php/list-extensions"', + 'applications/list-extensions/working_directory', + ) + + extensions = self.getjson()['body'] -if __name__ == '__main__': - TestPHPIsolation.main() + assert 'json' in extensions, 'json in extensions list' + assert 'unit' in extensions, 'unit in extensions list' diff --git a/test/test_php_targets.py b/test/test_php_targets.py index 0657554a..e64cd6b6 100644 --- a/test/test_php_targets.py +++ b/test/test_php_targets.py @@ -1,128 +1,98 @@ +from conftest import option from unit.applications.lang.php import TestApplicationPHP + class TestPHPTargets(TestApplicationPHP): prerequisites = {'modules': {'php': 'any'}} def test_php_application_targets(self): - self.assertIn( - 'success', - self.conf( - { - "listeners": {"*:7080": {"pass": "routes"}}, - "routes": [ - { - "match": {"uri": "/1"}, - "action": {"pass": "applications/targets/1"}, - }, - { - "match": {"uri": "/2"}, - "action": {"pass": "applications/targets/2"}, - }, - {"action": {"pass": "applications/targets/default"}}, - ], - "applications": { + assert 'success' in self.conf( + { + "listeners": {"*:7080": {"pass": "routes"}}, + "routes": [ + { + "match": {"uri": "/1"}, + "action": {"pass": "applications/targets/1"}, + }, + { + "match": {"uri": "/2"}, + "action": {"pass": "applications/targets/2"}, + }, + {"action": {"pass": "applications/targets/default"}}, + ], + "applications": { + "targets": { + "type": "php", + "processes": {"spare": 0}, "targets": { - "type": "php", - "processes": {"spare": 0}, - "targets": { - "1": { - "script": "1.php", - "root": self.current_dir + "/php/targets", - }, - "2": { - "script": "2.php", - "root": self.current_dir - + "/php/targets/2", - }, - "default": { - "index": "index.php", - "root": self.current_dir + "/php/targets", - }, + "1": { + "script": "1.php", + "root": option.test_dir + "/php/targets", }, - } - }, - } - ), + "2": { + "script": "2.php", + "root": option.test_dir + "/php/targets/2", + }, + "default": { + "index": "index.php", + "root": option.test_dir + "/php/targets", + }, + }, + } + }, + } ) - self.assertEqual(self.get(url='/1')['body'], '1') - self.assertEqual(self.get(url='/2')['body'], '2') - self.assertEqual(self.get(url='/blah')['status'], 503) # TODO 404 - self.assertEqual(self.get(url='/')['body'], 'index') + assert self.get(url='/1')['body'] == '1' + assert self.get(url='/2')['body'] == '2' + assert self.get(url='/blah')['status'] == 503 # TODO 404 + assert self.get(url='/')['body'] == 'index' - self.assertIn( - 'success', - self.conf( - "\"1.php\"", 'applications/targets/targets/default/index' - ), - 'change targets index', - ) - self.assertEqual(self.get(url='/')['body'], '1') + assert 'success' in self.conf( + "\"1.php\"", 'applications/targets/targets/default/index' + ), 'change targets index' + assert self.get(url='/')['body'] == '1' - self.assertIn( - 'success', - self.conf_delete('applications/targets/targets/default/index'), - 'remove targets index', - ) - self.assertEqual(self.get(url='/')['body'], 'index') + assert 'success' in self.conf_delete( + 'applications/targets/targets/default/index' + ), 'remove targets index' + assert self.get(url='/')['body'] == 'index' def test_php_application_targets_error(self): - self.assertIn( - 'success', - self.conf( - { - "listeners": { - "*:7080": {"pass": "applications/targets/default"} - }, - "applications": { + assert 'success' in self.conf( + { + "listeners": { + "*:7080": {"pass": "applications/targets/default"} + }, + "applications": { + "targets": { + "type": "php", + "processes": {"spare": 0}, "targets": { - "type": "php", - "processes": {"spare": 0}, - "targets": { - "default": { - "index": "index.php", - "root": self.current_dir + "/php/targets", - }, + "default": { + "index": "index.php", + "root": option.test_dir + "/php/targets", }, - } - }, - } - ), - 'initial configuration', - ) - self.assertEqual(self.get()['status'], 200) - - self.assertIn( - 'error', - self.conf( - {"pass": "applications/targets/blah"}, 'listeners/*:7080' - ), - 'invalid targets pass', - ) - self.assertIn( - 'error', - self.conf( - '"' + self.current_dir + '/php/targets\"', - 'applications/targets/root', - ), - 'invalid root', - ) - self.assertIn( - 'error', - self.conf('"index.php"', 'applications/targets/index'), - 'invalid index', - ) - self.assertIn( - 'error', - self.conf('"index.php"', 'applications/targets/script'), - 'invalid script', - ) - self.assertIn( - 'error', - self.conf_delete('applications/targets/default/root'), - 'root remove', - ) - + }, + } + }, + } + ), 'initial configuration' + assert self.get()['status'] == 200 -if __name__ == '__main__': - TestPHPTargets.main() + assert 'error' in self.conf( + {"pass": "applications/targets/blah"}, 'listeners/*:7080' + ), 'invalid targets pass' + assert 'error' in self.conf( + '"' + option.test_dir + '/php/targets\"', + 'applications/targets/root', + ), 'invalid root' + assert 'error' in self.conf( + '"index.php"', 'applications/targets/index' + ), 'invalid index' + assert 'error' in self.conf( + '"index.php"', 'applications/targets/script' + ), 'invalid script' + assert 'error' in self.conf_delete( + 'applications/targets/default/root' + ), 'root remove' diff --git a/test/test_proxy.py b/test/test_proxy.py index feec1ac4..d02c96a7 100644 --- a/test/test_proxy.py +++ b/test/test_proxy.py @@ -1,8 +1,11 @@ import re import socket import time -import unittest +import pytest + +from conftest import option +from conftest import skip_alert from unit.applications.lang.python import TestApplicationPython @@ -42,7 +45,7 @@ Content-Length: 10 to_send = req - m = re.search('X-Len: (\d+)', data) + m = re.search(r'X-Len: (\d+)', data) if m: to_send += b'X' * int(m.group(1)) @@ -56,145 +59,127 @@ Content-Length: 10 def post_http10(self, *args, **kwargs): return self.post(*args, http_10=True, **kwargs) - def setUp(self): - super().setUp() + def setup_method(self): + super().setup_method() self.run_process(self.run_server, self.SERVER_PORT) self.waitforsocket(self.SERVER_PORT) - self.assertIn( - 'success', - self.conf( - { - "listeners": { - "*:7080": {"pass": "routes"}, - "*:7081": {"pass": "applications/mirror"}, + assert 'success' in self.conf( + { + "listeners": { + "*:7080": {"pass": "routes"}, + "*:7081": {"pass": "applications/mirror"}, + }, + "routes": [{"action": {"proxy": "http://127.0.0.1:7081"}}], + "applications": { + "mirror": { + "type": "python", + "processes": {"spare": 0}, + "path": option.test_dir + "/python/mirror", + "working_directory": option.test_dir + + "/python/mirror", + "module": "wsgi", }, - "routes": [{"action": {"proxy": "http://127.0.0.1:7081"}}], - "applications": { - "mirror": { - "type": "python", - "processes": {"spare": 0}, - "path": self.current_dir + "/python/mirror", - "working_directory": self.current_dir - + "/python/mirror", - "module": "wsgi", - }, - "custom_header": { - "type": "python", - "processes": {"spare": 0}, - "path": self.current_dir + "/python/custom_header", - "working_directory": self.current_dir - + "/python/custom_header", - "module": "wsgi", - }, - "delayed": { - "type": "python", - "processes": {"spare": 0}, - "path": self.current_dir + "/python/delayed", - "working_directory": self.current_dir - + "/python/delayed", - "module": "wsgi", - }, + "custom_header": { + "type": "python", + "processes": {"spare": 0}, + "path": option.test_dir + "/python/custom_header", + "working_directory": option.test_dir + + "/python/custom_header", + "module": "wsgi", }, - } - ), - 'proxy initial configuration', - ) + "delayed": { + "type": "python", + "processes": {"spare": 0}, + "path": option.test_dir + "/python/delayed", + "working_directory": option.test_dir + + "/python/delayed", + "module": "wsgi", + }, + }, + } + ), 'proxy initial configuration' def test_proxy_http10(self): for _ in range(10): - self.assertEqual(self.get_http10()['status'], 200, 'status') + assert self.get_http10()['status'] == 200, 'status' def test_proxy_chain(self): - self.assertIn( - 'success', - self.conf( - { - "listeners": { - "*:7080": {"pass": "routes/first"}, - "*:7081": {"pass": "routes/second"}, - "*:7082": {"pass": "routes/third"}, - "*:7083": {"pass": "routes/fourth"}, - "*:7084": {"pass": "routes/fifth"}, - "*:7085": {"pass": "applications/mirror"}, - }, - "routes": { - "first": [ - {"action": {"proxy": "http://127.0.0.1:7081"}} - ], - "second": [ - {"action": {"proxy": "http://127.0.0.1:7082"}} - ], - "third": [ - {"action": {"proxy": "http://127.0.0.1:7083"}} - ], - "fourth": [ - {"action": {"proxy": "http://127.0.0.1:7084"}} - ], - "fifth": [ - {"action": {"proxy": "http://127.0.0.1:7085"}} - ], - }, - "applications": { - "mirror": { - "type": "python", - "processes": {"spare": 0}, - "path": self.current_dir + "/python/mirror", - "working_directory": self.current_dir - + "/python/mirror", - "module": "wsgi", - } - }, - } - ), - 'proxy chain configuration', - ) + assert 'success' in self.conf( + { + "listeners": { + "*:7080": {"pass": "routes/first"}, + "*:7081": {"pass": "routes/second"}, + "*:7082": {"pass": "routes/third"}, + "*:7083": {"pass": "routes/fourth"}, + "*:7084": {"pass": "routes/fifth"}, + "*:7085": {"pass": "applications/mirror"}, + }, + "routes": { + "first": [{"action": {"proxy": "http://127.0.0.1:7081"}}], + "second": [{"action": {"proxy": "http://127.0.0.1:7082"}}], + "third": [{"action": {"proxy": "http://127.0.0.1:7083"}}], + "fourth": [{"action": {"proxy": "http://127.0.0.1:7084"}}], + "fifth": [{"action": {"proxy": "http://127.0.0.1:7085"}}], + }, + "applications": { + "mirror": { + "type": "python", + "processes": {"spare": 0}, + "path": option.test_dir + "/python/mirror", + "working_directory": option.test_dir + + "/python/mirror", + "module": "wsgi", + } + }, + } + ), 'proxy chain configuration' - self.assertEqual(self.get_http10()['status'], 200, 'status') + assert self.get_http10()['status'] == 200, 'status' def test_proxy_body(self): payload = '0123456789' for _ in range(10): resp = self.post_http10(body=payload) - self.assertEqual(resp['status'], 200, 'status') - self.assertEqual(resp['body'], payload, 'body') + assert resp['status'] == 200, 'status' + assert resp['body'] == payload, 'body' payload = 'X' * 4096 for _ in range(10): resp = self.post_http10(body=payload) - self.assertEqual(resp['status'], 200, 'status') - self.assertEqual(resp['body'], payload, 'body') + assert resp['status'] == 200, 'status' + assert resp['body'] == payload, 'body' payload = 'X' * 4097 for _ in range(10): resp = self.post_http10(body=payload) - self.assertEqual(resp['status'], 200, 'status') - self.assertEqual(resp['body'], payload, 'body') + assert resp['status'] == 200, 'status' + assert resp['body'] == payload, 'body' payload = 'X' * 4096 * 256 for _ in range(10): resp = self.post_http10(body=payload, read_buffer_size=4096 * 128) - self.assertEqual(resp['status'], 200, 'status') - self.assertEqual(resp['body'], payload, 'body') + assert resp['status'] == 200, 'status' + assert resp['body'] == payload, 'body' payload = 'X' * 4096 * 257 for _ in range(10): resp = self.post_http10(body=payload, read_buffer_size=4096 * 128) - self.assertEqual(resp['status'], 200, 'status') - self.assertEqual(resp['body'], payload, 'body') + assert resp['status'] == 200, 'status' + assert resp['body'] == payload, 'body' self.conf({'http': {'max_body_size': 32 * 1024 * 1024}}, 'settings') payload = '0123456789abcdef' * 32 * 64 * 1024 resp = self.post_http10(body=payload, read_buffer_size=1024 * 1024) - self.assertEqual(resp['status'], 200, 'status') - self.assertEqual(resp['body'], payload, 'body') + assert resp['status'] == 200, 'status' + assert resp['body'] == payload, 'body' def test_proxy_parallel(self): payload = 'X' * 4096 * 257 @@ -216,62 +201,53 @@ Content-Length: 10 resp = self._resp_to_dict(resp) - self.assertEqual(resp['status'], 200, 'status') - self.assertEqual(resp['body'], payload + str(i), 'body') + assert resp['status'] == 200, 'status' + assert resp['body'] == payload + str(i), 'body' def test_proxy_header(self): - self.assertIn( - 'success', - self.conf( - {"pass": "applications/custom_header"}, 'listeners/*:7081' - ), - 'custom_header configure', - ) + assert 'success' in self.conf( + {"pass": "applications/custom_header"}, 'listeners/*:7081' + ), 'custom_header configure' header_value = 'blah' - self.assertEqual( + assert ( self.get_http10( headers={'Host': 'localhost', 'Custom-Header': header_value} - )['headers']['Custom-Header'], - header_value, - 'custom header', - ) + )['headers']['Custom-Header'] + == header_value + ), 'custom header' - header_value = '(),/:;<=>?@[\]{}\t !#$%&\'*+-.^_`|~' - self.assertEqual( + header_value = r'(),/:;<=>?@[\]{}\t !#$%&\'*+-.^_`|~' + assert ( self.get_http10( headers={'Host': 'localhost', 'Custom-Header': header_value} - )['headers']['Custom-Header'], - header_value, - 'custom header 2', - ) + )['headers']['Custom-Header'] + == header_value + ), 'custom header 2' header_value = 'X' * 4096 - self.assertEqual( + assert ( self.get_http10( headers={'Host': 'localhost', 'Custom-Header': header_value} - )['headers']['Custom-Header'], - header_value, - 'custom header 3', - ) + )['headers']['Custom-Header'] + == header_value + ), 'custom header 3' header_value = 'X' * 8191 - self.assertEqual( + assert ( self.get_http10( headers={'Host': 'localhost', 'Custom-Header': header_value} - )['headers']['Custom-Header'], - header_value, - 'custom header 4', - ) + )['headers']['Custom-Header'] + == header_value + ), 'custom header 4' header_value = 'X' * 8192 - self.assertEqual( + assert ( self.get_http10( headers={'Host': 'localhost', 'Custom-Header': header_value} - )['status'], - 431, - 'custom header 5', - ) + )['status'] + == 431 + ), 'custom header 5' def test_proxy_fragmented(self): _, sock = self.http( @@ -286,9 +262,9 @@ Content-Length: 10 sock.sendall("t\r\n\r\n".encode()) - self.assertRegex( - self.recvall(sock).decode(), '200 OK', 'fragmented send' - ) + assert re.search( + '200 OK', self.recvall(sock).decode() + ), 'fragmented send' sock.close() def test_proxy_fragmented_close(self): @@ -328,8 +304,8 @@ Content-Length: 10 resp = self._resp_to_dict(self.recvall(sock).decode()) sock.close() - self.assertEqual(resp['status'], 200, 'status') - self.assertEqual(resp['body'], "X" * 30000, 'body') + assert resp['status'] == 200, 'status' + assert resp['body'] == "X" * 30000, 'body' def test_proxy_fragmented_body_close(self): _, sock = self.http( @@ -349,70 +325,48 @@ Content-Length: 10 sock.close() def test_proxy_nowhere(self): - self.assertIn( - 'success', - self.conf( - [{"action": {"proxy": "http://127.0.0.1:7082"}}], 'routes' - ), - 'proxy path changed', - ) + assert 'success' in self.conf( + [{"action": {"proxy": "http://127.0.0.1:7082"}}], 'routes' + ), 'proxy path changed' - self.assertEqual(self.get_http10()['status'], 502, 'status') + assert self.get_http10()['status'] == 502, 'status' def test_proxy_ipv6(self): - self.assertIn( - 'success', - self.conf( - { - "*:7080": {"pass": "routes"}, - "[::1]:7081": {'application': 'mirror'}, - }, - 'listeners', - ), - 'add ipv6 listener configure', - ) + assert 'success' in self.conf( + { + "*:7080": {"pass": "routes"}, + "[::1]:7081": {'application': 'mirror'}, + }, + 'listeners', + ), 'add ipv6 listener configure' - self.assertIn( - 'success', - self.conf([{"action": {"proxy": "http://[::1]:7081"}}], 'routes'), - 'proxy ipv6 configure', - ) + assert 'success' in self.conf( + [{"action": {"proxy": "http://[::1]:7081"}}], 'routes' + ), 'proxy ipv6 configure' - self.assertEqual(self.get_http10()['status'], 200, 'status') + assert self.get_http10()['status'] == 200, 'status' def test_proxy_unix(self): - addr = self.testdir + '/sock' + addr = self.temp_dir + '/sock' - self.assertIn( - 'success', - self.conf( - { - "*:7080": {"pass": "routes"}, - "unix:" + addr: {'application': 'mirror'}, - }, - 'listeners', - ), - 'add unix listener configure', - ) + assert 'success' in self.conf( + { + "*:7080": {"pass": "routes"}, + "unix:" + addr: {'application': 'mirror'}, + }, + 'listeners', + ), 'add unix listener configure' - self.assertIn( - 'success', - self.conf( - [{"action": {"proxy": 'http://unix:' + addr}}], 'routes' - ), - 'proxy unix configure', - ) + assert 'success' in self.conf( + [{"action": {"proxy": 'http://unix:' + addr}}], 'routes' + ), 'proxy unix configure' - self.assertEqual(self.get_http10()['status'], 200, 'status') + assert self.get_http10()['status'] == 200, 'status' def test_proxy_delayed(self): - self.assertIn( - 'success', - self.conf( - {"pass": "applications/delayed"}, 'listeners/*:7081' - ), - 'delayed configure', - ) + assert 'success' in self.conf( + {"pass": "applications/delayed"}, 'listeners/*:7081' + ), 'delayed configure' body = '0123456789' * 1000 resp = self.post_http10( @@ -426,8 +380,8 @@ Content-Length: 10 body=body, ) - self.assertEqual(resp['status'], 200, 'status') - self.assertEqual(resp['body'], body, 'body') + assert resp['status'] == 200, 'status' + assert resp['body'] == body, 'body' resp = self.post_http10( headers={ @@ -440,17 +394,13 @@ Content-Length: 10 body=body, ) - self.assertEqual(resp['status'], 200, 'status') - self.assertEqual(resp['body'], body, 'body') + assert resp['status'] == 200, 'status' + assert resp['body'] == body, 'body' def test_proxy_delayed_close(self): - self.assertIn( - 'success', - self.conf( - {"pass": "applications/delayed"}, 'listeners/*:7081' - ), - 'delayed configure', - ) + assert 'success' in self.conf( + {"pass": "applications/delayed"}, 'listeners/*:7081' + ), 'delayed configure' _, sock = self.post_http10( headers={ @@ -465,9 +415,7 @@ Content-Length: 10 no_recv=True, ) - self.assertRegex( - sock.recv(100).decode(), '200 OK', 'first' - ) + assert re.search('200 OK', sock.recv(100).decode()), 'first' sock.close() _, sock = self.post_http10( @@ -483,51 +431,42 @@ Content-Length: 10 no_recv=True, ) - self.assertRegex( - sock.recv(100).decode(), '200 OK', 'second' - ) + assert re.search('200 OK', sock.recv(100).decode()), 'second' sock.close() - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_proxy_content_length(self): - self.assertIn( - 'success', - self.conf( - [ - { - "action": { - "proxy": "http://127.0.0.1:" - + str(self.SERVER_PORT) - } + assert 'success' in self.conf( + [ + { + "action": { + "proxy": "http://127.0.0.1:" + str(self.SERVER_PORT) } - ], - 'routes', - ), - 'proxy backend configure', - ) + } + ], + 'routes', + ), 'proxy backend configure' resp = self.get_http10() - self.assertEqual(len(resp['body']), 0, 'body lt Content-Length 0') + assert len(resp['body']) == 0, 'body lt Content-Length 0' resp = self.get_http10(headers={'Host': 'localhost', 'X-Len': '5'}) - self.assertEqual(len(resp['body']), 5, 'body lt Content-Length 5') + assert len(resp['body']) == 5, 'body lt Content-Length 5' resp = self.get_http10(headers={'Host': 'localhost', 'X-Len': '9'}) - self.assertEqual(len(resp['body']), 9, 'body lt Content-Length 9') + assert len(resp['body']) == 9, 'body lt Content-Length 9' resp = self.get_http10(headers={'Host': 'localhost', 'X-Len': '11'}) - self.assertEqual(len(resp['body']), 10, 'body gt Content-Length 11') + assert len(resp['body']) == 10, 'body gt Content-Length 11' resp = self.get_http10(headers={'Host': 'localhost', 'X-Len': '15'}) - self.assertEqual(len(resp['body']), 10, 'body gt Content-Length 15') + assert len(resp['body']) == 10, 'body gt Content-Length 15' def test_proxy_invalid(self): def check_proxy(proxy): - self.assertIn( - 'error', - self.conf([{"action": {"proxy": proxy}}], 'routes'), - 'proxy invalid', - ) + assert 'error' in \ + self.conf([{"action": {"proxy": proxy}}], 'routes'), \ + 'proxy invalid' check_proxy('blah') check_proxy('/blah') @@ -544,12 +483,10 @@ Content-Length: 10 check_proxy('http://[::7080') def test_proxy_loop(self): - self.skip_alerts.extend( - [ - r'socket.*failed', - r'accept.*failed', - r'new connections are not accepted', - ] + skip_alert( + r'socket.*failed', + r'accept.*failed', + r'new connections are not accepted', ) self.conf( { @@ -563,9 +500,8 @@ Content-Length: 10 "mirror": { "type": "python", "processes": {"spare": 0}, - "path": self.current_dir + "/python/mirror", - "working_directory": self.current_dir - + "/python/mirror", + "path": option.test_dir + "/python/mirror", + "working_directory": option.test_dir + "/python/mirror", "module": "wsgi", }, }, @@ -574,6 +510,3 @@ Content-Length: 10 self.get_http10(no_recv=True) self.get_http10(read_timeout=1) - -if __name__ == '__main__': - TestProxy.main() diff --git a/test/test_proxy_chunked.py b/test/test_proxy_chunked.py index f344b69a..26023617 100644 --- a/test/test_proxy_chunked.py +++ b/test/test_proxy_chunked.py @@ -12,7 +12,7 @@ class TestProxyChunked(TestApplicationPython): SERVER_PORT = 7999 @staticmethod - def run_server(server_port, testdir): + def run_server(server_port, temp_dir): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) @@ -51,7 +51,7 @@ class TestProxyChunked(TestApplicationPython): for line in re.split('\r\n', body): add = '' - m1 = re.search('(.*)\sX\s(\d+)', line) + m1 = re.search(r'(.*)\sX\s(\d+)', line) if m1 is not None: add = m1.group(1) * int(m1.group(2)) @@ -81,68 +81,62 @@ class TestProxyChunked(TestApplicationPython): def get_http10(self, *args, **kwargs): return self.get(*args, http_10=True, **kwargs) - def setUp(self): - super().setUp() + def setup_method(self): + super().setup_method() - self.run_process(self.run_server, self.SERVER_PORT, self.testdir) + self.run_process(self.run_server, self.SERVER_PORT, self.temp_dir) self.waitforsocket(self.SERVER_PORT) - self.assertIn( - 'success', - self.conf( - { - "listeners": {"*:7080": {"pass": "routes"},}, - "routes": [ - { - "action": { - "proxy": "http://127.0.0.1:" - + str(self.SERVER_PORT) - } + assert 'success' in self.conf( + { + "listeners": {"*:7080": {"pass": "routes"},}, + "routes": [ + { + "action": { + "proxy": "http://127.0.0.1:" + + str(self.SERVER_PORT) } - ], - } - ), - 'proxy initial configuration', - ) + } + ], + } + ), 'proxy initial configuration' def test_proxy_chunked(self): for _ in range(10): - self.assertEqual( - self.get_http10(body='\r\n\r\n0\r\n\r\n')['status'], 200 - ) + assert self.get_http10(body='\r\n\r\n0\r\n\r\n')['status'] == 200 def test_proxy_chunked_body(self): part = '0123456789abcdef' - self.assertEqual( + assert ( self.get_http10(body=self.chunks([('1000', part + ' X 256')]))[ 'body' - ], - part * 256, + ] + == part * 256 ) - self.assertEqual( + assert ( self.get_http10(body=self.chunks([('100000', part + ' X 65536')]))[ 'body' - ], - part * 65536, + ] + == part * 65536 ) - self.assertEqual( + assert ( self.get_http10( body=self.chunks([('1000000', part + ' X 1048576')]), read_buffer_size=4096 * 4096, - )['body'], - part * 1048576, + )['body'] + == part * 1048576 ) - self.assertEqual( + assert ( self.get_http10( body=self.chunks( [('1000', part + ' X 256'), ('1000', part + ' X 256')] ) - )['body'], - part * 256 * 2, + )['body'] + == part * 256 * 2 ) - self.assertEqual( + assert ( self.get_http10( body=self.chunks( [ @@ -150,10 +144,10 @@ class TestProxyChunked(TestApplicationPython): ('100000', part + ' X 65536'), ] ) - )['body'], - part * 65536 * 2, + )['body'] + == part * 65536 * 2 ) - self.assertEqual( + assert ( self.get_http10( body=self.chunks( [ @@ -162,42 +156,40 @@ class TestProxyChunked(TestApplicationPython): ] ), read_buffer_size=4096 * 4096, - )['body'], - part * 1048576 * 2, + )['body'] + == part * 1048576 * 2 ) def test_proxy_chunked_fragmented(self): part = '0123456789abcdef' - self.assertEqual( + assert ( self.get_http10( body=self.chunks( [('1', hex(i % 16)[2:]) for i in range(4096)] ), - )['body'], - part * 256, + )['body'] + == part * 256 ) def test_proxy_chunked_send(self): - self.assertEqual( - self.get_http10(body='\r\n\r\n@0@\r\n\r\n')['status'], 200 - ) - self.assertEqual( + assert self.get_http10(body='\r\n\r\n@0@\r\n\r\n')['status'] == 200 + assert ( self.get_http10( body='\r@\n\r\n2\r@\na@b\r\n2\r\ncd@\r\n0\r@\n\r\n' - )['body'], - 'abcd', + )['body'] + == 'abcd' ) - self.assertEqual( + assert ( self.get_http10( body='\r\n\r\n2\r#\na#b\r\n##2\r\n#cd\r\n0\r\n#\r#\n' - )['body'], - 'abcd', + )['body'] + == 'abcd' ) def test_proxy_chunked_invalid(self): def check_invalid(body): - self.assertNotEqual(self.get_http10(body=body)['status'], 200) + assert self.get_http10(body=body)['status'] != 200 check_invalid('\r\n\r0') check_invalid('\r\n\r\n\r0') @@ -209,41 +201,38 @@ class TestProxyChunked(TestApplicationPython): check_invalid('\r\n\r\n0\r\nX') resp = self.get_http10(body='\r\n\r\n65#\r\nA X 100') - self.assertEqual(resp['status'], 200, 'incomplete chunk status') - self.assertNotEqual(resp['body'][-5:], '0\r\n\r\n', 'incomplete chunk') + assert resp['status'] == 200, 'incomplete chunk status' + assert resp['body'][-5:] != '0\r\n\r\n', 'incomplete chunk' resp = self.get_http10(body='\r\n\r\n64#\r\nA X 100') - self.assertEqual(resp['status'], 200, 'no zero chunk status') - self.assertNotEqual(resp['body'][-5:], '0\r\n\r\n', 'no zero chunk') + assert resp['status'] == 200, 'no zero chunk status' + assert resp['body'][-5:] != '0\r\n\r\n', 'no zero chunk' - self.assertEqual( - self.get_http10(body='\r\n\r\n80000000\r\nA X 100')['status'], 200, + assert ( + self.get_http10(body='\r\n\r\n80000000\r\nA X 100')['status'] + == 200 ) - self.assertEqual( + assert ( self.get_http10(body='\r\n\r\n10000000000000000\r\nA X 100')[ 'status' - ], - 502, + ] + == 502 ) - self.assertGreaterEqual( + assert ( len( self.get_http10( body='\r\n\r\n1000000\r\nA X 1048576\r\n1000000\r\nA X 100', read_buffer_size=4096 * 4096, )['body'] - ), - 1048576, + ) + >= 1048576 ) - self.assertGreaterEqual( + assert ( len( self.get_http10( body='\r\n\r\n1000000\r\nA X 1048576\r\nXXX\r\nA X 100', read_buffer_size=4096 * 4096, )['body'] - ), - 1048576, + ) + >= 1048576 ) - - -if __name__ == '__main__': - TestProxyChunked.main() diff --git a/test/test_python_application.py b/test/test_python_application.py index 4b8983ff..3e27a24c 100644 --- a/test/test_python_application.py +++ b/test/test_python_application.py @@ -2,8 +2,10 @@ import grp import pwd import re import time -import unittest +import pytest + +from conftest import skip_alert from unit.applications.lang.python import TestApplicationPython @@ -11,7 +13,7 @@ class TestPythonApplication(TestApplicationPython): prerequisites = {'modules': {'python': 'all'}} def findall(self, pattern): - with open(self.testdir + '/unit.log', 'r', errors='ignore') as f: + with open(self.temp_dir + '/unit.log', 'r', errors='ignore') as f: return re.findall(pattern, f.read()) def test_python_application_variables(self): @@ -19,145 +21,123 @@ class TestPythonApplication(TestApplicationPython): body = 'Test body string.' - resp = self.post( - headers={ - 'Host': 'localhost', - 'Content-Type': 'text/html', - 'Custom-Header': 'blah', - 'Connection': 'close', - }, - body=body, + resp = self.http( + b"""POST / HTTP/1.1 +Host: localhost +Content-Length: %d +Custom-Header: blah +Custom-hEader: Blah +Content-Type: text/html +Connection: close +custom-header: BLAH + +%s""" % (len(body), body.encode()), + raw=True, ) - self.assertEqual(resp['status'], 200, 'status') + assert resp['status'] == 200, 'status' headers = resp['headers'] header_server = headers.pop('Server') - self.assertRegex(header_server, r'Unit/[\d\.]+', 'server header') - self.assertEqual( - headers.pop('Server-Software'), - header_server, - 'server software header', - ) + assert re.search(r'Unit/[\d\.]+', header_server), 'server header' + assert ( + headers.pop('Server-Software') == header_server + ), 'server software header' date = headers.pop('Date') - self.assertEqual(date[-4:], ' GMT', 'date header timezone') - self.assertLess( - abs(self.date_to_sec_epoch(date) - self.sec_epoch()), - 5, - 'date header', - ) + assert date[-4:] == ' GMT', 'date header timezone' + assert ( + abs(self.date_to_sec_epoch(date) - self.sec_epoch()) < 5 + ), 'date header' - self.assertDictEqual( - headers, - { - 'Connection': 'close', - 'Content-Length': str(len(body)), - 'Content-Type': 'text/html', - 'Request-Method': 'POST', - 'Request-Uri': '/', - 'Http-Host': 'localhost', - 'Server-Protocol': 'HTTP/1.1', - 'Custom-Header': 'blah', - 'Wsgi-Version': '(1, 0)', - 'Wsgi-Url-Scheme': 'http', - 'Wsgi-Multithread': 'False', - 'Wsgi-Multiprocess': 'True', - 'Wsgi-Run-Once': 'False', - }, - 'headers', - ) - self.assertEqual(resp['body'], body, 'body') + assert headers == { + 'Connection': 'close', + 'Content-Length': str(len(body)), + 'Content-Type': 'text/html', + 'Request-Method': 'POST', + 'Request-Uri': '/', + 'Http-Host': 'localhost', + 'Server-Protocol': 'HTTP/1.1', + 'Custom-Header': 'blah, Blah, BLAH', + 'Wsgi-Version': '(1, 0)', + 'Wsgi-Url-Scheme': 'http', + 'Wsgi-Multithread': 'False', + 'Wsgi-Multiprocess': 'True', + 'Wsgi-Run-Once': 'False', + }, 'headers' + assert resp['body'] == body, 'body' def test_python_application_query_string(self): self.load('query_string') resp = self.get(url='/?var1=val1&var2=val2') - self.assertEqual( - resp['headers']['Query-String'], - 'var1=val1&var2=val2', - 'Query-String header', - ) + assert ( + resp['headers']['Query-String'] == 'var1=val1&var2=val2' + ), 'Query-String header' def test_python_application_query_string_space(self): self.load('query_string') resp = self.get(url='/ ?var1=val1&var2=val2') - self.assertEqual( - resp['headers']['Query-String'], - 'var1=val1&var2=val2', - 'Query-String space', - ) + assert ( + resp['headers']['Query-String'] == 'var1=val1&var2=val2' + ), 'Query-String space' resp = self.get(url='/ %20?var1=val1&var2=val2') - self.assertEqual( - resp['headers']['Query-String'], - 'var1=val1&var2=val2', - 'Query-String space 2', - ) + assert ( + resp['headers']['Query-String'] == 'var1=val1&var2=val2' + ), 'Query-String space 2' resp = self.get(url='/ %20 ?var1=val1&var2=val2') - self.assertEqual( - resp['headers']['Query-String'], - 'var1=val1&var2=val2', - 'Query-String space 3', - ) + assert ( + resp['headers']['Query-String'] == 'var1=val1&var2=val2' + ), 'Query-String space 3' resp = self.get(url='/blah %20 blah? var1= val1 & var2=val2') - self.assertEqual( - resp['headers']['Query-String'], - ' var1= val1 & var2=val2', - 'Query-String space 4', - ) + assert ( + resp['headers']['Query-String'] == ' var1= val1 & var2=val2' + ), 'Query-String space 4' def test_python_application_query_string_empty(self): self.load('query_string') resp = self.get(url='/?') - self.assertEqual(resp['status'], 200, 'query string empty status') - self.assertEqual( - resp['headers']['Query-String'], '', 'query string empty' - ) + assert resp['status'] == 200, 'query string empty status' + assert resp['headers']['Query-String'] == '', 'query string empty' def test_python_application_query_string_absent(self): self.load('query_string') resp = self.get() - self.assertEqual(resp['status'], 200, 'query string absent status') - self.assertEqual( - resp['headers']['Query-String'], '', 'query string absent' - ) + assert resp['status'] == 200, 'query string absent status' + assert resp['headers']['Query-String'] == '', 'query string absent' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_python_application_server_port(self): self.load('server_port') - self.assertEqual( - self.get()['headers']['Server-Port'], '7080', 'Server-Port header' - ) + assert ( + self.get()['headers']['Server-Port'] == '7080' + ), 'Server-Port header' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_python_application_working_directory_invalid(self): self.load('empty') - self.assertIn( - 'success', - self.conf('"/blah"', 'applications/empty/working_directory'), - 'configure invalid working_directory', - ) + assert 'success' in self.conf( + '"/blah"', 'applications/empty/working_directory' + ), 'configure invalid working_directory' - self.assertEqual(self.get()['status'], 500, 'status') + assert self.get()['status'] == 500, 'status' def test_python_application_204_transfer_encoding(self): self.load('204_no_content') - self.assertNotIn( - 'Transfer-Encoding', - self.get()['headers'], - '204 header transfer encoding', - ) + assert ( + 'Transfer-Encoding' not in self.get()['headers'] + ), '204 header transfer encoding' def test_python_application_ctx_iter_atexit(self): self.load('ctx_iter_atexit') @@ -171,21 +151,21 @@ class TestPythonApplication(TestApplicationPython): body='0123456789', ) - self.assertEqual(resp['status'], 200, 'ctx iter status') - self.assertEqual(resp['body'], '0123456789', 'ctx iter body') + assert resp['status'] == 200, 'ctx iter status' + assert resp['body'] == '0123456789', 'ctx iter body' self.conf({"listeners": {}, "applications": {}}) self.stop() - self.assertIsNotNone( - self.wait_for_record(r'RuntimeError'), 'ctx iter atexit' - ) + assert ( + self.wait_for_record(r'RuntimeError') is not None + ), 'ctx iter atexit' def test_python_keepalive_body(self): self.load('mirror') - self.assertEqual(self.get()['status'], 200, 'init') + assert self.get()['status'] == 200, 'init' body = '0123456789' * 500 (resp, sock) = self.post( @@ -199,7 +179,7 @@ class TestPythonApplication(TestApplicationPython): read_timeout=1, ) - self.assertEqual(resp['body'], body, 'keep-alive 1') + assert resp['body'] == body, 'keep-alive 1' body = '0123456789' resp = self.post( @@ -212,19 +192,17 @@ class TestPythonApplication(TestApplicationPython): body=body, ) - self.assertEqual(resp['body'], body, 'keep-alive 2') + assert resp['body'] == body, 'keep-alive 2' def test_python_keepalive_reconfigure(self): - self.skip_alerts.extend( - [ - r'pthread_mutex.+failed', - r'failed to apply', - r'process \d+ exited on signal', - ] + skip_alert( + r'pthread_mutex.+failed', + r'failed to apply', + r'process \d+ exited on signal', ) self.load('mirror') - self.assertEqual(self.get()['status'], 200, 'init') + assert self.get()['status'] == 200, 'init' body = '0123456789' conns = 3 @@ -242,12 +220,10 @@ class TestPythonApplication(TestApplicationPython): read_timeout=1, ) - self.assertEqual(resp['body'], body, 'keep-alive open') - self.assertIn( - 'success', - self.conf(str(i + 1), 'applications/mirror/processes'), - 'reconfigure', - ) + assert resp['body'] == body, 'keep-alive open' + assert 'success' in self.conf( + str(i + 1), 'applications/mirror/processes' + ), 'reconfigure' socks.append(sock) @@ -264,12 +240,10 @@ class TestPythonApplication(TestApplicationPython): read_timeout=1, ) - self.assertEqual(resp['body'], body, 'keep-alive request') - self.assertIn( - 'success', - self.conf(str(i + 1), 'applications/mirror/processes'), - 'reconfigure 2', - ) + assert resp['body'] == body, 'keep-alive request' + assert 'success' in self.conf( + str(i + 1), 'applications/mirror/processes' + ), 'reconfigure 2' for i in range(conns): resp = self.post( @@ -282,17 +256,15 @@ class TestPythonApplication(TestApplicationPython): body=body, ) - self.assertEqual(resp['body'], body, 'keep-alive close') - self.assertIn( - 'success', - self.conf(str(i + 1), 'applications/mirror/processes'), - 'reconfigure 3', - ) + assert resp['body'] == body, 'keep-alive close' + assert 'success' in self.conf( + str(i + 1), 'applications/mirror/processes' + ), 'reconfigure 3' def test_python_keepalive_reconfigure_2(self): self.load('mirror') - self.assertEqual(self.get()['status'], 200, 'init') + assert self.get()['status'] == 200, 'init' body = '0123456789' @@ -307,11 +279,11 @@ class TestPythonApplication(TestApplicationPython): read_timeout=1, ) - self.assertEqual(resp['body'], body, 'reconfigure 2 keep-alive 1') + assert resp['body'] == body, 'reconfigure 2 keep-alive 1' self.load('empty') - self.assertEqual(self.get()['status'], 200, 'init') + assert self.get()['status'] == 200, 'init' (resp, sock) = self.post( headers={ @@ -324,23 +296,21 @@ class TestPythonApplication(TestApplicationPython): body=body, ) - self.assertEqual(resp['status'], 200, 'reconfigure 2 keep-alive 2') - self.assertEqual(resp['body'], '', 'reconfigure 2 keep-alive 2 body') + assert resp['status'] == 200, 'reconfigure 2 keep-alive 2' + assert resp['body'] == '', 'reconfigure 2 keep-alive 2 body' - self.assertIn( - 'success', - self.conf({"listeners": {}, "applications": {}}), - 'reconfigure 2 clear configuration', - ) + assert 'success' in self.conf( + {"listeners": {}, "applications": {}} + ), 'reconfigure 2 clear configuration' resp = self.get(sock=sock) - self.assertEqual(resp, {}, 'reconfigure 2 keep-alive 3') + assert resp == {}, 'reconfigure 2 keep-alive 3' def test_python_keepalive_reconfigure_3(self): self.load('empty') - self.assertEqual(self.get()['status'], 200, 'init') + assert self.get()['status'] == 200, 'init' (_, sock) = self.http( b"""GET / HTTP/1.1 @@ -350,13 +320,11 @@ class TestPythonApplication(TestApplicationPython): no_recv=True, ) - self.assertEqual(self.get()['status'], 200) + assert self.get()['status'] == 200 - self.assertIn( - 'success', - self.conf({"listeners": {}, "applications": {}}), - 'reconfigure 3 clear configuration', - ) + assert 'success' in self.conf( + {"listeners": {}, "applications": {}} + ), 'reconfigure 3 clear configuration' resp = self.http( b"""Host: localhost @@ -367,7 +335,7 @@ Connection: close raw=True, ) - self.assertEqual(resp['status'], 200, 'reconfigure 3') + assert resp['status'] == 200, 'reconfigure 3' def test_python_atexit(self): self.load('atexit') @@ -378,25 +346,24 @@ Connection: close self.stop() - self.assertIsNotNone( - self.wait_for_record(r'At exit called\.'), 'atexit' - ) + assert self.wait_for_record(r'At exit called\.') is not None, 'atexit' def test_python_process_switch(self): self.load('delayed') - self.assertIn( - 'success', - self.conf('2', 'applications/delayed/processes'), - 'configure 2 processes', - ) + assert 'success' in self.conf( + '2', 'applications/delayed/processes' + ), 'configure 2 processes' - self.get(headers={ - 'Host': 'localhost', - 'Content-Length': '0', - 'X-Delay': '5', - 'Connection': 'close', - }, no_recv=True) + self.get( + headers={ + 'Host': 'localhost', + 'Content-Length': '0', + 'X-Delay': '5', + 'Connection': 'close', + }, + no_recv=True, + ) headers_delay_1 = { 'Connection': 'close', @@ -414,11 +381,11 @@ Connection: close self.get(headers=headers_delay_1) - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_python_application_start_response_exit(self): self.load('start_response_exit') - self.assertEqual(self.get()['status'], 500, 'start response exit') + assert self.get()['status'] == 500, 'start response exit' def test_python_application_input_iter(self): self.load('input_iter') @@ -429,10 +396,8 @@ next line last line''' resp = self.post(body=body) - self.assertEqual(resp['body'], body, 'input iter') - self.assertEqual( - resp['headers']['X-Lines-Count'], '4', 'input iter lines' - ) + assert resp['body'] == body, 'input iter' + assert resp['headers']['X-Lines-Count'] == '4', 'input iter lines' def test_python_application_input_readline(self): self.load('input_readline') @@ -443,10 +408,8 @@ next line last line''' resp = self.post(body=body) - self.assertEqual(resp['body'], body, 'input readline') - self.assertEqual( - resp['headers']['X-Lines-Count'], '4', 'input readline lines' - ) + assert resp['body'] == body, 'input readline' + assert resp['headers']['X-Lines-Count'] == '4', 'input readline lines' def test_python_application_input_readline_size(self): self.load('input_readline_size') @@ -456,12 +419,10 @@ next line last line''' - self.assertEqual( - self.post(body=body)['body'], body, 'input readline size' - ) - self.assertEqual( - self.post(body='0123')['body'], '0123', 'input readline size less' - ) + assert self.post(body=body)['body'] == body, 'input readline size' + assert ( + self.post(body='0123')['body'] == '0123' + ), 'input readline size less' def test_python_application_input_readlines(self): self.load('input_readlines') @@ -472,10 +433,8 @@ next line last line''' resp = self.post(body=body) - self.assertEqual(resp['body'], body, 'input readlines') - self.assertEqual( - resp['headers']['X-Lines-Count'], '4', 'input readlines lines' - ) + assert resp['body'] == body, 'input readlines' + assert resp['headers']['X-Lines-Count'] == '4', 'input readlines lines' def test_python_application_input_readlines_huge(self): self.load('input_readlines') @@ -489,11 +448,9 @@ last line: 987654321 * 512 ) - self.assertEqual( - self.post(body=body, read_buffer_size=16384)['body'], - body, - 'input readlines huge', - ) + assert ( + self.post(body=body, read_buffer_size=16384)['body'] == body + ), 'input readlines huge' def test_python_application_input_read_length(self): self.load('input_read_length') @@ -509,7 +466,7 @@ last line: 987654321 body=body, ) - self.assertEqual(resp['body'], body[:5], 'input read length lt body') + assert resp['body'] == body[:5], 'input read length lt body' resp = self.post( headers={ @@ -520,7 +477,7 @@ last line: 987654321 body=body, ) - self.assertEqual(resp['body'], body, 'input read length gt body') + assert resp['body'] == body, 'input read length gt body' resp = self.post( headers={ @@ -531,7 +488,7 @@ last line: 987654321 body=body, ) - self.assertEqual(resp['body'], '', 'input read length zero') + assert resp['body'] == '', 'input read length zero' resp = self.post( headers={ @@ -542,9 +499,9 @@ last line: 987654321 body=body, ) - self.assertEqual(resp['body'], body, 'input read length negative') + assert resp['body'] == body, 'input read length negative' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_python_application_errors_write(self): self.load('errors_write') @@ -552,43 +509,41 @@ last line: 987654321 self.stop() - self.assertIsNotNone( - self.wait_for_record(r'\[error\].+Error in application\.'), - 'errors write', - ) + assert ( + self.wait_for_record(r'\[error\].+Error in application\.') + is not None + ), 'errors write' def test_python_application_body_array(self): self.load('body_array') - self.assertEqual(self.get()['body'], '0123456789', 'body array') + assert self.get()['body'] == '0123456789', 'body array' def test_python_application_body_io(self): self.load('body_io') - self.assertEqual(self.get()['body'], '0123456789', 'body io') + assert self.get()['body'] == '0123456789', 'body io' def test_python_application_body_io_file(self): self.load('body_io_file') - self.assertEqual(self.get()['body'], 'body\n', 'body io file') + assert self.get()['body'] == 'body\n', 'body io file' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_python_application_syntax_error(self): - self.skip_alerts.append(r'Python failed to import module "wsgi"') + skip_alert(r'Python failed to import module "wsgi"') self.load('syntax_error') - self.assertEqual(self.get()['status'], 500, 'syntax error') + assert self.get()['status'] == 500, 'syntax error' def test_python_application_loading_error(self): - self.skip_alerts.append(r'Python failed to import module "blah"') + skip_alert(r'Python failed to import module "blah"') self.load('empty') - self.assertIn( - 'success', self.conf('"blah"', 'applications/empty/module'), - ) + assert 'success' in self.conf('"blah"', 'applications/empty/module') - self.assertEqual(self.get()['status'], 503, 'loading error') + assert self.get()['status'] == 503, 'loading error' def test_python_application_close(self): self.load('close') @@ -597,7 +552,7 @@ last line: 987654321 self.stop() - self.assertIsNotNone(self.wait_for_record(r'Close called\.'), 'close') + assert self.wait_for_record(r'Close called\.') is not None, 'close' def test_python_application_close_error(self): self.load('close_error') @@ -606,9 +561,9 @@ last line: 987654321 self.stop() - self.assertIsNotNone( - self.wait_for_record(r'Close called\.'), 'close error' - ) + assert ( + self.wait_for_record(r'Close called\.') is not None + ), 'close error' def test_python_application_not_iterable(self): self.load('not_iterable') @@ -617,17 +572,17 @@ last line: 987654321 self.stop() - self.assertIsNotNone( + assert ( self.wait_for_record( r'\[error\].+the application returned not an iterable object' - ), - 'not iterable', - ) + ) + is not None + ), 'not iterable' def test_python_application_write(self): self.load('write') - self.assertEqual(self.get()['body'], '0123456789', 'write') + assert self.get()['body'] == '0123456789', 'write' def test_python_application_threading(self): """wait_for_record() timeouts after 5s while every thread works at @@ -639,9 +594,9 @@ last line: 987654321 for _ in range(10): self.get(no_recv=True) - self.assertIsNotNone( - self.wait_for_record(r'\(5\) Thread: 100'), 'last thread finished' - ) + assert ( + self.wait_for_record(r'\(5\) Thread: 100') is not None + ), 'last thread finished' def test_python_application_iter_exception(self): self.load('iter_exception') @@ -656,43 +611,38 @@ last line: 987654321 'Connection': 'close', } ) - self.assertEqual(resp['status'], 200, 'status') - self.assertEqual(resp['body'], 'XXXXXXX', 'body') + assert resp['status'] == 200, 'status' + assert resp['body'] == 'XXXXXXX', 'body' # Exception before start_response(). - self.assertEqual(self.get()['status'], 503, 'error') + assert self.get()['status'] == 503, 'error' - self.assertIsNotNone(self.wait_for_record(r'Traceback'), 'traceback') - self.assertIsNotNone( - self.wait_for_record(r'raise Exception\(\'first exception\'\)'), - 'first exception raise', - ) - self.assertEqual( - len(self.findall(r'Traceback')), 1, 'traceback count 1' - ) + assert self.wait_for_record(r'Traceback') is not None, 'traceback' + assert ( + self.wait_for_record(r'raise Exception\(\'first exception\'\)') + is not None + ), 'first exception raise' + assert len(self.findall(r'Traceback')) == 1, 'traceback count 1' # Exception after start_response(), before first write(). - self.assertEqual( + assert ( self.get( headers={ 'Host': 'localhost', 'X-Skip': '1', 'Connection': 'close', } - )['status'], - 503, - 'error 2', - ) + )['status'] + == 503 + ), 'error 2' - self.assertIsNotNone( - self.wait_for_record(r'raise Exception\(\'second exception\'\)'), - 'exception raise second', - ) - self.assertEqual( - len(self.findall(r'Traceback')), 2, 'traceback count 2' - ) + assert ( + self.wait_for_record(r'raise Exception\(\'second exception\'\)') + is not None + ), 'exception raise second' + assert len(self.findall(r'Traceback')) == 2, 'traceback count 2' # Exception after first write(), before first __next__(). @@ -705,15 +655,13 @@ last line: 987654321 start=True, ) - self.assertIsNotNone( - self.wait_for_record(r'raise Exception\(\'third exception\'\)'), - 'exception raise third', - ) - self.assertEqual( - len(self.findall(r'Traceback')), 3, 'traceback count 3' - ) + assert ( + self.wait_for_record(r'raise Exception\(\'third exception\'\)') + is not None + ), 'exception raise third' + assert len(self.findall(r'Traceback')) == 3, 'traceback count 3' - self.assertDictEqual(self.get(sock=sock), {}, 'closed connection') + assert self.get(sock=sock) == {}, 'closed connection' # Exception after first write(), before first __next__(), # chunked (incomplete body). @@ -725,13 +673,11 @@ last line: 987654321 'X-Chunked': '1', 'Connection': 'close', }, - raw_resp=True + raw_resp=True, ) if resp: - self.assertNotEqual(resp[-5:], '0\r\n\r\n', 'incomplete body') - self.assertEqual( - len(self.findall(r'Traceback')), 4, 'traceback count 4' - ) + assert resp[-5:] != '0\r\n\r\n', 'incomplete body' + assert len(self.findall(r'Traceback')) == 4, 'traceback count 4' # Exception in __next__(). @@ -744,15 +690,13 @@ last line: 987654321 start=True, ) - self.assertIsNotNone( - self.wait_for_record(r'raise Exception\(\'next exception\'\)'), - 'exception raise next', - ) - self.assertEqual( - len(self.findall(r'Traceback')), 5, 'traceback count 5' - ) + assert ( + self.wait_for_record(r'raise Exception\(\'next exception\'\)') + is not None + ), 'exception raise next' + assert len(self.findall(r'Traceback')) == 5, 'traceback count 5' - self.assertDictEqual(self.get(sock=sock), {}, 'closed connection 2') + assert self.get(sock=sock) == {}, 'closed connection 2' # Exception in __next__(), chunked (incomplete body). @@ -763,40 +707,34 @@ last line: 987654321 'X-Chunked': '1', 'Connection': 'close', }, - raw_resp=True + raw_resp=True, ) if resp: - self.assertNotEqual(resp[-5:], '0\r\n\r\n', 'incomplete body 2') - self.assertEqual( - len(self.findall(r'Traceback')), 6, 'traceback count 6' - ) + assert resp[-5:] != '0\r\n\r\n', 'incomplete body 2' + assert len(self.findall(r'Traceback')) == 6, 'traceback count 6' # Exception before start_response() and in close(). - self.assertEqual( + assert ( self.get( headers={ 'Host': 'localhost', 'X-Not-Skip-Close': '1', 'Connection': 'close', } - )['status'], - 503, - 'error', - ) + )['status'] + == 503 + ), 'error' - self.assertIsNotNone( - self.wait_for_record(r'raise Exception\(\'close exception\'\)'), - 'exception raise close', - ) - self.assertEqual( - len(self.findall(r'Traceback')), 8, 'traceback count 8' - ) + assert ( + self.wait_for_record(r'raise Exception\(\'close exception\'\)') + is not None + ), 'exception raise close' + assert len(self.findall(r'Traceback')) == 8, 'traceback count 8' - def test_python_user_group(self): - if not self.is_su: - print("requires root") - raise unittest.SkipTest() + def test_python_user_group(self, is_su): + if not is_su: + pytest.skip('requires root') nobody_uid = pwd.getpwnam('nobody').pw_uid @@ -811,40 +749,38 @@ last line: 987654321 self.load('user_group') obj = self.getjson()['body'] - self.assertEqual(obj['UID'], nobody_uid, 'nobody uid') - self.assertEqual(obj['GID'], group_id, 'nobody gid') + assert obj['UID'] == nobody_uid, 'nobody uid' + assert obj['GID'] == group_id, 'nobody gid' self.load('user_group', user='nobody') obj = self.getjson()['body'] - self.assertEqual(obj['UID'], nobody_uid, 'nobody uid user=nobody') - self.assertEqual(obj['GID'], group_id, 'nobody gid user=nobody') + assert obj['UID'] == nobody_uid, 'nobody uid user=nobody' + assert obj['GID'] == group_id, 'nobody gid user=nobody' self.load('user_group', user='nobody', group=group) obj = self.getjson()['body'] - self.assertEqual( - obj['UID'], nobody_uid, 'nobody uid user=nobody group=%s' % group + assert obj['UID'] == nobody_uid, ( + 'nobody uid user=nobody group=%s' % group ) - self.assertEqual( - obj['GID'], group_id, 'nobody gid user=nobody group=%s' % group + assert obj['GID'] == group_id, ( + 'nobody gid user=nobody group=%s' % group ) self.load('user_group', group=group) obj = self.getjson()['body'] - self.assertEqual( - obj['UID'], nobody_uid, 'nobody uid group=%s' % group - ) + assert obj['UID'] == nobody_uid, 'nobody uid group=%s' % group - self.assertEqual(obj['GID'], group_id, 'nobody gid group=%s' % group) + assert obj['GID'] == group_id, 'nobody gid group=%s' % group self.load('user_group', user='root') obj = self.getjson()['body'] - self.assertEqual(obj['UID'], 0, 'root uid user=root') - self.assertEqual(obj['GID'], 0, 'root gid user=root') + assert obj['UID'] == 0, 'root uid user=root' + assert obj['GID'] == 0, 'root gid user=root' group = 'root' @@ -858,14 +794,39 @@ last line: 987654321 self.load('user_group', user='root', group='root') obj = self.getjson()['body'] - self.assertEqual(obj['UID'], 0, 'root uid user=root group=root') - self.assertEqual(obj['GID'], 0, 'root gid user=root group=root') + assert obj['UID'] == 0, 'root uid user=root group=root' + assert obj['GID'] == 0, 'root gid user=root group=root' self.load('user_group', group='root') obj = self.getjson()['body'] - self.assertEqual(obj['UID'], nobody_uid, 'root uid group=root') - self.assertEqual(obj['GID'], 0, 'root gid group=root') + assert obj['UID'] == nobody_uid, 'root uid group=root' + assert obj['GID'] == 0, 'root gid group=root' + + def test_python_application_callable(self): + skip_alert(r'Python failed to get "blah" from module') + self.load('callable') + + assert self.get()['status'] == 204, 'default application response' + + assert 'success' in self.conf( + '"app"', 'applications/callable/callable' + ) + + assert self.get()['status'] == 200, 'callable response' + + assert 'success' in self.conf( + '"blah"', 'applications/callable/callable' + ) + + assert self.get()['status'] not in [200, 204], 'callable response inv' + + assert 'success' in self.conf( + '"app"', 'applications/callable/callable' + ) + + assert self.get()['status'] == 200, 'callable response 2' + + assert 'success' in self.conf_delete('applications/callable/callable') -if __name__ == '__main__': - TestPythonApplication.main() + assert self.get()['status'] == 204, 'default response 2' diff --git a/test/test_python_basic.py b/test/test_python_basic.py index d6445ac2..0cc70e51 100644 --- a/test/test_python_basic.py +++ b/test/test_python_basic.py @@ -19,142 +19,104 @@ class TestPythonBasic(TestControl): } def test_python_get_empty(self): - self.assertEqual(self.conf_get(), {'listeners': {}, 'applications': {}}) - self.assertEqual(self.conf_get('listeners'), {}) - self.assertEqual(self.conf_get('applications'), {}) + assert self.conf_get() == {'listeners': {}, 'applications': {}} + assert self.conf_get('listeners') == {} + assert self.conf_get('applications') == {} def test_python_get_applications(self): self.conf(self.conf_app, 'applications') conf = self.conf_get() - self.assertEqual(conf['listeners'], {}, 'listeners') - self.assertEqual( - conf['applications'], - { - "app": { - "type": "python", - "processes": {"spare": 0}, - "path": "/app", - "module": "wsgi", - } - }, - 'applications', - ) - - self.assertEqual( - self.conf_get('applications'), - { - "app": { - "type": "python", - "processes": {"spare": 0}, - "path": "/app", - "module": "wsgi", - } - }, - 'applications prefix', - ) - - self.assertEqual( - self.conf_get('applications/app'), - { + assert conf['listeners'] == {}, 'listeners' + assert conf['applications'] == { + "app": { "type": "python", "processes": {"spare": 0}, "path": "/app", "module": "wsgi", - }, - 'applications prefix 2', - ) + } + }, 'applications' - self.assertEqual( - self.conf_get('applications/app/type'), 'python', 'type' - ) - self.assertEqual( - self.conf_get('applications/app/processes/spare'), 0, 'spare' - ) + assert self.conf_get('applications') == { + "app": { + "type": "python", + "processes": {"spare": 0}, + "path": "/app", + "module": "wsgi", + } + }, 'applications prefix' + + assert self.conf_get('applications/app') == { + "type": "python", + "processes": {"spare": 0}, + "path": "/app", + "module": "wsgi", + }, 'applications prefix 2' + + assert self.conf_get('applications/app/type') == 'python', 'type' + assert self.conf_get('applications/app/processes/spare') == 0, 'spare' def test_python_get_listeners(self): self.conf(self.conf_basic) - self.assertEqual( - self.conf_get()['listeners'], - {"*:7080": {"pass": "applications/app"}}, - 'listeners', - ) + assert self.conf_get()['listeners'] == { + "*:7080": {"pass": "applications/app"} + }, 'listeners' - self.assertEqual( - self.conf_get('listeners'), - {"*:7080": {"pass": "applications/app"}}, - 'listeners prefix', - ) + assert self.conf_get('listeners') == { + "*:7080": {"pass": "applications/app"} + }, 'listeners prefix' - self.assertEqual( - self.conf_get('listeners/*:7080'), - {"pass": "applications/app"}, - 'listeners prefix 2', - ) + assert self.conf_get('listeners/*:7080') == { + "pass": "applications/app" + }, 'listeners prefix 2' def test_python_change_listener(self): self.conf(self.conf_basic) self.conf({"*:7081": {"pass": "applications/app"}}, 'listeners') - self.assertEqual( - self.conf_get('listeners'), - {"*:7081": {"pass": "applications/app"}}, - 'change listener', - ) + assert self.conf_get('listeners') == { + "*:7081": {"pass": "applications/app"} + }, 'change listener' def test_python_add_listener(self): self.conf(self.conf_basic) self.conf({"pass": "applications/app"}, 'listeners/*:7082') - self.assertEqual( - self.conf_get('listeners'), - { - "*:7080": {"pass": "applications/app"}, - "*:7082": {"pass": "applications/app"}, - }, - 'add listener', - ) + assert self.conf_get('listeners') == { + "*:7080": {"pass": "applications/app"}, + "*:7082": {"pass": "applications/app"}, + }, 'add listener' def test_python_change_application(self): self.conf(self.conf_basic) self.conf('30', 'applications/app/processes/max') - self.assertEqual( - self.conf_get('applications/app/processes/max'), - 30, - 'change application max', - ) + assert ( + self.conf_get('applications/app/processes/max') == 30 + ), 'change application max' self.conf('"/www"', 'applications/app/path') - self.assertEqual( - self.conf_get('applications/app/path'), - '/www', - 'change application path', - ) + assert ( + self.conf_get('applications/app/path') == '/www' + ), 'change application path' def test_python_delete(self): self.conf(self.conf_basic) - self.assertIn('error', self.conf_delete('applications/app')) - self.assertIn('success', self.conf_delete('listeners/*:7080')) - self.assertIn('success', self.conf_delete('applications/app')) - self.assertIn('error', self.conf_delete('applications/app')) + assert 'error' in self.conf_delete('applications/app') + assert 'success' in self.conf_delete('listeners/*:7080') + assert 'success' in self.conf_delete('applications/app') + assert 'error' in self.conf_delete('applications/app') def test_python_delete_blocks(self): self.conf(self.conf_basic) - self.assertIn('success', self.conf_delete('listeners')) - self.assertIn('success', self.conf_delete('applications')) - - self.assertIn('success', self.conf(self.conf_app, 'applications')) - self.assertIn( - 'success', - self.conf({"*:7081": {"pass": "applications/app"}}, 'listeners'), - 'applications restore', - ) - + assert 'success' in self.conf_delete('listeners') + assert 'success' in self.conf_delete('applications') -if __name__ == '__main__': - TestPythonBasic.main() + assert 'success' in self.conf(self.conf_app, 'applications') + assert 'success' in self.conf( + {"*:7081": {"pass": "applications/app"}}, 'listeners' + ), 'applications restore' diff --git a/test/test_python_environment.py b/test/test_python_environment.py index a03b96e6..2d7d1595 100644 --- a/test/test_python_environment.py +++ b/test/test_python_environment.py @@ -7,97 +7,81 @@ class TestPythonEnvironment(TestApplicationPython): def test_python_environment_name_null(self): self.load('environment') - self.assertIn( - 'error', - self.conf( - {"va\0r": "val1"}, 'applications/environment/environment' - ), - 'name null', - ) + assert 'error' in self.conf( + {"va\0r": "val1"}, 'applications/environment/environment' + ), 'name null' def test_python_environment_name_equals(self): self.load('environment') - self.assertIn( - 'error', - self.conf( - {"var=": "val1"}, 'applications/environment/environment' - ), - 'name equals', - ) + assert 'error' in self.conf( + {"var=": "val1"}, 'applications/environment/environment' + ), 'name equals' def test_python_environment_value_null(self): self.load('environment') - self.assertIn( - 'error', - self.conf( - {"var": "\0val"}, 'applications/environment/environment' - ), - 'value null', - ) + assert 'error' in self.conf( + {"var": "\0val"}, 'applications/environment/environment' + ), 'value null' def test_python_environment_update(self): self.load('environment') self.conf({"var": "val1"}, 'applications/environment/environment') - self.assertEqual( + assert ( self.get( headers={ 'Host': 'localhost', 'X-Variables': 'var', 'Connection': 'close', } - )['body'], - 'val1,', - 'set', - ) + )['body'] + == 'val1,' + ), 'set' self.conf({"var": "val2"}, 'applications/environment/environment') - self.assertEqual( + assert ( self.get( headers={ 'Host': 'localhost', 'X-Variables': 'var', 'Connection': 'close', } - )['body'], - 'val2,', - 'update', - ) + )['body'] + == 'val2,' + ), 'update' def test_python_environment_replace(self): self.load('environment') self.conf({"var1": "val1"}, 'applications/environment/environment') - self.assertEqual( + assert ( self.get( headers={ 'Host': 'localhost', 'X-Variables': 'var1', 'Connection': 'close', } - )['body'], - 'val1,', - 'set', - ) + )['body'] + == 'val1,' + ), 'set' self.conf({"var2": "val2"}, 'applications/environment/environment') - self.assertEqual( + assert ( self.get( headers={ 'Host': 'localhost', 'X-Variables': 'var1,var2', 'Connection': 'close', } - )['body'], - 'val2,', - 'replace', - ) + )['body'] + == 'val2,' + ), 'replace' def test_python_environment_clear(self): self.load('environment') @@ -107,31 +91,29 @@ class TestPythonEnvironment(TestApplicationPython): 'applications/environment/environment', ) - self.assertEqual( + assert ( self.get( headers={ 'Host': 'localhost', 'X-Variables': 'var1,var2', 'Connection': 'close', } - )['body'], - 'val1,val2,', - 'set', - ) + )['body'] + == 'val1,val2,' + ), 'set' self.conf({}, 'applications/environment/environment') - self.assertEqual( + assert ( self.get( headers={ 'Host': 'localhost', 'X-Variables': 'var1,var2', 'Connection': 'close', } - )['body'], - '', - 'clear', - ) + )['body'] + == '' + ), 'clear' def test_python_environment_replace_default(self): self.load('environment') @@ -144,36 +126,30 @@ class TestPythonEnvironment(TestApplicationPython): } )['body'] - self.assertGreater(len(home_default), 1, 'get default') + assert len(home_default) > 1, 'get default' self.conf({"HOME": "/"}, 'applications/environment/environment') - self.assertEqual( + assert ( self.get( headers={ 'Host': 'localhost', 'X-Variables': 'HOME', 'Connection': 'close', } - )['body'], - '/,', - 'replace default', - ) + )['body'] + == '/,' + ), 'replace default' self.conf({}, 'applications/environment/environment') - self.assertEqual( + assert ( self.get( headers={ 'Host': 'localhost', 'X-Variables': 'HOME', 'Connection': 'close', } - )['body'], - home_default, - 'restore default', - ) - - -if __name__ == '__main__': - TestPythonEnvironment.main() + )['body'] + == home_default + ), 'restore default' diff --git a/test/test_python_isolation.py b/test/test_python_isolation.py index 1bed64ba..ac678103 100644 --- a/test/test_python_isolation.py +++ b/test/test_python_isolation.py @@ -1,4 +1,4 @@ -import unittest +import pytest from unit.applications.lang.python import TestApplicationPython from unit.feature.isolation import TestFeatureIsolation @@ -10,70 +10,87 @@ class TestPythonIsolation(TestApplicationPython): isolation = TestFeatureIsolation() @classmethod - def setUpClass(cls, complete_check=True): - unit = super().setUpClass(complete_check=False) + def setup_class(cls, complete_check=True): + unit = super().setup_class(complete_check=False) - TestFeatureIsolation().check(cls.available, unit.testdir) + TestFeatureIsolation().check(cls.available, unit.temp_dir) return unit if not complete_check else unit.complete() - def test_python_isolation_rootfs(self): + def test_python_isolation_rootfs(self, is_su): isolation_features = self.available['features']['isolation'].keys() if 'mnt' not in isolation_features: - print('requires mnt ns') - raise unittest.SkipTest() + pytest.skip('requires mnt ns') - if not self.is_su: + if not is_su: if 'user' not in isolation_features: - print('requires unprivileged userns or root') - raise unittest.SkipTest() + pytest.skip('requires unprivileged userns or root') if not 'unprivileged_userns_clone' in isolation_features: - print('requires unprivileged userns or root') - raise unittest.SkipTest() + pytest.skip('requires unprivileged userns or root') isolation = { - 'namespaces': {'credential': not self.is_su, 'mount': True}, - 'rootfs': self.testdir, + 'namespaces': {'credential': not is_su, 'mount': True}, + 'rootfs': self.temp_dir, } self.load('empty', isolation=isolation) - self.assertEqual(self.get()['status'], 200, 'python rootfs') + assert self.get()['status'] == 200, 'python rootfs' self.load('ns_inspect', isolation=isolation) - self.assertEqual( - self.getjson(url='/?path=' + self.testdir)['body']['FileExists'], - False, - 'testdir does not exists in rootfs', - ) - - self.assertEqual( - self.getjson(url='/?path=/proc/self')['body']['FileExists'], - False, - 'no /proc/self', - ) - - self.assertEqual( - self.getjson(url='/?path=/dev/pts')['body']['FileExists'], - False, - 'no /dev/pts', - ) - - self.assertEqual( - self.getjson(url='/?path=/sys/kernel')['body']['FileExists'], - False, - 'no /sys/kernel', - ) + assert ( + self.getjson(url='/?path=' + self.temp_dir)['body']['FileExists'] + == False + ), 'temp_dir does not exists in rootfs' + + assert ( + self.getjson(url='/?path=/proc/self')['body']['FileExists'] + == False + ), 'no /proc/self' + + assert ( + self.getjson(url='/?path=/dev/pts')['body']['FileExists'] == False + ), 'no /dev/pts' + + assert ( + self.getjson(url='/?path=/sys/kernel')['body']['FileExists'] + == False + ), 'no /sys/kernel' ret = self.getjson(url='/?path=/app/python/ns_inspect') - self.assertEqual( - ret['body']['FileExists'], True, 'application exists in rootfs', - ) + assert ( + ret['body']['FileExists'] == True + ), 'application exists in rootfs' + + def test_python_isolation_rootfs_no_language_deps(self, is_su): + isolation_features = self.available['features']['isolation'].keys() + + if 'mnt' not in isolation_features: + pytest.skip('requires mnt ns') + if not is_su: + if 'user' not in isolation_features: + pytest.skip('requires unprivileged userns or root') + + if not 'unprivileged_userns_clone' in isolation_features: + pytest.skip('requires unprivileged userns or root') + + isolation = { + 'namespaces': {'credential': not is_su, 'mount': True}, + 'rootfs': self.temp_dir, + 'automount': {'language_deps': False} + } + + self.load('empty', isolation=isolation) + + assert (self.get()['status'] != 200), 'disabled language_deps' + + isolation['automount']['language_deps'] = True + + self.load('empty', isolation=isolation) -if __name__ == '__main__': - TestPythonIsolation.main() + assert (self.get()['status'] == 200), 'enabled language_deps' diff --git a/test/test_python_isolation_chroot.py b/test/test_python_isolation_chroot.py index 7761128e..315fee9f 100644 --- a/test/test_python_isolation_chroot.py +++ b/test/test_python_isolation_chroot.py @@ -1,4 +1,4 @@ -import unittest +import pytest from unit.applications.lang.python import TestApplicationPython from unit.feature.isolation import TestFeatureIsolation @@ -7,51 +7,41 @@ from unit.feature.isolation import TestFeatureIsolation class TestPythonIsolation(TestApplicationPython): prerequisites = {'modules': {'python': 'any'}} - def test_python_isolation_chroot(self): - if not self.is_su: - print('requires root') - raise unittest.SkipTest() + def test_python_isolation_chroot(self, is_su): + if not is_su: + pytest.skip('requires root') isolation = { - 'rootfs': self.testdir, + 'rootfs': self.temp_dir, } self.load('empty', isolation=isolation) - self.assertEqual(self.get()['status'], 200, 'python chroot') + assert self.get()['status'] == 200, 'python chroot' self.load('ns_inspect', isolation=isolation) - self.assertEqual( - self.getjson(url='/?path=' + self.testdir)['body']['FileExists'], - False, - 'testdir does not exists in rootfs', - ) - - self.assertEqual( - self.getjson(url='/?path=/proc/self')['body']['FileExists'], - False, - 'no /proc/self', - ) - - self.assertEqual( - self.getjson(url='/?path=/dev/pts')['body']['FileExists'], - False, - 'no /dev/pts', - ) - - self.assertEqual( - self.getjson(url='/?path=/sys/kernel')['body']['FileExists'], - False, - 'no /sys/kernel', - ) + assert ( + self.getjson(url='/?path=' + self.temp_dir)['body']['FileExists'] + == False + ), 'temp_dir does not exists in rootfs' - ret = self.getjson(url='/?path=/app/python/ns_inspect') + assert ( + self.getjson(url='/?path=/proc/self')['body']['FileExists'] + == False + ), 'no /proc/self' + + assert ( + self.getjson(url='/?path=/dev/pts')['body']['FileExists'] == False + ), 'no /dev/pts' - self.assertEqual( - ret['body']['FileExists'], True, 'application exists in rootfs', - ) + assert ( + self.getjson(url='/?path=/sys/kernel')['body']['FileExists'] + == False + ), 'no /sys/kernel' + ret = self.getjson(url='/?path=/app/python/ns_inspect') -if __name__ == '__main__': - TestPythonIsolation.main() + assert ( + ret['body']['FileExists'] == True + ), 'application exists in rootfs' diff --git a/test/test_python_procman.py b/test/test_python_procman.py index c327ab14..8eccae3e 100644 --- a/test/test_python_procman.py +++ b/test/test_python_procman.py @@ -1,7 +1,8 @@ import re import subprocess import time -import unittest + +import pytest from unit.applications.lang.python import TestApplicationPython @@ -9,10 +10,10 @@ from unit.applications.lang.python import TestApplicationPython class TestPythonProcman(TestApplicationPython): prerequisites = {'modules': {'python': 'any'}} - def setUp(self): - super().setUp() + def setup_method(self): + super().setup_method() - self.app_name = "app-" + self.testdir.split('/')[-1] + self.app_name = "app-" + self.temp_dir.split('/')[-1] self.app_proc = 'applications/' + self.app_name + '/processes' self.load('empty', self.app_name) @@ -23,7 +24,7 @@ class TestPythonProcman(TestApplicationPython): pids = set() for m in re.findall('.*' + self.app_name, output.decode()): - pids.add(re.search('^\s*(\d+)', m).group(1)) + pids.add(re.search(r'^\s*(\d+)', m).group(1)) return pids @@ -31,35 +32,35 @@ class TestPythonProcman(TestApplicationPython): if path is None: path = self.app_proc - self.assertIn('success', self.conf(conf, path), 'configure processes') + assert 'success' in self.conf(conf, path), 'configure processes' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_python_processes_idle_timeout_zero(self): self.conf_proc({"spare": 0, "max": 2, "idle_timeout": 0}) self.get() - self.assertEqual(len(self.pids_for_process()), 0, 'idle timeout 0') + assert len(self.pids_for_process()) == 0, 'idle timeout 0' def test_python_prefork(self): self.conf_proc('2') pids = self.pids_for_process() - self.assertEqual(len(pids), 2, 'prefork 2') + assert len(pids) == 2, 'prefork 2' self.get() - self.assertSetEqual(self.pids_for_process(), pids, 'prefork still 2') + assert self.pids_for_process() == pids, 'prefork still 2' self.conf_proc('4') pids = self.pids_for_process() - self.assertEqual(len(pids), 4, 'prefork 4') + assert len(pids) == 4, 'prefork 4' self.get() - self.assertSetEqual(self.pids_for_process(), pids, 'prefork still 4') + assert self.pids_for_process() == pids, 'prefork still 4' self.stop_all() - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_python_prefork_same_processes(self): self.conf_proc('2') pids = self.pids_for_process() @@ -67,25 +68,23 @@ class TestPythonProcman(TestApplicationPython): self.conf_proc('4') pids_new = self.pids_for_process() - self.assertTrue(pids.issubset(pids_new), 'prefork same processes') + assert pids.issubset(pids_new), 'prefork same processes' def test_python_ondemand(self): self.conf_proc({"spare": 0, "max": 8, "idle_timeout": 1}) - self.assertEqual(len(self.pids_for_process()), 0, 'on-demand 0') + assert len(self.pids_for_process()) == 0, 'on-demand 0' self.get() pids = self.pids_for_process() - self.assertEqual(len(pids), 1, 'on-demand 1') + assert len(pids) == 1, 'on-demand 1' self.get() - self.assertSetEqual(self.pids_for_process(), pids, 'on-demand still 1') + assert self.pids_for_process() == pids, 'on-demand still 1' time.sleep(1) - self.assertEqual( - len(self.pids_for_process()), 0, 'on-demand stop idle' - ) + assert len(self.pids_for_process()) == 0, 'on-demand stop idle' self.stop_all() @@ -93,27 +92,25 @@ class TestPythonProcman(TestApplicationPython): self.conf_proc({"spare": 2, "max": 8, "idle_timeout": 1}) pids = self.pids_for_process() - self.assertEqual(len(pids), 2, 'updown 2') + assert len(pids) == 2, 'updown 2' self.get() pids_new = self.pids_for_process() - self.assertEqual(len(pids_new), 3, 'updown 3') - self.assertTrue(pids.issubset(pids_new), 'updown 3 only 1 new') + assert len(pids_new) == 3, 'updown 3' + assert pids.issubset(pids_new), 'updown 3 only 1 new' self.get() - self.assertSetEqual( - self.pids_for_process(), pids_new, 'updown still 3' - ) + assert self.pids_for_process() == pids_new, 'updown still 3' time.sleep(1) pids = self.pids_for_process() - self.assertEqual(len(pids), 2, 'updown stop idle') + assert len(pids) == 2, 'updown stop idle' self.get() pids_new = self.pids_for_process() - self.assertEqual(len(pids_new), 3, 'updown again 3') - self.assertTrue(pids.issubset(pids_new), 'updown again 3 only 1 new') + assert len(pids_new) == 3, 'updown again 3' + assert pids.issubset(pids_new), 'updown again 3 only 1 new' self.stop_all() @@ -121,20 +118,20 @@ class TestPythonProcman(TestApplicationPython): self.conf_proc({"spare": 2, "max": 6, "idle_timeout": 1}) pids = self.pids_for_process() - self.assertEqual(len(pids), 2, 'reconf 2') + assert len(pids) == 2, 'reconf 2' self.get() pids_new = self.pids_for_process() - self.assertEqual(len(pids_new), 3, 'reconf 3') - self.assertTrue(pids.issubset(pids_new), 'reconf 3 only 1 new') + assert len(pids_new) == 3, 'reconf 3' + assert pids.issubset(pids_new), 'reconf 3 only 1 new' self.conf_proc('6', self.app_proc + '/spare') pids = self.pids_for_process() - self.assertEqual(len(pids), 6, 'reconf 6') + assert len(pids) == 6, 'reconf 6' self.get() - self.assertSetEqual(self.pids_for_process(), pids, 'reconf still 6') + assert self.pids_for_process() == pids, 'reconf still 6' self.stop_all() @@ -143,7 +140,7 @@ class TestPythonProcman(TestApplicationPython): self.get() pids = self.pids_for_process() - self.assertEqual(len(pids), 1, 'idle timeout 1') + assert len(pids) == 1, 'idle timeout 1' time.sleep(1) @@ -152,14 +149,12 @@ class TestPythonProcman(TestApplicationPython): time.sleep(1) pids_new = self.pids_for_process() - self.assertEqual(len(pids_new), 1, 'idle timeout still 1') - self.assertSetEqual( - self.pids_for_process(), pids, 'idle timeout still 1 same pid' - ) + assert len(pids_new) == 1, 'idle timeout still 1' + assert self.pids_for_process() == pids, 'idle timeout still 1 same pid' time.sleep(1) - self.assertEqual(len(self.pids_for_process()), 0, 'idle timed out') + assert len(self.pids_for_process()) == 0, 'idle timed out' def test_python_processes_connection_keepalive(self): self.conf_proc({"spare": 0, "max": 6, "idle_timeout": 2}) @@ -169,15 +164,11 @@ class TestPythonProcman(TestApplicationPython): start=True, read_timeout=1, ) - self.assertEqual( - len(self.pids_for_process()), 1, 'keepalive connection 1' - ) + assert len(self.pids_for_process()) == 1, 'keepalive connection 1' time.sleep(2) - self.assertEqual( - len(self.pids_for_process()), 0, 'keepalive connection 0' - ) + assert len(self.pids_for_process()) == 0, 'keepalive connection 0' sock.close() @@ -185,43 +176,29 @@ class TestPythonProcman(TestApplicationPython): self.conf_proc('1') path = '/' + self.app_proc - self.assertIn('error', self.conf_get(path + '/max')) - self.assertIn('error', self.conf_get(path + '/spare')) - self.assertIn('error', self.conf_get(path + '/idle_timeout')) + assert 'error' in self.conf_get(path + '/max') + assert 'error' in self.conf_get(path + '/spare') + assert 'error' in self.conf_get(path + '/idle_timeout') def test_python_processes_invalid(self): - self.assertIn( - 'error', self.conf({"spare": -1}, self.app_proc), 'negative spare', - ) - self.assertIn( - 'error', self.conf({"max": -1}, self.app_proc), 'negative max', - ) - self.assertIn( - 'error', - self.conf({"idle_timeout": -1}, self.app_proc), - 'negative idle_timeout', - ) - self.assertIn( - 'error', - self.conf({"spare": 2}, self.app_proc), - 'spare gt max default', - ) - self.assertIn( - 'error', - self.conf({"spare": 2, "max": 1}, self.app_proc), - 'spare gt max', - ) - self.assertIn( - 'error', - self.conf({"spare": 0, "max": 0}, self.app_proc), - 'max zero', - ) + assert 'error' in self.conf( + {"spare": -1}, self.app_proc + ), 'negative spare' + assert 'error' in self.conf({"max": -1}, self.app_proc), 'negative max' + assert 'error' in self.conf( + {"idle_timeout": -1}, self.app_proc + ), 'negative idle_timeout' + assert 'error' in self.conf( + {"spare": 2}, self.app_proc + ), 'spare gt max default' + assert 'error' in self.conf( + {"spare": 2, "max": 1}, self.app_proc + ), 'spare gt max' + assert 'error' in self.conf( + {"spare": 0, "max": 0}, self.app_proc + ), 'max zero' def stop_all(self): self.conf({"listeners": {}, "applications": {}}) - self.assertEqual(len(self.pids_for_process()), 0, 'stop all') - - -if __name__ == '__main__': - TestPythonProcman.main() + assert len(self.pids_for_process()) == 0, 'stop all' diff --git a/test/test_respawn.py b/test/test_respawn.py index f1c71a20..18b9d535 100644 --- a/test/test_respawn.py +++ b/test/test_respawn.py @@ -2,6 +2,7 @@ import re import subprocess import time +from conftest import skip_alert from unit.applications.lang.python import TestApplicationPython @@ -11,21 +12,20 @@ class TestRespawn(TestApplicationPython): PATTERN_ROUTER = 'unit: router' PATTERN_CONTROLLER = 'unit: controller' - def setUp(self): - super().setUp() + def setup_method(self): + super().setup_method() - self.app_name = "app-" + self.testdir.split('/')[-1] + self.app_name = "app-" + self.temp_dir.split('/')[-1] self.load('empty', self.app_name) - self.assertIn( - 'success', - self.conf('1', 'applications/' + self.app_name + '/processes') + assert 'success' in self.conf( + '1', 'applications/' + self.app_name + '/processes' ) def pid_by_name(self, name): output = subprocess.check_output(['ps', 'ax']).decode() - m = re.search('\s*(\d+).*' + name, output) + m = re.search(r'\s*(\d+).*' + name, output) return m if m is None else m.group(1) def kill_pids(self, *pids): @@ -44,27 +44,26 @@ class TestRespawn(TestApplicationPython): def smoke_test(self): for _ in range(5): - self.assertIn( - 'success', - self.conf('1', 'applications/' + self.app_name + '/processes') + assert 'success' in self.conf( + '1', 'applications/' + self.app_name + '/processes' ) - self.assertEqual(self.get()['status'], 200) + assert self.get()['status'] == 200 # Check if the only one router, controller, # and application processes running. output = subprocess.check_output(['ps', 'ax']).decode() - self.assertEqual(len(re.findall(self.PATTERN_ROUTER, output)), 1) - self.assertEqual(len(re.findall(self.PATTERN_CONTROLLER, output)), 1) - self.assertEqual(len(re.findall(self.app_name, output)), 1) + assert len(re.findall(self.PATTERN_ROUTER, output)) == 1 + assert len(re.findall(self.PATTERN_CONTROLLER, output)) == 1 + assert len(re.findall(self.app_name, output)) == 1 def test_respawn_router(self): pid = self.pid_by_name(self.PATTERN_ROUTER) self.kill_pids(pid) - self.skip_alerts.append(r'process %s exited on signal 9' % pid) + skip_alert(r'process %s exited on signal 9' % pid) - self.assertIsNotNone(self.wait_for_process(self.PATTERN_ROUTER)) + assert self.wait_for_process(self.PATTERN_ROUTER) is not None self.smoke_test() @@ -72,11 +71,11 @@ class TestRespawn(TestApplicationPython): pid = self.pid_by_name(self.PATTERN_CONTROLLER) self.kill_pids(pid) - self.skip_alerts.append(r'process %s exited on signal 9' % pid) + skip_alert(r'process %s exited on signal 9' % pid) - self.assertIsNotNone(self.wait_for_process(self.PATTERN_CONTROLLER)) + assert self.wait_for_process(self.PATTERN_CONTROLLER) is not None - self.assertEqual(self.get()['status'], 200) + assert self.get()['status'] == 200 self.smoke_test() @@ -84,12 +83,8 @@ class TestRespawn(TestApplicationPython): pid = self.pid_by_name(self.app_name) self.kill_pids(pid) - self.skip_alerts.append(r'process %s exited on signal 9' % pid) + skip_alert(r'process %s exited on signal 9' % pid) - self.assertIsNotNone(self.wait_for_process(self.app_name)) + assert self.wait_for_process(self.app_name) is not None self.smoke_test() - - -if __name__ == '__main__': - TestRespawn.main() diff --git a/test/test_return.py b/test/test_return.py index a89d97e6..64050022 100644 --- a/test/test_return.py +++ b/test/test_return.py @@ -6,8 +6,8 @@ from unit.applications.proto import TestApplicationProto class TestReturn(TestApplicationProto): prerequisites = {} - def setUp(self): - super().setUp() + def setup_method(self): + super().setup_method() self._load_conf( { @@ -35,59 +35,61 @@ Connection: close def test_return(self): resp = self.get() - self.assertEqual(resp['status'], 200) - self.assertIn('Server', resp['headers']) - self.assertIn('Date', resp['headers']) - self.assertEqual(resp['headers']['Content-Length'], '0') - self.assertEqual(resp['headers']['Connection'], 'close') - self.assertEqual(resp['body'], '', 'body') + assert resp['status'] == 200 + assert 'Server' in resp['headers'] + assert 'Date' in resp['headers'] + assert resp['headers']['Content-Length'] == '0' + assert resp['headers']['Connection'] == 'close' + assert resp['body'] == '', 'body' resp = self.post(body='blah') - self.assertEqual(resp['status'], 200) - self.assertEqual(resp['body'], '', 'body') + assert resp['status'] == 200 + assert resp['body'] == '', 'body' resp = self.get_resps_sc() - self.assertEqual(len(re.findall('200 OK', resp)), 10) - self.assertEqual(len(re.findall('Connection:', resp)), 1) - self.assertEqual(len(re.findall('Connection: close', resp)), 1) + assert len(re.findall('200 OK', resp)) == 10 + assert len(re.findall('Connection:', resp)) == 1 + assert len(re.findall('Connection: close', resp)) == 1 resp = self.get(http_10=True) - self.assertEqual(resp['status'], 200) - self.assertIn('Server', resp['headers']) - self.assertIn('Date', resp['headers']) - self.assertEqual(resp['headers']['Content-Length'], '0') - self.assertNotIn('Connection', resp['headers']) - self.assertEqual(resp['body'], '', 'body') + assert resp['status'] == 200 + assert 'Server' in resp['headers'] + assert 'Date' in resp['headers'] + assert resp['headers']['Content-Length'] == '0' + assert 'Connection' not in resp['headers'] + assert resp['body'] == '', 'body' def test_return_update(self): - self.assertIn('success', self.conf('0', 'routes/0/action/return')) + assert 'success' in self.conf('0', 'routes/0/action/return') resp = self.get() - self.assertEqual(resp['status'], 0) - self.assertEqual(resp['body'], '') + assert resp['status'] == 0 + assert resp['body'] == '' - self.assertIn('success', self.conf('404', 'routes/0/action/return')) + assert 'success' in self.conf('404', 'routes/0/action/return') resp = self.get() - self.assertEqual(resp['status'], 404) - self.assertNotEqual(resp['body'], '') + assert resp['status'] == 404 + assert resp['body'] != '' - self.assertIn('success', self.conf('598', 'routes/0/action/return')) + assert 'success' in self.conf('598', 'routes/0/action/return') resp = self.get() - self.assertEqual(resp['status'], 598) - self.assertNotEqual(resp['body'], '') + assert resp['status'] == 598 + assert resp['body'] != '' - self.assertIn('success', self.conf('999', 'routes/0/action/return')) + assert 'success' in self.conf('999', 'routes/0/action/return') resp = self.get() - self.assertEqual(resp['status'], 999) - self.assertEqual(resp['body'], '') + assert resp['status'] == 999 + assert resp['body'] == '' def test_return_location(self): reserved = ":/?#[]@!$&'()*+,;=" - unreserved = ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" - "0123456789-._~") + unreserved = ( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + "0123456789-._~" + ) unsafe = " \"%<>\\^`{|}" unsafe_enc = "%20%22%25%3C%3E%5C%5E%60%7B%7C%7D" @@ -95,15 +97,11 @@ Connection: close if expect is None: expect = location - self.assertIn( - 'success', - self.conf( - {"return": 301, "location": location}, 'routes/0/action' - ), - 'configure location' - ) + assert 'success' in self.conf( + {"return": 301, "location": location}, 'routes/0/action' + ), 'configure location' - self.assertEqual(self.get()['headers']['Location'], expect) + assert self.get()['headers']['Location'] == expect # FAIL: can't specify empty header value. # check_location("") @@ -145,39 +143,29 @@ Connection: close check_location("/%20?%20#%20 ", "/%2520?%2520#%2520%20") def test_return_location_edit(self): - self.assertIn( - 'success', - self.conf( - {"return": 302, "location": "blah"}, 'routes/0/action' - ), - 'configure init location' - ) - self.assertEqual(self.get()['headers']['Location'], 'blah') - - self.assertIn( - 'success', - self.conf_delete('routes/0/action/location'), - 'location delete' - ) - self.assertNotIn('Location', self.get()['headers']) - - self.assertIn( - 'success', - self.conf('"blah"', 'routes/0/action/location'), - 'location restore' - ) - self.assertEqual(self.get()['headers']['Location'], 'blah') - - self.assertIn( - 'error', - self.conf_post('"blah"', 'routes/0/action/location'), - 'location method not allowed' - ) - self.assertEqual(self.get()['headers']['Location'], 'blah') + assert 'success' in self.conf( + {"return": 302, "location": "blah"}, 'routes/0/action' + ), 'configure init location' + assert self.get()['headers']['Location'] == 'blah' + + assert 'success' in self.conf_delete( + 'routes/0/action/location' + ), 'location delete' + assert 'Location' not in self.get()['headers'] + + assert 'success' in self.conf( + '"blah"', 'routes/0/action/location' + ), 'location restore' + assert self.get()['headers']['Location'] == 'blah' + + assert 'error' in self.conf_post( + '"blah"', 'routes/0/action/location' + ), 'location method not allowed' + assert self.get()['headers']['Location'] == 'blah' def test_return_invalid(self): def check_error(conf): - self.assertIn('error', self.conf(conf, 'routes/0/action')) + assert 'error' in self.conf(conf, 'routes/0/action') check_error({"return": "200"}) check_error({"return": []}) @@ -186,13 +174,9 @@ Connection: close check_error({"return": -1}) check_error({"return": 200, "share": "/blah"}) - self.assertIn( - 'error', self.conf('001', 'routes/0/action/return'), 'leading zero' - ) + assert 'error' in self.conf( + '001', 'routes/0/action/return' + ), 'leading zero' check_error({"return": 301, "location": 0}) check_error({"return": 301, "location": []}) - - -if __name__ == '__main__': - TestReturn.main() diff --git a/test/test_routing.py b/test/test_routing.py index 269e8efc..2b528435 100644 --- a/test/test_routing.py +++ b/test/test_routing.py @@ -1,117 +1,104 @@ # -*- coding: utf-8 -*- -import unittest +import pytest +from conftest import option +from conftest import skip_alert from unit.applications.proto import TestApplicationProto class TestRouting(TestApplicationProto): prerequisites = {'modules': {'python': 'any'}} - def setUp(self): - super().setUp() + def setup_method(self): + super().setup_method() - self.assertIn( - 'success', - self.conf( - { - "listeners": {"*:7080": {"pass": "routes"}}, - "routes": [ - { - "match": {"method": "GET"}, - "action": {"return": 200}, - } - ], - "applications": {}, - } - ), - 'routing configure', - ) + assert 'success' in self.conf( + { + "listeners": {"*:7080": {"pass": "routes"}}, + "routes": [ + {"match": {"method": "GET"}, "action": {"return": 200},} + ], + "applications": {}, + } + ), 'routing configure' def route(self, route): return self.conf([route], 'routes') def route_match(self, match): - self.assertIn( - 'success', - self.route({"match": match, "action": {"return": 200}}), - 'route match configure', - ) + assert 'success' in self.route( + {"match": match, "action": {"return": 200}} + ), 'route match configure' def route_match_invalid(self, match): - self.assertIn( - 'error', - self.route({"match": match, "action": {"return": 200}}), - 'route match configure invalid', - ) + assert 'error' in self.route( + {"match": match, "action": {"return": 200}} + ), 'route match configure invalid' def host(self, host, status): - self.assertEqual( - self.get(headers={'Host': host, 'Connection': 'close'})[ - 'status' - ], - status, - 'match host', - ) + assert ( + self.get(headers={'Host': host, 'Connection': 'close'})['status'] + == status + ), 'match host' def cookie(self, cookie, status): - self.assertEqual( + assert ( self.get( headers={ 'Host': 'localhost', 'Cookie': cookie, 'Connection': 'close', }, - )['status'], - status, - 'match cookie', - ) + )['status'] + == status + ), 'match cookie' def test_routes_match_method_positive(self): - self.assertEqual(self.get()['status'], 200, 'GET') - self.assertEqual(self.post()['status'], 404, 'POST') + assert self.get()['status'] == 200, 'GET' + assert self.post()['status'] == 404, 'POST' def test_routes_match_method_positive_many(self): self.route_match({"method": ["GET", "POST"]}) - self.assertEqual(self.get()['status'], 200, 'GET') - self.assertEqual(self.post()['status'], 200, 'POST') - self.assertEqual(self.delete()['status'], 404, 'DELETE') + assert self.get()['status'] == 200, 'GET' + assert self.post()['status'] == 200, 'POST' + assert self.delete()['status'] == 404, 'DELETE' def test_routes_match_method_negative(self): self.route_match({"method": "!GET"}) - self.assertEqual(self.get()['status'], 404, 'GET') - self.assertEqual(self.post()['status'], 200, 'POST') + assert self.get()['status'] == 404, 'GET' + assert self.post()['status'] == 200, 'POST' def test_routes_match_method_negative_many(self): self.route_match({"method": ["!GET", "!POST"]}) - self.assertEqual(self.get()['status'], 404, 'GET') - self.assertEqual(self.post()['status'], 404, 'POST') - self.assertEqual(self.delete()['status'], 200, 'DELETE') + assert self.get()['status'] == 404, 'GET' + assert self.post()['status'] == 404, 'POST' + assert self.delete()['status'] == 200, 'DELETE' def test_routes_match_method_wildcard_left(self): self.route_match({"method": "*ET"}) - self.assertEqual(self.get()['status'], 200, 'GET') - self.assertEqual(self.post()['status'], 404, 'POST') + assert self.get()['status'] == 200, 'GET' + assert self.post()['status'] == 404, 'POST' def test_routes_match_method_wildcard_right(self): self.route_match({"method": "GE*"}) - self.assertEqual(self.get()['status'], 200, 'GET') - self.assertEqual(self.post()['status'], 404, 'POST') + assert self.get()['status'] == 200, 'GET' + assert self.post()['status'] == 404, 'POST' def test_routes_match_method_wildcard_left_right(self): self.route_match({"method": "*GET*"}) - self.assertEqual(self.get()['status'], 200, 'GET') - self.assertEqual(self.post()['status'], 404, 'POST') + assert self.get()['status'] == 200, 'GET' + assert self.post()['status'] == 404, 'POST' def test_routes_match_method_wildcard(self): self.route_match({"method": "*"}) - self.assertEqual(self.get()['status'], 200, 'GET') + assert self.get()['status'] == 200, 'GET' def test_routes_match_invalid(self): self.route_match_invalid({"method": "**"}) @@ -124,32 +111,35 @@ class TestRouting(TestApplicationProto): def test_routes_match_empty_exact(self): self.route_match({"uri": ""}) - self.assertEqual(self.get()['status'], 404) + assert self.get()['status'] == 404 self.route_match({"uri": "/"}) - self.assertEqual(self.get()['status'], 200) - self.assertEqual(self.get(url='/blah')['status'], 404) + assert self.get()['status'] == 200 + assert self.get(url='/blah')['status'] == 404 def test_routes_match_negative(self): self.route_match({"uri": "!"}) - self.assertEqual(self.get()['status'], 404) + assert self.get()['status'] == 200 + + self.route_match({"uri": "!*"}) + assert self.get()['status'] == 404 self.route_match({"uri": "!/"}) - self.assertEqual(self.get()['status'], 404) - self.assertEqual(self.get(url='/blah')['status'], 200) + assert self.get()['status'] == 404 + assert self.get(url='/blah')['status'] == 200 self.route_match({"uri": "!*blah"}) - self.assertEqual(self.get()['status'], 200) - self.assertEqual(self.get(url='/bla')['status'], 200) - self.assertEqual(self.get(url='/blah')['status'], 404) - self.assertEqual(self.get(url='/blah1')['status'], 200) + assert self.get()['status'] == 200 + assert self.get(url='/bla')['status'] == 200 + assert self.get(url='/blah')['status'] == 404 + assert self.get(url='/blah1')['status'] == 200 self.route_match({"uri": "!/blah*1*"}) - self.assertEqual(self.get()['status'], 200) - self.assertEqual(self.get(url='/blah')['status'], 200) - self.assertEqual(self.get(url='/blah1')['status'], 404) - self.assertEqual(self.get(url='/blah12')['status'], 404) - self.assertEqual(self.get(url='/blah2')['status'], 200) + assert self.get()['status'] == 200 + assert self.get(url='/blah')['status'] == 200 + assert self.get(url='/blah1')['status'] == 404 + assert self.get(url='/blah12')['status'] == 404 + assert self.get(url='/blah2')['status'] == 200 def test_routes_match_wildcard_middle(self): self.route_match({"host": "ex*le"}) @@ -162,110 +152,105 @@ class TestRouting(TestApplicationProto): def test_routes_match_method_case_insensitive(self): self.route_match({"method": "get"}) - self.assertEqual(self.get()['status'], 200, 'GET') + assert self.get()['status'] == 200, 'GET' def test_routes_match_wildcard_left_case_insensitive(self): self.route_match({"method": "*get"}) - self.assertEqual(self.get()['status'], 200, 'GET') + assert self.get()['status'] == 200, 'GET' self.route_match({"method": "*et"}) - self.assertEqual(self.get()['status'], 200, 'GET') + assert self.get()['status'] == 200, 'GET' def test_routes_match_wildcard_middle_case_insensitive(self): self.route_match({"method": "g*t"}) - self.assertEqual(self.get()['status'], 200, 'GET') + assert self.get()['status'] == 200, 'GET' def test_routes_match_wildcard_right_case_insensitive(self): self.route_match({"method": "get*"}) - self.assertEqual(self.get()['status'], 200, 'GET') + assert self.get()['status'] == 200, 'GET' self.route_match({"method": "ge*"}) - self.assertEqual(self.get()['status'], 200, 'GET') + assert self.get()['status'] == 200, 'GET' def test_routes_match_wildcard_substring_case_insensitive(self): self.route_match({"method": "*et*"}) - self.assertEqual(self.get()['status'], 200, 'GET') + assert self.get()['status'] == 200, 'GET' def test_routes_match_wildcard_left_case_sensitive(self): self.route_match({"uri": "*blah"}) - self.assertEqual(self.get(url='/blah')['status'], 200, '/blah') - self.assertEqual(self.get(url='/BLAH')['status'], 404, '/BLAH') + assert self.get(url='/blah')['status'] == 200, '/blah' + assert self.get(url='/BLAH')['status'] == 404, '/BLAH' def test_routes_match_wildcard_middle_case_sensitive(self): self.route_match({"uri": "/b*h"}) - self.assertEqual(self.get(url='/blah')['status'], 200, '/blah') - self.assertEqual(self.get(url='/BLAH')['status'], 404, '/BLAH') + assert self.get(url='/blah')['status'] == 200, '/blah' + assert self.get(url='/BLAH')['status'] == 404, '/BLAH' def test_route_match_wildcards_ordered(self): self.route_match({"uri": "/a*x*y*"}) - self.assertEqual(self.get(url='/axy')['status'], 200, '/axy') - self.assertEqual(self.get(url='/ayx')['status'], 404, '/ayx') + assert self.get(url='/axy')['status'] == 200, '/axy' + assert self.get(url='/ayx')['status'] == 404, '/ayx' def test_route_match_wildcards_adjust_start(self): self.route_match({"uri": "/bla*bla*"}) - self.assertEqual(self.get(url='/bla_foo')['status'], 404, '/bla_foo') + assert self.get(url='/bla_foo')['status'] == 404, '/bla_foo' def test_route_match_wildcards_adjust_start_substr(self): self.route_match({"uri": "*bla*bla*"}) - self.assertEqual(self.get(url='/bla_foo')['status'], 404, '/bla_foo') + assert self.get(url='/bla_foo')['status'] == 404, '/bla_foo' def test_route_match_wildcards_adjust_end(self): self.route_match({"uri": "/bla*bla"}) - self.assertEqual(self.get(url='/foo_bla')['status'], 404, '/foo_bla') + assert self.get(url='/foo_bla')['status'] == 404, '/foo_bla' def test_routes_match_wildcard_right_case_sensitive(self): self.route_match({"uri": "/bla*"}) - self.assertEqual(self.get(url='/blah')['status'], 200, '/blah') - self.assertEqual(self.get(url='/BLAH')['status'], 404, '/BLAH') + assert self.get(url='/blah')['status'] == 200, '/blah' + assert self.get(url='/BLAH')['status'] == 404, '/BLAH' def test_routes_match_wildcard_substring_case_sensitive(self): self.route_match({"uri": "*bla*"}) - self.assertEqual(self.get(url='/blah')['status'], 200, '/blah') - self.assertEqual(self.get(url='/BLAH')['status'], 404, '/BLAH') + assert self.get(url='/blah')['status'] == 200, '/blah' + assert self.get(url='/BLAH')['status'] == 404, '/BLAH' def test_routes_match_many_wildcard_substrings_case_sensitive(self): self.route_match({"uri": "*a*B*c*"}) - self.assertEqual(self.get(url='/blah-a-B-c-blah')['status'], 200) - self.assertEqual(self.get(url='/a-B-c')['status'], 200) - self.assertEqual(self.get(url='/aBc')['status'], 200) - self.assertEqual(self.get(url='/aBCaBbc')['status'], 200) - self.assertEqual(self.get(url='/ABc')['status'], 404) + assert self.get(url='/blah-a-B-c-blah')['status'] == 200 + assert self.get(url='/a-B-c')['status'] == 200 + assert self.get(url='/aBc')['status'] == 200 + assert self.get(url='/aBCaBbc')['status'] == 200 + assert self.get(url='/ABc')['status'] == 404 def test_routes_pass_encode(self): def check_pass(path, name): - self.assertIn( - 'success', - self.conf( - { - "listeners": { - "*:7080": {"pass": "applications/" + path} - }, - "applications": { - name: { - "type": "python", - "processes": {"spare": 0}, - "path": self.current_dir + '/python/empty', - "working_directory": self.current_dir - + '/python/empty', - "module": "wsgi", - } - }, - } - ), + assert 'success' in self.conf( + { + "listeners": {"*:7080": {"pass": "applications/" + path}}, + "applications": { + name: { + "type": "python", + "processes": {"spare": 0}, + "path": option.test_dir + '/python/empty', + "working_directory": option.test_dir + + '/python/empty', + "module": "wsgi", + } + }, + } ) - self.assertEqual(self.get()['status'], 200) + assert self.get()['status'] == 200 check_pass("%25", "%") check_pass("blah%2Fblah", "blah/blah") @@ -273,25 +258,20 @@ class TestRouting(TestApplicationProto): check_pass("%20blah%252Fblah%7E", " blah%2Fblah~") def check_pass_error(path, name): - self.assertIn( - 'error', - self.conf( - { - "listeners": { - "*:7080": {"pass": "applications/" + path} - }, - "applications": { - name: { - "type": "python", - "processes": {"spare": 0}, - "path": self.current_dir + '/python/empty', - "working_directory": self.current_dir - + '/python/empty', - "module": "wsgi", - } - }, - } - ), + assert 'error' in self.conf( + { + "listeners": {"*:7080": {"pass": "applications/" + path}}, + "applications": { + name: { + "type": "python", + "processes": {"spare": 0}, + "path": option.test_dir + '/python/empty', + "working_directory": option.test_dir + + '/python/empty', + "module": "wsgi", + } + }, + } ) check_pass_error("%", "%") @@ -305,8 +285,8 @@ class TestRouting(TestApplicationProto): "empty": { "type": "python", "processes": {"spare": 0}, - "path": self.current_dir + '/python/empty', - "working_directory": self.current_dir + "path": option.test_dir + '/python/empty', + "working_directory": option.test_dir + '/python/empty', "module": "wsgi", } @@ -314,179 +294,214 @@ class TestRouting(TestApplicationProto): } ) - self.assertEqual(self.get(port=7081)['status'], 200, 'routes absent') + assert self.get(port=7081)['status'] == 200, 'routes absent' def test_routes_pass_invalid(self): - self.assertIn( - 'error', - self.conf({"pass": "routes/blah"}, 'listeners/*:7080'), - 'routes invalid', - ) + assert 'error' in self.conf( + {"pass": "routes/blah"}, 'listeners/*:7080' + ), 'routes invalid' def test_route_empty(self): - self.assertIn( - 'success', - self.conf( - { - "listeners": {"*:7080": {"pass": "routes/main"}}, - "routes": {"main": []}, - "applications": {}, - } - ), - 'route empty configure', - ) + assert 'success' in self.conf( + { + "listeners": {"*:7080": {"pass": "routes/main"}}, + "routes": {"main": []}, + "applications": {}, + } + ), 'route empty configure' - self.assertEqual(self.get()['status'], 404, 'route empty') + assert self.get()['status'] == 404, 'route empty' def test_routes_route_empty(self): - self.assertIn( - 'success', - self.conf({}, 'listeners'), - 'routes empty listeners configure', - ) + assert 'success' in self.conf( + {}, 'listeners' + ), 'routes empty listeners configure' - self.assertIn( - 'success', self.conf({}, 'routes'), 'routes empty configure' - ) + assert 'success' in self.conf({}, 'routes'), 'routes empty configure' def test_routes_route_match_absent(self): - self.assertIn( - 'success', - self.conf([{"action": {"return": 200}}], 'routes'), - 'route match absent configure', - ) + assert 'success' in self.conf( + [{"action": {"return": 200}}], 'routes' + ), 'route match absent configure' - self.assertEqual(self.get()['status'], 200, 'route match absent') + assert self.get()['status'] == 200, 'route match absent' def test_routes_route_action_absent(self): - self.skip_alerts.append(r'failed to apply new conf') + skip_alert(r'failed to apply new conf') + + assert 'error' in self.conf( + [{"match": {"method": "GET"}}], 'routes' + ), 'route pass absent configure' - self.assertIn( - 'error', - self.conf([{"match": {"method": "GET"}}], 'routes'), - 'route pass absent configure', + def test_routes_route_pass(self): + assert 'success' in self.conf( + { + "applications": { + "app": { + "type": "python", + "processes": {"spare": 0}, + "path": "/app", + "module": "wsgi", + } + }, + "upstreams": { + "one": { + "servers": { + "127.0.0.1:7081": {}, + "127.0.0.1:7082": {}, + }, + }, + "two": { + "servers": { + "127.0.0.1:7081": {}, + "127.0.0.1:7082": {}, + }, + }, + }, + } ) - def test_routes_route_pass_absent(self): - self.assertIn( - 'error', - self.conf([{"match": {"method": "GET"}, "action": {}}], 'routes'), - 'route pass absent configure', + assert 'success' in self.conf( + [{"action": {"pass": "routes"}}], 'routes' + ) + assert 'success' in self.conf( + [{"action": {"pass": "applications/app"}}], 'routes' + ) + assert 'success' in self.conf( + [{"action": {"pass": "upstreams/one"}}], 'routes' ) - def test_routes_action_unique(self): - self.assertIn( - 'success', - self.conf( - { - "listeners": { - "*:7080": {"pass": "routes"}, - "*:7081": {"pass": "applications/app"}, + def test_routes_route_pass_absent(self): + assert 'error' in self.conf( + [{"match": {"method": "GET"}, "action": {}}], 'routes' + ), 'route pass absent configure' + + def test_routes_route_pass_invalid(self): + assert 'success' in self.conf( + { + "applications": { + "app": { + "type": "python", + "processes": {"spare": 0}, + "path": "/app", + "module": "wsgi", + } + }, + "upstreams": { + "one": { + "servers": { + "127.0.0.1:7081": {}, + "127.0.0.1:7082": {}, + }, }, - "routes": [{"action": {"proxy": "http://127.0.0.1:7081"}}], - "applications": { - "app": { - "type": "python", - "processes": {"spare": 0}, - "path": "/app", - "module": "wsgi", - } + "two": { + "servers": { + "127.0.0.1:7081": {}, + "127.0.0.1:7082": {}, + }, }, - } - ), + }, + } ) - self.assertIn( - 'error', - self.conf( - {"proxy": "http://127.0.0.1:7081", "share": self.testdir}, - 'routes/0/action', - ), - 'proxy share', - ) - self.assertIn( - 'error', - self.conf( - { - "proxy": "http://127.0.0.1:7081", - "pass": "applications/app", + assert 'error' in self.conf( + [{"action": {"pass": "blah"}}], 'routes' + ), 'route pass invalid' + assert 'error' in self.conf( + [{"action": {"pass": "routes/blah"}}], 'routes' + ), 'route pass routes invalid' + assert 'error' in self.conf( + [{"action": {"pass": "applications/blah"}}], 'routes' + ), 'route pass applications invalid' + assert 'error' in self.conf( + [{"action": {"pass": "upstreams/blah"}}], 'routes' + ), 'route pass upstreams invalid' + + def test_routes_action_unique(self): + assert 'success' in self.conf( + { + "listeners": { + "*:7080": {"pass": "routes"}, + "*:7081": {"pass": "applications/app"}, }, - 'routes/0/action', - ), - 'proxy pass', - ) - self.assertIn( - 'error', - self.conf( - {"share": self.testdir, "pass": "applications/app"}, - 'routes/0/action', - ), - 'share pass', + "routes": [{"action": {"proxy": "http://127.0.0.1:7081"}}], + "applications": { + "app": { + "type": "python", + "processes": {"spare": 0}, + "path": "/app", + "module": "wsgi", + } + }, + } ) + assert 'error' in self.conf( + {"proxy": "http://127.0.0.1:7081", "share": self.temp_dir}, + 'routes/0/action', + ), 'proxy share' + assert 'error' in self.conf( + {"proxy": "http://127.0.0.1:7081", "pass": "applications/app",}, + 'routes/0/action', + ), 'proxy pass' + assert 'error' in self.conf( + {"share": self.temp_dir, "pass": "applications/app"}, + 'routes/0/action', + ), 'share pass' + def test_routes_rules_two(self): - self.assertIn( - 'success', - self.conf( - [ - {"match": {"method": "GET"}, "action": {"return": 200}}, - {"match": {"method": "POST"}, "action": {"return": 201}}, - ], - 'routes', - ), - 'rules two configure', - ) + assert 'success' in self.conf( + [ + {"match": {"method": "GET"}, "action": {"return": 200}}, + {"match": {"method": "POST"}, "action": {"return": 201}}, + ], + 'routes', + ), 'rules two configure' - self.assertEqual(self.get()['status'], 200, 'rules two match first') - self.assertEqual(self.post()['status'], 201, 'rules two match second') + assert self.get()['status'] == 200, 'rules two match first' + assert self.post()['status'] == 201, 'rules two match second' def test_routes_two(self): - self.assertIn( - 'success', - self.conf( - { - "listeners": {"*:7080": {"pass": "routes/first"}}, - "routes": { - "first": [ - { - "match": {"method": "GET"}, - "action": {"pass": "routes/second"}, - } - ], - "second": [ - { - "match": {"host": "localhost"}, - "action": {"return": 200}, - } - ], - }, - "applications": {}, - } - ), - 'routes two configure', - ) + assert 'success' in self.conf( + { + "listeners": {"*:7080": {"pass": "routes/first"}}, + "routes": { + "first": [ + { + "match": {"method": "GET"}, + "action": {"pass": "routes/second"}, + } + ], + "second": [ + { + "match": {"host": "localhost"}, + "action": {"return": 200}, + } + ], + }, + "applications": {}, + } + ), 'routes two configure' - self.assertEqual(self.get()['status'], 200, 'routes two') + assert self.get()['status'] == 200, 'routes two' def test_routes_match_host_positive(self): self.route_match({"host": "localhost"}) - self.assertEqual(self.get()['status'], 200, 'localhost') + assert self.get()['status'] == 200, 'localhost' self.host('localhost.', 200) self.host('localhost.', 200) self.host('.localhost', 404) self.host('www.localhost', 404) self.host('localhost1', 404) - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_routes_match_host_absent(self): self.route_match({"host": "localhost"}) - self.assertEqual( - self.get(headers={'Connection': 'close'})['status'], - 400, - 'match host absent', - ) + assert ( + self.get(headers={'Connection': 'close'})['status'] == 400 + ), 'match host absent' def test_routes_match_host_ipv4(self): self.route_match({"host": "127.0.0.1"}) @@ -503,13 +518,13 @@ class TestRouting(TestApplicationProto): def test_routes_match_host_positive_many(self): self.route_match({"host": ["localhost", "example.com"]}) - self.assertEqual(self.get()['status'], 200, 'localhost') + assert self.get()['status'] == 200, 'localhost' self.host('example.com', 200) def test_routes_match_host_positive_and_negative(self): self.route_match({"host": ["*example.com", "!www.example.com"]}) - self.assertEqual(self.get()['status'], 404, 'localhost') + assert self.get()['status'] == 404, 'localhost' self.host('example.com', 200) self.host('www.example.com', 404) self.host('!www.example.com', 200) @@ -535,380 +550,278 @@ class TestRouting(TestApplicationProto): self.route_match({"host": ""}) self.host('', 200) - self.assertEqual( - self.get(http_10=True, headers={})['status'], - 200, - 'match host empty 2', - ) - self.assertEqual(self.get()['status'], 404, 'match host empty 3') + assert ( + self.get(http_10=True, headers={})['status'] == 200 + ), 'match host empty 2' + assert self.get()['status'] == 404, 'match host empty 3' def test_routes_match_uri_positive(self): self.route_match({"uri": ["/blah", "/slash/"]}) - self.assertEqual(self.get()['status'], 404, '/') - self.assertEqual(self.get(url='/blah')['status'], 200, '/blah') - self.assertEqual(self.get(url='/blah#foo')['status'], 200, '/blah#foo') - self.assertEqual(self.get(url='/blah?var')['status'], 200, '/blah?var') - self.assertEqual(self.get(url='//blah')['status'], 200, '//blah') - self.assertEqual( - self.get(url='/slash/foo/../')['status'], 200, 'relative' - ) - self.assertEqual(self.get(url='/slash/./')['status'], 200, '/slash/./') - self.assertEqual( - self.get(url='/slash//.//')['status'], 200, 'adjacent slashes' - ) - self.assertEqual(self.get(url='/%')['status'], 400, 'percent') - self.assertEqual(self.get(url='/%1')['status'], 400, 'percent digit') - self.assertEqual(self.get(url='/%A')['status'], 400, 'percent letter') - self.assertEqual( - self.get(url='/slash/.?args')['status'], 200, 'dot args' - ) - self.assertEqual( - self.get(url='/slash/.#frag')['status'], 200, 'dot frag' - ) - self.assertEqual( - self.get(url='/slash/foo/..?args')['status'], - 200, - 'dot dot args', - ) - self.assertEqual( - self.get(url='/slash/foo/..#frag')['status'], - 200, - 'dot dot frag', - ) - self.assertEqual( - self.get(url='/slash/.')['status'], 200, 'trailing dot' - ) - self.assertEqual( - self.get(url='/slash/foo/..')['status'], - 200, - 'trailing dot dot', - ) + assert self.get()['status'] == 404, '/' + assert self.get(url='/blah')['status'] == 200, '/blah' + assert self.get(url='/blah#foo')['status'] == 200, '/blah#foo' + assert self.get(url='/blah?var')['status'] == 200, '/blah?var' + assert self.get(url='//blah')['status'] == 200, '//blah' + assert self.get(url='/slash/foo/../')['status'] == 200, 'relative' + assert self.get(url='/slash/./')['status'] == 200, '/slash/./' + assert self.get(url='/slash//.//')['status'] == 200, 'adjacent slashes' + assert self.get(url='/%')['status'] == 400, 'percent' + assert self.get(url='/%1')['status'] == 400, 'percent digit' + assert self.get(url='/%A')['status'] == 400, 'percent letter' + assert self.get(url='/slash/.?args')['status'] == 200, 'dot args' + assert self.get(url='/slash/.#frag')['status'] == 200, 'dot frag' + assert ( + self.get(url='/slash/foo/..?args')['status'] == 200 + ), 'dot dot args' + assert ( + self.get(url='/slash/foo/..#frag')['status'] == 200 + ), 'dot dot frag' + assert self.get(url='/slash/.')['status'] == 200, 'trailing dot' + assert ( + self.get(url='/slash/foo/..')['status'] == 200 + ), 'trailing dot dot' def test_routes_match_uri_case_sensitive(self): self.route_match({"uri": "/BLAH"}) - self.assertEqual(self.get(url='/blah')['status'], 404, '/blah') - self.assertEqual(self.get(url='/BlaH')['status'], 404, '/BlaH') - self.assertEqual(self.get(url='/BLAH')['status'], 200, '/BLAH') + assert self.get(url='/blah')['status'] == 404, '/blah' + assert self.get(url='/BlaH')['status'] == 404, '/BlaH' + assert self.get(url='/BLAH')['status'] == 200, '/BLAH' def test_routes_match_uri_normalize(self): self.route_match({"uri": "/blah"}) - self.assertEqual( - self.get(url='/%62%6c%61%68')['status'], 200, 'normalize' - ) + assert self.get(url='/%62%6c%61%68')['status'] == 200, 'normalize' def test_routes_match_empty_array(self): self.route_match({"uri": []}) - self.assertEqual(self.get(url='/blah')['status'], 200, 'empty array') + assert self.get(url='/blah')['status'] == 200, 'empty array' def test_routes_reconfigure(self): - self.assertIn('success', self.conf([], 'routes'), 'redefine') - self.assertEqual(self.get()['status'], 404, 'redefine request') + assert 'success' in self.conf([], 'routes'), 'redefine' + assert self.get()['status'] == 404, 'redefine request' - self.assertIn( - 'success', - self.conf([{"action": {"return": 200}}], 'routes'), - 'redefine 2', - ) - self.assertEqual(self.get()['status'], 200, 'redefine request 2') + assert 'success' in self.conf( + [{"action": {"return": 200}}], 'routes' + ), 'redefine 2' + assert self.get()['status'] == 200, 'redefine request 2' - self.assertIn('success', self.conf([], 'routes'), 'redefine 3') - self.assertEqual(self.get()['status'], 404, 'redefine request 3') + assert 'success' in self.conf([], 'routes'), 'redefine 3' + assert self.get()['status'] == 404, 'redefine request 3' - self.assertIn( - 'success', - self.conf( - { - "listeners": {"*:7080": {"pass": "routes/main"}}, - "routes": {"main": [{"action": {"return": 200}}]}, - "applications": {}, - } - ), - 'redefine 4', - ) - self.assertEqual(self.get()['status'], 200, 'redefine request 4') + assert 'success' in self.conf( + { + "listeners": {"*:7080": {"pass": "routes/main"}}, + "routes": {"main": [{"action": {"return": 200}}]}, + "applications": {}, + } + ), 'redefine 4' + assert self.get()['status'] == 200, 'redefine request 4' - self.assertIn( - 'success', self.conf_delete('routes/main/0'), 'redefine 5' - ) - self.assertEqual(self.get()['status'], 404, 'redefine request 5') + assert 'success' in self.conf_delete('routes/main/0'), 'redefine 5' + assert self.get()['status'] == 404, 'redefine request 5' - self.assertIn( - 'success', - self.conf_post({"action": {"return": 200}}, 'routes/main'), - 'redefine 6', - ) - self.assertEqual(self.get()['status'], 200, 'redefine request 6') + assert 'success' in self.conf_post( + {"action": {"return": 200}}, 'routes/main' + ), 'redefine 6' + assert self.get()['status'] == 200, 'redefine request 6' - self.assertIn( - 'error', - self.conf({"action": {"return": 200}}, 'routes/main/2'), - 'redefine 7', - ) - self.assertIn( - 'success', - self.conf({"action": {"return": 201}}, 'routes/main/1'), - 'redefine 8', - ) + assert 'error' in self.conf( + {"action": {"return": 200}}, 'routes/main/2' + ), 'redefine 7' + assert 'success' in self.conf( + {"action": {"return": 201}}, 'routes/main/1' + ), 'redefine 8' - self.assertEqual( - len(self.conf_get('routes/main')), 2, 'redefine conf 8' - ) - self.assertEqual(self.get()['status'], 200, 'redefine request 8') + assert len(self.conf_get('routes/main')) == 2, 'redefine conf 8' + assert self.get()['status'] == 200, 'redefine request 8' def test_routes_edit(self): self.route_match({"method": "GET"}) - self.assertEqual(self.get()['status'], 200, 'routes edit GET') - self.assertEqual(self.post()['status'], 404, 'routes edit POST') - - self.assertIn( - 'success', - self.conf_post( - {"match": {"method": "POST"}, "action": {"return": 200}}, - 'routes', - ), - 'routes edit configure 2', - ) - self.assertEqual( - 'GET', - self.conf_get('routes/0/match/method'), - 'routes edit configure 2 check', - ) - self.assertEqual( - 'POST', - self.conf_get('routes/1/match/method'), - 'routes edit configure 2 check 2', - ) - - self.assertEqual(self.get()['status'], 200, 'routes edit GET 2') - self.assertEqual(self.post()['status'], 200, 'routes edit POST 2') - - self.assertIn( - 'success', self.conf_delete('routes/0'), 'routes edit configure 3', - ) - - self.assertEqual(self.get()['status'], 404, 'routes edit GET 3') - self.assertEqual(self.post()['status'], 200, 'routes edit POST 3') - - self.assertIn( - 'error', - self.conf_delete('routes/1'), - 'routes edit configure invalid', - ) - self.assertIn( - 'error', - self.conf_delete('routes/-1'), - 'routes edit configure invalid 2', - ) - self.assertIn( - 'error', - self.conf_delete('routes/blah'), - 'routes edit configure invalid 3', - ) - - self.assertEqual(self.get()['status'], 404, 'routes edit GET 4') - self.assertEqual(self.post()['status'], 200, 'routes edit POST 4') - - self.assertIn( - 'success', self.conf_delete('routes/0'), 'routes edit configure 5', - ) - - self.assertEqual(self.get()['status'], 404, 'routes edit GET 5') - self.assertEqual(self.post()['status'], 404, 'routes edit POST 5') + assert self.get()['status'] == 200, 'routes edit GET' + assert self.post()['status'] == 404, 'routes edit POST' + + assert 'success' in self.conf_post( + {"match": {"method": "POST"}, "action": {"return": 200}}, 'routes', + ), 'routes edit configure 2' + assert 'GET' == self.conf_get( + 'routes/0/match/method' + ), 'routes edit configure 2 check' + assert 'POST' == self.conf_get( + 'routes/1/match/method' + ), 'routes edit configure 2 check 2' + + assert self.get()['status'] == 200, 'routes edit GET 2' + assert self.post()['status'] == 200, 'routes edit POST 2' + + assert 'success' in self.conf_delete( + 'routes/0' + ), 'routes edit configure 3' + + assert self.get()['status'] == 404, 'routes edit GET 3' + assert self.post()['status'] == 200, 'routes edit POST 3' + + assert 'error' in self.conf_delete( + 'routes/1' + ), 'routes edit configure invalid' + assert 'error' in self.conf_delete( + 'routes/-1' + ), 'routes edit configure invalid 2' + assert 'error' in self.conf_delete( + 'routes/blah' + ), 'routes edit configure invalid 3' + + assert self.get()['status'] == 404, 'routes edit GET 4' + assert self.post()['status'] == 200, 'routes edit POST 4' + + assert 'success' in self.conf_delete( + 'routes/0' + ), 'routes edit configure 5' + + assert self.get()['status'] == 404, 'routes edit GET 5' + assert self.post()['status'] == 404, 'routes edit POST 5' + + assert 'success' in self.conf_post( + {"match": {"method": "POST"}, "action": {"return": 200}}, 'routes', + ), 'routes edit configure 6' + + assert self.get()['status'] == 404, 'routes edit GET 6' + assert self.post()['status'] == 200, 'routes edit POST 6' + + assert 'success' in self.conf( + { + "listeners": {"*:7080": {"pass": "routes/main"}}, + "routes": {"main": [{"action": {"return": 200}}]}, + "applications": {}, + } + ), 'route edit configure 7' - self.assertIn( - 'success', - self.conf_post( - {"match": {"method": "POST"}, "action": {"return": 200}}, - 'routes', - ), - 'routes edit configure 6', - ) + assert 'error' in self.conf_delete( + 'routes/0' + ), 'routes edit configure invalid 4' + assert 'error' in self.conf_delete( + 'routes/main' + ), 'routes edit configure invalid 5' - self.assertEqual(self.get()['status'], 404, 'routes edit GET 6') - self.assertEqual(self.post()['status'], 200, 'routes edit POST 6') + assert self.get()['status'] == 200, 'routes edit GET 7' - self.assertIn( - 'success', - self.conf( - { - "listeners": {"*:7080": {"pass": "routes/main"}}, - "routes": {"main": [{"action": {"return": 200}}]}, - "applications": {}, - } - ), - 'route edit configure 7', - ) - - self.assertIn( - 'error', - self.conf_delete('routes/0'), - 'routes edit configure invalid 4', - ) - self.assertIn( - 'error', - self.conf_delete('routes/main'), - 'routes edit configure invalid 5', - ) - - self.assertEqual(self.get()['status'], 200, 'routes edit GET 7') - - self.assertIn( - 'success', - self.conf_delete('listeners/*:7080'), - 'route edit configure 8', - ) - self.assertIn( - 'success', - self.conf_delete('routes/main'), - 'route edit configure 9', - ) + assert 'success' in self.conf_delete( + 'listeners/*:7080' + ), 'route edit configure 8' + assert 'success' in self.conf_delete( + 'routes/main' + ), 'route edit configure 9' def test_match_edit(self): - self.skip_alerts.append(r'failed to apply new conf') + skip_alert(r'failed to apply new conf') self.route_match({"method": ["GET", "POST"]}) - self.assertEqual(self.get()['status'], 200, 'match edit GET') - self.assertEqual(self.post()['status'], 200, 'match edit POST') - self.assertEqual(self.put()['status'], 404, 'match edit PUT') - - self.assertIn( - 'success', - self.conf_post('\"PUT\"', 'routes/0/match/method'), - 'match edit configure 2', - ) - self.assertListEqual( - ['GET', 'POST', 'PUT'], - self.conf_get('routes/0/match/method'), - 'match edit configure 2 check', - ) - - self.assertEqual(self.get()['status'], 200, 'match edit GET 2') - self.assertEqual(self.post()['status'], 200, 'match edit POST 2') - self.assertEqual(self.put()['status'], 200, 'match edit PUT 2') - - self.assertIn( - 'success', - self.conf_delete('routes/0/match/method/1'), - 'match edit configure 3', - ) - self.assertListEqual( - ['GET', 'PUT'], - self.conf_get('routes/0/match/method'), - 'match edit configure 3 check', - ) - - self.assertEqual(self.get()['status'], 200, 'match edit GET 3') - self.assertEqual(self.post()['status'], 404, 'match edit POST 3') - self.assertEqual(self.put()['status'], 200, 'match edit PUT 3') - - self.assertIn( - 'success', - self.conf_delete('routes/0/match/method/1'), - 'match edit configure 4', - ) - self.assertListEqual( - ['GET'], - self.conf_get('routes/0/match/method'), - 'match edit configure 4 check', - ) - - self.assertEqual(self.get()['status'], 200, 'match edit GET 4') - self.assertEqual(self.post()['status'], 404, 'match edit POST 4') - self.assertEqual(self.put()['status'], 404, 'match edit PUT 4') - - self.assertIn( - 'error', - self.conf_delete('routes/0/match/method/1'), - 'match edit configure invalid', - ) - self.assertIn( - 'error', - self.conf_delete('routes/0/match/method/-1'), - 'match edit configure invalid 2', - ) - self.assertIn( - 'error', - self.conf_delete('routes/0/match/method/blah'), - 'match edit configure invalid 3', - ) - self.assertListEqual( - ['GET'], - self.conf_get('routes/0/match/method'), - 'match edit configure 5 check', - ) - - self.assertEqual(self.get()['status'], 200, 'match edit GET 5') - self.assertEqual(self.post()['status'], 404, 'match edit POST 5') - self.assertEqual(self.put()['status'], 404, 'match edit PUT 5') - - self.assertIn( - 'success', - self.conf_delete('routes/0/match/method/0'), - 'match edit configure 6', - ) - self.assertListEqual( - [], - self.conf_get('routes/0/match/method'), - 'match edit configure 6 check', - ) - - self.assertEqual(self.get()['status'], 200, 'match edit GET 6') - self.assertEqual(self.post()['status'], 200, 'match edit POST 6') - self.assertEqual(self.put()['status'], 200, 'match edit PUT 6') - - self.assertIn( - 'success', - self.conf('"GET"', 'routes/0/match/method'), - 'match edit configure 7', - ) - - self.assertEqual(self.get()['status'], 200, 'match edit GET 7') - self.assertEqual(self.post()['status'], 404, 'match edit POST 7') - self.assertEqual(self.put()['status'], 404, 'match edit PUT 7') - - self.assertIn( - 'error', - self.conf_delete('routes/0/match/method/0'), - 'match edit configure invalid 5', - ) - self.assertIn( - 'error', - self.conf({}, 'routes/0/action'), - 'match edit configure invalid 6', - ) - - self.assertIn( - 'success', - self.conf({}, 'routes/0/match'), - 'match edit configure 8', - ) - - self.assertEqual(self.get()['status'], 200, 'match edit GET 8') + assert self.get()['status'] == 200, 'match edit GET' + assert self.post()['status'] == 200, 'match edit POST' + assert self.put()['status'] == 404, 'match edit PUT' + + assert 'success' in self.conf_post( + '\"PUT\"', 'routes/0/match/method' + ), 'match edit configure 2' + assert ['GET', 'POST', 'PUT'] == self.conf_get( + 'routes/0/match/method' + ), 'match edit configure 2 check' + + assert self.get()['status'] == 200, 'match edit GET 2' + assert self.post()['status'] == 200, 'match edit POST 2' + assert self.put()['status'] == 200, 'match edit PUT 2' + + assert 'success' in self.conf_delete( + 'routes/0/match/method/1' + ), 'match edit configure 3' + assert ['GET', 'PUT'] == self.conf_get( + 'routes/0/match/method' + ), 'match edit configure 3 check' + + assert self.get()['status'] == 200, 'match edit GET 3' + assert self.post()['status'] == 404, 'match edit POST 3' + assert self.put()['status'] == 200, 'match edit PUT 3' + + assert 'success' in self.conf_delete( + 'routes/0/match/method/1' + ), 'match edit configure 4' + assert ['GET'] == self.conf_get( + 'routes/0/match/method' + ), 'match edit configure 4 check' + + assert self.get()['status'] == 200, 'match edit GET 4' + assert self.post()['status'] == 404, 'match edit POST 4' + assert self.put()['status'] == 404, 'match edit PUT 4' + + assert 'error' in self.conf_delete( + 'routes/0/match/method/1' + ), 'match edit configure invalid' + assert 'error' in self.conf_delete( + 'routes/0/match/method/-1' + ), 'match edit configure invalid 2' + assert 'error' in self.conf_delete( + 'routes/0/match/method/blah' + ), 'match edit configure invalid 3' + assert ['GET'] == self.conf_get( + 'routes/0/match/method' + ), 'match edit configure 5 check' + + assert self.get()['status'] == 200, 'match edit GET 5' + assert self.post()['status'] == 404, 'match edit POST 5' + assert self.put()['status'] == 404, 'match edit PUT 5' + + assert 'success' in self.conf_delete( + 'routes/0/match/method/0' + ), 'match edit configure 6' + assert [] == self.conf_get( + 'routes/0/match/method' + ), 'match edit configure 6 check' + + assert self.get()['status'] == 200, 'match edit GET 6' + assert self.post()['status'] == 200, 'match edit POST 6' + assert self.put()['status'] == 200, 'match edit PUT 6' + + assert 'success' in self.conf( + '"GET"', 'routes/0/match/method' + ), 'match edit configure 7' + + assert self.get()['status'] == 200, 'match edit GET 7' + assert self.post()['status'] == 404, 'match edit POST 7' + assert self.put()['status'] == 404, 'match edit PUT 7' + + assert 'error' in self.conf_delete( + 'routes/0/match/method/0' + ), 'match edit configure invalid 5' + assert 'error' in self.conf( + {}, 'routes/0/action' + ), 'match edit configure invalid 6' + + assert 'success' in self.conf( + {}, 'routes/0/match' + ), 'match edit configure 8' + + assert self.get()['status'] == 200, 'match edit GET 8' def test_routes_match_rules(self): self.route_match({"method": "GET", "host": "localhost", "uri": "/"}) - self.assertEqual(self.get()['status'], 200, 'routes match rules') + assert self.get()['status'] == 200, 'routes match rules' def test_routes_loop(self): - self.assertIn( - 'success', - self.route({"match": {"uri": "/"}, "action": {"pass": "routes"}}), - 'routes loop configure', - ) + assert 'success' in self.route( + {"match": {"uri": "/"}, "action": {"pass": "routes"}} + ), 'routes loop configure' - self.assertEqual(self.get()['status'], 500, 'routes loop') + assert self.get()['status'] == 500, 'routes loop' def test_routes_match_headers(self): self.route_match({"headers": {"host": "localhost"}}) - self.assertEqual(self.get()['status'], 200, 'match headers') + assert self.get()['status'] == 200, 'match headers' self.host('Localhost', 200) self.host('localhost.com', 404) self.host('llocalhost', 404) @@ -917,134 +830,122 @@ class TestRouting(TestApplicationProto): def test_routes_match_headers_multiple(self): self.route_match({"headers": {"host": "localhost", "x-blah": "test"}}) - self.assertEqual(self.get()['status'], 404, 'match headers multiple') - self.assertEqual( + assert self.get()['status'] == 404, 'match headers multiple' + assert ( self.get( headers={ "Host": "localhost", "X-blah": "test", "Connection": "close", } - )['status'], - 200, - 'match headers multiple 2', - ) + )['status'] + == 200 + ), 'match headers multiple 2' - self.assertEqual( + assert ( self.get( headers={ "Host": "localhost", "X-blah": "", "Connection": "close", } - )['status'], - 404, - 'match headers multiple 3', - ) + )['status'] + == 404 + ), 'match headers multiple 3' def test_routes_match_headers_multiple_values(self): self.route_match({"headers": {"x-blah": "test"}}) - self.assertEqual( + assert ( self.get( headers={ "Host": "localhost", "X-blah": ["test", "test", "test"], "Connection": "close", } - )['status'], - 200, - 'match headers multiple values', - ) - self.assertEqual( + )['status'] + == 200 + ), 'match headers multiple values' + assert ( self.get( headers={ "Host": "localhost", "X-blah": ["test", "blah", "test"], "Connection": "close", } - )['status'], - 404, - 'match headers multiple values 2', - ) - self.assertEqual( + )['status'] + == 404 + ), 'match headers multiple values 2' + assert ( self.get( headers={ "Host": "localhost", "X-blah": ["test", "", "test"], "Connection": "close", } - )['status'], - 404, - 'match headers multiple values 3', - ) + )['status'] + == 404 + ), 'match headers multiple values 3' def test_routes_match_headers_multiple_rules(self): self.route_match({"headers": {"x-blah": ["test", "blah"]}}) - self.assertEqual( - self.get()['status'], 404, 'match headers multiple rules' - ) - self.assertEqual( + assert self.get()['status'] == 404, 'match headers multiple rules' + assert ( self.get( headers={ "Host": "localhost", "X-blah": "test", "Connection": "close", } - )['status'], - 200, - 'match headers multiple rules 2', - ) - self.assertEqual( + )['status'] + == 200 + ), 'match headers multiple rules 2' + assert ( self.get( headers={ "Host": "localhost", "X-blah": "blah", "Connection": "close", } - )['status'], - 200, - 'match headers multiple rules 3', - ) - self.assertEqual( + )['status'] + == 200 + ), 'match headers multiple rules 3' + assert ( self.get( headers={ "Host": "localhost", "X-blah": ["test", "blah", "test"], "Connection": "close", } - )['status'], - 200, - 'match headers multiple rules 4', - ) + )['status'] + == 200 + ), 'match headers multiple rules 4' - self.assertEqual( + assert ( self.get( headers={ "Host": "localhost", "X-blah": ["blah", ""], "Connection": "close", } - )['status'], - 404, - 'match headers multiple rules 5', - ) + )['status'] + == 404 + ), 'match headers multiple rules 5' def test_routes_match_headers_case_insensitive(self): self.route_match({"headers": {"X-BLAH": "TEST"}}) - self.assertEqual( + assert ( self.get( headers={ "Host": "localhost", "x-blah": "test", "Connection": "close", } - )['status'], - 200, - 'match headers case insensitive', - ) + )['status'] + == 200 + ), 'match headers case insensitive' def test_routes_match_headers_invalid(self): self.route_match_invalid({"headers": ["blah"]}) @@ -1054,29 +955,30 @@ class TestRouting(TestApplicationProto): def test_routes_match_headers_empty_rule(self): self.route_match({"headers": {"host": ""}}) - self.assertEqual(self.get()['status'], 404, 'localhost') + assert self.get()['status'] == 404, 'localhost' self.host('', 200) def test_routes_match_headers_empty(self): self.route_match({"headers": {}}) - self.assertEqual(self.get()['status'], 200, 'empty') + assert self.get()['status'] == 200, 'empty' self.route_match({"headers": []}) - self.assertEqual(self.get()['status'], 200, 'empty 2') + assert self.get()['status'] == 200, 'empty 2' def test_routes_match_headers_rule_array_empty(self): self.route_match({"headers": {"blah": []}}) - self.assertEqual(self.get()['status'], 404, 'array empty') - self.assertEqual( + assert self.get()['status'] == 404, 'array empty' + assert ( self.get( headers={ "Host": "localhost", "blah": "foo", "Connection": "close", } - )['status'], 200, 'match headers rule array empty 2' - ) + )['status'] + == 200 + ), 'match headers rule array empty 2' def test_routes_match_headers_array(self): self.route_match( @@ -1090,52 +992,48 @@ class TestRouting(TestApplicationProto): } ) - self.assertEqual(self.get()['status'], 404, 'match headers array') - self.assertEqual( + assert self.get()['status'] == 404, 'match headers array' + assert ( self.get( headers={ "Host": "localhost", "x-header1": "foo123", "Connection": "close", } - )['status'], - 200, - 'match headers array 2', - ) - self.assertEqual( + )['status'] + == 200 + ), 'match headers array 2' + assert ( self.get( headers={ "Host": "localhost", "x-header2": "bar", "Connection": "close", } - )['status'], - 200, - 'match headers array 3', - ) - self.assertEqual( + )['status'] + == 200 + ), 'match headers array 3' + assert ( self.get( headers={ "Host": "localhost", "x-header3": "bar", "Connection": "close", } - )['status'], - 200, - 'match headers array 4', - ) - self.assertEqual( + )['status'] + == 200 + ), 'match headers array 4' + assert ( self.get( headers={ "Host": "localhost", "x-header1": "bar", "Connection": "close", } - )['status'], - 404, - 'match headers array 5', - ) - self.assertEqual( + )['status'] + == 404 + ), 'match headers array 5' + assert ( self.get( headers={ "Host": "localhost", @@ -1143,49 +1041,44 @@ class TestRouting(TestApplicationProto): "x-header4": "foo", "Connection": "close", } - )['status'], - 200, - 'match headers array 6', - ) + )['status'] + == 200 + ), 'match headers array 6' - self.assertIn( - 'success', - self.conf_delete('routes/0/match/headers/1'), - 'match headers array configure 2', - ) + assert 'success' in self.conf_delete( + 'routes/0/match/headers/1' + ), 'match headers array configure 2' - self.assertEqual( + assert ( self.get( headers={ "Host": "localhost", "x-header2": "bar", "Connection": "close", } - )['status'], - 404, - 'match headers array 7', - ) - self.assertEqual( + )['status'] + == 404 + ), 'match headers array 7' + assert ( self.get( headers={ "Host": "localhost", "x-header3": "foo", "Connection": "close", } - )['status'], - 200, - 'match headers array 8', - ) + )['status'] + == 200 + ), 'match headers array 8' def test_routes_match_arguments(self): self.route_match({"arguments": {"foo": "bar"}}) - self.assertEqual(self.get()['status'], 404, 'args') - self.assertEqual(self.get(url='/?foo=bar')['status'], 200, 'args 2') - self.assertEqual(self.get(url='/?foo=bar1')['status'], 404, 'args 3') - self.assertEqual(self.get(url='/?1foo=bar')['status'], 404, 'args 4') - self.assertEqual(self.get(url='/?Foo=bar')['status'], 404, 'case') - self.assertEqual(self.get(url='/?foo=Bar')['status'], 404, 'case 2') + assert self.get()['status'] == 404, 'args' + assert self.get(url='/?foo=bar')['status'] == 200, 'args 2' + assert self.get(url='/?foo=bar1')['status'] == 404, 'args 3' + assert self.get(url='/?1foo=bar')['status'] == 404, 'args 4' + assert self.get(url='/?Foo=bar')['status'] == 404, 'case' + assert self.get(url='/?foo=Bar')['status'] == 404, 'case 2' def test_routes_match_arguments_chars(self): chars = ( @@ -1195,15 +1088,30 @@ class TestRouting(TestApplicationProto): chars_enc = "" for h1 in ["2", "3", "4", "5", "6", "7"]: - for h2 in ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", - "B", "C", "D", "E", "F", + for h2 in [ + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "A", + "B", + "C", + "D", + "E", + "F", ]: chars_enc += "%" + h1 + h2 chars_enc = chars_enc[:-3] def check_args(args, query): self.route_match({"arguments": args}) - self.assertEqual(self.get(url='/?' + query)['status'], 200) + assert self.get(url='/?' + query)['status'] == 200 check_args({chars: chars}, chars + '=' + chars) check_args({chars: chars}, chars + '=' + chars_enc) @@ -1216,175 +1124,167 @@ class TestRouting(TestApplicationProto): def test_routes_match_arguments_empty(self): self.route_match({"arguments": {}}) - self.assertEqual(self.get()['status'], 200, 'arguments empty') + assert self.get()['status'] == 200, 'arguments empty' self.route_match({"arguments": []}) - self.assertEqual(self.get()['status'], 200, 'arguments empty 2') + assert self.get()['status'] == 200, 'arguments empty 2' def test_routes_match_arguments_space(self): self.route_match({"arguments": {"+fo o%20": "%20b+a r"}}) - self.assertEqual(self.get(url='/? fo o = b a r&')['status'], 200) - self.assertEqual(self.get(url='/?+fo+o+=+b+a+r&')['status'], 200) - self.assertEqual( - self.get(url='/?%20fo%20o%20=%20b%20a%20r&')['status'], 200 - ) + assert self.get(url='/? fo o = b a r&')['status'] == 200 + assert self.get(url='/?+fo+o+=+b+a+r&')['status'] == 200 + assert self.get(url='/?%20fo%20o%20=%20b%20a%20r&')['status'] == 200 self.route_match({"arguments": {"%20foo": " bar"}}) - self.assertEqual(self.get(url='/? foo= bar')['status'], 200) - self.assertEqual(self.get(url='/?+foo=+bar')['status'], 200) - self.assertEqual(self.get(url='/?%20foo=%20bar')['status'], 200) - self.assertEqual(self.get(url='/?+foo= bar')['status'], 200) - self.assertEqual(self.get(url='/?%20foo=+bar')['status'], 200) + assert self.get(url='/? foo= bar')['status'] == 200 + assert self.get(url='/?+foo=+bar')['status'] == 200 + assert self.get(url='/?%20foo=%20bar')['status'] == 200 + assert self.get(url='/?+foo= bar')['status'] == 200 + assert self.get(url='/?%20foo=+bar')['status'] == 200 def test_routes_match_arguments_equal(self): self.route_match({"arguments": {"=": "="}}) - self.assertEqual(self.get(url='/?%3D=%3D')['status'], 200) - self.assertEqual(self.get(url='/?%3D==')['status'], 200) - self.assertEqual(self.get(url='/?===')['status'], 404) - self.assertEqual(self.get(url='/?%3D%3D%3D')['status'], 404) - self.assertEqual(self.get(url='/?==%3D')['status'], 404) + assert self.get(url='/?%3D=%3D')['status'] == 200 + assert self.get(url='/?%3D==')['status'] == 200 + assert self.get(url='/?===')['status'] == 404 + assert self.get(url='/?%3D%3D%3D')['status'] == 404 + assert self.get(url='/?==%3D')['status'] == 404 def test_routes_match_arguments_enc(self): self.route_match({"arguments": {"Ю": "н"}}) - self.assertEqual(self.get(url='/?%D0%AE=%D0%BD')['status'], 200) - self.assertEqual(self.get(url='/?%d0%ae=%d0%Bd')['status'], 200) + assert self.get(url='/?%D0%AE=%D0%BD')['status'] == 200 + assert self.get(url='/?%d0%ae=%d0%Bd')['status'] == 200 def test_routes_match_arguments_hash(self): self.route_match({"arguments": {"#": "#"}}) - self.assertEqual(self.get(url='/?%23=%23')['status'], 200) - self.assertEqual(self.get(url='/?%23=%23#')['status'], 200) - self.assertEqual(self.get(url='/?#=#')['status'], 404) - self.assertEqual(self.get(url='/?%23=#')['status'], 404) + assert self.get(url='/?%23=%23')['status'] == 200 + assert self.get(url='/?%23=%23#')['status'] == 200 + assert self.get(url='/?#=#')['status'] == 404 + assert self.get(url='/?%23=#')['status'] == 404 def test_routes_match_arguments_wildcard(self): self.route_match({"arguments": {"foo": "*"}}) - self.assertEqual(self.get(url='/?foo')['status'], 200) - self.assertEqual(self.get(url='/?foo=')['status'], 200) - self.assertEqual(self.get(url='/?foo=blah')['status'], 200) - self.assertEqual(self.get(url='/?blah=foo')['status'], 404) + assert self.get(url='/?foo')['status'] == 200 + assert self.get(url='/?foo=')['status'] == 200 + assert self.get(url='/?foo=blah')['status'] == 200 + assert self.get(url='/?blah=foo')['status'] == 404 self.route_match({"arguments": {"foo": "%25*"}}) - self.assertEqual(self.get(url='/?foo=%xx')['status'], 200) + assert self.get(url='/?foo=%xx')['status'] == 200 self.route_match({"arguments": {"foo": "%2A*"}}) - self.assertEqual(self.get(url='/?foo=*xx')['status'], 200) - self.assertEqual(self.get(url='/?foo=xx')['status'], 404) + assert self.get(url='/?foo=*xx')['status'] == 200 + assert self.get(url='/?foo=xx')['status'] == 404 self.route_match({"arguments": {"foo": "*%2A"}}) - self.assertEqual(self.get(url='/?foo=xx*')['status'], 200) - self.assertEqual(self.get(url='/?foo=xx*x')['status'], 404) + assert self.get(url='/?foo=xx*')['status'] == 200 + assert self.get(url='/?foo=xx*x')['status'] == 404 self.route_match({"arguments": {"foo": "1*2"}}) - self.assertEqual(self.get(url='/?foo=12')['status'], 200) - self.assertEqual(self.get(url='/?foo=1blah2')['status'], 200) - self.assertEqual(self.get(url='/?foo=1%2A2')['status'], 200) - self.assertEqual(self.get(url='/?foo=x12')['status'], 404) + assert self.get(url='/?foo=12')['status'] == 200 + assert self.get(url='/?foo=1blah2')['status'] == 200 + assert self.get(url='/?foo=1%2A2')['status'] == 200 + assert self.get(url='/?foo=x12')['status'] == 404 self.route_match({"arguments": {"foo": "bar*", "%25": "%25"}}) - self.assertEqual(self.get(url='/?foo=barxx&%=%')['status'], 200) - self.assertEqual(self.get(url='/?foo=barxx&x%=%')['status'], 404) + assert self.get(url='/?foo=barxx&%=%')['status'] == 200 + assert self.get(url='/?foo=barxx&x%=%')['status'] == 404 def test_routes_match_arguments_negative(self): + self.route_match({"arguments": {"foo": "!"}}) + assert self.get(url='/?bar')['status'] == 404 + assert self.get(url='/?foo')['status'] == 404 + assert self.get(url='/?foo=')['status'] == 404 + assert self.get(url='/?foo=%25')['status'] == 200 + + self.route_match({"arguments": {"foo": "!*"}}) + assert self.get(url='/?bar')['status'] == 404 + assert self.get(url='/?foo')['status'] == 404 + assert self.get(url='/?foo=')['status'] == 404 + assert self.get(url='/?foo=blah')['status'] == 404 + self.route_match({"arguments": {"foo": "!%25"}}) - self.assertEqual(self.get(url='/?foo=blah')['status'], 200) - self.assertEqual(self.get(url='/?foo=%')['status'], 404) + assert self.get(url='/?foo=blah')['status'] == 200 + assert self.get(url='/?foo=%')['status'] == 404 self.route_match({"arguments": {"foo": "%21blah"}}) - self.assertEqual(self.get(url='/?foo=%21blah')['status'], 200) - self.assertEqual(self.get(url='/?foo=!blah')['status'], 200) - self.assertEqual(self.get(url='/?foo=bar')['status'], 404) + assert self.get(url='/?foo=%21blah')['status'] == 200 + assert self.get(url='/?foo=!blah')['status'] == 200 + assert self.get(url='/?foo=bar')['status'] == 404 self.route_match({"arguments": {"foo": "!!%21*a"}}) - self.assertEqual(self.get(url='/?foo=blah')['status'], 200) - self.assertEqual(self.get(url='/?foo=!blah')['status'], 200) - self.assertEqual(self.get(url='/?foo=!!a')['status'], 404) - self.assertEqual(self.get(url='/?foo=!!bla')['status'], 404) + assert self.get(url='/?foo=blah')['status'] == 200 + assert self.get(url='/?foo=!blah')['status'] == 200 + assert self.get(url='/?foo=!!a')['status'] == 404 + assert self.get(url='/?foo=!!bla')['status'] == 404 def test_routes_match_arguments_percent(self): self.route_match({"arguments": {"%25": "%25"}}) - self.assertEqual(self.get(url='/?%=%')['status'], 200) - self.assertEqual(self.get(url='/?%25=%25')['status'], 200) - self.assertEqual(self.get(url='/?%25=%')['status'], 200) + assert self.get(url='/?%=%')['status'] == 200 + assert self.get(url='/?%25=%25')['status'] == 200 + assert self.get(url='/?%25=%')['status'] == 200 self.route_match({"arguments": {"%251": "%252"}}) - self.assertEqual(self.get(url='/?%1=%2')['status'], 200) - self.assertEqual(self.get(url='/?%251=%252')['status'], 200) - self.assertEqual(self.get(url='/?%251=%2')['status'], 200) + assert self.get(url='/?%1=%2')['status'] == 200 + assert self.get(url='/?%251=%252')['status'] == 200 + assert self.get(url='/?%251=%2')['status'] == 200 self.route_match({"arguments": {"%25%21%251": "%25%24%252"}}) - self.assertEqual(self.get(url='/?%!%1=%$%2')['status'], 200) - self.assertEqual(self.get(url='/?%25!%251=%25$%252')['status'], 200) - self.assertEqual(self.get(url='/?%25!%1=%$%2')['status'], 200) + assert self.get(url='/?%!%1=%$%2')['status'] == 200 + assert self.get(url='/?%25!%251=%25$%252')['status'] == 200 + assert self.get(url='/?%25!%1=%$%2')['status'] == 200 def test_routes_match_arguments_ampersand(self): self.route_match({"arguments": {"foo": "&"}}) - self.assertEqual(self.get(url='/?foo=%26')['status'], 200) - self.assertEqual(self.get(url='/?foo=%26&')['status'], 200) - self.assertEqual(self.get(url='/?foo=%26%26')['status'], 404) - self.assertEqual(self.get(url='/?foo=&')['status'], 404) + assert self.get(url='/?foo=%26')['status'] == 200 + assert self.get(url='/?foo=%26&')['status'] == 200 + assert self.get(url='/?foo=%26%26')['status'] == 404 + assert self.get(url='/?foo=&')['status'] == 404 self.route_match({"arguments": {"&": ""}}) - self.assertEqual(self.get(url='/?%26=')['status'], 200) - self.assertEqual(self.get(url='/?%26=&')['status'], 200) - self.assertEqual(self.get(url='/?%26=%26')['status'], 404) - self.assertEqual(self.get(url='/?&=')['status'], 404) + assert self.get(url='/?%26=')['status'] == 200 + assert self.get(url='/?%26=&')['status'] == 200 + assert self.get(url='/?%26=%26')['status'] == 404 + assert self.get(url='/?&=')['status'] == 404 def test_routes_match_arguments_complex(self): self.route_match({"arguments": {"foo": ""}}) - self.assertEqual(self.get(url='/?foo')['status'], 200, 'complex') - self.assertEqual( - self.get(url='/?blah=blah&foo=')['status'], 200, 'complex 2' - ) - self.assertEqual( - self.get(url='/?&&&foo&&&')['status'], 200, 'complex 3' - ) - self.assertEqual( - self.get(url='/?foo&foo=bar&foo')['status'], 404, 'complex 4' - ) - self.assertEqual( - self.get(url='/?foo=&foo')['status'], 200, 'complex 5' - ) - self.assertEqual( - self.get(url='/?&=&foo&==&')['status'], 200, 'complex 6' - ) - self.assertEqual( - self.get(url='/?&=&bar&==&')['status'], 404, 'complex 7' - ) + assert self.get(url='/?foo')['status'] == 200, 'complex' + assert self.get(url='/?blah=blah&foo=')['status'] == 200, 'complex 2' + assert self.get(url='/?&&&foo&&&')['status'] == 200, 'complex 3' + assert self.get(url='/?foo&foo=bar&foo')['status'] == 404, 'complex 4' + assert self.get(url='/?foo=&foo')['status'] == 200, 'complex 5' + assert self.get(url='/?&=&foo&==&')['status'] == 200, 'complex 6' + assert self.get(url='/?&=&bar&==&')['status'] == 404, 'complex 7' def test_routes_match_arguments_multiple(self): self.route_match({"arguments": {"foo": "bar", "blah": "test"}}) - self.assertEqual(self.get()['status'], 404, 'multiple') - self.assertEqual( - self.get(url='/?foo=bar&blah=test')['status'], 200, 'multiple 2' - ) - self.assertEqual( - self.get(url='/?foo=bar&blah')['status'], 404, 'multiple 3' - ) - self.assertEqual( - self.get(url='/?foo=bar&blah=tes')['status'], 404, 'multiple 4' - ) - self.assertEqual( - self.get(url='/?foo=b%61r&bl%61h=t%65st')['status'], - 200, - 'multiple 5', - ) + assert self.get()['status'] == 404, 'multiple' + assert ( + self.get(url='/?foo=bar&blah=test')['status'] == 200 + ), 'multiple 2' + assert self.get(url='/?foo=bar&blah')['status'] == 404, 'multiple 3' + assert ( + self.get(url='/?foo=bar&blah=tes')['status'] == 404 + ), 'multiple 4' + assert ( + self.get(url='/?foo=b%61r&bl%61h=t%65st')['status'] == 200 + ), 'multiple 5' def test_routes_match_arguments_multiple_rules(self): self.route_match({"arguments": {"foo": ["bar", "blah"]}}) - self.assertEqual(self.get()['status'], 404, 'rules') - self.assertEqual(self.get(url='/?foo=bar')['status'], 200, 'rules 2') - self.assertEqual(self.get(url='/?foo=blah')['status'], 200, 'rules 3') - self.assertEqual( - self.get(url='/?foo=blah&foo=bar&foo=blah')['status'], - 200, - 'rules 4', - ) - self.assertEqual( - self.get(url='/?foo=blah&foo=bar&foo=')['status'], 404, 'rules 5' - ) + assert self.get()['status'] == 404, 'rules' + assert self.get(url='/?foo=bar')['status'] == 200, 'rules 2' + assert self.get(url='/?foo=blah')['status'] == 200, 'rules 3' + assert ( + self.get(url='/?foo=blah&foo=bar&foo=blah')['status'] == 200 + ), 'rules 4' + assert ( + self.get(url='/?foo=blah&foo=bar&foo=')['status'] == 404 + ), 'rules 5' def test_routes_match_arguments_array(self): self.route_match( @@ -1398,27 +1298,23 @@ class TestRouting(TestApplicationProto): } ) - self.assertEqual(self.get()['status'], 404, 'arr') - self.assertEqual(self.get(url='/?var1=val123')['status'], 200, 'arr 2') - self.assertEqual(self.get(url='/?var2=val2')['status'], 200, 'arr 3') - self.assertEqual(self.get(url='/?var3=bar')['status'], 200, 'arr 4') - self.assertEqual(self.get(url='/?var1=bar')['status'], 404, 'arr 5') - self.assertEqual( - self.get(url='/?var1=bar&var4=foo')['status'], 200, 'arr 6' - ) + assert self.get()['status'] == 404, 'arr' + assert self.get(url='/?var1=val123')['status'] == 200, 'arr 2' + assert self.get(url='/?var2=val2')['status'] == 200, 'arr 3' + assert self.get(url='/?var3=bar')['status'] == 200, 'arr 4' + assert self.get(url='/?var1=bar')['status'] == 404, 'arr 5' + assert self.get(url='/?var1=bar&var4=foo')['status'] == 200, 'arr 6' - self.assertIn( - 'success', - self.conf_delete('routes/0/match/arguments/1'), - 'match arguments array configure 2', - ) + assert 'success' in self.conf_delete( + 'routes/0/match/arguments/1' + ), 'match arguments array configure 2' - self.assertEqual(self.get(url='/?var2=val2')['status'], 404, 'arr 7') - self.assertEqual(self.get(url='/?var3=foo')['status'], 200, 'arr 8') + assert self.get(url='/?var2=val2')['status'] == 404, 'arr 7' + assert self.get(url='/?var3=foo')['status'] == 200, 'arr 8' def test_routes_match_arguments_invalid(self): # TODO remove it after controller fixed - self.skip_alerts.append(r'failed to apply new conf') + skip_alert(r'failed to apply new conf') self.route_match_invalid({"arguments": ["var"]}) self.route_match_invalid({"arguments": [{"var1": {}}]}) @@ -1434,7 +1330,7 @@ class TestRouting(TestApplicationProto): def test_routes_match_cookies(self): self.route_match({"cookies": {"foO": "bar"}}) - self.assertEqual(self.get()['status'], 404, 'cookie') + assert self.get()['status'] == 404, 'cookie' self.cookie('foO=bar', 200) self.cookie('foO=bar;1', 200) self.cookie(['foO=bar', 'blah=blah'], 200) @@ -1446,10 +1342,10 @@ class TestRouting(TestApplicationProto): def test_routes_match_cookies_empty(self): self.route_match({"cookies": {}}) - self.assertEqual(self.get()['status'], 200, 'cookies empty') + assert self.get()['status'] == 200, 'cookies empty' self.route_match({"cookies": []}) - self.assertEqual(self.get()['status'], 200, 'cookies empty 2') + assert self.get()['status'] == 200, 'cookies empty 2' def test_routes_match_cookies_invalid(self): self.route_match_invalid({"cookies": ["var"]}) @@ -1458,7 +1354,7 @@ class TestRouting(TestApplicationProto): def test_routes_match_cookies_multiple(self): self.route_match({"cookies": {"foo": "bar", "blah": "blah"}}) - self.assertEqual(self.get()['status'], 404, 'multiple') + assert self.get()['status'] == 404, 'multiple' self.cookie('foo=bar; blah=blah', 200) self.cookie(['foo=bar', 'blah=blah'], 200) self.cookie(['foo=bar; blah', 'blah'], 404) @@ -1474,12 +1370,12 @@ class TestRouting(TestApplicationProto): def test_routes_match_cookies_multiple_rules(self): self.route_match({"cookies": {"blah": ["test", "blah"]}}) - self.assertEqual(self.get()['status'], 404, 'multiple rules') + assert self.get()['status'] == 404, 'multiple rules' self.cookie('blah=test', 200) self.cookie('blah=blah', 200) self.cookie(['blah=blah', 'blah=test', 'blah=blah'], 200) self.cookie(['blah=blah; blah=test', 'blah=blah'], 200) - self.cookie(['blah=blah', 'blah'], 200) # invalid cookie + self.cookie(['blah=blah', 'blah'], 200) # invalid cookie def test_routes_match_cookies_array(self): self.route_match( @@ -1493,7 +1389,7 @@ class TestRouting(TestApplicationProto): } ) - self.assertEqual(self.get()['status'], 404, 'cookies array') + assert self.get()['status'] == 404, 'cookies array' self.cookie('var1=val123', 200) self.cookie('var2=val2', 200) self.cookie(' var2=val2 ', 200) @@ -1503,11 +1399,9 @@ class TestRouting(TestApplicationProto): self.cookie('var1=bar; var4=foo;', 200) self.cookie(['var1=bar', 'var4=foo'], 200) - self.assertIn( - 'success', - self.conf_delete('routes/0/match/cookies/1'), - 'match cookies array configure 2', - ) + assert 'success' in self.conf_delete( + 'routes/0/match/cookies/1' + ), 'match cookies array configure 2' self.cookie('var2=val2', 404) self.cookie('var3=foo', 200) @@ -1535,22 +1429,22 @@ class TestRouting(TestApplicationProto): sock2, port2 = sock_port() self.route_match({"source": "127.0.0.1:" + str(port)}) - self.assertEqual(self.get(sock=sock)['status'], 200, 'exact') - self.assertEqual(self.get(sock=sock2)['status'], 404, 'exact 2') + assert self.get(sock=sock)['status'] == 200, 'exact' + assert self.get(sock=sock2)['status'] == 404, 'exact 2' sock, port = sock_port() sock2, port2 = sock_port() self.route_match({"source": "!127.0.0.1:" + str(port)}) - self.assertEqual(self.get(sock=sock)['status'], 404, 'negative') - self.assertEqual(self.get(sock=sock2)['status'], 200, 'negative 2') + assert self.get(sock=sock)['status'] == 404, 'negative' + assert self.get(sock=sock2)['status'] == 200, 'negative 2' sock, port = sock_port() sock2, port2 = sock_port() self.route_match({"source": ["*:" + str(port), "!127.0.0.1"]}) - self.assertEqual(self.get(sock=sock)['status'], 404, 'negative 3') - self.assertEqual(self.get(sock=sock2)['status'], 404, 'negative 4') + assert self.get(sock=sock)['status'] == 404, 'negative 3' + assert self.get(sock=sock2)['status'] == 404, 'negative 4' sock, port = sock_port() sock2, port2 = sock_port() @@ -1558,8 +1452,8 @@ class TestRouting(TestApplicationProto): self.route_match( {"source": "127.0.0.1:" + str(port) + "-" + str(port)} ) - self.assertEqual(self.get(sock=sock)['status'], 200, 'range single') - self.assertEqual(self.get(sock=sock2)['status'], 404, 'range single 2') + assert self.get(sock=sock)['status'] == 200, 'range single' + assert self.get(sock=sock2)['status'] == 404, 'range single 2' socks = [ sock_port(), @@ -1578,11 +1472,11 @@ class TestRouting(TestApplicationProto): + str(socks[3][1]) # fourth port number } ) - self.assertEqual(self.get(sock=socks[0][0])['status'], 404, 'range') - self.assertEqual(self.get(sock=socks[1][0])['status'], 200, 'range 2') - self.assertEqual(self.get(sock=socks[2][0])['status'], 200, 'range 3') - self.assertEqual(self.get(sock=socks[3][0])['status'], 200, 'range 4') - self.assertEqual(self.get(sock=socks[4][0])['status'], 404, 'range 5') + assert self.get(sock=socks[0][0])['status'] == 404, 'range' + assert self.get(sock=socks[1][0])['status'] == 200, 'range 2' + assert self.get(sock=socks[2][0])['status'] == 200, 'range 3' + assert self.get(sock=socks[3][0])['status'] == 200, 'range 4' + assert self.get(sock=socks[4][0])['status'] == 404, 'range 5' socks = [ sock_port(), @@ -1599,218 +1493,194 @@ class TestRouting(TestApplicationProto): ] } ) - self.assertEqual(self.get(sock=socks[0][0])['status'], 200, 'array') - self.assertEqual(self.get(sock=socks[1][0])['status'], 404, 'array 2') - self.assertEqual(self.get(sock=socks[2][0])['status'], 200, 'array 3') + assert self.get(sock=socks[0][0])['status'] == 200, 'array' + assert self.get(sock=socks[1][0])['status'] == 404, 'array 2' + assert self.get(sock=socks[2][0])['status'] == 200, 'array 3' def test_routes_source_addr(self): - self.assertIn( - 'success', - self.conf( - { - "*:7080": {"pass": "routes"}, - "[::1]:7081": {"pass": "routes"}, - }, - 'listeners', - ), - 'source listeners configure', - ) + assert 'success' in self.conf( + {"*:7080": {"pass": "routes"}, "[::1]:7081": {"pass": "routes"},}, + 'listeners', + ), 'source listeners configure' def get_ipv6(): return self.get(sock_type='ipv6', port=7081) self.route_match({"source": "127.0.0.1"}) - self.assertEqual(self.get()['status'], 200, 'exact') - self.assertEqual(get_ipv6()['status'], 404, 'exact ipv6') + assert self.get()['status'] == 200, 'exact' + assert get_ipv6()['status'] == 404, 'exact ipv6' self.route_match({"source": ["127.0.0.1"]}) - self.assertEqual(self.get()['status'], 200, 'exact 2') - self.assertEqual(get_ipv6()['status'], 404, 'exact 2 ipv6') + assert self.get()['status'] == 200, 'exact 2' + assert get_ipv6()['status'] == 404, 'exact 2 ipv6' self.route_match({"source": "!127.0.0.1"}) - self.assertEqual(self.get()['status'], 404, 'exact neg') - self.assertEqual(get_ipv6()['status'], 200, 'exact neg ipv6') + assert self.get()['status'] == 404, 'exact neg' + assert get_ipv6()['status'] == 200, 'exact neg ipv6' self.route_match({"source": "127.0.0.2"}) - self.assertEqual(self.get()['status'], 404, 'exact 3') - self.assertEqual(get_ipv6()['status'], 404, 'exact 3 ipv6') + assert self.get()['status'] == 404, 'exact 3' + assert get_ipv6()['status'] == 404, 'exact 3 ipv6' self.route_match({"source": "127.0.0.1-127.0.0.1"}) - self.assertEqual(self.get()['status'], 200, 'range single') - self.assertEqual(get_ipv6()['status'], 404, 'range single ipv6') + assert self.get()['status'] == 200, 'range single' + assert get_ipv6()['status'] == 404, 'range single ipv6' self.route_match({"source": "127.0.0.2-127.0.0.2"}) - self.assertEqual(self.get()['status'], 404, 'range single 2') - self.assertEqual(get_ipv6()['status'], 404, 'range single 2 ipv6') + assert self.get()['status'] == 404, 'range single 2' + assert get_ipv6()['status'] == 404, 'range single 2 ipv6' self.route_match({"source": "127.0.0.2-127.0.0.3"}) - self.assertEqual(self.get()['status'], 404, 'range') - self.assertEqual(get_ipv6()['status'], 404, 'range ipv6') + assert self.get()['status'] == 404, 'range' + assert get_ipv6()['status'] == 404, 'range ipv6' self.route_match({"source": "127.0.0.1-127.0.0.2"}) - self.assertEqual(self.get()['status'], 200, 'range 2') - self.assertEqual(get_ipv6()['status'], 404, 'range 2 ipv6') + assert self.get()['status'] == 200, 'range 2' + assert get_ipv6()['status'] == 404, 'range 2 ipv6' self.route_match({"source": "127.0.0.0-127.0.0.2"}) - self.assertEqual(self.get()['status'], 200, 'range 3') - self.assertEqual(get_ipv6()['status'], 404, 'range 3 ipv6') + assert self.get()['status'] == 200, 'range 3' + assert get_ipv6()['status'] == 404, 'range 3 ipv6' self.route_match({"source": "127.0.0.0-127.0.0.1"}) - self.assertEqual(self.get()['status'], 200, 'range 4') - self.assertEqual(get_ipv6()['status'], 404, 'range 4 ipv6') + assert self.get()['status'] == 200, 'range 4' + assert get_ipv6()['status'] == 404, 'range 4 ipv6' self.route_match({"source": "126.0.0.0-127.0.0.0"}) - self.assertEqual(self.get()['status'], 404, 'range 5') - self.assertEqual(get_ipv6()['status'], 404, 'range 5 ipv6') + assert self.get()['status'] == 404, 'range 5' + assert get_ipv6()['status'] == 404, 'range 5 ipv6' self.route_match({"source": "126.126.126.126-127.0.0.2"}) - self.assertEqual(self.get()['status'], 200, 'range 6') - self.assertEqual(get_ipv6()['status'], 404, 'range 6 ipv6') + assert self.get()['status'] == 200, 'range 6' + assert get_ipv6()['status'] == 404, 'range 6 ipv6' def test_routes_source_ipv6(self): - self.assertIn( - 'success', - self.conf( - { - "[::1]:7080": {"pass": "routes"}, - "127.0.0.1:7081": {"pass": "routes"}, - }, - 'listeners', - ), - 'source listeners configure', - ) + assert 'success' in self.conf( + { + "[::1]:7080": {"pass": "routes"}, + "127.0.0.1:7081": {"pass": "routes"}, + }, + 'listeners', + ), 'source listeners configure' self.route_match({"source": "::1"}) - self.assertEqual(self.get(sock_type='ipv6')['status'], 200, 'exact') - self.assertEqual(self.get(port=7081)['status'], 404, 'exact ipv4') + assert self.get(sock_type='ipv6')['status'] == 200, 'exact' + assert self.get(port=7081)['status'] == 404, 'exact ipv4' self.route_match({"source": ["::1"]}) - self.assertEqual(self.get(sock_type='ipv6')['status'], 200, 'exact 2') - self.assertEqual(self.get(port=7081)['status'], 404, 'exact 2 ipv4') + assert self.get(sock_type='ipv6')['status'] == 200, 'exact 2' + assert self.get(port=7081)['status'] == 404, 'exact 2 ipv4' self.route_match({"source": "!::1"}) - self.assertEqual(self.get(sock_type='ipv6')['status'], 404, 'exact neg') - self.assertEqual(self.get(port=7081)['status'], 200, 'exact neg ipv4') + assert self.get(sock_type='ipv6')['status'] == 404, 'exact neg' + assert self.get(port=7081)['status'] == 200, 'exact neg ipv4' self.route_match({"source": "::2"}) - self.assertEqual(self.get(sock_type='ipv6')['status'], 404, 'exact 3') - self.assertEqual(self.get(port=7081)['status'], 404, 'exact 3 ipv4') + assert self.get(sock_type='ipv6')['status'] == 404, 'exact 3' + assert self.get(port=7081)['status'] == 404, 'exact 3 ipv4' self.route_match({"source": "::1-::1"}) - self.assertEqual(self.get(sock_type='ipv6')['status'], 200, 'range') - self.assertEqual(self.get(port=7081)['status'], 404, 'range ipv4') + assert self.get(sock_type='ipv6')['status'] == 200, 'range' + assert self.get(port=7081)['status'] == 404, 'range ipv4' self.route_match({"source": "::2-::2"}) - self.assertEqual(self.get(sock_type='ipv6')['status'], 404, 'range 2') - self.assertEqual(self.get(port=7081)['status'], 404, 'range 2 ipv4') + assert self.get(sock_type='ipv6')['status'] == 404, 'range 2' + assert self.get(port=7081)['status'] == 404, 'range 2 ipv4' self.route_match({"source": "::2-::3"}) - self.assertEqual(self.get(sock_type='ipv6')['status'], 404, 'range 3') - self.assertEqual(self.get(port=7081)['status'], 404, 'range 3 ipv4') + assert self.get(sock_type='ipv6')['status'] == 404, 'range 3' + assert self.get(port=7081)['status'] == 404, 'range 3 ipv4' self.route_match({"source": "::1-::2"}) - self.assertEqual(self.get(sock_type='ipv6')['status'], 200, 'range 4') - self.assertEqual(self.get(port=7081)['status'], 404, 'range 4 ipv4') + assert self.get(sock_type='ipv6')['status'] == 200, 'range 4' + assert self.get(port=7081)['status'] == 404, 'range 4 ipv4' self.route_match({"source": "::0-::2"}) - self.assertEqual(self.get(sock_type='ipv6')['status'], 200, 'range 5') - self.assertEqual(self.get(port=7081)['status'], 404, 'range 5 ipv4') + assert self.get(sock_type='ipv6')['status'] == 200, 'range 5' + assert self.get(port=7081)['status'] == 404, 'range 5 ipv4' self.route_match({"source": "::0-::1"}) - self.assertEqual(self.get(sock_type='ipv6')['status'], 200, 'range 6') - self.assertEqual(self.get(port=7081)['status'], 404, 'range 6 ipv4') + assert self.get(sock_type='ipv6')['status'] == 200, 'range 6' + assert self.get(port=7081)['status'] == 404, 'range 6 ipv4' def test_routes_source_cidr(self): - self.assertIn( - 'success', - self.conf( - { - "*:7080": {"pass": "routes"}, - "[::1]:7081": {"pass": "routes"}, - }, - 'listeners', - ), - 'source listeners configure', - ) + assert 'success' in self.conf( + {"*:7080": {"pass": "routes"}, "[::1]:7081": {"pass": "routes"},}, + 'listeners', + ), 'source listeners configure' def get_ipv6(): return self.get(sock_type='ipv6', port=7081) self.route_match({"source": "127.0.0.1/32"}) - self.assertEqual(self.get()['status'], 200, '32') - self.assertEqual(get_ipv6()['status'], 404, '32 ipv6') + assert self.get()['status'] == 200, '32' + assert get_ipv6()['status'] == 404, '32 ipv6' self.route_match({"source": "127.0.0.0/32"}) - self.assertEqual(self.get()['status'], 404, '32 2') - self.assertEqual(get_ipv6()['status'], 404, '32 2 ipv6') + assert self.get()['status'] == 404, '32 2' + assert get_ipv6()['status'] == 404, '32 2 ipv6' self.route_match({"source": "127.0.0.0/31"}) - self.assertEqual(self.get()['status'], 200, '31') - self.assertEqual(get_ipv6()['status'], 404, '31 ipv6') + assert self.get()['status'] == 200, '31' + assert get_ipv6()['status'] == 404, '31 ipv6' self.route_match({"source": "0.0.0.0/1"}) - self.assertEqual(self.get()['status'], 200, '1') - self.assertEqual(get_ipv6()['status'], 404, '1 ipv6') + assert self.get()['status'] == 200, '1' + assert get_ipv6()['status'] == 404, '1 ipv6' self.route_match({"source": "0.0.0.0/0"}) - self.assertEqual(self.get()['status'], 200, '0') - self.assertEqual(get_ipv6()['status'], 404, '0 ipv6') + assert self.get()['status'] == 200, '0' + assert get_ipv6()['status'] == 404, '0 ipv6' def test_routes_source_cidr_ipv6(self): - self.assertIn( - 'success', - self.conf( - { - "[::1]:7080": {"pass": "routes"}, - "127.0.0.1:7081": {"pass": "routes"}, - }, - 'listeners', - ), - 'source listeners configure', - ) + assert 'success' in self.conf( + { + "[::1]:7080": {"pass": "routes"}, + "127.0.0.1:7081": {"pass": "routes"}, + }, + 'listeners', + ), 'source listeners configure' self.route_match({"source": "::1/128"}) - self.assertEqual(self.get(sock_type='ipv6')['status'], 200, '128') - self.assertEqual(self.get(port=7081)['status'], 404, '128 ipv4') + assert self.get(sock_type='ipv6')['status'] == 200, '128' + assert self.get(port=7081)['status'] == 404, '128 ipv4' self.route_match({"source": "::0/128"}) - self.assertEqual(self.get(sock_type='ipv6')['status'], 404, '128 2') - self.assertEqual(self.get(port=7081)['status'], 404, '128 ipv4') + assert self.get(sock_type='ipv6')['status'] == 404, '128 2' + assert self.get(port=7081)['status'] == 404, '128 ipv4' self.route_match({"source": "::0/127"}) - self.assertEqual(self.get(sock_type='ipv6')['status'], 200, '127') - self.assertEqual(self.get(port=7081)['status'], 404, '127 ipv4') + assert self.get(sock_type='ipv6')['status'] == 200, '127' + assert self.get(port=7081)['status'] == 404, '127 ipv4' self.route_match({"source": "::0/32"}) - self.assertEqual(self.get(sock_type='ipv6')['status'], 200, '32') - self.assertEqual(self.get(port=7081)['status'], 404, '32 ipv4') + assert self.get(sock_type='ipv6')['status'] == 200, '32' + assert self.get(port=7081)['status'] == 404, '32 ipv4' self.route_match({"source": "::0/1"}) - self.assertEqual(self.get(sock_type='ipv6')['status'], 200, '1') - self.assertEqual(self.get(port=7081)['status'], 404, '1 ipv4') + assert self.get(sock_type='ipv6')['status'] == 200, '1' + assert self.get(port=7081)['status'] == 404, '1 ipv4' self.route_match({"source": "::/0"}) - self.assertEqual(self.get(sock_type='ipv6')['status'], 200, '0') - self.assertEqual(self.get(port=7081)['status'], 404, '0 ipv4') + assert self.get(sock_type='ipv6')['status'] == 200, '0' + assert self.get(port=7081)['status'] == 404, '0 ipv4' def test_routes_source_unix(self): - addr = self.testdir + '/sock' + addr = self.temp_dir + '/sock' - self.assertIn( - 'success', - self.conf({"unix:" + addr: {"pass": "routes"}}, 'listeners'), - 'source listeners configure', - ) + assert 'success' in self.conf( + {"unix:" + addr: {"pass": "routes"}}, 'listeners' + ), 'source listeners configure' self.route_match({"source": "!0.0.0.0/0"}) - self.assertEqual( - self.get(sock_type='unix', addr=addr)['status'], 200, 'unix ipv4' - ) + assert ( + self.get(sock_type='unix', addr=addr)['status'] == 200 + ), 'unix ipv4' self.route_match({"source": "!::/0"}) - self.assertEqual( - self.get(sock_type='unix', addr=addr)['status'], 200, 'unix ipv6' - ) + assert ( + self.get(sock_type='unix', addr=addr)['status'] == 200 + ), 'unix ipv6' def test_routes_match_source(self): self.route_match({"source": "::"}) @@ -1863,7 +1733,7 @@ class TestRouting(TestApplicationProto): } ) self.route_match({"source": "*:0-65535"}) - self.assertEqual(self.get()['status'], 200, 'source any') + assert self.get()['status'] == 200, 'source any' def test_routes_match_source_invalid(self): self.route_match_invalid({"source": "127"}) @@ -1888,104 +1758,84 @@ class TestRouting(TestApplicationProto): self.route_match_invalid({"source": "*:65536"}) def test_routes_match_destination(self): - self.assertIn( - 'success', - self.conf( - {"*:7080": {"pass": "routes"}, "*:7081": {"pass": "routes"}}, - 'listeners', - ), - 'listeners configure', - ) + assert 'success' in self.conf( + {"*:7080": {"pass": "routes"}, "*:7081": {"pass": "routes"}}, + 'listeners', + ), 'listeners configure' self.route_match({"destination": "*:7080"}) - self.assertEqual(self.get()['status'], 200, 'dest') - self.assertEqual(self.get(port=7081)['status'], 404, 'dest 2') + assert self.get()['status'] == 200, 'dest' + assert self.get(port=7081)['status'] == 404, 'dest 2' self.route_match({"destination": ["127.0.0.1:7080"]}) - self.assertEqual(self.get()['status'], 200, 'dest 3') - self.assertEqual(self.get(port=7081)['status'], 404, 'dest 4') + assert self.get()['status'] == 200, 'dest 3' + assert self.get(port=7081)['status'] == 404, 'dest 4' self.route_match({"destination": "!*:7080"}) - self.assertEqual(self.get()['status'], 404, 'dest neg') - self.assertEqual(self.get(port=7081)['status'], 200, 'dest neg 2') + assert self.get()['status'] == 404, 'dest neg' + assert self.get(port=7081)['status'] == 200, 'dest neg 2' self.route_match({"destination": ['!*:7080', '!*:7081']}) - self.assertEqual(self.get()['status'], 404, 'dest neg 3') - self.assertEqual(self.get(port=7081)['status'], 404, 'dest neg 4') + assert self.get()['status'] == 404, 'dest neg 3' + assert self.get(port=7081)['status'] == 404, 'dest neg 4' self.route_match({"destination": ['!*:7081', '!*:7082']}) - self.assertEqual(self.get()['status'], 200, 'dest neg 5') + assert self.get()['status'] == 200, 'dest neg 5' self.route_match({"destination": ['*:7080', '!*:7080']}) - self.assertEqual(self.get()['status'], 404, 'dest neg 6') + assert self.get()['status'] == 404, 'dest neg 6' self.route_match( {"destination": ['127.0.0.1:7080', '*:7081', '!*:7080']} ) - self.assertEqual(self.get()['status'], 404, 'dest neg 7') - self.assertEqual(self.get(port=7081)['status'], 200, 'dest neg 8') + assert self.get()['status'] == 404, 'dest neg 7' + assert self.get(port=7081)['status'] == 200, 'dest neg 8' self.route_match({"destination": ['!*:7081', '!*:7082', '*:7083']}) - self.assertEqual(self.get()['status'], 404, 'dest neg 9') + assert self.get()['status'] == 404, 'dest neg 9' self.route_match( {"destination": ['*:7081', '!127.0.0.1:7080', '*:7080']} ) - self.assertEqual(self.get()['status'], 404, 'dest neg 10') - self.assertEqual(self.get(port=7081)['status'], 200, 'dest neg 11') + assert self.get()['status'] == 404, 'dest neg 10' + assert self.get(port=7081)['status'] == 200, 'dest neg 11' - self.assertIn( - 'success', - self.conf_delete('routes/0/match/destination/0'), - 'remove destination rule', - ) - self.assertEqual(self.get()['status'], 404, 'dest neg 12') - self.assertEqual(self.get(port=7081)['status'], 404, 'dest neg 13') + assert 'success' in self.conf_delete( + 'routes/0/match/destination/0' + ), 'remove destination rule' + assert self.get()['status'] == 404, 'dest neg 12' + assert self.get(port=7081)['status'] == 404, 'dest neg 13' - self.assertIn( - 'success', - self.conf_delete('routes/0/match/destination/0'), - 'remove destination rule 2', - ) - self.assertEqual(self.get()['status'], 200, 'dest neg 14') - self.assertEqual(self.get(port=7081)['status'], 404, 'dest neg 15') + assert 'success' in self.conf_delete( + 'routes/0/match/destination/0' + ), 'remove destination rule 2' + assert self.get()['status'] == 200, 'dest neg 14' + assert self.get(port=7081)['status'] == 404, 'dest neg 15' - self.assertIn( - 'success', - self.conf_post("\"!127.0.0.1\"", 'routes/0/match/destination'), - 'add destination rule', - ) - self.assertEqual(self.get()['status'], 404, 'dest neg 16') - self.assertEqual(self.get(port=7081)['status'], 404, 'dest neg 17') + assert 'success' in self.conf_post( + "\"!127.0.0.1\"", 'routes/0/match/destination' + ), 'add destination rule' + assert self.get()['status'] == 404, 'dest neg 16' + assert self.get(port=7081)['status'] == 404, 'dest neg 17' def test_routes_match_destination_proxy(self): - self.assertIn( - 'success', - self.conf( - { - "listeners": { - "*:7080": {"pass": "routes/first"}, - "*:7081": {"pass": "routes/second"}, - }, - "routes": { - "first": [ - {"action": {"proxy": "http://127.0.0.1:7081"}} - ], - "second": [ - { - "match": {"destination": ["127.0.0.1:7081"]}, - "action": {"return": 200}, - } - ], - }, - "applications": {}, - } - ), - 'proxy configure', - ) - - self.assertEqual(self.get()['status'], 200, 'proxy') - + assert 'success' in self.conf( + { + "listeners": { + "*:7080": {"pass": "routes/first"}, + "*:7081": {"pass": "routes/second"}, + }, + "routes": { + "first": [{"action": {"proxy": "http://127.0.0.1:7081"}}], + "second": [ + { + "match": {"destination": ["127.0.0.1:7081"]}, + "action": {"return": 200}, + } + ], + }, + "applications": {}, + } + ), 'proxy configure' -if __name__ == '__main__': - TestRouting.main() + assert self.get()['status'] == 200, 'proxy' diff --git a/test/test_routing_tls.py b/test/test_routing_tls.py index a9b8f88d..76cfb485 100644 --- a/test/test_routing_tls.py +++ b/test/test_routing_tls.py @@ -7,36 +7,22 @@ class TestRoutingTLS(TestApplicationTLS): def test_routes_match_scheme_tls(self): self.certificate() - self.assertIn( - 'success', - self.conf( - { - "listeners": { - "*:7080": {"pass": "routes"}, - "*:7081": { - "pass": "routes", - "tls": {"certificate": 'default'}, - }, + assert 'success' in self.conf( + { + "listeners": { + "*:7080": {"pass": "routes"}, + "*:7081": { + "pass": "routes", + "tls": {"certificate": 'default'}, }, - "routes": [ - { - "match": {"scheme": "http"}, - "action": {"return": 200}, - }, - { - "match": {"scheme": "https"}, - "action": {"return": 201}, - }, - ], - "applications": {}, - } - ), - 'scheme configure', - ) + }, + "routes": [ + {"match": {"scheme": "http"}, "action": {"return": 200}}, + {"match": {"scheme": "https"}, "action": {"return": 201}}, + ], + "applications": {}, + } + ), 'scheme configure' - self.assertEqual(self.get()['status'], 200, 'http') - self.assertEqual(self.get_ssl(port=7081)['status'], 201, 'https') - - -if __name__ == '__main__': - TestRoutingTLS.main() + assert self.get()['status'] == 200, 'http' + assert self.get_ssl(port=7081)['status'] == 201, 'https' diff --git a/test/test_ruby_application.py b/test/test_ruby_application.py index 4709df6c..f84935f8 100644 --- a/test/test_ruby_application.py +++ b/test/test_ruby_application.py @@ -1,5 +1,8 @@ -import unittest +import re +import pytest + +from conftest import skip_alert from unit.applications.lang.ruby import TestApplicationRuby @@ -21,173 +24,151 @@ class TestRubyApplication(TestApplicationRuby): body=body, ) - self.assertEqual(resp['status'], 200, 'status') + assert resp['status'] == 200, 'status' headers = resp['headers'] header_server = headers.pop('Server') - self.assertRegex(header_server, r'Unit/[\d\.]+', 'server header') - self.assertEqual( - headers.pop('Server-Software'), - header_server, - 'server software header', - ) + assert re.search(r'Unit/[\d\.]+', header_server), 'server header' + assert ( + headers.pop('Server-Software') == header_server + ), 'server software header' date = headers.pop('Date') - self.assertEqual(date[-4:], ' GMT', 'date header timezone') - self.assertLess( - abs(self.date_to_sec_epoch(date) - self.sec_epoch()), - 5, - 'date header', - ) - - self.assertDictEqual( - headers, - { - 'Connection': 'close', - 'Content-Length': str(len(body)), - 'Content-Type': 'text/html', - 'Request-Method': 'POST', - 'Request-Uri': '/', - 'Http-Host': 'localhost', - 'Server-Protocol': 'HTTP/1.1', - 'Custom-Header': 'blah', - 'Rack-Version': '13', - 'Rack-Url-Scheme': 'http', - 'Rack-Multithread': 'false', - 'Rack-Multiprocess': 'true', - 'Rack-Run-Once': 'false', - 'Rack-Hijack-Q': 'false', - 'Rack-Hijack': '', - 'Rack-Hijack-IO': '', - }, - 'headers', - ) - self.assertEqual(resp['body'], body, 'body') + assert date[-4:] == ' GMT', 'date header timezone' + assert ( + abs(self.date_to_sec_epoch(date) - self.sec_epoch()) < 5 + ), 'date header' + + assert headers == { + 'Connection': 'close', + 'Content-Length': str(len(body)), + 'Content-Type': 'text/html', + 'Request-Method': 'POST', + 'Request-Uri': '/', + 'Http-Host': 'localhost', + 'Server-Protocol': 'HTTP/1.1', + 'Custom-Header': 'blah', + 'Rack-Version': '13', + 'Rack-Url-Scheme': 'http', + 'Rack-Multithread': 'false', + 'Rack-Multiprocess': 'true', + 'Rack-Run-Once': 'false', + 'Rack-Hijack-Q': 'false', + 'Rack-Hijack': '', + 'Rack-Hijack-IO': '', + }, 'headers' + assert resp['body'] == body, 'body' def test_ruby_application_query_string(self): self.load('query_string') resp = self.get(url='/?var1=val1&var2=val2') - self.assertEqual( - resp['headers']['Query-String'], - 'var1=val1&var2=val2', - 'Query-String header', - ) + assert ( + resp['headers']['Query-String'] == 'var1=val1&var2=val2' + ), 'Query-String header' def test_ruby_application_query_string_empty(self): self.load('query_string') resp = self.get(url='/?') - self.assertEqual(resp['status'], 200, 'query string empty status') - self.assertEqual( - resp['headers']['Query-String'], '', 'query string empty' - ) + assert resp['status'] == 200, 'query string empty status' + assert resp['headers']['Query-String'] == '', 'query string empty' def test_ruby_application_query_string_absent(self): self.load('query_string') resp = self.get() - self.assertEqual(resp['status'], 200, 'query string absent status') - self.assertEqual( - resp['headers']['Query-String'], '', 'query string absent' - ) + assert resp['status'] == 200, 'query string absent status' + assert resp['headers']['Query-String'] == '', 'query string absent' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_ruby_application_server_port(self): self.load('server_port') - self.assertEqual( - self.get()['headers']['Server-Port'], '7080', 'Server-Port header' - ) + assert ( + self.get()['headers']['Server-Port'] == '7080' + ), 'Server-Port header' def test_ruby_application_status_int(self): self.load('status_int') - self.assertEqual(self.get()['status'], 200, 'status int') + assert self.get()['status'] == 200, 'status int' def test_ruby_application_input_read_empty(self): self.load('input_read_empty') - self.assertEqual(self.get()['body'], '', 'read empty') + assert self.get()['body'] == '', 'read empty' def test_ruby_application_input_read_parts(self): self.load('input_read_parts') - self.assertEqual( - self.post(body='0123456789')['body'], - '012345678', - 'input read parts', - ) + assert ( + self.post(body='0123456789')['body'] == '012345678' + ), 'input read parts' def test_ruby_application_input_read_buffer(self): self.load('input_read_buffer') - self.assertEqual( - self.post(body='0123456789')['body'], - '0123456789', - 'input read buffer', - ) + assert ( + self.post(body='0123456789')['body'] == '0123456789' + ), 'input read buffer' def test_ruby_application_input_read_buffer_not_empty(self): self.load('input_read_buffer_not_empty') - self.assertEqual( - self.post(body='0123456789')['body'], - '0123456789', - 'input read buffer not empty', - ) + assert ( + self.post(body='0123456789')['body'] == '0123456789' + ), 'input read buffer not empty' def test_ruby_application_input_gets(self): self.load('input_gets') body = '0123456789' - self.assertEqual(self.post(body=body)['body'], body, 'input gets') + assert self.post(body=body)['body'] == body, 'input gets' def test_ruby_application_input_gets_2(self): self.load('input_gets') - self.assertEqual( - self.post(body='01234\n56789\n')['body'], '01234\n', 'input gets 2' - ) + assert ( + self.post(body='01234\n56789\n')['body'] == '01234\n' + ), 'input gets 2' def test_ruby_application_input_gets_all(self): self.load('input_gets_all') body = '\n01234\n56789\n\n' - self.assertEqual(self.post(body=body)['body'], body, 'input gets all') + assert self.post(body=body)['body'] == body, 'input gets all' def test_ruby_application_input_each(self): self.load('input_each') body = '\n01234\n56789\n\n' - self.assertEqual(self.post(body=body)['body'], body, 'input each') + assert self.post(body=body)['body'] == body, 'input each' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_ruby_application_input_rewind(self): self.load('input_rewind') body = '0123456789' - self.assertEqual(self.post(body=body)['body'], body, 'input rewind') + assert self.post(body=body)['body'] == body, 'input rewind' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_ruby_application_syntax_error(self): - self.skip_alerts.extend( - [ - r'Failed to parse rack script', - r'syntax error', - r'new_from_string', - r'parse_file', - ] + skip_alert( + r'Failed to parse rack script', + r'syntax error', + r'new_from_string', + r'parse_file', ) self.load('syntax_error') - self.assertEqual(self.get()['status'], 500, 'syntax error') + assert self.get()['status'] == 500, 'syntax error' def test_ruby_application_errors_puts(self): self.load('errors_puts') @@ -196,10 +177,10 @@ class TestRubyApplication(TestApplicationRuby): self.stop() - self.assertIsNotNone( - self.wait_for_record(r'\[error\].+Error in application'), - 'errors puts', - ) + assert ( + self.wait_for_record(r'\[error\].+Error in application') + is not None + ), 'errors puts' def test_ruby_application_errors_puts_int(self): self.load('errors_puts_int') @@ -208,9 +189,9 @@ class TestRubyApplication(TestApplicationRuby): self.stop() - self.assertIsNotNone( - self.wait_for_record(r'\[error\].+1234567890'), 'errors puts int' - ) + assert ( + self.wait_for_record(r'\[error\].+1234567890') is not None + ), 'errors puts int' def test_ruby_application_errors_write(self): self.load('errors_write') @@ -219,15 +200,15 @@ class TestRubyApplication(TestApplicationRuby): self.stop() - self.assertIsNotNone( - self.wait_for_record(r'\[error\].+Error in application'), - 'errors write', - ) + assert ( + self.wait_for_record(r'\[error\].+Error in application') + is not None + ), 'errors write' def test_ruby_application_errors_write_to_s_custom(self): self.load('errors_write_to_s_custom') - self.assertEqual(self.get()['status'], 200, 'errors write to_s custom') + assert self.get()['status'] == 200, 'errors write to_s custom' def test_ruby_application_errors_write_int(self): self.load('errors_write_int') @@ -236,9 +217,9 @@ class TestRubyApplication(TestApplicationRuby): self.stop() - self.assertIsNotNone( - self.wait_for_record(r'\[error\].+1234567890'), 'errors write int' - ) + assert ( + self.wait_for_record(r'\[error\].+1234567890') is not None + ), 'errors write int' def test_ruby_application_at_exit(self): self.load('at_exit') @@ -249,79 +230,81 @@ class TestRubyApplication(TestApplicationRuby): self.stop() - self.assertIsNotNone( - self.wait_for_record(r'\[error\].+At exit called\.'), 'at exit' - ) + assert ( + self.wait_for_record(r'\[error\].+At exit called\.') is not None + ), 'at exit' def test_ruby_application_header_custom(self): self.load('header_custom') resp = self.post(body="\ntc=one,two\ntc=three,four,\n\n") - self.assertEqual( - resp['headers']['Custom-Header'], - ['', 'tc=one,two', 'tc=three,four,', '', ''], - 'header custom', - ) + assert resp['headers']['Custom-Header'] == [ + '', + 'tc=one,two', + 'tc=three,four,', + '', + '', + ], 'header custom' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_ruby_application_header_custom_non_printable(self): self.load('header_custom') - self.assertEqual( - self.post(body='\b')['status'], 500, 'header custom non printable' - ) + assert ( + self.post(body='\b')['status'] == 500 + ), 'header custom non printable' def test_ruby_application_header_status(self): self.load('header_status') - self.assertEqual(self.get()['status'], 200, 'header status') + assert self.get()['status'] == 200, 'header status' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_ruby_application_header_rack(self): self.load('header_rack') - self.assertEqual(self.get()['status'], 500, 'header rack') + assert self.get()['status'] == 500, 'header rack' def test_ruby_application_body_empty(self): self.load('body_empty') - self.assertEqual(self.get()['body'], '', 'body empty') + assert self.get()['body'] == '', 'body empty' def test_ruby_application_body_array(self): self.load('body_array') - self.assertEqual(self.get()['body'], '0123456789', 'body array') + assert self.get()['body'] == '0123456789', 'body array' def test_ruby_application_body_large(self): self.load('mirror') body = '0123456789' * 1000 - self.assertEqual(self.post(body=body)['body'], body, 'body large') + assert self.post(body=body)['body'] == body, 'body large' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_ruby_application_body_each_error(self): self.load('body_each_error') - self.assertEqual(self.get()['status'], 500, 'body each error status') + assert self.get()['status'] == 500, 'body each error status' self.stop() - self.assertIsNotNone( - self.wait_for_record(r'\[error\].+Failed to run ruby script'), - 'body each error', - ) + assert ( + self.wait_for_record(r'\[error\].+Failed to run ruby script') + is not None + ), 'body each error' def test_ruby_application_body_file(self): self.load('body_file') - self.assertEqual(self.get()['body'], 'body\n', 'body file') + assert self.get()['body'] == 'body\n', 'body file' def test_ruby_keepalive_body(self): self.load('mirror') - self.assertEqual(self.get()['status'], 200, 'init') + assert self.get()['status'] == 200, 'init' body = '0123456789' * 500 (resp, sock) = self.post( @@ -335,7 +318,7 @@ class TestRubyApplication(TestApplicationRuby): read_timeout=1, ) - self.assertEqual(resp['body'], body, 'keep-alive 1') + assert resp['body'] == body, 'keep-alive 1' body = '0123456789' resp = self.post( @@ -348,31 +331,22 @@ class TestRubyApplication(TestApplicationRuby): body=body, ) - self.assertEqual(resp['body'], body, 'keep-alive 2') + assert resp['body'] == body, 'keep-alive 2' def test_ruby_application_constants(self): self.load('constants') resp = self.get() - self.assertEqual(resp['status'], 200, 'status') + assert resp['status'] == 200, 'status' headers = resp['headers'] - self.assertGreater(len(headers['X-Copyright']), 0, 'RUBY_COPYRIGHT') - self.assertGreater( - len(headers['X-Description']), 0, 'RUBY_DESCRIPTION' - ) - self.assertGreater(len(headers['X-Engine']), 0, 'RUBY_ENGINE') - self.assertGreater( - len(headers['X-Engine-Version']), 0, 'RUBY_ENGINE_VERSION' - ) - self.assertGreater(len(headers['X-Patchlevel']), 0, 'RUBY_PATCHLEVEL') - self.assertGreater(len(headers['X-Platform']), 0, 'RUBY_PLATFORM') - self.assertGreater( - len(headers['X-Release-Date']), 0, 'RUBY_RELEASE_DATE' - ) - self.assertGreater(len(headers['X-Revision']), 0, 'RUBY_REVISION') - self.assertGreater(len(headers['X-Version']), 0, 'RUBY_VERSION') - -if __name__ == '__main__': - TestRubyApplication.main() + assert len(headers['X-Copyright']) > 0, 'RUBY_COPYRIGHT' + assert len(headers['X-Description']) > 0, 'RUBY_DESCRIPTION' + assert len(headers['X-Engine']) > 0, 'RUBY_ENGINE' + assert len(headers['X-Engine-Version']) > 0, 'RUBY_ENGINE_VERSION' + assert len(headers['X-Patchlevel']) > 0, 'RUBY_PATCHLEVEL' + assert len(headers['X-Platform']) > 0, 'RUBY_PLATFORM' + assert len(headers['X-Release-Date']) > 0, 'RUBY_RELEASE_DATE' + assert len(headers['X-Revision']) > 0, 'RUBY_REVISION' + assert len(headers['X-Version']) > 0, 'RUBY_VERSION' diff --git a/test/test_ruby_isolation.py b/test/test_ruby_isolation.py index 9bac162e..13ca0e16 100644 --- a/test/test_ruby_isolation.py +++ b/test/test_ruby_isolation.py @@ -1,7 +1,7 @@ -import os -import shutil -import unittest +import pytest + +from conftest import option from unit.applications.lang.ruby import TestApplicationRuby from unit.feature.isolation import TestFeatureIsolation @@ -12,60 +12,39 @@ class TestRubyIsolation(TestApplicationRuby): isolation = TestFeatureIsolation() @classmethod - def setUpClass(cls, complete_check=True): - unit = super().setUpClass(complete_check=False) + def setup_class(cls, complete_check=True): + unit = super().setup_class(complete_check=False) - TestFeatureIsolation().check(cls.available, unit.testdir) + TestFeatureIsolation().check(cls.available, unit.temp_dir) return unit if not complete_check else unit.complete() - def test_ruby_isolation_rootfs(self): + def test_ruby_isolation_rootfs(self, is_su): isolation_features = self.available['features']['isolation'].keys() if 'mnt' not in isolation_features: - print('requires mnt ns') - raise unittest.SkipTest() + pytest.skip('requires mnt ns') - if not self.is_su: + if not is_su: if 'user' not in isolation_features: - print('requires unprivileged userns or root') - raise unittest.SkipTest() + pytest.skip('requires unprivileged userns or root') if not 'unprivileged_userns_clone' in isolation_features: - print('requires unprivileged userns or root') - raise unittest.SkipTest() - - os.mkdir(self.testdir + '/ruby') + pytest.skip('requires unprivileged userns or root') - shutil.copytree( - self.current_dir + '/ruby/status_int', - self.testdir + '/ruby/status_int', - ) isolation = { - 'namespaces': {'credential': not self.is_su, 'mount': True}, - 'rootfs': self.testdir, + 'namespaces': {'credential': not is_su, 'mount': True}, + 'rootfs': option.test_dir, } self.load('status_int', isolation=isolation) - self.assertIn( - 'success', - self.conf( - '"/ruby/status_int/config.ru"', - 'applications/status_int/script', - ), + assert 'success' in self.conf( + '"/ruby/status_int/config.ru"', 'applications/status_int/script', ) - self.assertIn( - 'success', - self.conf( - '"/ruby/status_int"', - 'applications/status_int/working_directory', - ), + assert 'success' in self.conf( + '"/ruby/status_int"', 'applications/status_int/working_directory', ) - self.assertEqual(self.get()['status'], 200, 'status int') - - -if __name__ == '__main__': - TestRubyIsolation.main() + assert self.get()['status'] == 200, 'status int' diff --git a/test/test_settings.py b/test/test_settings.py index 6600358d..b0af6b04 100644 --- a/test/test_settings.py +++ b/test/test_settings.py @@ -1,6 +1,8 @@ +import re import socket import time -import unittest + +import pytest from unit.applications.lang.python import TestApplicationPython @@ -32,7 +34,7 @@ Connection: close raw=True, ) - self.assertEqual(resp['status'], 408, 'status header read timeout') + assert resp['status'] == 408, 'status header read timeout' def test_settings_header_read_timeout_update(self): self.load('empty') @@ -83,9 +85,7 @@ Connection: close raw=True, ) - self.assertEqual( - resp['status'], 408, 'status header read timeout update' - ) + assert resp['status'] == 408, 'status header read timeout update' def test_settings_body_read_timeout(self): self.load('empty') @@ -109,7 +109,7 @@ Connection: close resp = self.http(b"""0123456789""", sock=sock, raw=True) - self.assertEqual(resp['status'], 408, 'status body read timeout') + assert resp['status'] == 408, 'status body read timeout' def test_settings_body_read_timeout_update(self): self.load('empty') @@ -144,9 +144,7 @@ Connection: close resp = self.http(b"""6789""", sock=sock, raw=True) - self.assertEqual( - resp['status'], 200, 'status body read timeout update' - ) + assert resp['status'] == 200, 'status body read timeout update' def test_settings_send_timeout(self): self.load('mirror') @@ -155,7 +153,7 @@ Connection: close self.conf({'http': {'send_timeout': 1}}, 'settings') - addr = self.testdir + '/sock' + addr = self.temp_dir + '/sock' self.conf({"unix:" + addr: {'application': 'mirror'}}, 'listeners') @@ -182,13 +180,13 @@ Connection: close sock.close() - self.assertRegex(data, r'200 OK', 'status send timeout') - self.assertLess(len(data), data_len, 'data send timeout') + assert re.search(r'200 OK', data), 'status send timeout' + assert len(data) < data_len, 'data send timeout' def test_settings_idle_timeout(self): self.load('empty') - self.assertEqual(self.get()['status'], 200, 'init') + assert self.get()['status'] == 200, 'init' self.conf({'http': {'idle_timeout': 2}}, 'settings') @@ -204,17 +202,33 @@ Connection: close headers={'Host': 'localhost', 'Connection': 'close'}, sock=sock ) - self.assertEqual(resp['status'], 408, 'status idle timeout') + assert resp['status'] == 408, 'status idle timeout' + + def test_settings_idle_timeout_2(self): + self.load('empty') + + assert self.get()['status'] == 200, 'init' + + self.conf({'http': {'idle_timeout': 1}}, 'settings') + + _, sock = self.http(b'', start=True, raw=True, no_recv=True) + + time.sleep(3) + + assert ( + self.get( + headers={'Host': 'localhost', 'Connection': 'close'}, sock=sock + )['status'] + == 408 + ), 'status idle timeout' def test_settings_max_body_size(self): self.load('empty') self.conf({'http': {'max_body_size': 5}}, 'settings') - self.assertEqual(self.post(body='01234')['status'], 200, 'status size') - self.assertEqual( - self.post(body='012345')['status'], 413, 'status size max' - ) + assert self.post(body='01234')['status'] == 200, 'status size' + assert self.post(body='012345')['status'] == 413, 'status size max' def test_settings_max_body_size_large(self): self.load('mirror') @@ -223,32 +237,26 @@ Connection: close body = '0123456789abcdef' * 4 * 64 * 1024 resp = self.post(body=body, read_buffer_size=1024 * 1024) - self.assertEqual(resp['status'], 200, 'status size 4') - self.assertEqual(resp['body'], body, 'status body 4') + assert resp['status'] == 200, 'status size 4' + assert resp['body'] == body, 'status body 4' body = '0123456789abcdef' * 8 * 64 * 1024 resp = self.post(body=body, read_buffer_size=1024 * 1024) - self.assertEqual(resp['status'], 200, 'status size 8') - self.assertEqual(resp['body'], body, 'status body 8') + assert resp['status'] == 200, 'status size 8' + assert resp['body'] == body, 'status body 8' body = '0123456789abcdef' * 16 * 64 * 1024 resp = self.post(body=body, read_buffer_size=1024 * 1024) - self.assertEqual(resp['status'], 200, 'status size 16') - self.assertEqual(resp['body'], body, 'status body 16') + assert resp['status'] == 200, 'status size 16' + assert resp['body'] == body, 'status body 16' body = '0123456789abcdef' * 32 * 64 * 1024 resp = self.post(body=body, read_buffer_size=1024 * 1024) - self.assertEqual(resp['status'], 200, 'status size 32') - self.assertEqual(resp['body'], body, 'status body 32') + assert resp['status'] == 200, 'status size 32' + assert resp['body'] == body, 'status body 32' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_settings_negative_value(self): - self.assertIn( - 'error', - self.conf({'http': {'max_body_size': -1}}, 'settings'), - 'settings negative value', - ) - - -if __name__ == '__main__': - TestSettings.main() + assert 'error' in self.conf( + {'http': {'max_body_size': -1}}, 'settings' + ), 'settings negative value' diff --git a/test/test_share_fallback.py b/test/test_share_fallback.py index ca5e2678..391d0836 100644 --- a/test/test_share_fallback.py +++ b/test/test_share_fallback.py @@ -1,20 +1,21 @@ import os +from conftest import skip_alert from unit.applications.proto import TestApplicationProto class TestStatic(TestApplicationProto): prerequisites = {} - def setUp(self): - super().setUp() + def setup_method(self): + super().setup_method() - os.makedirs(self.testdir + '/assets/dir') - with open(self.testdir + '/assets/index.html', 'w') as index: + os.makedirs(self.temp_dir + '/assets/dir') + with open(self.temp_dir + '/assets/index.html', 'w') as index: index.write('0123456789') - os.makedirs(self.testdir + '/assets/403') - os.chmod(self.testdir + '/assets/403', 0o000) + os.makedirs(self.temp_dir + '/assets/403') + os.chmod(self.temp_dir + '/assets/403', 0o000) self._load_conf( { @@ -22,48 +23,46 @@ class TestStatic(TestApplicationProto): "*:7080": {"pass": "routes"}, "*:7081": {"pass": "routes"}, }, - "routes": [{"action": {"share": self.testdir + "/assets"}}], + "routes": [{"action": {"share": self.temp_dir + "/assets"}}], "applications": {}, } ) - def tearDown(self): - os.chmod(self.testdir + '/assets/403', 0o777) + def teardown_method(self): + os.chmod(self.temp_dir + '/assets/403', 0o777) - super().tearDown() + super().teardown_method() def action_update(self, conf): - self.assertIn('success', self.conf(conf, 'routes/0/action')) + assert 'success' in self.conf(conf, 'routes/0/action') def test_fallback(self): self.action_update({"share": "/blah"}) - self.assertEqual(self.get()['status'], 404, 'bad path no fallback') + assert self.get()['status'] == 404, 'bad path no fallback' self.action_update({"share": "/blah", "fallback": {"return": 200}}) resp = self.get() - self.assertEqual(resp['status'], 200, 'bad path fallback status') - self.assertEqual(resp['body'], '', 'bad path fallback') + assert resp['status'] == 200, 'bad path fallback status' + assert resp['body'] == '', 'bad path fallback' def test_fallback_valid_path(self): self.action_update( - {"share": self.testdir + "/assets", "fallback": {"return": 200}} + {"share": self.temp_dir + "/assets", "fallback": {"return": 200}} ) resp = self.get() - self.assertEqual(resp['status'], 200, 'fallback status') - self.assertEqual(resp['body'], '0123456789', 'fallback') + assert resp['status'] == 200, 'fallback status' + assert resp['body'] == '0123456789', 'fallback' resp = self.get(url='/403/') - self.assertEqual(resp['status'], 200, 'fallback status 403') - self.assertEqual(resp['body'], '', 'fallback 403') + assert resp['status'] == 200, 'fallback status 403' + assert resp['body'] == '', 'fallback 403' resp = self.post() - self.assertEqual(resp['status'], 200, 'fallback status 405') - self.assertEqual(resp['body'], '', 'fallback 405') + assert resp['status'] == 200, 'fallback status 405' + assert resp['body'] == '', 'fallback 405' - self.assertEqual( - self.get(url='/dir')['status'], 301, 'fallback status 301' - ) + assert self.get(url='/dir')['status'] == 301, 'fallback status 301' def test_fallback_nested(self): self.action_update( @@ -77,62 +76,56 @@ class TestStatic(TestApplicationProto): ) resp = self.get() - self.assertEqual(resp['status'], 200, 'fallback nested status') - self.assertEqual(resp['body'], '', 'fallback nested') + assert resp['status'] == 200, 'fallback nested status' + assert resp['body'] == '', 'fallback nested' def test_fallback_share(self): self.action_update( { "share": "/blah", - "fallback": {"share": self.testdir + "/assets"}, + "fallback": {"share": self.temp_dir + "/assets"}, } ) resp = self.get() - self.assertEqual(resp['status'], 200, 'fallback share status') - self.assertEqual(resp['body'], '0123456789', 'fallback share') + assert resp['status'] == 200, 'fallback share status' + assert resp['body'] == '0123456789', 'fallback share' resp = self.head() - self.assertEqual(resp['status'], 200, 'fallback share status HEAD') - self.assertEqual(resp['body'], '', 'fallback share HEAD') + assert resp['status'] == 200, 'fallback share status HEAD' + assert resp['body'] == '', 'fallback share HEAD' - self.assertEqual( - self.get(url='/dir')['status'], 301, 'fallback share status 301' - ) + assert ( + self.get(url='/dir')['status'] == 301 + ), 'fallback share status 301' def test_fallback_proxy(self): - self.assertIn( - 'success', - self.conf( - [ - { - "match": {"destination": "*:7081"}, - "action": {"return": 200}, - }, - { - "action": { - "share": "/blah", - "fallback": {"proxy": "http://127.0.0.1:7081"}, - } - }, - ], - 'routes', - ), - 'configure fallback proxy route', - ) + assert 'success' in self.conf( + [ + { + "match": {"destination": "*:7081"}, + "action": {"return": 200}, + }, + { + "action": { + "share": "/blah", + "fallback": {"proxy": "http://127.0.0.1:7081"}, + } + }, + ], + 'routes', + ), 'configure fallback proxy route' resp = self.get() - self.assertEqual(resp['status'], 200, 'fallback proxy status') - self.assertEqual(resp['body'], '', 'fallback proxy') + assert resp['status'] == 200, 'fallback proxy status' + assert resp['body'] == '', 'fallback proxy' def test_fallback_proxy_loop(self): - self.skip_alerts.extend( - [ - r'open.*/blah/index.html.*failed', - r'accept.*failed', - r'socket.*failed', - r'new connections are not accepted', - ] + skip_alert( + r'open.*/blah/index.html.*failed', + r'accept.*failed', + r'socket.*failed', + r'new connections are not accepted', ) self.action_update( @@ -140,12 +133,12 @@ class TestStatic(TestApplicationProto): ) self.get(no_recv=True) - self.assertIn('success', self.conf_delete('listeners/*:7081')) + assert 'success' in self.conf_delete('listeners/*:7081') self.get(read_timeout=1) def test_fallback_invalid(self): def check_error(conf): - self.assertIn('error', self.conf(conf, 'routes/0/action')) + assert 'error' in self.conf(conf, 'routes/0/action') check_error({"share": "/blah", "fallback": {}}) check_error({"share": "/blah", "fallback": ""}) @@ -154,7 +147,3 @@ class TestStatic(TestApplicationProto): {"proxy": "http://127.0.0.1:7081", "fallback": {"share": "/blah"}} ) check_error({"fallback": {"share": "/blah"}}) - - -if __name__ == '__main__': - TestStatic.main() diff --git a/test/test_static.py b/test/test_static.py index bee5db28..0b82b4e8 100644 --- a/test/test_static.py +++ b/test/test_static.py @@ -1,21 +1,24 @@ import os import socket -import unittest +import pytest + +from conftest import waitforfiles from unit.applications.proto import TestApplicationProto class TestStatic(TestApplicationProto): prerequisites = {} - def setUp(self): - super().setUp() + def setup_method(self): + super().setup_method() - os.makedirs(self.testdir + '/assets/dir') - with open(self.testdir + '/assets/index.html', 'w') as index, \ - open(self.testdir + '/assets/README', 'w') as readme, \ - open(self.testdir + '/assets/log.log', 'w') as log, \ - open(self.testdir + '/assets/dir/file', 'w') as file: + os.makedirs(self.temp_dir + '/assets/dir') + with open(self.temp_dir + '/assets/index.html', 'w') as index, open( + self.temp_dir + '/assets/README', 'w' + ) as readme, open(self.temp_dir + '/assets/log.log', 'w') as log, open( + self.temp_dir + '/assets/dir/file', 'w' + ) as file: index.write('0123456789') readme.write('readme') log.write('[debug]') @@ -24,7 +27,7 @@ class TestStatic(TestApplicationProto): self._load_conf( { "listeners": {"*:7080": {"pass": "routes"}}, - "routes": [{"action": {"share": self.testdir + "/assets"}}], + "routes": [{"action": {"share": self.temp_dir + "/assets"}}], "settings": { "http": { "static": { @@ -36,126 +39,98 @@ class TestStatic(TestApplicationProto): ) def test_static_index(self): - self.assertEqual( - self.get(url='/index.html')['body'], '0123456789', 'index' - ) - self.assertEqual(self.get(url='/')['body'], '0123456789', 'index 2') - self.assertEqual(self.get(url='//')['body'], '0123456789', 'index 3') - self.assertEqual(self.get(url='/.')['body'], '0123456789', 'index 4') - self.assertEqual(self.get(url='/./')['body'], '0123456789', 'index 5') - self.assertEqual( - self.get(url='/?blah')['body'], '0123456789', 'index vars' - ) - self.assertEqual( - self.get(url='/#blah')['body'], '0123456789', 'index anchor' - ) - self.assertEqual( - self.get(url='/dir/')['status'], 404, 'index not found' - ) + assert self.get(url='/index.html')['body'] == '0123456789', 'index' + assert self.get(url='/')['body'] == '0123456789', 'index 2' + assert self.get(url='//')['body'] == '0123456789', 'index 3' + assert self.get(url='/.')['body'] == '0123456789', 'index 4' + assert self.get(url='/./')['body'] == '0123456789', 'index 5' + assert self.get(url='/?blah')['body'] == '0123456789', 'index vars' + assert self.get(url='/#blah')['body'] == '0123456789', 'index anchor' + assert self.get(url='/dir/')['status'] == 404, 'index not found' resp = self.get(url='/index.html/') - self.assertEqual(resp['status'], 404, 'index not found 2 status') - self.assertEqual( - resp['headers']['Content-Type'], - 'text/html', - 'index not found 2 Content-Type', - ) + assert resp['status'] == 404, 'index not found 2 status' + assert ( + resp['headers']['Content-Type'] == 'text/html' + ), 'index not found 2 Content-Type' def test_static_large_file(self): file_size = 32 * 1024 * 1024 - with open(self.testdir + '/assets/large', 'wb') as f: + with open(self.temp_dir + '/assets/large', 'wb') as f: f.seek(file_size - 1) f.write(b'\0') - self.assertEqual( - len( - self.get(url='/large', read_buffer_size=1024 * 1024)['body'] - ), - file_size, - 'large file', - ) + assert ( + len(self.get(url='/large', read_buffer_size=1024 * 1024)['body']) + == file_size + ), 'large file' def test_static_etag(self): etag = self.get(url='/')['headers']['ETag'] etag_2 = self.get(url='/README')['headers']['ETag'] - self.assertNotEqual(etag, etag_2, 'different ETag') - self.assertEqual( - etag, self.get(url='/')['headers']['ETag'], 'same ETag' - ) + assert etag != etag_2, 'different ETag' + assert etag == self.get(url='/')['headers']['ETag'], 'same ETag' - with open(self.testdir + '/assets/index.html', 'w') as f: + with open(self.temp_dir + '/assets/index.html', 'w') as f: f.write('blah') - self.assertNotEqual( - etag, self.get(url='/')['headers']['ETag'], 'new ETag' - ) + assert etag != self.get(url='/')['headers']['ETag'], 'new ETag' def test_static_redirect(self): resp = self.get(url='/dir') - self.assertEqual(resp['status'], 301, 'redirect status') - self.assertEqual( - resp['headers']['Location'], '/dir/', 'redirect Location' - ) - self.assertNotIn( - 'Content-Type', resp['headers'], 'redirect Content-Type' - ) + assert resp['status'] == 301, 'redirect status' + assert resp['headers']['Location'] == '/dir/', 'redirect Location' + assert 'Content-Type' not in resp['headers'], 'redirect Content-Type' def test_static_space_in_name(self): os.rename( - self.testdir + '/assets/dir/file', - self.testdir + '/assets/dir/fi le', - ) - self.waitforfiles(self.testdir + '/assets/dir/fi le') - self.assertEqual( - self.get(url='/dir/fi le')['body'], 'blah', 'file name' + self.temp_dir + '/assets/dir/file', + self.temp_dir + '/assets/dir/fi le', ) + assert waitforfiles(self.temp_dir + '/assets/dir/fi le') + assert self.get(url='/dir/fi le')['body'] == 'blah', 'file name' - os.rename(self.testdir + '/assets/dir', self.testdir + '/assets/di r') - self.waitforfiles(self.testdir + '/assets/di r/fi le') - self.assertEqual( - self.get(url='/di r/fi le')['body'], 'blah', 'dir name' - ) + os.rename(self.temp_dir + '/assets/dir', self.temp_dir + '/assets/di r') + assert waitforfiles(self.temp_dir + '/assets/di r/fi le') + assert self.get(url='/di r/fi le')['body'] == 'blah', 'dir name' os.rename( - self.testdir + '/assets/di r', self.testdir + '/assets/ di r ' - ) - self.waitforfiles(self.testdir + '/assets/ di r /fi le') - self.assertEqual( - self.get(url='/ di r /fi le')['body'], 'blah', 'dir name enclosing' - ) - - self.assertEqual( - self.get(url='/%20di%20r%20/fi le')['body'], 'blah', 'dir encoded' - ) - self.assertEqual( - self.get(url='/ di r %2Ffi le')['body'], 'blah', 'slash encoded' - ) - self.assertEqual( - self.get(url='/ di r /fi%20le')['body'], 'blah', 'file encoded' - ) - self.assertEqual( - self.get(url='/%20di%20r%20%2Ffi%20le')['body'], 'blah', 'encoded' - ) - self.assertEqual( - self.get(url='/%20%64%69%20%72%20%2F%66%69%20%6C%65')['body'], - 'blah', - 'encoded 2', - ) + self.temp_dir + '/assets/di r', self.temp_dir + '/assets/ di r ' + ) + assert waitforfiles(self.temp_dir + '/assets/ di r /fi le') + assert ( + self.get(url='/ di r /fi le')['body'] == 'blah' + ), 'dir name enclosing' + + assert ( + self.get(url='/%20di%20r%20/fi le')['body'] == 'blah' + ), 'dir encoded' + assert ( + self.get(url='/ di r %2Ffi le')['body'] == 'blah' + ), 'slash encoded' + assert ( + self.get(url='/ di r /fi%20le')['body'] == 'blah' + ), 'file encoded' + assert ( + self.get(url='/%20di%20r%20%2Ffi%20le')['body'] == 'blah' + ), 'encoded' + assert ( + self.get(url='/%20%64%69%20%72%20%2F%66%69%20%6C%65')['body'] + == 'blah' + ), 'encoded 2' os.rename( - self.testdir + '/assets/ di r /fi le', - self.testdir + '/assets/ di r / fi le ', - ) - self.waitforfiles(self.testdir + '/assets/ di r / fi le ') - self.assertEqual( - self.get(url='/%20di%20r%20/%20fi%20le%20')['body'], - 'blah', - 'file name enclosing', + self.temp_dir + '/assets/ di r /fi le', + self.temp_dir + '/assets/ di r / fi le ', ) + assert waitforfiles(self.temp_dir + '/assets/ di r / fi le ') + assert ( + self.get(url='/%20di%20r%20/%20fi%20le%20')['body'] == 'blah' + ), 'file name enclosing' try: - print('файл') + open(self.temp_dir + '/ф а', 'a').close() utf8 = True except: @@ -163,267 +138,190 @@ class TestStatic(TestApplicationProto): if utf8: os.rename( - self.testdir + '/assets/ di r / fi le ', - self.testdir + '/assets/ di r /фа йл', - ) - self.waitforfiles(self.testdir + '/assets/ di r /фа йл') - self.assertEqual( - self.get(url='/ di r /фа йл')['body'], 'blah', 'file name 2' + self.temp_dir + '/assets/ di r / fi le ', + self.temp_dir + '/assets/ di r /фа йл', ) + assert waitforfiles(self.temp_dir + '/assets/ di r /фа йл') + assert ( + self.get(url='/ di r /фа йл')['body'] == 'blah' + ), 'file name 2' os.rename( - self.testdir + '/assets/ di r ', - self.testdir + '/assets/ди ректория', - ) - self.waitforfiles(self.testdir + '/assets/ди ректория/фа йл') - self.assertEqual( - self.get(url='/ди ректория/фа йл')['body'], 'blah', 'dir name 2' + self.temp_dir + '/assets/ di r ', + self.temp_dir + '/assets/ди ректория', ) + assert waitforfiles(self.temp_dir + '/assets/ди ректория/фа йл') + assert ( + self.get(url='/ди ректория/фа йл')['body'] == 'blah' + ), 'dir name 2' def test_static_unix_socket(self): sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - sock.bind(self.testdir + '/assets/unix_socket') + sock.bind(self.temp_dir + '/assets/unix_socket') - self.assertEqual(self.get(url='/unix_socket')['status'], 404, 'socket') + assert self.get(url='/unix_socket')['status'] == 404, 'socket' sock.close() def test_static_unix_fifo(self): - os.mkfifo(self.testdir + '/assets/fifo') + os.mkfifo(self.temp_dir + '/assets/fifo') - self.assertEqual(self.get(url='/fifo')['status'], 404, 'fifo') + assert self.get(url='/fifo')['status'] == 404, 'fifo' def test_static_symlink(self): - os.symlink(self.testdir + '/assets/dir', self.testdir + '/assets/link') + os.symlink(self.temp_dir + '/assets/dir', self.temp_dir + '/assets/link') - self.assertEqual(self.get(url='/dir')['status'], 301, 'dir') - self.assertEqual(self.get(url='/dir/file')['status'], 200, 'file') - self.assertEqual(self.get(url='/link')['status'], 301, 'symlink dir') - self.assertEqual( - self.get(url='/link/file')['status'], 200, 'symlink file' - ) + assert self.get(url='/dir')['status'] == 301, 'dir' + assert self.get(url='/dir/file')['status'] == 200, 'file' + assert self.get(url='/link')['status'] == 301, 'symlink dir' + assert self.get(url='/link/file')['status'] == 200, 'symlink file' def test_static_method(self): resp = self.head() - self.assertEqual(resp['status'], 200, 'HEAD status') - self.assertEqual(resp['body'], '', 'HEAD empty body') + assert resp['status'] == 200, 'HEAD status' + assert resp['body'] == '', 'HEAD empty body' - self.assertEqual(self.delete()['status'], 405, 'DELETE') - self.assertEqual(self.post()['status'], 405, 'POST') - self.assertEqual(self.put()['status'], 405, 'PUT') + assert self.delete()['status'] == 405, 'DELETE' + assert self.post()['status'] == 405, 'POST' + assert self.put()['status'] == 405, 'PUT' def test_static_path(self): - self.assertEqual( - self.get(url='/dir/../dir/file')['status'], 200, 'relative' - ) + assert self.get(url='/dir/../dir/file')['status'] == 200, 'relative' - self.assertEqual(self.get(url='./')['status'], 400, 'path invalid') - self.assertEqual(self.get(url='../')['status'], 400, 'path invalid 2') - self.assertEqual(self.get(url='/..')['status'], 400, 'path invalid 3') - self.assertEqual( - self.get(url='../assets/')['status'], 400, 'path invalid 4' - ) - self.assertEqual( - self.get(url='/../assets/')['status'], 400, 'path invalid 5' - ) + assert self.get(url='./')['status'] == 400, 'path invalid' + assert self.get(url='../')['status'] == 400, 'path invalid 2' + assert self.get(url='/..')['status'] == 400, 'path invalid 3' + assert self.get(url='../assets/')['status'] == 400, 'path invalid 4' + assert self.get(url='/../assets/')['status'] == 400, 'path invalid 5' def test_static_two_clients(self): _, sock = self.get(url='/', start=True, no_recv=True) _, sock2 = self.get(url='/', start=True, no_recv=True) - self.assertEqual(sock.recv(1), b'H', 'client 1') - self.assertEqual(sock2.recv(1), b'H', 'client 2') - self.assertEqual(sock.recv(1), b'T', 'client 1 again') - self.assertEqual(sock2.recv(1), b'T', 'client 2 again') + assert sock.recv(1) == b'H', 'client 1' + assert sock2.recv(1) == b'H', 'client 2' + assert sock.recv(1) == b'T', 'client 1 again' + assert sock2.recv(1) == b'T', 'client 2 again' sock.close() sock2.close() def test_static_mime_types(self): - self.assertIn( - 'success', - self.conf( - { - "text/x-code/x-blah/x-blah": "readme", - "text/plain": [".html", ".log", "file"], - }, - 'settings/http/static/mime_types', - ), - 'configure mime_types', - ) - - self.assertEqual( - self.get(url='/README')['headers']['Content-Type'], - 'text/x-code/x-blah/x-blah', - 'mime_types string case insensitive', - ) - self.assertEqual( - self.get(url='/index.html')['headers']['Content-Type'], - 'text/plain', - 'mime_types html', - ) - self.assertEqual( - self.get(url='/')['headers']['Content-Type'], - 'text/plain', - 'mime_types index default', - ) - self.assertEqual( - self.get(url='/dir/file')['headers']['Content-Type'], - 'text/plain', - 'mime_types file in dir', - ) + assert 'success' in self.conf( + { + "text/x-code/x-blah/x-blah": "readme", + "text/plain": [".html", ".log", "file"], + }, + 'settings/http/static/mime_types', + ), 'configure mime_types' + + assert ( + self.get(url='/README')['headers']['Content-Type'] + == 'text/x-code/x-blah/x-blah' + ), 'mime_types string case insensitive' + assert ( + self.get(url='/index.html')['headers']['Content-Type'] + == 'text/plain' + ), 'mime_types html' + assert ( + self.get(url='/')['headers']['Content-Type'] == 'text/plain' + ), 'mime_types index default' + assert ( + self.get(url='/dir/file')['headers']['Content-Type'] + == 'text/plain' + ), 'mime_types file in dir' def test_static_mime_types_partial_match(self): - self.assertIn( - 'success', - self.conf( - { - "text/x-blah": ["ile", "fil", "f", "e", ".file"], - }, - 'settings/http/static/mime_types', - ), - 'configure mime_types', - ) - self.assertNotIn( - 'Content-Type', self.get(url='/dir/file'), 'partial match' - ) + assert 'success' in self.conf( + {"text/x-blah": ["ile", "fil", "f", "e", ".file"],}, + 'settings/http/static/mime_types', + ), 'configure mime_types' + assert 'Content-Type' not in self.get(url='/dir/file'), 'partial match' def test_static_mime_types_reconfigure(self): - self.assertIn( - 'success', - self.conf( - { - "text/x-code": "readme", - "text/plain": [".html", ".log", "file"], - }, - 'settings/http/static/mime_types', - ), - 'configure mime_types', - ) - - self.assertEqual( - self.conf_get('settings/http/static/mime_types'), - {'text/x-code': 'readme', 'text/plain': ['.html', '.log', 'file']}, - 'mime_types get', - ) - self.assertEqual( - self.conf_get('settings/http/static/mime_types/text%2Fx-code'), - 'readme', - 'mime_types get string', - ) - self.assertEqual( - self.conf_get('settings/http/static/mime_types/text%2Fplain'), - ['.html', '.log', 'file'], - 'mime_types get array', - ) - self.assertEqual( - self.conf_get('settings/http/static/mime_types/text%2Fplain/1'), - '.log', - 'mime_types get array element', - ) - - self.assertIn( - 'success', - self.conf_delete('settings/http/static/mime_types/text%2Fplain/2'), - 'mime_types remove array element', - ) - self.assertNotIn( - 'Content-Type', - self.get(url='/dir/file')['headers'], - 'mime_types removed', - ) - - self.assertIn( - 'success', - self.conf_post( - '"file"', 'settings/http/static/mime_types/text%2Fplain' - ), - 'mime_types add array element', - ) - self.assertEqual( - self.get(url='/dir/file')['headers']['Content-Type'], - 'text/plain', - 'mime_types reverted', - ) - - self.assertIn( - 'success', - self.conf( - '"file"', 'settings/http/static/mime_types/text%2Fplain' - ), - 'configure mime_types update', - ) - self.assertEqual( - self.get(url='/dir/file')['headers']['Content-Type'], - 'text/plain', - 'mime_types updated', - ) - self.assertNotIn( - 'Content-Type', - self.get(url='/log.log')['headers'], - 'mime_types updated 2', - ) - - self.assertIn( - 'success', - self.conf( - '".log"', 'settings/http/static/mime_types/text%2Fblahblahblah' - ), - 'configure mime_types create', - ) - self.assertEqual( - self.get(url='/log.log')['headers']['Content-Type'], - 'text/blahblahblah', - 'mime_types create', - ) + assert 'success' in self.conf( + { + "text/x-code": "readme", + "text/plain": [".html", ".log", "file"], + }, + 'settings/http/static/mime_types', + ), 'configure mime_types' + + assert self.conf_get('settings/http/static/mime_types') == { + 'text/x-code': 'readme', + 'text/plain': ['.html', '.log', 'file'], + }, 'mime_types get' + assert ( + self.conf_get('settings/http/static/mime_types/text%2Fx-code') + == 'readme' + ), 'mime_types get string' + assert self.conf_get( + 'settings/http/static/mime_types/text%2Fplain' + ) == ['.html', '.log', 'file'], 'mime_types get array' + assert ( + self.conf_get('settings/http/static/mime_types/text%2Fplain/1') + == '.log' + ), 'mime_types get array element' + + assert 'success' in self.conf_delete( + 'settings/http/static/mime_types/text%2Fplain/2' + ), 'mime_types remove array element' + assert ( + 'Content-Type' not in self.get(url='/dir/file')['headers'] + ), 'mime_types removed' + + assert 'success' in self.conf_post( + '"file"', 'settings/http/static/mime_types/text%2Fplain' + ), 'mime_types add array element' + assert ( + self.get(url='/dir/file')['headers']['Content-Type'] + == 'text/plain' + ), 'mime_types reverted' + + assert 'success' in self.conf( + '"file"', 'settings/http/static/mime_types/text%2Fplain' + ), 'configure mime_types update' + assert ( + self.get(url='/dir/file')['headers']['Content-Type'] + == 'text/plain' + ), 'mime_types updated' + assert ( + 'Content-Type' not in self.get(url='/log.log')['headers'] + ), 'mime_types updated 2' + + assert 'success' in self.conf( + '".log"', 'settings/http/static/mime_types/text%2Fblahblahblah' + ), 'configure mime_types create' + assert ( + self.get(url='/log.log')['headers']['Content-Type'] + == 'text/blahblahblah' + ), 'mime_types create' def test_static_mime_types_correct(self): - self.assertIn( - 'error', - self.conf( - {"text/x-code": "readme", "text/plain": "readme"}, - 'settings/http/static/mime_types', - ), - 'mime_types same extensions', - ) - self.assertIn( - 'error', - self.conf( - {"text/x-code": [".h", ".c"], "text/plain": ".c"}, - 'settings/http/static/mime_types', - ), - 'mime_types same extensions array', - ) - self.assertIn( - 'error', - self.conf( - { - "text/x-code": [".h", ".c", "readme"], - "text/plain": "README", - }, - 'settings/http/static/mime_types', - ), - 'mime_types same extensions case insensitive', - ) - - @unittest.skip('not yet') + assert 'error' in self.conf( + {"text/x-code": "readme", "text/plain": "readme"}, + 'settings/http/static/mime_types', + ), 'mime_types same extensions' + assert 'error' in self.conf( + {"text/x-code": [".h", ".c"], "text/plain": ".c"}, + 'settings/http/static/mime_types', + ), 'mime_types same extensions array' + assert 'error' in self.conf( + {"text/x-code": [".h", ".c", "readme"], "text/plain": "README",}, + 'settings/http/static/mime_types', + ), 'mime_types same extensions case insensitive' + + @pytest.mark.skip('not yet') def test_static_mime_types_invalid(self): - self.assertIn( - 'error', - self.http( - b"""PUT /config/settings/http/static/mime_types/%0%00% HTTP/1.1\r + assert 'error' in self.http( + b"""PUT /config/settings/http/static/mime_types/%0%00% HTTP/1.1\r Host: localhost\r Connection: close\r Content-Length: 6\r \r \"blah\"""", - raw_resp=True, - raw=True, - sock_type='unix', - addr=self.testdir + '/control.unit.sock', - ), - 'mime_types invalid', - ) - -if __name__ == '__main__': - TestStatic.main() + raw_resp=True, + raw=True, + sock_type='unix', + addr=self.temp_dir + '/control.unit.sock', + ), 'mime_types invalid' diff --git a/test/test_tls.py b/test/test_tls.py index a0434174..518a834c 100644 --- a/test/test_tls.py +++ b/test/test_tls.py @@ -2,8 +2,10 @@ import io import re import ssl import subprocess -import unittest +import pytest + +from conftest import skip_alert from unit.applications.tls import TestApplicationTLS @@ -11,7 +13,7 @@ class TestTLS(TestApplicationTLS): prerequisites = {'modules': {'python': 'any', 'openssl': 'any'}} def findall(self, pattern): - with open(self.testdir + '/unit.log', 'r', errors='ignore') as f: + with open(self.temp_dir + '/unit.log', 'r', errors='ignore') as f: return re.findall(pattern, f.read()) def openssl_date_to_sec_epoch(self, date): @@ -38,7 +40,7 @@ class TestTLS(TestApplicationTLS): self.add_tls() - self.assertEqual(self.get_ssl()['status'], 200, 'add listener option') + assert self.get_ssl()['status'] == 200, 'add listener option' def test_tls_listener_option_remove(self): self.load('empty') @@ -51,18 +53,16 @@ class TestTLS(TestApplicationTLS): self.remove_tls() - self.assertEqual(self.get()['status'], 200, 'remove listener option') + assert self.get()['status'] == 200, 'remove listener option' def test_tls_certificate_remove(self): self.load('empty') self.certificate() - self.assertIn( - 'success', - self.conf_delete('/certificates/default'), - 'remove certificate', - ) + assert 'success' in self.conf_delete( + '/certificates/default' + ), 'remove certificate' def test_tls_certificate_remove_used(self): self.load('empty') @@ -71,11 +71,9 @@ class TestTLS(TestApplicationTLS): self.add_tls() - self.assertIn( - 'error', - self.conf_delete('/certificates/default'), - 'remove certificate', - ) + assert 'error' in self.conf_delete( + '/certificates/default' + ), 'remove certificate' def test_tls_certificate_remove_nonexisting(self): self.load('empty') @@ -84,13 +82,11 @@ class TestTLS(TestApplicationTLS): self.add_tls() - self.assertIn( - 'error', - self.conf_delete('/certificates/blah'), - 'remove nonexistings certificate', - ) + assert 'error' in self.conf_delete( + '/certificates/blah' + ), 'remove nonexistings certificate' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_tls_certificate_update(self): self.load('empty') @@ -102,20 +98,18 @@ class TestTLS(TestApplicationTLS): self.certificate() - self.assertNotEqual( - cert_old, self.get_server_certificate(), 'update certificate' - ) + assert cert_old != self.get_server_certificate(), 'update certificate' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_tls_certificate_key_incorrect(self): self.load('empty') self.certificate('first', False) self.certificate('second', False) - self.assertIn( - 'error', self.certificate_load('first', 'second'), 'key incorrect' - ) + assert 'error' in self.certificate_load( + 'first', 'second' + ), 'key incorrect' def test_tls_certificate_change(self): self.load('empty') @@ -129,20 +123,16 @@ class TestTLS(TestApplicationTLS): self.add_tls(cert='new') - self.assertNotEqual( - cert_old, self.get_server_certificate(), 'change certificate' - ) + assert cert_old != self.get_server_certificate(), 'change certificate' def test_tls_certificate_key_rsa(self): self.load('empty') self.certificate() - self.assertEqual( - self.conf_get('/certificates/default/key'), - 'RSA (2048 bits)', - 'certificate key rsa', - ) + assert ( + self.conf_get('/certificates/default/key') == 'RSA (2048 bits)' + ), 'certificate key rsa' def test_tls_certificate_key_ec(self): self.load('empty') @@ -155,8 +145,10 @@ class TestTLS(TestApplicationTLS): 'ecparam', '-noout', '-genkey', - '-out', self.testdir + '/ec.key', - '-name', 'prime256v1', + '-out', + self.temp_dir + '/ec.key', + '-name', + 'prime256v1', ], stderr=subprocess.STDOUT, ) @@ -167,19 +159,23 @@ class TestTLS(TestApplicationTLS): 'req', '-x509', '-new', - '-subj', '/CN=ec/', - '-config', self.testdir + '/openssl.conf', - '-key', self.testdir + '/ec.key', - '-out', self.testdir + '/ec.crt', + '-subj', + '/CN=ec/', + '-config', + self.temp_dir + '/openssl.conf', + '-key', + self.temp_dir + '/ec.key', + '-out', + self.temp_dir + '/ec.crt', ], stderr=subprocess.STDOUT, ) self.certificate_load('ec') - self.assertEqual( - self.conf_get('/certificates/ec/key'), 'ECDH', 'certificate key ec' - ) + assert ( + self.conf_get('/certificates/ec/key') == 'ECDH' + ), 'certificate key ec' def test_tls_certificate_chain_options(self): self.load('empty') @@ -188,35 +184,29 @@ class TestTLS(TestApplicationTLS): chain = self.conf_get('/certificates/default/chain') - self.assertEqual(len(chain), 1, 'certificate chain length') + assert len(chain) == 1, 'certificate chain length' cert = chain[0] - self.assertEqual( - cert['subject']['common_name'], - 'default', - 'certificate subject common name', - ) - self.assertEqual( - cert['issuer']['common_name'], - 'default', - 'certificate issuer common name', - ) + assert ( + cert['subject']['common_name'] == 'default' + ), 'certificate subject common name' + assert ( + cert['issuer']['common_name'] == 'default' + ), 'certificate issuer common name' - self.assertLess( + assert ( abs( self.sec_epoch() - self.openssl_date_to_sec_epoch(cert['validity']['since']) - ), - 5, - 'certificate validity since', - ) - self.assertEqual( + ) + < 5 + ), 'certificate validity since' + assert ( self.openssl_date_to_sec_epoch(cert['validity']['until']) - - self.openssl_date_to_sec_epoch(cert['validity']['since']), - 2592000, - 'certificate validity until', - ) + - self.openssl_date_to_sec_epoch(cert['validity']['since']) + == 2592000 + ), 'certificate validity until' def test_tls_certificate_chain(self): self.load('empty') @@ -228,10 +218,14 @@ class TestTLS(TestApplicationTLS): 'openssl', 'req', '-new', - '-subj', '/CN=int/', - '-config', self.testdir + '/openssl.conf', - '-out', self.testdir + '/int.csr', - '-keyout', self.testdir + '/int.key', + '-subj', + '/CN=int/', + '-config', + self.temp_dir + '/openssl.conf', + '-out', + self.temp_dir + '/int.csr', + '-keyout', + self.temp_dir + '/int.key', ], stderr=subprocess.STDOUT, ) @@ -241,15 +235,19 @@ class TestTLS(TestApplicationTLS): 'openssl', 'req', '-new', - '-subj', '/CN=end/', - '-config', self.testdir + '/openssl.conf', - '-out', self.testdir + '/end.csr', - '-keyout', self.testdir + '/end.key', + '-subj', + '/CN=end/', + '-config', + self.temp_dir + '/openssl.conf', + '-out', + self.temp_dir + '/end.csr', + '-keyout', + self.temp_dir + '/end.key', ], stderr=subprocess.STDOUT, ) - with open(self.testdir + '/ca.conf', 'w') as f: + with open(self.temp_dir + '/ca.conf', 'w') as f: f.write( """[ ca ] default_ca = myca @@ -269,16 +267,16 @@ commonName = supplied [ myca_extensions ] basicConstraints = critical,CA:TRUE""" % { - 'dir': self.testdir, - 'database': self.testdir + '/certindex', - 'certserial': self.testdir + '/certserial', + 'dir': self.temp_dir, + 'database': self.temp_dir + '/certindex', + 'certserial': self.temp_dir + '/certserial', } ) - with open(self.testdir + '/certserial', 'w') as f: + with open(self.temp_dir + '/certserial', 'w') as f: f.write('1000') - with open(self.testdir + '/certindex', 'w') as f: + with open(self.temp_dir + '/certindex', 'w') as f: f.write('') subprocess.call( @@ -286,12 +284,18 @@ basicConstraints = critical,CA:TRUE""" 'openssl', 'ca', '-batch', - '-subj', '/CN=int/', - '-config', self.testdir + '/ca.conf', - '-keyfile', self.testdir + '/root.key', - '-cert', self.testdir + '/root.crt', - '-in', self.testdir + '/int.csr', - '-out', self.testdir + '/int.crt', + '-subj', + '/CN=int/', + '-config', + self.temp_dir + '/ca.conf', + '-keyfile', + self.temp_dir + '/root.key', + '-cert', + self.temp_dir + '/root.crt', + '-in', + self.temp_dir + '/int.csr', + '-out', + self.temp_dir + '/int.crt', ], stderr=subprocess.STDOUT, ) @@ -301,50 +305,50 @@ basicConstraints = critical,CA:TRUE""" 'openssl', 'ca', '-batch', - '-subj', '/CN=end/', - '-config', self.testdir + '/ca.conf', - '-keyfile', self.testdir + '/int.key', - '-cert', self.testdir + '/int.crt', - '-in', self.testdir + '/end.csr', - '-out', self.testdir + '/end.crt', + '-subj', + '/CN=end/', + '-config', + self.temp_dir + '/ca.conf', + '-keyfile', + self.temp_dir + '/int.key', + '-cert', + self.temp_dir + '/int.crt', + '-in', + self.temp_dir + '/end.csr', + '-out', + self.temp_dir + '/end.crt', ], stderr=subprocess.STDOUT, ) - crt_path = self.testdir + '/end-int.crt' - end_path = self.testdir + '/end.crt' - int_path = self.testdir + '/int.crt' + crt_path = self.temp_dir + '/end-int.crt' + end_path = self.temp_dir + '/end.crt' + int_path = self.temp_dir + '/int.crt' - with open(crt_path, 'wb') as crt, \ - open(end_path, 'rb') as end, \ - open(int_path, 'rb') as int: + with open(crt_path, 'wb') as crt, open(end_path, 'rb') as end, open( + int_path, 'rb' + ) as int: crt.write(end.read() + int.read()) self.context = ssl.create_default_context() self.context.check_hostname = False self.context.verify_mode = ssl.CERT_REQUIRED - self.context.load_verify_locations(self.testdir + '/root.crt') + self.context.load_verify_locations(self.temp_dir + '/root.crt') # incomplete chain - self.assertIn( - 'success', - self.certificate_load('end', 'end'), - 'certificate chain end upload', - ) + assert 'success' in self.certificate_load( + 'end', 'end' + ), 'certificate chain end upload' chain = self.conf_get('/certificates/end/chain') - self.assertEqual(len(chain), 1, 'certificate chain end length') - self.assertEqual( - chain[0]['subject']['common_name'], - 'end', - 'certificate chain end subject common name', - ) - self.assertEqual( - chain[0]['issuer']['common_name'], - 'int', - 'certificate chain end issuer common name', - ) + assert len(chain) == 1, 'certificate chain end length' + assert ( + chain[0]['subject']['common_name'] == 'end' + ), 'certificate chain end subject common name' + assert ( + chain[0]['issuer']['common_name'] == 'int' + ), 'certificate chain end issuer common name' self.add_tls(cert='end') @@ -353,79 +357,61 @@ basicConstraints = critical,CA:TRUE""" except ssl.SSLError: resp = None - self.assertEqual(resp, None, 'certificate chain incomplete chain') + assert resp == None, 'certificate chain incomplete chain' # intermediate - self.assertIn( - 'success', - self.certificate_load('int', 'int'), - 'certificate chain int upload', - ) + assert 'success' in self.certificate_load( + 'int', 'int' + ), 'certificate chain int upload' chain = self.conf_get('/certificates/int/chain') - self.assertEqual(len(chain), 1, 'certificate chain int length') - self.assertEqual( - chain[0]['subject']['common_name'], - 'int', - 'certificate chain int subject common name', - ) - self.assertEqual( - chain[0]['issuer']['common_name'], - 'root', - 'certificate chain int issuer common name', - ) + assert len(chain) == 1, 'certificate chain int length' + assert ( + chain[0]['subject']['common_name'] == 'int' + ), 'certificate chain int subject common name' + assert ( + chain[0]['issuer']['common_name'] == 'root' + ), 'certificate chain int issuer common name' self.add_tls(cert='int') - self.assertEqual( - self.get_ssl()['status'], 200, 'certificate chain intermediate' - ) + assert ( + self.get_ssl()['status'] == 200 + ), 'certificate chain intermediate' # intermediate server - self.assertIn( - 'success', - self.certificate_load('end-int', 'end'), - 'certificate chain end-int upload', - ) + assert 'success' in self.certificate_load( + 'end-int', 'end' + ), 'certificate chain end-int upload' chain = self.conf_get('/certificates/end-int/chain') - self.assertEqual(len(chain), 2, 'certificate chain end-int length') - self.assertEqual( - chain[0]['subject']['common_name'], - 'end', - 'certificate chain end-int int subject common name', - ) - self.assertEqual( - chain[0]['issuer']['common_name'], - 'int', - 'certificate chain end-int int issuer common name', - ) - self.assertEqual( - chain[1]['subject']['common_name'], - 'int', - 'certificate chain end-int end subject common name', - ) - self.assertEqual( - chain[1]['issuer']['common_name'], - 'root', - 'certificate chain end-int end issuer common name', - ) + assert len(chain) == 2, 'certificate chain end-int length' + assert ( + chain[0]['subject']['common_name'] == 'end' + ), 'certificate chain end-int int subject common name' + assert ( + chain[0]['issuer']['common_name'] == 'int' + ), 'certificate chain end-int int issuer common name' + assert ( + chain[1]['subject']['common_name'] == 'int' + ), 'certificate chain end-int end subject common name' + assert ( + chain[1]['issuer']['common_name'] == 'root' + ), 'certificate chain end-int end issuer common name' self.add_tls(cert='end-int') - self.assertEqual( - self.get_ssl()['status'], - 200, - 'certificate chain intermediate server', - ) + assert ( + self.get_ssl()['status'] == 200 + ), 'certificate chain intermediate server' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_tls_reconfigure(self): self.load('empty') - self.assertEqual(self.get()['status'], 200, 'init') + assert self.get()['status'] == 200, 'init' self.certificate() @@ -435,21 +421,17 @@ basicConstraints = critical,CA:TRUE""" read_timeout=1, ) - self.assertEqual(resp['status'], 200, 'initial status') + assert resp['status'] == 200, 'initial status' self.add_tls() - self.assertEqual( - self.get(sock=sock)['status'], 200, 'reconfigure status' - ) - self.assertEqual( - self.get_ssl()['status'], 200, 'reconfigure tls status' - ) + assert self.get(sock=sock)['status'] == 200, 'reconfigure status' + assert self.get_ssl()['status'] == 200, 'reconfigure tls status' def test_tls_keepalive(self): self.load('mirror') - self.assertEqual(self.get()['status'], 200, 'init') + assert self.get()['status'] == 200, 'init' self.certificate() @@ -466,7 +448,7 @@ basicConstraints = critical,CA:TRUE""" read_timeout=1, ) - self.assertEqual(resp['body'], '0123456789', 'keepalive 1') + assert resp['body'] == '0123456789', 'keepalive 1' resp = self.post_ssl( headers={ @@ -478,13 +460,13 @@ basicConstraints = critical,CA:TRUE""" body='0123456789', ) - self.assertEqual(resp['body'], '0123456789', 'keepalive 2') + assert resp['body'] == '0123456789', 'keepalive 2' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_tls_keepalive_certificate_remove(self): self.load('empty') - self.assertEqual(self.get()['status'], 200, 'init') + assert self.get()['status'] == 200, 'init' self.certificate() @@ -506,19 +488,17 @@ basicConstraints = critical,CA:TRUE""" except: resp = None - self.assertEqual(resp, None, 'keepalive remove certificate') + assert resp == None, 'keepalive remove certificate' - @unittest.skip('not yet') + @pytest.mark.skip('not yet') def test_tls_certificates_remove_all(self): self.load('empty') self.certificate() - self.assertIn( - 'success', - self.conf_delete('/certificates'), - 'remove all certificates', - ) + assert 'success' in self.conf_delete( + '/certificates' + ), 'remove all certificates' def test_tls_application_respawn(self): self.load('mirror') @@ -544,11 +524,11 @@ basicConstraints = critical,CA:TRUE""" subprocess.call(['kill', '-9', app_id]) - self.skip_alerts.append(r'process %s exited on signal 9' % app_id) + skip_alert(r'process %s exited on signal 9' % app_id) self.wait_for_record( re.compile( - ' (?!' + app_id + '#)(\d+)#\d+ "mirror" application started' + r' (?!' + app_id + r'#)(\d+)#\d+ "mirror" application started' ) ) @@ -562,15 +542,13 @@ basicConstraints = critical,CA:TRUE""" body='0123456789', ) - self.assertEqual(resp['status'], 200, 'application respawn status') - self.assertEqual( - resp['body'], '0123456789', 'application respawn body' - ) + assert resp['status'] == 200, 'application respawn status' + assert resp['body'] == '0123456789', 'application respawn body' def test_tls_url_scheme(self): self.load('variables') - self.assertEqual( + assert ( self.post( headers={ 'Host': 'localhost', @@ -578,16 +556,15 @@ basicConstraints = critical,CA:TRUE""" 'Custom-Header': '', 'Connection': 'close', } - )['headers']['Wsgi-Url-Scheme'], - 'http', - 'url scheme http', - ) + )['headers']['Wsgi-Url-Scheme'] + == 'http' + ), 'url scheme http' self.certificate() self.add_tls(application='variables') - self.assertEqual( + assert ( self.post_ssl( headers={ 'Host': 'localhost', @@ -595,10 +572,9 @@ basicConstraints = critical,CA:TRUE""" 'Custom-Header': '', 'Connection': 'close', } - )['headers']['Wsgi-Url-Scheme'], - 'https', - 'url scheme https', - ) + )['headers']['Wsgi-Url-Scheme'] + == 'https' + ), 'url scheme https' def test_tls_big_upload(self): self.load('upload') @@ -610,15 +586,14 @@ basicConstraints = critical,CA:TRUE""" filename = 'test.txt' data = '0123456789' * 9000 - res = self.post_ssl(body={ - 'file': { - 'filename': filename, - 'type': 'text/plain', - 'data': io.StringIO(data), + res = self.post_ssl( + body={ + 'file': { + 'filename': filename, + 'type': 'text/plain', + 'data': io.StringIO(data), + } } - }) - self.assertEqual(res['status'], 200, 'status ok') - self.assertEqual(res['body'], filename + data) - -if __name__ == '__main__': - TestTLS.main() + ) + assert res['status'] == 200, 'status ok' + assert res['body'] == filename + data diff --git a/test/test_upstreams_rr.py b/test/test_upstreams_rr.py index 2f74fbde..2ecf1d9a 100644 --- a/test/test_upstreams_rr.py +++ b/test/test_upstreams_rr.py @@ -1,50 +1,47 @@ import os import re +from conftest import option from unit.applications.lang.python import TestApplicationPython class TestUpstreamsRR(TestApplicationPython): prerequisites = {'modules': {'python': 'any'}} - def setUp(self): - super().setUp() + def setup_method(self): + super().setup_method() - self.assertIn( - 'success', - self.conf( - { - "listeners": { - "*:7080": {"pass": "upstreams/one"}, - "*:7090": {"pass": "upstreams/two"}, - "*:7081": {"pass": "routes/one"}, - "*:7082": {"pass": "routes/two"}, - "*:7083": {"pass": "routes/three"}, - }, - "upstreams": { - "one": { - "servers": { - "127.0.0.1:7081": {}, - "127.0.0.1:7082": {}, - }, - }, - "two": { - "servers": { - "127.0.0.1:7081": {}, - "127.0.0.1:7082": {}, - }, + assert 'success' in self.conf( + { + "listeners": { + "*:7080": {"pass": "upstreams/one"}, + "*:7090": {"pass": "upstreams/two"}, + "*:7081": {"pass": "routes/one"}, + "*:7082": {"pass": "routes/two"}, + "*:7083": {"pass": "routes/three"}, + }, + "upstreams": { + "one": { + "servers": { + "127.0.0.1:7081": {}, + "127.0.0.1:7082": {}, }, }, - "routes": { - "one": [{"action": {"return": 200}}], - "two": [{"action": {"return": 201}}], - "three": [{"action": {"return": 202}}], + "two": { + "servers": { + "127.0.0.1:7081": {}, + "127.0.0.1:7082": {}, + }, }, - "applications": {}, }, - ), - 'upstreams initial configuration', - ) + "routes": { + "one": [{"action": {"return": 200}}], + "two": [{"action": {"return": 201}}], + "three": [{"action": {"return": 202}}], + }, + "applications": {}, + }, + ), 'upstreams initial configuration' self.cpu_count = os.cpu_count() @@ -91,113 +88,87 @@ Connection: close def test_upstreams_rr_no_weight(self): resps = self.get_resps() - self.assertEqual(sum(resps), 100, 'no weight sum') - self.assertLessEqual( - abs(resps[0] - resps[1]), self.cpu_count, 'no weight' - ) + assert sum(resps) == 100, 'no weight sum' + assert abs(resps[0] - resps[1]) <= self.cpu_count, 'no weight' - self.assertIn( - 'success', - self.conf_delete('upstreams/one/servers/127.0.0.1:7081'), - 'no weight server remove', - ) + assert 'success' in self.conf_delete( + 'upstreams/one/servers/127.0.0.1:7081' + ), 'no weight server remove' resps = self.get_resps(req=50) - self.assertEqual(resps[1], 50, 'no weight 2') + assert resps[1] == 50, 'no weight 2' - self.assertIn( - 'success', - self.conf({}, 'upstreams/one/servers/127.0.0.1:7081'), - 'no weight server revert', - ) + assert 'success' in self.conf( + {}, 'upstreams/one/servers/127.0.0.1:7081' + ), 'no weight server revert' resps = self.get_resps() - self.assertEqual(sum(resps), 100, 'no weight 3 sum') - self.assertLessEqual( - abs(resps[0] - resps[1]), self.cpu_count, 'no weight 3' - ) + assert sum(resps) == 100, 'no weight 3 sum' + assert abs(resps[0] - resps[1]) <= self.cpu_count, 'no weight 3' - self.assertIn( - 'success', - self.conf({}, 'upstreams/one/servers/127.0.0.1:7083'), - 'no weight server new', - ) + assert 'success' in self.conf( + {}, 'upstreams/one/servers/127.0.0.1:7083' + ), 'no weight server new' resps = self.get_resps() - self.assertEqual(sum(resps), 100, 'no weight 4 sum') - self.assertLessEqual( - max(resps) - min(resps), self.cpu_count, 'no weight 4' - ) + assert sum(resps) == 100, 'no weight 4 sum' + assert max(resps) - min(resps) <= self.cpu_count, 'no weight 4' resps = self.get_resps_sc(req=30) - self.assertEqual(resps[0], 10, 'no weight 4 0') - self.assertEqual(resps[1], 10, 'no weight 4 1') - self.assertEqual(resps[2], 10, 'no weight 4 2') + assert resps[0] == 10, 'no weight 4 0' + assert resps[1] == 10, 'no weight 4 1' + assert resps[2] == 10, 'no weight 4 2' def test_upstreams_rr_weight(self): - self.assertIn( - 'success', - self.conf({"weight": 3}, 'upstreams/one/servers/127.0.0.1:7081'), - 'configure weight', - ) + assert 'success' in self.conf( + {"weight": 3}, 'upstreams/one/servers/127.0.0.1:7081' + ), 'configure weight' resps = self.get_resps_sc() - self.assertEqual(resps[0], 75, 'weight 3 0') - self.assertEqual(resps[1], 25, 'weight 3 1') + assert resps[0] == 75, 'weight 3 0' + assert resps[1] == 25, 'weight 3 1' - self.assertIn( - 'success', - self.conf_delete('upstreams/one/servers/127.0.0.1:7081/weight'), - 'configure weight remove', - ) + assert 'success' in self.conf_delete( + 'upstreams/one/servers/127.0.0.1:7081/weight' + ), 'configure weight remove' resps = self.get_resps_sc(req=10) - self.assertEqual(resps[0], 5, 'weight 0 0') - self.assertEqual(resps[1], 5, 'weight 0 1') + assert resps[0] == 5, 'weight 0 0' + assert resps[1] == 5, 'weight 0 1' - self.assertIn( - 'success', - self.conf('1', 'upstreams/one/servers/127.0.0.1:7081/weight'), - 'configure weight 1', - ) + assert 'success' in self.conf( + '1', 'upstreams/one/servers/127.0.0.1:7081/weight' + ), 'configure weight 1' resps = self.get_resps_sc() - self.assertEqual(resps[0], 50, 'weight 1 0') - self.assertEqual(resps[1], 50, 'weight 1 1') + assert resps[0] == 50, 'weight 1 0' + assert resps[1] == 50, 'weight 1 1' - self.assertIn( - 'success', - self.conf( - { - "127.0.0.1:7081": {"weight": 3}, - "127.0.0.1:7083": {"weight": 2}, - }, - 'upstreams/one/servers', - ), - 'configure weight 2', - ) + assert 'success' in self.conf( + { + "127.0.0.1:7081": {"weight": 3}, + "127.0.0.1:7083": {"weight": 2}, + }, + 'upstreams/one/servers', + ), 'configure weight 2' resps = self.get_resps_sc() - self.assertEqual(resps[0], 60, 'weight 2 0') - self.assertEqual(resps[2], 40, 'weight 2 1') + assert resps[0] == 60, 'weight 2 0' + assert resps[2] == 40, 'weight 2 1' def test_upstreams_rr_weight_rational(self): def set_weights(w1, w2): - self.assertIn( - 'success', - self.conf( - { - "127.0.0.1:7081": {"weight": w1}, - "127.0.0.1:7082": {"weight": w2}, - }, - 'upstreams/one/servers', - ), - 'configure weights', - ) + assert 'success' in self.conf( + { + "127.0.0.1:7081": {"weight": w1}, + "127.0.0.1:7082": {"weight": w2}, + }, + 'upstreams/one/servers', + ), 'configure weights' def check_reqs(w1, w2, reqs=10): resps = self.get_resps_sc(req=reqs) - self.assertEqual(resps[0], reqs * w1 / (w1 + w2), 'weight 1') - self.assertEqual(resps[1], reqs * w2 / (w1 + w2), 'weight 2') + assert resps[0] == reqs * w1 / (w1 + w2), 'weight 1' + assert resps[1] == reqs * w2 / (w1 + w2), 'weight 2' def check_weights(w1, w2): set_weights(w1, w2) @@ -207,39 +178,33 @@ Connection: close check_weights(0, 999999.0123456) check_weights(1, 9) check_weights(100000, 900000) - check_weights(1, .25) check_weights(1, 0.25) - check_weights(0.2, .8) + check_weights(1, 0.25) + check_weights(0.2, 0.8) check_weights(1, 1.5) - check_weights(1e-3, 1E-3) + check_weights(1e-3, 1e-3) check_weights(1e-20, 1e-20) check_weights(1e4, 1e4) check_weights(1000000, 1000000) set_weights(0.25, 0.25) - self.assertIn( - 'success', - self.conf_delete('upstreams/one/servers/127.0.0.1:7081/weight'), - 'delete weight', - ) + assert 'success' in self.conf_delete( + 'upstreams/one/servers/127.0.0.1:7081/weight' + ), 'delete weight' check_reqs(1, 0.25) - self.assertIn( - 'success', - self.conf( - { - "127.0.0.1:7081": {"weight": 0.1}, - "127.0.0.1:7082": {"weight": 1}, - "127.0.0.1:7083": {"weight": 0.9}, - }, - 'upstreams/one/servers', - ), - 'configure weights', - ) + assert 'success' in self.conf( + { + "127.0.0.1:7081": {"weight": 0.1}, + "127.0.0.1:7082": {"weight": 1}, + "127.0.0.1:7083": {"weight": 0.9}, + }, + 'upstreams/one/servers', + ), 'configure weights' resps = self.get_resps_sc(req=20) - self.assertEqual(resps[0], 1, 'weight 3 1') - self.assertEqual(resps[1], 10, 'weight 3 2') - self.assertEqual(resps[2], 9, 'weight 3 3') + assert resps[0] == 1, 'weight 3 1' + assert resps[1] == 10, 'weight 3 2' + assert resps[2] == 9, 'weight 3 3' def test_upstreams_rr_independent(self): def sum_resps(*args): @@ -250,90 +215,77 @@ Connection: close return sum resps = self.get_resps_sc(req=30, port=7090) - self.assertEqual(resps[0], 15, 'dep two before 0') - self.assertEqual(resps[1], 15, 'dep two before 1') + assert resps[0] == 15, 'dep two before 0' + assert resps[1] == 15, 'dep two before 1' resps = self.get_resps_sc(req=30) - self.assertEqual(resps[0], 15, 'dep one before 0') - self.assertEqual(resps[1], 15, 'dep one before 1') + assert resps[0] == 15, 'dep one before 0' + assert resps[1] == 15, 'dep one before 1' - self.assertIn( - 'success', - self.conf('2', 'upstreams/two/servers/127.0.0.1:7081/weight'), - 'configure dep weight', - ) + assert 'success' in self.conf( + '2', 'upstreams/two/servers/127.0.0.1:7081/weight' + ), 'configure dep weight' resps = self.get_resps_sc(req=30, port=7090) - self.assertEqual(resps[0], 20, 'dep two 0') - self.assertEqual(resps[1], 10, 'dep two 1') + assert resps[0] == 20, 'dep two 0' + assert resps[1] == 10, 'dep two 1' resps = self.get_resps_sc(req=30) - self.assertEqual(resps[0], 15, 'dep one 0') - self.assertEqual(resps[1], 15, 'dep one 1') + assert resps[0] == 15, 'dep one 0' + assert resps[1] == 15, 'dep one 1' - self.assertIn( - 'success', - self.conf('1', 'upstreams/two/servers/127.0.0.1:7081/weight'), - 'configure dep weight 1', - ) + assert 'success' in self.conf( + '1', 'upstreams/two/servers/127.0.0.1:7081/weight' + ), 'configure dep weight 1' r_one, r_two = [0, 0], [0, 0] for _ in range(10): r_one = sum_resps(r_one, self.get_resps(req=10)) r_two = sum_resps(r_two, self.get_resps(req=10, port=7090)) - - self.assertEqual(sum(r_one), 100, 'dep one mix sum') - self.assertLessEqual( - abs(r_one[0] - r_one[1]), self.cpu_count, 'dep one mix' - ) - self.assertEqual(sum(r_two), 100, 'dep two mix sum') - self.assertLessEqual( - abs(r_two[0] - r_two[1]), self.cpu_count, 'dep two mix' - ) + assert sum(r_one) == 100, 'dep one mix sum' + assert abs(r_one[0] - r_one[1]) <= self.cpu_count, 'dep one mix' + assert sum(r_two) == 100, 'dep two mix sum' + assert abs(r_two[0] - r_two[1]) <= self.cpu_count, 'dep two mix' def test_upstreams_rr_delay(self): - self.assertIn( - 'success', - self.conf( - { - "listeners": { - "*:7080": {"pass": "upstreams/one"}, - "*:7081": {"pass": "routes"}, - "*:7082": {"pass": "routes"}, - }, - "upstreams": { - "one": { - "servers": { - "127.0.0.1:7081": {}, - "127.0.0.1:7082": {}, - }, + assert 'success' in self.conf( + { + "listeners": { + "*:7080": {"pass": "upstreams/one"}, + "*:7081": {"pass": "routes"}, + "*:7082": {"pass": "routes"}, + }, + "upstreams": { + "one": { + "servers": { + "127.0.0.1:7081": {}, + "127.0.0.1:7082": {}, }, }, - "routes": [ - { - "match": {"destination": "*:7081"}, - "action": {"pass": "applications/delayed"}, - }, - { - "match": {"destination": "*:7082"}, - "action": {"return": 201}, - }, - ], - "applications": { - "delayed": { - "type": "python", - "processes": {"spare": 0}, - "path": self.current_dir + "/python/delayed", - "working_directory": self.current_dir - + "/python/delayed", - "module": "wsgi", - } + }, + "routes": [ + { + "match": {"destination": "*:7081"}, + "action": {"pass": "applications/delayed"}, }, + { + "match": {"destination": "*:7082"}, + "action": {"return": 201}, + }, + ], + "applications": { + "delayed": { + "type": "python", + "processes": {"spare": 0}, + "path": option.test_dir + "/python/delayed", + "working_directory": option.test_dir + + "/python/delayed", + "module": "wsgi", + } }, - ), - 'upstreams initial configuration', - ) + }, + ), 'upstreams initial configuration' req = 50 @@ -357,12 +309,12 @@ Connection: close resp = self.recvall(socks[i]).decode() socks[i].close() - m = re.search('HTTP/1.1 20(\d)', resp) - self.assertIsNotNone(m, 'status') + m = re.search(r'HTTP/1.1 20(\d)', resp) + assert m is not None, 'status' resps[int(m.group(1))] += 1 - self.assertEqual(sum(resps), req, 'delay sum') - self.assertLessEqual(abs(resps[0] - resps[1]), self.cpu_count, 'delay') + assert sum(resps) == req, 'delay sum' + assert abs(resps[0] - resps[1]) <= self.cpu_count, 'delay' def test_upstreams_rr_active_req(self): conns = 5 @@ -389,59 +341,46 @@ Connection: close # Send one more request and read response to make sure that previous # requests had enough time to reach server. - self.assertEqual(self.get()['body'], '') - - self.assertIn( - 'success', - self.conf( - {"127.0.0.1:7083": {"weight": 2}}, 'upstreams/one/servers', - ), - 'active req new server', - ) - self.assertIn( - 'success', - self.conf_delete('upstreams/one/servers/127.0.0.1:7083'), - 'active req server remove', - ) - self.assertIn( - 'success', self.conf_delete('listeners/*:7080'), 'delete listener' - ) - self.assertIn( - 'success', - self.conf_delete('upstreams/one'), - 'active req upstream remove', - ) + assert self.get()['body'] == '' + + assert 'success' in self.conf( + {"127.0.0.1:7083": {"weight": 2}}, 'upstreams/one/servers', + ), 'active req new server' + assert 'success' in self.conf_delete( + 'upstreams/one/servers/127.0.0.1:7083' + ), 'active req server remove' + assert 'success' in self.conf_delete( + 'listeners/*:7080' + ), 'delete listener' + assert 'success' in self.conf_delete( + 'upstreams/one' + ), 'active req upstream remove' for i in range(conns): - self.assertEqual( - self.http(b'', sock=socks[i], raw=True)['body'], - '', - 'active req GET', - ) + assert ( + self.http(b'', sock=socks[i], raw=True)['body'] == '' + ), 'active req GET' - self.assertEqual( - self.http(b"""0123456789""", sock=socks2[i], raw=True)['body'], - '', - 'active req POST', - ) + assert ( + self.http(b"""0123456789""", sock=socks2[i], raw=True)['body'] + == '' + ), 'active req POST' def test_upstreams_rr_bad_server(self): - self.assertIn( - 'success', - self.conf({"weight": 1}, 'upstreams/one/servers/127.0.0.1:7084'), - 'configure bad server', - ) + assert 'success' in self.conf( + {"weight": 1}, 'upstreams/one/servers/127.0.0.1:7084' + ), 'configure bad server' resps = self.get_resps_sc(req=30) - self.assertEqual(resps[0], 10, 'bad server 0') - self.assertEqual(resps[1], 10, 'bad server 1') - self.assertEqual(sum(resps), 20, 'bad server sum') + assert resps[0] == 10, 'bad server 0' + assert resps[1] == 10, 'bad server 1' + assert sum(resps) == 20, 'bad server sum' def test_upstreams_rr_pipeline(self): resps = self.get_resps_sc() - self.assertEqual(resps[0], 50, 'pipeline 0') - self.assertEqual(resps[1], 50, 'pipeline 1') + assert resps[0] == 50, 'pipeline 0' + assert resps[1] == 50, 'pipeline 1' def test_upstreams_rr_post(self): resps = [0, 0] @@ -449,120 +388,87 @@ Connection: close resps[self.get()['status'] % 10] += 1 resps[self.post(body='0123456789')['status'] % 10] += 1 - self.assertEqual(sum(resps), 100, 'post sum') - self.assertLessEqual(abs(resps[0] - resps[1]), self.cpu_count, 'post') + assert sum(resps) == 100, 'post sum' + assert abs(resps[0] - resps[1]) <= self.cpu_count, 'post' def test_upstreams_rr_unix(self): - addr_0 = self.testdir + '/sock_0' - addr_1 = self.testdir + '/sock_1' - - self.assertIn( - 'success', - self.conf( - { - "*:7080": {"pass": "upstreams/one"}, - "unix:" + addr_0: {"pass": "routes/one"}, - "unix:" + addr_1: {"pass": "routes/two"}, - }, - 'listeners', - ), - 'configure listeners unix', - ) - - self.assertIn( - 'success', - self.conf( - {"unix:" + addr_0: {}, "unix:" + addr_1: {}}, - 'upstreams/one/servers', - ), - 'configure servers unix', - ) + addr_0 = self.temp_dir + '/sock_0' + addr_1 = self.temp_dir + '/sock_1' + + assert 'success' in self.conf( + { + "*:7080": {"pass": "upstreams/one"}, + "unix:" + addr_0: {"pass": "routes/one"}, + "unix:" + addr_1: {"pass": "routes/two"}, + }, + 'listeners', + ), 'configure listeners unix' + + assert 'success' in self.conf( + {"unix:" + addr_0: {}, "unix:" + addr_1: {}}, + 'upstreams/one/servers', + ), 'configure servers unix' resps = self.get_resps_sc() - self.assertEqual(resps[0], 50, 'unix 0') - self.assertEqual(resps[1], 50, 'unix 1') + assert resps[0] == 50, 'unix 0' + assert resps[1] == 50, 'unix 1' def test_upstreams_rr_ipv6(self): - self.assertIn( - 'success', - self.conf( - { - "*:7080": {"pass": "upstreams/one"}, - "[::1]:7081": {"pass": "routes/one"}, - "[::1]:7082": {"pass": "routes/two"}, - }, - 'listeners', - ), - 'configure listeners ipv6', - ) - - self.assertIn( - 'success', - self.conf( - {"[::1]:7081": {}, "[::1]:7082": {}}, 'upstreams/one/servers' - ), - 'configure servers ipv6', - ) + assert 'success' in self.conf( + { + "*:7080": {"pass": "upstreams/one"}, + "[::1]:7081": {"pass": "routes/one"}, + "[::1]:7082": {"pass": "routes/two"}, + }, + 'listeners', + ), 'configure listeners ipv6' + + assert 'success' in self.conf( + {"[::1]:7081": {}, "[::1]:7082": {}}, 'upstreams/one/servers' + ), 'configure servers ipv6' resps = self.get_resps_sc() - self.assertEqual(resps[0], 50, 'ipv6 0') - self.assertEqual(resps[1], 50, 'ipv6 1') + assert resps[0] == 50, 'ipv6 0' + assert resps[1] == 50, 'ipv6 1' def test_upstreams_rr_servers_empty(self): - self.assertIn( - 'success', - self.conf({}, 'upstreams/one/servers'), - 'configure servers empty', - ) - self.assertEqual(self.get()['status'], 502, 'servers empty') - - self.assertIn( - 'success', - self.conf( - {"127.0.0.1:7081": {"weight": 0}}, 'upstreams/one/servers' - ), - 'configure servers empty one', - ) - self.assertEqual(self.get()['status'], 502, 'servers empty one') - self.assertIn( - 'success', - self.conf( - { - "127.0.0.1:7081": {"weight": 0}, - "127.0.0.1:7082": {"weight": 0}, - }, - 'upstreams/one/servers', - ), - 'configure servers empty two', - ) - self.assertEqual(self.get()['status'], 502, 'servers empty two') + assert 'success' in self.conf( + {}, 'upstreams/one/servers' + ), 'configure servers empty' + assert self.get()['status'] == 502, 'servers empty' + + assert 'success' in self.conf( + {"127.0.0.1:7081": {"weight": 0}}, 'upstreams/one/servers' + ), 'configure servers empty one' + assert self.get()['status'] == 502, 'servers empty one' + assert 'success' in self.conf( + { + "127.0.0.1:7081": {"weight": 0}, + "127.0.0.1:7082": {"weight": 0}, + }, + 'upstreams/one/servers', + ), 'configure servers empty two' + assert self.get()['status'] == 502, 'servers empty two' def test_upstreams_rr_invalid(self): - self.assertIn( - 'error', self.conf({}, 'upstreams'), 'upstreams empty', - ) - self.assertIn( - 'error', self.conf({}, 'upstreams/one'), 'named upstreams empty', - ) - self.assertIn( - 'error', - self.conf({}, 'upstreams/one/servers/127.0.0.1'), - 'invalid address', - ) - self.assertIn( - 'error', - self.conf({}, 'upstreams/one/servers/127.0.0.1:7081/blah'), - 'invalid server option', - ) + assert 'error' in self.conf({}, 'upstreams'), 'upstreams empty' + assert 'error' in self.conf( + {}, 'upstreams/one' + ), 'named upstreams empty' + assert 'error' in self.conf( + {}, 'upstreams/one/servers/127.0.0.1' + ), 'invalid address' + assert 'error' in self.conf( + {}, 'upstreams/one/servers/127.0.0.1:7081/blah' + ), 'invalid server option' def check_weight(w): - self.assertIn( - 'error', - self.conf(w, 'upstreams/one/servers/127.0.0.1:7081/weight'), - 'invalid weight option', - ) + assert 'error' in self.conf( + w, 'upstreams/one/servers/127.0.0.1:7081/weight' + ), 'invalid weight option' + check_weight({}) check_weight('-1') check_weight('1.') @@ -571,7 +477,3 @@ Connection: close check_weight('.01234567890123') check_weight('1000001') check_weight('2e6') - - -if __name__ == '__main__': - TestUpstreamsRR.main() diff --git a/test/test_usr1.py b/test/test_usr1.py index d1db652f..2e48c18f 100644 --- a/test/test_usr1.py +++ b/test/test_usr1.py @@ -1,6 +1,7 @@ import os from subprocess import call +from conftest import waitforfiles from unit.applications.lang.python import TestApplicationPython @@ -12,83 +13,74 @@ class TestUSR1(TestApplicationPython): log = 'access.log' log_new = 'new.log' - log_path = self.testdir + '/' + log + log_path = self.temp_dir + '/' + log - self.assertIn( - 'success', - self.conf('"' + log_path + '"', 'access_log'), - 'access log configure', - ) + assert 'success' in self.conf( + '"' + log_path + '"', 'access_log' + ), 'access log configure' - self.assertTrue(self.waitforfiles(log_path), 'open') + assert waitforfiles(log_path), 'open' - os.rename(log_path, self.testdir + '/' + log_new) + os.rename(log_path, self.temp_dir + '/' + log_new) - self.assertEqual(self.get()['status'], 200) + assert self.get()['status'] == 200 - self.assertIsNotNone( - self.wait_for_record(r'"GET / HTTP/1.1" 200 0 "-" "-"', log_new), - 'rename new', - ) - self.assertFalse(os.path.isfile(log_path), 'rename old') + assert ( + self.wait_for_record(r'"GET / HTTP/1.1" 200 0 "-" "-"', log_new) + is not None + ), 'rename new' + assert not os.path.isfile(log_path), 'rename old' - with open(self.testdir + '/unit.pid', 'r') as f: + with open(self.temp_dir + '/unit.pid', 'r') as f: pid = f.read().rstrip() call(['kill', '-s', 'USR1', pid]) - self.assertTrue(self.waitforfiles(log_path), 'reopen') + assert waitforfiles(log_path), 'reopen' - self.assertEqual(self.get(url='/usr1')['status'], 200) + assert self.get(url='/usr1')['status'] == 200 self.stop() - self.assertIsNotNone( - self.wait_for_record(r'"GET /usr1 HTTP/1.1" 200 0 "-" "-"', log), - 'reopen 2', - ) - self.assertIsNone( - self.search_in_log(r'/usr1', log_new), 'rename new 2' - ) + assert ( + self.wait_for_record(r'"GET /usr1 HTTP/1.1" 200 0 "-" "-"', log) + is not None + ), 'reopen 2' + assert self.search_in_log(r'/usr1', log_new) is None, 'rename new 2' def test_usr1_unit_log(self): self.load('log_body') log_new = 'new.log' - log_path = self.testdir + '/unit.log' - log_path_new = self.testdir + '/' + log_new + log_path = self.temp_dir + '/unit.log' + log_path_new = self.temp_dir + '/' + log_new os.rename(log_path, log_path_new) body = 'body_for_a_log_new' - self.assertEqual(self.post(body=body)['status'], 200) + assert self.post(body=body)['status'] == 200 - self.assertIsNotNone( - self.wait_for_record(body, log_new), 'rename new' - ) - self.assertFalse(os.path.isfile(log_path), 'rename old') + assert self.wait_for_record(body, log_new) is not None, 'rename new' + assert not os.path.isfile(log_path), 'rename old' - with open(self.testdir + '/unit.pid', 'r') as f: + with open(self.temp_dir + '/unit.pid', 'r') as f: pid = f.read().rstrip() call(['kill', '-s', 'USR1', pid]) - self.assertTrue(self.waitforfiles(log_path), 'reopen') + assert waitforfiles(log_path), 'reopen' body = 'body_for_a_log_unit' - self.assertEqual(self.post(body=body)['status'], 200) + assert self.post(body=body)['status'] == 200 self.stop() - self.assertIsNotNone(self.wait_for_record(body), 'rename new') - self.assertIsNone(self.search_in_log(body, log_new), 'rename new 2') + assert self.wait_for_record(body) is not None, 'rename new' + assert self.search_in_log(body, log_new) is None, 'rename new 2' # merge two log files into unit.log to check alerts - with open(log_path, 'w') as unit_log, \ - open(log_path_new, 'r') as unit_log_new: + with open(log_path, 'w') as unit_log, open( + log_path_new, 'r' + ) as unit_log_new: unit_log.write(unit_log_new.read()) - - -if __name__ == '__main__': - TestUSR1.main() diff --git a/test/test_variables.py b/test/test_variables.py index 805c5144..c458b636 100644 --- a/test/test_variables.py +++ b/test/test_variables.py @@ -4,83 +4,117 @@ from unit.applications.proto import TestApplicationProto class TestVariables(TestApplicationProto): prerequisites = {} - def setUp(self): - super().setUp() - - self.assertIn( - 'success', - self.conf( - { - "listeners": {"*:7080": {"pass": "routes/$method"}}, - "routes": { - "GET": [{"action": {"return": 201}}], - "POST": [{"action": {"return": 202}}], - "3": [{"action": {"return": 203}}], - "4": [{"action": {"return": 204}}], - "blahGET}": [{"action": {"return": 205}}], - "5GET": [{"action": {"return": 206}}], - "GETGET": [{"action": {"return": 207}}], - }, + def setup_method(self): + super().setup_method() + + assert 'success' in self.conf( + { + "listeners": {"*:7080": {"pass": "routes/$method"}}, + "routes": { + "GET": [{"action": {"return": 201}}], + "POST": [{"action": {"return": 202}}], + "3": [{"action": {"return": 203}}], + "4*": [{"action": {"return": 204}}], + "blahGET}": [{"action": {"return": 205}}], + "5GET": [{"action": {"return": 206}}], + "GETGET": [{"action": {"return": 207}}], + "localhost": [{"action": {"return": 208}}], }, - ), - 'configure routes', - ) + }, + ), 'configure routes' def conf_routes(self, routes): - self.assertIn( - 'success', - self.conf(routes, 'listeners/*:7080/pass') - ) + assert 'success' in self.conf(routes, 'listeners/*:7080/pass') def test_variables_method(self): - self.assertEqual(self.get()['status'], 201, 'method GET') - self.assertEqual(self.post()['status'], 202, 'method POST') + assert self.get()['status'] == 201, 'method GET' + assert self.post()['status'] == 202, 'method POST' def test_variables_uri(self): self.conf_routes("\"routes$uri\"") - self.assertEqual(self.get(url='/3')['status'], 203, 'uri') - self.assertEqual(self.get(url='/4')['status'], 204, 'uri 2') + assert self.get(url='/3')['status'] == 203, 'uri' + assert self.get(url='/4*')['status'] == 204, 'uri 2' + assert self.get(url='/4%2A')['status'] == 204, 'uri 3' + + def test_variables_host(self): + self.conf_routes("\"routes/$host\"") + + def check_host(host, status=208): + assert ( + self.get(headers={'Host': host, 'Connection': 'close'})[ + 'status' + ] + == status + ) + + check_host('localhost') + check_host('localhost.') + check_host('localhost:7080') + check_host('.localhost', 404) + check_host('www.localhost', 404) + check_host('localhost1', 404) def test_variables_many(self): self.conf_routes("\"routes$uri$method\"") - self.assertEqual(self.get(url='/5')['status'], 206, 'many') + assert self.get(url='/5')['status'] == 206, 'many' self.conf_routes("\"routes${uri}${method}\"") - self.assertEqual(self.get(url='/5')['status'], 206, 'many 2') + assert self.get(url='/5')['status'] == 206, 'many 2' self.conf_routes("\"routes${uri}$method\"") - self.assertEqual(self.get(url='/5')['status'], 206, 'many 3') + assert self.get(url='/5')['status'] == 206, 'many 3' self.conf_routes("\"routes/$method$method\"") - self.assertEqual(self.get()['status'], 207, 'many 4') + assert self.get()['status'] == 207, 'many 4' self.conf_routes("\"routes/$method$uri\"") - self.assertEqual(self.get()['status'], 404, 'no route') - self.assertEqual(self.get(url='/blah')['status'], 404, 'no route 2') + assert self.get()['status'] == 404, 'no route' + assert self.get(url='/blah')['status'] == 404, 'no route 2' def test_variables_replace(self): - self.assertEqual(self.get()['status'], 201) + assert self.get()['status'] == 201 self.conf_routes("\"routes$uri\"") - self.assertEqual(self.get(url='/3')['status'], 203) + assert self.get(url='/3')['status'] == 203 self.conf_routes("\"routes/${method}\"") - self.assertEqual(self.post()['status'], 202) + assert self.post()['status'] == 202 self.conf_routes("\"routes${uri}\"") - self.assertEqual(self.get(url='/4')['status'], 204) + assert self.get(url='/4*')['status'] == 204 self.conf_routes("\"routes/blah$method}\"") - self.assertEqual(self.get()['status'], 205) + assert self.get()['status'] == 205 + + def test_variables_upstream(self): + assert 'success' in self.conf( + { + "listeners": { + "*:7080": {"pass": "upstreams$uri"}, + "*:7081": {"pass": "routes/one"}, + }, + "upstreams": { + "1": { + "servers": { + "127.0.0.1:7081": {}, + }, + }, + }, + "routes": { + "one": [{"action": {"return": 200}}], + }, + }, + ), 'upstreams initial configuration' + + assert self.get(url='/1')['status'] == 200 + assert self.get(url='/2')['status'] == 404 def test_variables_invalid(self): def check_variables(routes): - self.assertIn( - 'error', - self.conf(routes, 'listeners/*:7080/pass'), - 'invalid variables', - ) + assert 'error' in self.conf( + routes, 'listeners/*:7080/pass' + ), 'invalid variables' check_variables("\"routes$\"") check_variables("\"routes${\"") @@ -89,6 +123,3 @@ class TestVariables(TestApplicationProto): check_variables("\"routes$uriblah\"") check_variables("\"routes${uri\"") check_variables("\"routes${{uri}\"") - -if __name__ == '__main__': - TestVariables.main() diff --git a/test/unit/applications/lang/go.py b/test/unit/applications/lang/go.py index 83bde4d8..7715bd6c 100644 --- a/test/unit/applications/lang/go.py +++ b/test/unit/applications/lang/go.py @@ -1,30 +1,17 @@ import os import subprocess +from conftest import option from unit.applications.proto import TestApplicationProto class TestApplicationGo(TestApplicationProto): - @classmethod - def setUpClass(cls, complete_check=True): - unit = super().setUpClass(complete_check=False) - - # check go module - - go_app = TestApplicationGo() - go_app.testdir = unit.testdir - proc = go_app.prepare_env('empty', 'app') - if proc and proc.returncode == 0: - cls.available['modules']['go'] = [] - - return unit if not complete_check else unit.complete() - def prepare_env(self, script, name, static=False): - if not os.path.exists(self.testdir + '/go'): - os.mkdir(self.testdir + '/go') + if not os.path.exists(self.temp_dir + '/go'): + os.mkdir(self.temp_dir + '/go') env = os.environ.copy() - env['GOPATH'] = self.pardir + '/build/go' + env['GOPATH'] = option.current_dir + '/build/go' if static: args = [ @@ -35,16 +22,16 @@ class TestApplicationGo(TestApplicationProto): '-ldflags', '-extldflags "-static"', '-o', - self.testdir + '/go/' + name, - self.current_dir + '/go/' + script + '/' + name + '.go', + self.temp_dir + '/go/' + name, + option.test_dir + '/go/' + script + '/' + name + '.go', ] else: args = [ 'go', 'build', '-o', - self.testdir + '/go/' + name, - self.current_dir + '/go/' + script + '/' + name + '.go', + self.temp_dir + '/go/' + name, + option.test_dir + '/go/' + script + '/' + name + '.go', ] try: @@ -59,8 +46,8 @@ class TestApplicationGo(TestApplicationProto): def load(self, script, name='app', **kwargs): static_build = False - wdir = self.current_dir + "/go/" + script - executable = self.testdir + "/go/" + name + wdir = option.test_dir + "/go/" + script + executable = self.temp_dir + "/go/" + name if 'isolation' in kwargs and 'rootfs' in kwargs['isolation']: wdir = "/go/" diff --git a/test/unit/applications/lang/java.py b/test/unit/applications/lang/java.py index c2c6dc51..01cbfa0b 100644 --- a/test/unit/applications/lang/java.py +++ b/test/unit/applications/lang/java.py @@ -3,15 +3,17 @@ import os import shutil import subprocess +import pytest +from conftest import option from unit.applications.proto import TestApplicationProto class TestApplicationJava(TestApplicationProto): def load(self, script, name='app', **kwargs): - app_path = self.testdir + '/java' + app_path = self.temp_dir + '/java' web_inf_path = app_path + '/WEB-INF/' classes_path = web_inf_path + 'classes/' - script_path = self.current_dir + '/java/' + script + '/' + script_path = option.test_dir + '/java/' + script + '/' if not os.path.isdir(app_path): os.makedirs(app_path) @@ -47,14 +49,16 @@ class TestApplicationJava(TestApplicationProto): if not os.path.isdir(classes_path): os.makedirs(classes_path) - classpath = self.pardir + '/build/tomcat-servlet-api-9.0.13.jar' + classpath = ( + option.current_dir + '/build/tomcat-servlet-api-9.0.13.jar' + ) ws_jars = glob.glob( - self.pardir + '/build/websocket-api-java-*.jar' + option.current_dir + '/build/websocket-api-java-*.jar' ) if not ws_jars: - self.fail('websocket api jar not found.') + pytest.fail('websocket api jar not found.') javac = [ 'javac', @@ -69,14 +73,14 @@ class TestApplicationJava(TestApplicationProto): process.communicate() except: - self.fail('Cann\'t run javac process.') + pytest.fail('Cann\'t run javac process.') self._load_conf( { "listeners": {"*:7080": {"pass": "applications/" + script}}, "applications": { script: { - "unit_jars": self.pardir + '/build', + "unit_jars": option.current_dir + '/build', "type": 'java', "processes": {"spare": 0}, "working_directory": script_path, diff --git a/test/unit/applications/lang/node.py b/test/unit/applications/lang/node.py index cf2a99f6..877fc461 100644 --- a/test/unit/applications/lang/node.py +++ b/test/unit/applications/lang/node.py @@ -1,37 +1,27 @@ -import os import shutil from urllib.parse import quote +from conftest import option +from conftest import public_dir from unit.applications.proto import TestApplicationProto class TestApplicationNode(TestApplicationProto): - @classmethod - def setUpClass(cls, complete_check=True): - unit = super().setUpClass(complete_check=False) - - # check node module - - if os.path.exists(unit.pardir + '/node/node_modules'): - cls.available['modules']['node'] = [] - - return unit if not complete_check else unit.complete() - def load(self, script, name='app.js', **kwargs): # copy application shutil.copytree( - self.current_dir + '/node/' + script, self.testdir + '/node' + option.test_dir + '/node/' + script, self.temp_dir + '/node' ) # copy modules shutil.copytree( - self.pardir + '/node/node_modules', - self.testdir + '/node/node_modules', + option.current_dir + '/node/node_modules', + self.temp_dir + '/node/node_modules', ) - self.public_dir(self.testdir + '/node') + public_dir(self.temp_dir + '/node') self._load_conf( { @@ -42,7 +32,7 @@ class TestApplicationNode(TestApplicationProto): script: { "type": "external", "processes": {"spare": 0}, - "working_directory": self.testdir + '/node', + "working_directory": self.temp_dir + '/node', "executable": name, } }, diff --git a/test/unit/applications/lang/perl.py b/test/unit/applications/lang/perl.py index d32aca33..a27c7649 100644 --- a/test/unit/applications/lang/perl.py +++ b/test/unit/applications/lang/perl.py @@ -1,3 +1,4 @@ +from conftest import option from unit.applications.proto import TestApplicationProto @@ -5,14 +6,18 @@ class TestApplicationPerl(TestApplicationProto): application_type = "perl" def load(self, script, name='psgi.pl', **kwargs): - script_path = self.current_dir + '/perl/' + script + script_path = option.test_dir + '/perl/' + script + appication_type = self.get_appication_type() + + if appication_type is None: + appication_type = self.application_type self._load_conf( { "listeners": {"*:7080": {"pass": "applications/" + script}}, "applications": { script: { - "type": self.application_type, + "type": appication_type, "processes": {"spare": 0}, "working_directory": script_path, "script": script_path + '/' + name, diff --git a/test/unit/applications/lang/php.py b/test/unit/applications/lang/php.py index e8c70c62..2d50df2e 100644 --- a/test/unit/applications/lang/php.py +++ b/test/unit/applications/lang/php.py @@ -1,3 +1,4 @@ +from conftest import option from unit.applications.proto import TestApplicationProto @@ -5,14 +6,18 @@ class TestApplicationPHP(TestApplicationProto): application_type = "php" def load(self, script, index='index.php', **kwargs): - script_path = self.current_dir + '/php/' + script + script_path = option.test_dir + '/php/' + script + appication_type = self.get_appication_type() + + if appication_type is None: + appication_type = self.application_type self._load_conf( { "listeners": {"*:7080": {"pass": "applications/" + script}}, "applications": { script: { - "type": self.application_type, + "type": appication_type, "processes": {"spare": 0}, "root": script_path, "working_directory": script_path, diff --git a/test/unit/applications/lang/python.py b/test/unit/applications/lang/python.py index 91559f4b..47b95dac 100644 --- a/test/unit/applications/lang/python.py +++ b/test/unit/applications/lang/python.py @@ -1,20 +1,28 @@ import os import shutil +from urllib.parse import quote +import pytest +from conftest import option from unit.applications.proto import TestApplicationProto class TestApplicationPython(TestApplicationProto): application_type = "python" + load_module = "wsgi" - def load(self, script, name=None, **kwargs): + def load(self, script, name=None, module=None, **kwargs): + print() if name is None: name = script + if module is None: + module = self.load_module + if script[0] == '/': script_path = script else: - script_path = self.current_dir + '/python/' + script + script_path = option.test_dir + '/python/' + script if kwargs.get('isolation') and kwargs['isolation'].get('rootfs'): rootfs = kwargs['isolation']['rootfs'] @@ -27,16 +35,23 @@ class TestApplicationPython(TestApplicationProto): script_path = '/app/python/' + name + appication_type = self.get_appication_type() + + if appication_type is None: + appication_type = self.application_type + self._load_conf( { - "listeners": {"*:7080": {"pass": "applications/" + name}}, + "listeners": { + "*:7080": {"pass": "applications/" + quote(name, '')} + }, "applications": { name: { - "type": self.application_type, + "type": appication_type, "processes": {"spare": 0}, "path": script_path, "working_directory": script_path, - "module": "wsgi", + "module": module, } }, }, diff --git a/test/unit/applications/lang/ruby.py b/test/unit/applications/lang/ruby.py index 8c8acecc..bc3cefc6 100644 --- a/test/unit/applications/lang/ruby.py +++ b/test/unit/applications/lang/ruby.py @@ -1,3 +1,4 @@ +from conftest import option from unit.applications.proto import TestApplicationProto @@ -5,14 +6,18 @@ class TestApplicationRuby(TestApplicationProto): application_type = "ruby" def load(self, script, name='config.ru', **kwargs): - script_path = self.current_dir + '/ruby/' + script + script_path = option.test_dir + '/ruby/' + script + appication_type = self.get_appication_type() + + if appication_type is None: + appication_type = self.application_type self._load_conf( { "listeners": {"*:7080": {"pass": "applications/" + script}}, "applications": { script: { - "type": self.application_type, + "type": appication_type, "processes": {"spare": 0}, "working_directory": script_path, "script": script_path + '/' + name, diff --git a/test/unit/applications/proto.py b/test/unit/applications/proto.py index 244cb5be..2f748c21 100644 --- a/test/unit/applications/proto.py +++ b/test/unit/applications/proto.py @@ -1,6 +1,8 @@ +import os import re import time +from conftest import option from unit.control import TestControl @@ -12,7 +14,7 @@ class TestApplicationProto(TestControl): return time.mktime(time.strptime(date, template)) def search_in_log(self, pattern, name='unit.log'): - with open(self.testdir + '/' + name, 'r', errors='ignore') as f: + with open(self.temp_dir + '/' + name, 'r', errors='ignore') as f: return re.search(pattern, f.read()) def wait_for_record(self, pattern, name='unit.log'): @@ -26,6 +28,16 @@ class TestApplicationProto(TestControl): return found + def get_appication_type(self): + current_test = ( + os.environ.get('PYTEST_CURRENT_TEST').split(':')[-1].split(' ')[0] + ) + + if current_test in option.generated_tests: + return option.generated_tests[current_test] + + return None + def _load_conf(self, conf, **kwargs): if 'applications' in conf: for app in conf['applications'].keys(): @@ -39,6 +51,4 @@ class TestApplicationProto(TestControl): if 'isolation' in kwargs: app_conf['isolation'] = kwargs['isolation'] - self.assertIn( - 'success', self.conf(conf), 'load application configuration' - ) + assert 'success' in self.conf(conf), 'load application configuration' diff --git a/test/unit/applications/tls.py b/test/unit/applications/tls.py index e6a846b2..fdf681ae 100644 --- a/test/unit/applications/tls.py +++ b/test/unit/applications/tls.py @@ -1,40 +1,19 @@ import os -import re import ssl import subprocess +from conftest import option from unit.applications.proto import TestApplicationProto class TestApplicationTLS(TestApplicationProto): - def __init__(self, test): - super().__init__(test) + def setup_method(self): + super().setup_method() self.context = ssl.create_default_context() self.context.check_hostname = False self.context.verify_mode = ssl.CERT_NONE - @classmethod - def setUpClass(cls, complete_check=True): - unit = super().setUpClass(complete_check=False) - - # check tls module - - try: - subprocess.check_output(['which', 'openssl']) - - output = subprocess.check_output( - [unit.unitd, '--version'], stderr=subprocess.STDOUT - ) - - if re.search('--openssl', output.decode()): - cls.available['modules']['openssl'] = [] - - except: - pass - - return unit if not complete_check else unit.complete() - def certificate(self, name='default', load=True): self.openssl_conf() @@ -45,9 +24,9 @@ class TestApplicationTLS(TestApplicationProto): '-x509', '-new', '-subj', '/CN=' + name + '/', - '-config', self.testdir + '/openssl.conf', - '-out', self.testdir + '/' + name + '.crt', - '-keyout', self.testdir + '/' + name + '.key', + '-config', self.temp_dir + '/openssl.conf', + '-out', self.temp_dir + '/' + name + '.crt', + '-keyout', self.temp_dir + '/' + name + '.key', ], stderr=subprocess.STDOUT, ) @@ -59,8 +38,8 @@ class TestApplicationTLS(TestApplicationProto): if key is None: key = crt - key_path = self.testdir + '/' + key + '.key' - crt_path = self.testdir + '/' + crt + '.crt' + key_path = self.temp_dir + '/' + key + '.key' + crt_path = self.temp_dir + '/' + crt + '.crt' with open(key_path, 'rb') as k, open(crt_path, 'rb') as c: return self.conf(k.read() + c.read(), '/certificates/' + crt) @@ -87,7 +66,7 @@ class TestApplicationTLS(TestApplicationProto): return ssl.get_server_certificate(addr, ssl_version=ssl_version) def openssl_conf(self): - conf_path = self.testdir + '/openssl.conf' + conf_path = self.temp_dir + '/openssl.conf' if os.path.exists(conf_path): return @@ -105,7 +84,7 @@ distinguished_name = req_distinguished_name if name is None: name = script - script_path = self.current_dir + '/python/' + script + script_path = option.test_dir + '/python/' + script self._load_conf( { diff --git a/test/unit/applications/websockets.py b/test/unit/applications/websockets.py index e0dd2c0d..cc720a98 100644 --- a/test/unit/applications/websockets.py +++ b/test/unit/applications/websockets.py @@ -2,10 +2,10 @@ import base64 import hashlib import itertools import random -import re import select import struct +import pytest from unit.applications.proto import TestApplicationProto GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" @@ -21,9 +21,6 @@ class TestApplicationWebsocket(TestApplicationProto): OP_PONG = 0x0A CLOSE_CODES = [1000, 1001, 1002, 1003, 1007, 1008, 1009, 1010, 1011] - def __init__(self, preinit=False): - self.preinit = preinit - def key(self): raw_key = bytes(random.getrandbits(8) for _ in range(16)) return base64.b64encode(raw_key).decode() @@ -42,7 +39,7 @@ class TestApplicationWebsocket(TestApplicationProto): 'Upgrade': 'websocket', 'Connection': 'Upgrade', 'Sec-WebSocket-Key': key, - 'Sec-WebSocket-Protocol': 'chat', + 'Sec-WebSocket-Protocol': 'chat, phone, video', 'Sec-WebSocket-Version': 13, } @@ -56,14 +53,11 @@ class TestApplicationWebsocket(TestApplicationProto): while True: rlist = select.select([sock], [], [], 60)[0] if not rlist: - self.fail('Can\'t read response from server.') + pytest.fail('Can\'t read response from server.') resp += sock.recv(4096).decode() - if ( - re.search('101 Switching Protocols', resp) - and resp[-4:] == '\r\n\r\n' - ): + if (resp.startswith('HTTP/') and '\r\n\r\n' in resp): resp = self._resp_to_dict(resp) break @@ -84,7 +78,7 @@ class TestApplicationWebsocket(TestApplicationProto): # For all current cases if the "read_timeout" was changed # than test do not expect to get a response from server. if read_timeout == 60: - self.fail('Can\'t read response from server.') + pytest.fail('Can\'t read response from server.') break data += sock.recv(bytes - len(data)) @@ -130,19 +124,19 @@ class TestApplicationWebsocket(TestApplicationProto): code, = struct.unpack('!H', data[:2]) reason = data[2:].decode('utf-8') if not (code in self.CLOSE_CODES or 3000 <= code < 5000): - self.fail('Invalid status code') + pytest.fail('Invalid status code') frame['code'] = code frame['reason'] = reason elif length == 0: frame['code'] = 1005 frame['reason'] = '' else: - self.fail('Close frame too short') + pytest.fail('Close frame too short') frame['data'] = data if frame['mask']: - self.fail('Received frame with mask') + pytest.fail('Received frame with mask') return frame diff --git a/test/unit/check/go.py b/test/unit/check/go.py new file mode 100644 index 00000000..dd2150eb --- /dev/null +++ b/test/unit/check/go.py @@ -0,0 +1,29 @@ +import os +import subprocess + + +def check_go(current_dir, temp_dir, test_dir): + if not os.path.exists(temp_dir + '/go'): + os.mkdir(temp_dir + '/go') + + env = os.environ.copy() + env['GOPATH'] = current_dir + '/build/go' + + try: + process = subprocess.Popen( + [ + 'go', + 'build', + '-o', + temp_dir + '/go/app', + test_dir + '/go/empty/app.go', + ], + env=env, + ) + process.communicate() + + if process.returncode == 0: + return True + + except: + return None diff --git a/test/unit/check/node.py b/test/unit/check/node.py new file mode 100644 index 00000000..236ba7b5 --- /dev/null +++ b/test/unit/check/node.py @@ -0,0 +1,6 @@ +import os + + +def check_node(current_dir): + if os.path.exists(current_dir + '/node/node_modules'): + return True diff --git a/test/unit/check/tls.py b/test/unit/check/tls.py new file mode 100644 index 00000000..b878ff7d --- /dev/null +++ b/test/unit/check/tls.py @@ -0,0 +1,13 @@ +import re +import subprocess + + +def check_openssl(unitd): + subprocess.check_output(['which', 'openssl']) + + output = subprocess.check_output( + [unitd, '--version'], stderr=subprocess.STDOUT + ) + + if re.search('--openssl', output.decode()): + return True diff --git a/test/unit/control.py b/test/unit/control.py index 029072b5..6fd350f4 100644 --- a/test/unit/control.py +++ b/test/unit/control.py @@ -53,7 +53,7 @@ class TestControl(TestHTTP): args = { 'url': url, 'sock_type': 'unix', - 'addr': self.testdir + '/control.unit.sock', + 'addr': self.temp_dir + '/control.unit.sock', } if conf is not None: diff --git a/test/unit/feature/isolation.py b/test/unit/feature/isolation.py index 4f33d04a..c6f6f3c0 100644 --- a/test/unit/feature/isolation.py +++ b/test/unit/feature/isolation.py @@ -13,7 +13,7 @@ from unit.applications.proto import TestApplicationProto class TestFeatureIsolation(TestApplicationProto): allns = ['pid', 'mnt', 'ipc', 'uts', 'cgroup', 'net'] - def check(self, available, testdir): + def check(self, available, temp_dir): test_conf = {"namespaces": {"credential": True}} module = '' @@ -45,7 +45,7 @@ class TestFeatureIsolation(TestApplicationProto): if not module: return - module.testdir = testdir + module.temp_dir = temp_dir module.load(app) resp = module.conf(test_conf, 'applications/' + app + '/isolation') diff --git a/test/unit/http.py b/test/unit/http.py index de3bb2a4..7845f9a8 100644 --- a/test/unit/http.py +++ b/test/unit/http.py @@ -7,25 +7,22 @@ import select import socket import time +import pytest +from conftest import option from unit.main import TestUnit class TestHTTP(TestUnit): def http(self, start_str, **kwargs): - sock_type = ( - 'ipv4' if 'sock_type' not in kwargs else kwargs['sock_type'] - ) - port = 7080 if 'port' not in kwargs else kwargs['port'] - url = '/' if 'url' not in kwargs else kwargs['url'] + sock_type = kwargs.get('sock_type', 'ipv4') + port = kwargs.get('port', 7080) + url = kwargs.get('url', '/') http = 'HTTP/1.0' if 'http_10' in kwargs else 'HTTP/1.1' - headers = ( - {'Host': 'localhost', 'Connection': 'close'} - if 'headers' not in kwargs - else kwargs['headers'] - ) + headers = kwargs.get('headers', + {'Host': 'localhost', 'Connection': 'close'}) - body = b'' if 'body' not in kwargs else kwargs['body'] + body = kwargs.get('body', b'') crlf = '\r\n' if 'addr' not in kwargs: @@ -56,7 +53,7 @@ class TestHTTP(TestUnit): sock.connect(connect_args) except ConnectionRefusedError: sock.close() - self.fail('Client can\'t connect to the server.') + pytest.fail('Client can\'t connect to the server.') else: sock = kwargs['sock'] @@ -90,7 +87,7 @@ class TestHTTP(TestUnit): sock.sendall(req) - encoding = 'utf-8' if 'encoding' not in kwargs else kwargs['encoding'] + encoding = kwargs.get('encoding', 'utf-8') self.log_out(req, encoding) @@ -128,7 +125,7 @@ class TestHTTP(TestUnit): return (resp, sock) def log_out(self, log, encoding): - if TestUnit.detailed: + if option.detailed: print('>>>') log = self.log_truncate(log) try: @@ -137,7 +134,7 @@ class TestHTTP(TestUnit): print(log) def log_in(self, log): - if TestUnit.detailed: + if option.detailed: print('<<<') log = self.log_truncate(log) try: @@ -176,12 +173,8 @@ class TestHTTP(TestUnit): def recvall(self, sock, **kwargs): timeout_default = 60 - timeout = ( - timeout_default - if 'read_timeout' not in kwargs - else kwargs['read_timeout'] - ) - buff_size = 4096 if 'buff_size' not in kwargs else kwargs['buff_size'] + timeout = kwargs.get('read_timeout', timeout_default) + buff_size = kwargs.get('buff_size', 4096) data = b'' while True: @@ -190,7 +183,7 @@ class TestHTTP(TestUnit): # For all current cases if the "read_timeout" was changed # than test do not expect to get a response from server. if timeout == timeout_default: - self.fail('Can\'t read response from server.') + pytest.fail('Can\'t read response from server.') break try: @@ -243,28 +236,28 @@ class TestHTTP(TestUnit): chunks = raw_body.split(crlf) if len(chunks) < 3: - self.fail('Invalid chunked body') + pytest.fail('Invalid chunked body') if chunks.pop() != b'': - self.fail('No CRLF at the end of the body') + pytest.fail('No CRLF at the end of the body') try: last_size = int(chunks[-2], 16) except: - self.fail('Invalid zero size chunk') + pytest.fail('Invalid zero size chunk') if last_size != 0 or chunks[-1] != b'': - self.fail('Incomplete body') + pytest.fail('Incomplete body') body = b'' while len(chunks) >= 2: try: size = int(chunks.pop(0), 16) except: - self.fail('Invalid chunk size %s' % str(size)) + pytest.fail('Invalid chunk size %s' % str(size)) if size == 0: - self.assertEqual(len(chunks), 1, 'last zero size') + assert len(chunks) == 1, 'last zero size' break temp_body = crlf.join(chunks) @@ -280,8 +273,8 @@ class TestHTTP(TestUnit): def _parse_json(self, resp): headers = resp['headers'] - self.assertIn('Content-Type', headers) - self.assertEqual(headers['Content-Type'], 'application/json') + assert 'Content-Type' in headers + assert headers['Content-Type'] == 'application/json' resp['body'] = json.loads(resp['body']) @@ -305,7 +298,7 @@ class TestHTTP(TestUnit): sock.close() - self.assertTrue(ret, 'socket connected') + assert ret, 'socket connected' def form_encode(self, fields): is_multipart = False @@ -345,7 +338,7 @@ class TestHTTP(TestUnit): datatype = value['type'] if not isinstance(value['data'], io.IOBase): - self.fail('multipart encoding of file requires a stream.') + pytest.fail('multipart encoding of file requires a stream.') data = value['data'].read() @@ -353,7 +346,7 @@ class TestHTTP(TestUnit): data = value else: - self.fail('multipart requires a string or stream data') + pytest.fail('multipart requires a string or stream data') body += ( "--%s\r\nContent-Disposition: form-data; name=\"%s\"" diff --git a/test/unit/main.py b/test/unit/main.py index 83aa9139..d5940995 100644 --- a/test/unit/main.py +++ b/test/unit/main.py @@ -1,90 +1,26 @@ -import argparse import atexit -import fcntl import os -import platform import re import shutil import signal import stat import subprocess -import sys import tempfile import time -import unittest from multiprocessing import Process +import pytest +from conftest import _check_alerts +from conftest import _print_log +from conftest import option +from conftest import public_dir +from conftest import waitforfiles -class TestUnit(unittest.TestCase): - - current_dir = os.path.abspath( - os.path.join(os.path.dirname(__file__), os.pardir) - ) - pardir = os.path.abspath( - os.path.join(os.path.dirname(__file__), os.pardir, os.pardir) - ) - is_su = os.geteuid() == 0 - uid = os.geteuid() - gid = os.getegid() - architecture = platform.architecture()[0] - system = platform.system() - maxDiff = None - - detailed = False - save_log = False - print_log = False - unsafe = False - - def __init__(self, methodName='runTest'): - super().__init__(methodName) - - if re.match(r'.*\/run\.py$', sys.argv[0]): - args, rest = TestUnit._parse_args() - - TestUnit._set_args(args) - - def run(self, result=None): - if not hasattr(self, 'application_type'): - return super().run(result) - - # rerun test for each available module version - - type = self.application_type - for module in self.prerequisites['modules']: - if module in self.available['modules']: - prereq_version = self.prerequisites['modules'][module] - available_versions = self.available['modules'][module] - - if prereq_version == 'all': - for version in available_versions: - self.application_type = type + ' ' + version - super().run(result) - elif prereq_version == 'any': - self.application_type = type + ' ' + available_versions[0] - super().run(result) - else: - for version in available_versions: - if version.startswith(prereq_version): - self.application_type = type + ' ' + version - super().run(result) +class TestUnit(): @classmethod - def main(cls): - args, rest = TestUnit._parse_args() - - for i, arg in enumerate(rest): - if arg[:5] == 'test_': - rest[i] = cls.__name__ + '.' + arg - - sys.argv = sys.argv[:1] + rest - - TestUnit._set_args(args) - - unittest.main() - - @classmethod - def setUpClass(cls, complete_check=True): - cls.available = {'modules': {}, 'features': {}} + def setup_class(cls, complete_check=True): + cls.available = option.available unit = TestUnit() unit._run() @@ -92,7 +28,7 @@ class TestUnit(unittest.TestCase): # read unit.log for i in range(50): - with open(unit.testdir + '/unit.log', 'r') as f: + with open(unit.temp_dir + '/unit.log', 'r') as f: log = f.read() m = re.search('controller started', log) @@ -102,17 +38,9 @@ class TestUnit(unittest.TestCase): break if m is None: - unit._print_log() + _print_log(path=unit.temp_dir + '/unit.log') exit("Unit is writing log too long") - # discover available modules from unit.log - - for module in re.findall(r'module: ([a-zA-Z]+) (.*) ".*"$', log, re.M): - if module[0] not in cls.available['modules']: - cls.available['modules'][module[0]] = [module[1]] - else: - cls.available['modules'][module[0]].append(module[1]) - def check(available, prerequisites): missed = [] @@ -128,8 +56,7 @@ class TestUnit(unittest.TestCase): missed.append(module) if missed: - print('Unit has no ' + ', '.join(missed) + ' module(s)') - raise unittest.SkipTest() + pytest.skip('Unit has no ' + ', '.join(missed) + ' module(s)') # check features @@ -143,13 +70,12 @@ class TestUnit(unittest.TestCase): missed.append(feature) if missed: - print(', '.join(missed) + ' feature(s) not supported') - raise unittest.SkipTest() + pytest.skip(', '.join(missed) + ' feature(s) not supported') def destroy(): unit.stop() - unit._check_alerts(log) - shutil.rmtree(unit.testdir) + _check_alerts(log) + shutil.rmtree(unit.temp_dir) def complete(): destroy() @@ -161,92 +87,66 @@ class TestUnit(unittest.TestCase): unit.complete = complete return unit - def setUp(self): + def setup_method(self): self._run() def _run(self): - build_dir = self.pardir + '/build' + build_dir = option.current_dir + '/build' self.unitd = build_dir + '/unitd' if not os.path.isfile(self.unitd): exit("Could not find unit") - self.testdir = tempfile.mkdtemp(prefix='unit-test-') + self.temp_dir = tempfile.mkdtemp(prefix='unit-test-') - self.public_dir(self.testdir) + public_dir(self.temp_dir) if oct(stat.S_IMODE(os.stat(build_dir).st_mode)) != '0o777': - self.public_dir(build_dir) + public_dir(build_dir) - os.mkdir(self.testdir + '/state') + os.mkdir(self.temp_dir + '/state') - with open(self.testdir + '/unit.log', 'w') as log: + with open(self.temp_dir + '/unit.log', 'w') as log: self._p = subprocess.Popen( [ self.unitd, '--no-daemon', - '--modules', self.pardir + '/build', - '--state', self.testdir + '/state', - '--pid', self.testdir + '/unit.pid', - '--log', self.testdir + '/unit.log', - '--control', 'unix:' + self.testdir + '/control.unit.sock', - '--tmp', self.testdir, + '--modules', build_dir, + '--state', self.temp_dir + '/state', + '--pid', self.temp_dir + '/unit.pid', + '--log', self.temp_dir + '/unit.log', + '--control', 'unix:' + self.temp_dir + '/control.unit.sock', + '--tmp', self.temp_dir, ], stderr=log, ) atexit.register(self.stop) - if not self.waitforfiles(self.testdir + '/control.unit.sock'): - self._print_log() + if not waitforfiles(self.temp_dir + '/control.unit.sock'): + _print_log(path=self.temp_dir + '/unit.log') exit("Could not start unit") self._started = True - self.skip_alerts = [ - r'read signalfd\(4\) failed', - r'sendmsg.+failed', - r'recvmsg.+failed', - ] - self.skip_sanitizer = False - - def tearDown(self): + def teardown_method(self): self.stop() - # detect errors and failures for current test - - def list2reason(exc_list): - if exc_list and exc_list[-1][0] is self: - return exc_list[-1][1] - - if hasattr(self, '_outcome'): - result = self.defaultTestResult() - self._feedErrorsToResult(result, self._outcome.errors) - else: - result = getattr( - self, '_outcomeForDoCleanups', self._resultForDoCleanups - ) - - success = not list2reason(result.errors) and not list2reason( - result.failures - ) - # check unit.log for alerts - unit_log = self.testdir + '/unit.log' + unit_log = self.temp_dir + '/unit.log' with open(unit_log, 'r', encoding='utf-8', errors='ignore') as f: - self._check_alerts(f.read()) + _check_alerts(f.read()) # remove unit.log - if not TestUnit.save_log and success: - shutil.rmtree(self.testdir) - + if not option.save_log: + shutil.rmtree(self.temp_dir) else: - self._print_log() + _print_log(path=self.temp_dir) - self.assertListEqual(self.stop_errors, [None, None], 'stop errors') + assert self.stop_errors == [None, None], 'stop errors' def stop(self): if not self._started: @@ -301,121 +201,3 @@ class TestUnit(unittest.TestCase): if fail: return 'Fail to stop process' - - def waitforfiles(self, *files): - for i in range(50): - wait = False - ret = False - - for f in files: - if not os.path.exists(f): - wait = True - break - - if wait: - time.sleep(0.1) - - else: - ret = True - break - - return ret - - def public_dir(self, path): - os.chmod(path, 0o777) - - for root, dirs, files in os.walk(path): - for d in dirs: - os.chmod(os.path.join(root, d), 0o777) - for f in files: - os.chmod(os.path.join(root, f), 0o777) - - def _check_alerts(self, log): - found = False - - alerts = re.findall('.+\[alert\].+', log) - - if alerts: - print('All alerts/sanitizer errors found in log:') - [print(alert) for alert in alerts] - found = True - - if self.skip_alerts: - for skip in self.skip_alerts: - alerts = [al for al in alerts if re.search(skip, al) is None] - - if alerts: - self._print_log(log) - self.assertFalse(alerts, 'alert(s)') - - if not self.skip_sanitizer: - sanitizer_errors = re.findall('.+Sanitizer.+', log) - - if sanitizer_errors: - self._print_log(log) - self.assertFalse(sanitizer_errors, 'sanitizer error(s)') - - if found: - print('skipped.') - - @staticmethod - def _parse_args(): - parser = argparse.ArgumentParser(add_help=False) - - parser.add_argument( - '-d', - '--detailed', - dest='detailed', - action='store_true', - help='Detailed output for tests', - ) - parser.add_argument( - '-l', - '--log', - dest='save_log', - action='store_true', - help='Save unit.log after the test execution', - ) - parser.add_argument( - '-r', - '--reprint_log', - dest='print_log', - action='store_true', - help='Print unit.log to stdout in case of errors', - ) - parser.add_argument( - '-u', - '--unsafe', - dest='unsafe', - action='store_true', - help='Run unsafe tests', - ) - - return parser.parse_known_args() - - @staticmethod - def _set_args(args): - TestUnit.detailed = args.detailed - TestUnit.save_log = args.save_log - TestUnit.print_log = args.print_log - TestUnit.unsafe = args.unsafe - - # set stdout to non-blocking - - if TestUnit.detailed or TestUnit.print_log: - fcntl.fcntl(sys.stdout.fileno(), fcntl.F_SETFL, 0) - - def _print_log(self, data=None): - path = self.testdir + '/unit.log' - - print('Path to unit.log:\n' + path + '\n') - - if TestUnit.print_log: - os.set_blocking(sys.stdout.fileno(), True) - sys.stdout.flush() - - if data is None: - with open(path, 'r', encoding='utf-8', errors='ignore') as f: - shutil.copyfileobj(f, sys.stdout) - else: - sys.stdout.write(data) @@ -1,5 +1,5 @@ # Copyright (C) NGINX, Inc. -NXT_VERSION=1.19.0 -NXT_VERNUM=11900 +NXT_VERSION=1.20.0 +NXT_VERNUM=12000 |