create basic integration tests with .env file support for specific engines

This commit is contained in:
Grant Lanham 2024-01-20 16:52:11 -05:00
parent a19028bd90
commit 3877dbc764
7 changed files with 189 additions and 78 deletions

3
.env.test Normal file
View file

@ -0,0 +1,3 @@
# Add comma separated list of engines to test. Match the file name. If no values provided, all engines will be tested
# TEST_INTEGRATION_ENGINES=google,bing,yahoo
TEST_INTEGRATION_ENGINES=

View file

@ -84,7 +84,7 @@ MANAGE += py.build py.clean
MANAGE += pyenv pyenv.install pyenv.uninstall MANAGE += pyenv pyenv.install pyenv.uninstall
MANAGE += pypi.upload pypi.upload.test MANAGE += pypi.upload pypi.upload.test
MANAGE += format.python MANAGE += format.python
MANAGE += test.yamllint test.pylint test.pyright test.black test.pybabel test.unit test.coverage test.robot test.rst test.clean MANAGE += test.yamllint test.pylint test.pyright test.black test.pybabel test.unit test.coverage test.robot test.rst test.int test.clean
MANAGE += themes.all themes.simple themes.simple.test pygments.less MANAGE += themes.all themes.simple themes.simple.test pygments.less
MANAGE += static.build.commit static.build.drop static.build.restore MANAGE += static.build.commit static.build.drop static.build.restore
MANAGE += nvm.install nvm.clean nvm.status nvm.nodejs MANAGE += nvm.install nvm.clean nvm.status nvm.nodejs

130
manage
View file

@ -45,6 +45,8 @@ GECKODRIVER_VERSION="v0.33.0"
# SPHINXOPTS= # SPHINXOPTS=
BLACK_OPTIONS=("--target-version" "py311" "--line-length" "120" "--skip-string-normalization") BLACK_OPTIONS=("--target-version" "py311" "--line-length" "120" "--skip-string-normalization")
BLACK_TARGETS=("--exclude" "(searx/static|searx/languages.py)" "--include" 'searxng.msg|\.pyi?$' "searx" "searxng_extra" "tests") BLACK_TARGETS=("--exclude" "(searx/static|searx/languages.py)" "--include" 'searxng.msg|\.pyi?$' "searx" "searxng_extra" "tests")
# add one or more engines, comma seperated, here to only test a subset of engines
INTEGRATION_ENGINES="google"
_dev_redis_sock="/usr/local/searxng-redis/run/redis.sock" _dev_redis_sock="/usr/local/searxng-redis/run/redis.sock"
# set SEARXNG_REDIS_URL if it is not defined and "{_dev_redis_sock}" exists. # set SEARXNG_REDIS_URL if it is not defined and "{_dev_redis_sock}" exists.
@ -66,8 +68,8 @@ pylint.FILES() {
YAMLLINT_FILES=() YAMLLINT_FILES=()
while IFS= read -r line; do while IFS= read -r line; do
YAMLLINT_FILES+=("$line") YAMLLINT_FILES+=("$line")
done <<< "$(git ls-files './tests/*.yml' './searx/*.yml' './utils/templates/etc/searxng/*.yml')" done <<<"$(git ls-files './tests/*.yml' './searx/*.yml' './utils/templates/etc/searxng/*.yml')"
RST_FILES=( RST_FILES=(
'README.rst' 'README.rst'
@ -129,7 +131,6 @@ environment ...
EOF EOF
} }
if [ "$VERBOSE" = "1" ]; then if [ "$VERBOSE" = "1" ]; then
SPHINX_VERBOSE="-v" SPHINX_VERBOSE="-v"
PYLINT_VERBOSE="-v" PYLINT_VERBOSE="-v"
@ -142,14 +143,14 @@ webapp.run() {
local parent_proc="$$" local parent_proc="$$"
( (
if [ "${LIVE_THEME}" ]; then if [ "${LIVE_THEME}" ]; then
( themes.live "${LIVE_THEME}" ) (themes.live "${LIVE_THEME}")
kill $parent_proc kill $parent_proc
fi fi
)& ) &
( (
sleep 3 sleep 3
xdg-open http://127.0.0.1:8888/ xdg-open http://127.0.0.1:8888/
)& ) &
SEARXNG_DEBUG=1 pyenv.cmd python -m searx.webapp SEARXNG_DEBUG=1 pyenv.cmd python -m searx.webapp
} }
@ -176,19 +177,20 @@ docker.build() {
# See https://www.shellcheck.net/wiki/SC1001 and others .. # See https://www.shellcheck.net/wiki/SC1001 and others ..
# shellcheck disable=SC2031,SC2230,SC2002,SC2236,SC2143,SC1001 # shellcheck disable=SC2031,SC2230,SC2002,SC2236,SC2143,SC1001
( set -e (
set -e
pyenv.activate pyenv.activate
# Check if it is a git repository # Check if it is a git repository
if [ ! -d .git ]; then if [ ! -d .git ]; then
die 1 "This is not Git repository" die 1 "This is not Git repository"
fi fi
if [ ! -x "$(which git)" ]; then if [ ! -x "$(which git)" ]; then
die 1 "git is not installed" die 1 "git is not installed"
fi fi
if ! git remote get-url origin 2> /dev/null; then if ! git remote get-url origin 2>/dev/null; then
die 1 "there is no remote origin" die 1 "there is no remote origin"
fi fi
# This is a git repository # This is a git repository
@ -217,22 +219,22 @@ docker.build() {
build_msg DOCKER "Building image ${SEARXNG_IMAGE_NAME}:${SEARXNG_GIT_VERSION}" build_msg DOCKER "Building image ${SEARXNG_IMAGE_NAME}:${SEARXNG_GIT_VERSION}"
# shellcheck disable=SC2086 # shellcheck disable=SC2086
docker $BUILD \ docker $BUILD \
--build-arg BASE_IMAGE="${DEPENDENCIES_IMAGE_NAME}" \ --build-arg BASE_IMAGE="${DEPENDENCIES_IMAGE_NAME}" \
--build-arg GIT_URL="${GIT_URL}" \ --build-arg GIT_URL="${GIT_URL}" \
--build-arg SEARXNG_DOCKER_TAG="${DOCKER_TAG}" \ --build-arg SEARXNG_DOCKER_TAG="${DOCKER_TAG}" \
--build-arg SEARXNG_GIT_VERSION="${VERSION_STRING}" \ --build-arg SEARXNG_GIT_VERSION="${VERSION_STRING}" \
--build-arg VERSION_GITCOMMIT="${VERSION_GITCOMMIT}" \ --build-arg VERSION_GITCOMMIT="${VERSION_GITCOMMIT}" \
--build-arg LABEL_DATE="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \ --build-arg LABEL_DATE="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
--build-arg LABEL_VCS_REF="$(git rev-parse HEAD)" \ --build-arg LABEL_VCS_REF="$(git rev-parse HEAD)" \
--build-arg LABEL_VCS_URL="${GIT_URL}" \ --build-arg LABEL_VCS_URL="${GIT_URL}" \
--build-arg TIMESTAMP_SETTINGS="$(git log -1 --format="%cd" --date=unix -- searx/settings.yml)" \ --build-arg TIMESTAMP_SETTINGS="$(git log -1 --format="%cd" --date=unix -- searx/settings.yml)" \
--build-arg TIMESTAMP_UWSGI="$(git log -1 --format="%cd" --date=unix -- dockerfiles/uwsgi.ini)" \ --build-arg TIMESTAMP_UWSGI="$(git log -1 --format="%cd" --date=unix -- dockerfiles/uwsgi.ini)" \
-t "${SEARXNG_IMAGE_NAME}:latest" -t "${SEARXNG_IMAGE_NAME}:${DOCKER_TAG}" . -t "${SEARXNG_IMAGE_NAME}:latest" -t "${SEARXNG_IMAGE_NAME}:${DOCKER_TAG}" .
if [ "$1" = "push" ]; then if [ "$1" = "push" ]; then
docker push "${SEARXNG_IMAGE_NAME}:latest" docker push "${SEARXNG_IMAGE_NAME}:latest"
docker push "${SEARXNG_IMAGE_NAME}:${DOCKER_TAG}" docker push "${SEARXNG_IMAGE_NAME}:${DOCKER_TAG}"
fi fi
) )
dump_return $? dump_return $?
} }
@ -243,10 +245,11 @@ gecko.driver() {
build_msg INSTALL "gecko.driver" build_msg INSTALL "gecko.driver"
# run installation in a subprocess and activate pyenv # run installation in a subprocess and activate pyenv
( set -e (
set -e
pyenv.activate pyenv.activate
INSTALLED_VERSION=$(geckodriver -V 2> /dev/null | head -1 | awk '{ print "v" $2}') || INSTALLED_VERSION="" INSTALLED_VERSION=$(geckodriver -V 2>/dev/null | head -1 | awk '{ print "v" $2}') || INSTALLED_VERSION=""
set +e set +e
if [ "${INSTALLED_VERSION}" = "${GECKODRIVER_VERSION}" ]; then if [ "${INSTALLED_VERSION}" = "${GECKODRIVER_VERSION}" ]; then
build_msg INSTALL "geckodriver already installed" build_msg INSTALL "geckodriver already installed"
@ -254,13 +257,13 @@ gecko.driver() {
fi fi
PLATFORM="$(python3 -c 'import platform; print(platform.system().lower(), platform.architecture()[0])')" PLATFORM="$(python3 -c 'import platform; print(platform.system().lower(), platform.architecture()[0])')"
case "$PLATFORM" in case "$PLATFORM" in
"linux 32bit" | "linux2 32bit") ARCH="linux32";; "linux 32bit" | "linux2 32bit") ARCH="linux32" ;;
"linux 64bit" | "linux2 64bit") ARCH="linux64";; "linux 64bit" | "linux2 64bit") ARCH="linux64" ;;
"windows 32 bit") ARCH="win32";; "windows 32 bit") ARCH="win32" ;;
"windows 64 bit") ARCH="win64";; "windows 64 bit") ARCH="win64" ;;
"mac 64bit") ARCH="macos";; "mac 64bit") ARCH="macos" ;;
esac esac
GECKODRIVER_URL="https://github.com/mozilla/geckodriver/releases/download/$GECKODRIVER_VERSION/geckodriver-$GECKODRIVER_VERSION-$ARCH.tar.gz"; GECKODRIVER_URL="https://github.com/mozilla/geckodriver/releases/download/$GECKODRIVER_VERSION/geckodriver-$GECKODRIVER_VERSION-$ARCH.tar.gz"
build_msg GECKO "Installing ${PY_ENV_BIN}/geckodriver from $GECKODRIVER_URL" build_msg GECKO "Installing ${PY_ENV_BIN}/geckodriver from $GECKODRIVER_URL"
@ -284,13 +287,14 @@ pygments.less() {
py.build() { py.build() {
build_msg BUILD "python package ${PYDIST}" build_msg BUILD "python package ${PYDIST}"
pyenv.cmd python setup.py \ pyenv.cmd python setup.py \
sdist -d "${PYDIST}" \ sdist -d "${PYDIST}" \
bdist_wheel --bdist-dir "${PYBUILD}" -d "${PYDIST}" bdist_wheel --bdist-dir "${PYBUILD}" -d "${PYDIST}"
} }
py.clean() { py.clean() {
build_msg CLEAN pyenv build_msg CLEAN pyenv
( set -e (
set -e
pyenv.drop pyenv.drop
[ "$VERBOSE" = "1" ] && set -x [ "$VERBOSE" = "1" ] && set -x
rm -rf "${PYDIST}" "${PYBUILD}" "${PY_ENV}" ./.tox ./*.egg-info rm -rf "${PYDIST}" "${PYBUILD}" "${PY_ENV}" ./.tox ./*.egg-info
@ -301,7 +305,7 @@ py.clean() {
} }
pyenv.check() { pyenv.check() {
cat <<EOF cat <<EOF
import yaml import yaml
print('import yaml --> OK') print('import yaml --> OK')
EOF EOF
@ -310,13 +314,14 @@ EOF
pyenv.install() { pyenv.install() {
if ! pyenv.OK; then if ! pyenv.OK; then
py.clean > /dev/null py.clean >/dev/null
fi fi
if pyenv.install.OK > /dev/null; then if pyenv.install.OK >/dev/null; then
return 0 return 0
fi fi
( set -e (
set -e
pyenv pyenv
build_msg PYENV "[install] pip install -e 'searx${PY_SETUP_EXTRAS}'" build_msg PYENV "[install] pip install -e 'searx${PY_SETUP_EXTRAS}'"
"${PY_ENV_BIN}/python" -m pip install -e ".${PY_SETUP_EXTRAS}" "${PY_ENV_BIN}/python" -m pip install -e ".${PY_SETUP_EXTRAS}"
@ -329,8 +334,8 @@ pyenv.install() {
pyenv.uninstall() { pyenv.uninstall() {
build_msg PYENV "[pyenv.uninstall] uninstall packages: ${PYOBJECTS}" build_msg PYENV "[pyenv.uninstall] uninstall packages: ${PYOBJECTS}"
pyenv.cmd python setup.py develop --uninstall 2>&1 \ pyenv.cmd python setup.py develop --uninstall 2>&1 |
| prefix_stdout "${_Blue}PYENV ${_creset}[pyenv.uninstall] " prefix_stdout "${_Blue}PYENV ${_creset}[pyenv.uninstall] "
} }
@ -353,17 +358,17 @@ format.python() {
dump_return $? dump_return $?
} }
PYLINT_FILES=() PYLINT_FILES=()
while IFS= read -r line; do while IFS= read -r line; do
PYLINT_FILES+=("$line") PYLINT_FILES+=("$line")
done <<< "$(pylint.FILES)" done <<<"$(pylint.FILES)"
# shellcheck disable=SC2119 # shellcheck disable=SC2119
main() { main() {
local _type local _type
local cmd="$1"; shift local cmd="$1"
shift
if [ "$cmd" == "" ]; then if [ "$cmd" == "" ]; then
help help
@ -372,22 +377,25 @@ main() {
fi fi
case "$cmd" in case "$cmd" in
--getenv) var="$1"; echo "${!var}";; --getenv)
--help) help;; var="$1"
--*) echo "${!var}"
help ;;
err_msg "unknown option $cmd" --help) help ;;
--*)
help
err_msg "unknown option $cmd"
return 42
;;
*)
_type="$(type -t "$cmd")"
if [ "$_type" != 'function' ]; then
err_msg "unknown command: $cmd / use --help"
return 42 return 42
;; else
*) "$cmd" "$@"
_type="$(type -t "$cmd")" fi
if [ "$_type" != 'function' ]; then ;;
err_msg "unknown command: $cmd / use --help"
return 42
else
"$cmd" "$@"
fi
;;
esac esac
} }

View file

@ -21,3 +21,4 @@ aiounittest==1.4.2
yamllint==1.33.0 yamllint==1.33.0
wlc==1.13 wlc==1.13
coloredlogs==15.0.1 coloredlogs==15.0.1
python-dotenv==1.0.0

View file

View file

@ -0,0 +1,91 @@
from os import getenv
from searx import settings, engines, settings
from searx.search import SearchQuery, Search, EngineRef, initialize
from tests import SearxTestCase
from typing import Tuple, Optional
import sys
import logging
from flask import Flask
from dotenv import load_dotenv, find_dotenv
logger = logging.getLogger()
logger.level = logging.INFO
stream_handler = logging.StreamHandler(sys.stdout)
logger.addHandler(stream_handler)
SAFESEARCH = 0
PAGENO = 1
def test_single_engine(app: Flask, engine_name: str) -> Tuple[str, Optional[Exception], int]:
logger.debug('---------------------------')
logger.info(f'Testing Engine: {engine_name}')
try:
with app.test_request_context():
# test your app context code
search_query = SearchQuery(
'test', [EngineRef(engine_name, 'general')], 'en-US', SAFESEARCH, PAGENO, None, None
)
search = Search(search_query)
info = search.search()
return (engine_name, None, info.results_length())
except Exception as e:
return (engine_name, e, 0)
finally:
logger.debug('---------------------------')
def get_specific_engines() -> list[str]:
load_dotenv(find_dotenv("../../.env.test", raise_error_if_not_found=True))
integration_engines = getenv("TEST_INTEGRATION_ENGINES")
if integration_engines is None or integration_engines == '':
return []
return integration_engines.split(',')
class TestEnginesSingleSearch(SearxTestCase):
@classmethod
def setUpClass(cls):
cls.app = Flask(__name__)
specific_engines = get_specific_engines()
if len(specific_engines) > 0:
cls.engines = [eng for eng in settings['engines'] if eng['name'] in specific_engines]
else:
cls.engines = settings['engines']
cls.engine_names = [eng['name'] for eng in cls.engines]
initialize(cls.engines)
@classmethod
def tearDownClass(cls):
settings['outgoing']['using_tor_proxy'] = False
settings['outgoing']['extra_proxy_timeout'] = 0
def test_all_engines(self):
results = [test_single_engine(self.app, engine_name) for engine_name in self.engine_names]
engines_passed = []
engines_exception = []
engines_no_results = []
for r in results:
if r[1] is not None:
engines_exception.append(r)
elif r[2] <= 0:
engines_no_results.append(r)
else:
engines_passed.append(r)
def log_results(lst, name: str, level: int):
logger.log(level, f'{name}: {len(lst)}')
for e in lst:
logger.log(level, f'{name}: {e[0]}')
if e[1] is not None:
logger.log(level, f'{name}: {e[1]}')
log_results(engines_passed, 'engines_passed', logging.INFO)
log_results(engines_exception, 'engines_exception', logging.ERROR)
log_results(engines_no_results, 'engines_no_results', logging.WARN)
self.assertEqual(len(engines_exception), 0)
self.assertEqual(len(engines_no_results), 0)

View file

@ -1,4 +1,4 @@
test.help(){ test.help() {
cat <<EOF cat <<EOF
test.: test.:
yamllint : lint YAML files (YAMLLINT_FILES) yamllint : lint YAML files (YAMLLINT_FILES)
@ -9,6 +9,7 @@ test.:
coverage : run unit tests with coverage coverage : run unit tests with coverage
robot : run robot test robot : run robot test
rst : test .rst files incl. README.rst rst : test .rst files incl. README.rst
int : run integraiton tests on engines
clean : clean intermediate test stuff clean : clean intermediate test stuff
EOF EOF
} }
@ -21,7 +22,8 @@ test.yamllint() {
test.pylint() { test.pylint() {
# shellcheck disable=SC2086 # shellcheck disable=SC2086
( set -e (
set -e
build_msg TEST "[pylint] \$PYLINT_FILES" build_msg TEST "[pylint] \$PYLINT_FILES"
pyenv.activate pyenv.activate
python ${PYLINT_OPTIONS} ${PYLINT_VERBOSE} \ python ${PYLINT_OPTIONS} ${PYLINT_VERBOSE} \
@ -37,8 +39,8 @@ test.pylint() {
build_msg TEST "[pylint] searx tests" build_msg TEST "[pylint] searx tests"
python ${PYLINT_OPTIONS} ${PYLINT_VERBOSE} \ python ${PYLINT_OPTIONS} ${PYLINT_VERBOSE} \
--disable="${PYLINT_SEARXNG_DISABLE_OPTION}" \ --disable="${PYLINT_SEARXNG_DISABLE_OPTION}" \
--ignore=searx/engines \ --ignore=searx/engines \
searx tests searx tests
) )
dump_return $? dump_return $?
} }
@ -49,13 +51,13 @@ test.pyright() {
# We run Pyright in the virtual environment because Pyright # We run Pyright in the virtual environment because Pyright
# executes "python" to determine the Python version. # executes "python" to determine the Python version.
build_msg TEST "[pyright] suppress warnings related to intentional monkey patching" build_msg TEST "[pyright] suppress warnings related to intentional monkey patching"
pyenv.cmd npx --no-install pyright -p pyrightconfig-ci.json \ pyenv.cmd npx --no-install pyright -p pyrightconfig-ci.json |
| grep -v ".py$" \ grep -v ".py$" |
| grep -v '/engines/.*.py.* - warning: "logger" is not defined'\ grep -v '/engines/.*.py.* - warning: "logger" is not defined' |
| grep -v '/plugins/.*.py.* - error: "logger" is not defined'\ grep -v '/plugins/.*.py.* - error: "logger" is not defined' |
| grep -v '/engines/.*.py.* - warning: "supported_languages" is not defined' \ grep -v '/engines/.*.py.* - warning: "supported_languages" is not defined' |
| grep -v '/engines/.*.py.* - warning: "language_aliases" is not defined' \ grep -v '/engines/.*.py.* - warning: "language_aliases" is not defined' |
| grep -v '/engines/.*.py.* - warning: "categories" is not defined' grep -v '/engines/.*.py.* - warning: "categories" is not defined'
dump_return $? dump_return $?
} }
@ -73,7 +75,8 @@ test.unit() {
test.coverage() { test.coverage() {
build_msg TEST 'unit test coverage' build_msg TEST 'unit test coverage'
( set -e (
set -e
pyenv.activate pyenv.activate
python -m nose2 -C --log-capture --with-coverage --coverage searx -s tests/unit python -m nose2 -C --log-capture --with-coverage --coverage searx -s tests/unit
coverage report coverage report
@ -92,7 +95,7 @@ test.robot() {
test.rst() { test.rst() {
build_msg TEST "[reST markup] ${RST_FILES[*]}" build_msg TEST "[reST markup] ${RST_FILES[*]}"
for rst in "${RST_FILES[@]}"; do for rst in "${RST_FILES[@]}"; do
pyenv.cmd rst2html.py --halt error "$rst" > /dev/null || die 42 "fix issue in $rst" pyenv.cmd rst2html.py --halt error "$rst" >/dev/null || die 42 "fix issue in $rst"
done done
} }
@ -103,9 +106,14 @@ test.pybabel() {
pyenv.cmd pybabel extract -F babel.cfg -o "${TEST_BABEL_FOLDER}/messages.pot" searx pyenv.cmd pybabel extract -F babel.cfg -o "${TEST_BABEL_FOLDER}/messages.pot" searx
} }
test.clean() { test.int() {
build_msg CLEAN "test stuff" build_msg TEST 'tests/integration'
rm -rf geckodriver.log .coverage coverage/ pyenv.cmd python -m nose2 -s tests/integration
dump_return $? dump_return $?
} }
test.clean() {
build_msg CLEAN "test stuff"
rm -rf geckodriver.log .coverage coverage/
dump_return $?
}