You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

355 lines
11 KiB
Python

# Copyright (C) 2022 CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT
import re
from http import HTTPStatus
from pathlib import Path
from subprocess import PIPE, CalledProcessError, run
from time import sleep
import pytest
import requests
from shared.utils.config import ASSETS_DIR, get_api_url
CVAT_ROOT_DIR = next(dir.parent for dir in Path(__file__).parents if dir.name == "tests")
CVAT_DB_DIR = ASSETS_DIR / "cvat_db"
PREFIX = "test"
CONTAINER_NAME_FILES = [
CVAT_ROOT_DIR / dc_file
for dc_file in (
"components/analytics/docker-compose.analytics.tests.yml",
"docker-compose.tests.yml",
)
]
DC_FILES = [
CVAT_ROOT_DIR / dc_file
for dc_file in (
"docker-compose.dev.yml",
"tests/docker-compose.file_share.yml",
"tests/docker-compose.minio.yml",
"tests/docker-compose.test_servers.yml",
)
] + CONTAINER_NAME_FILES
def pytest_addoption(parser):
group = parser.getgroup("CVAT REST API testing options")
group._addoption(
"--start-services",
action="store_true",
help="Start all necessary CVAT containers without running tests. (default: %(default)s)",
)
group._addoption(
"--stop-services",
action="store_true",
help="Stop all testing containers without running tests. (default: %(default)s)",
)
group._addoption(
"--rebuild",
action="store_true",
help="Rebuild CVAT images and then start containers. (default: %(default)s)",
)
group._addoption(
"--cleanup",
action="store_true",
help="Delete files that was create by tests without running tests. (default: %(default)s)",
)
group._addoption(
"--dumpdb",
action="store_true",
help="Update data.json without running tests. (default: %(default)s)",
)
group._addoption(
"--platform",
action="store",
default="local",
choices=("kube", "local"),
help="Platform identifier - 'kube' or 'local'. (default: %(default)s)",
)
def _run(command, capture_output=True):
_command = command.split() if isinstance(command, str) else command
try:
stdout, stderr = "", ""
if capture_output:
proc = run(_command, check=True, stdout=PIPE, stderr=PIPE) # nosec
stdout, stderr = proc.stdout.decode(), proc.stderr.decode()
else:
proc = run(_command, check=True) # nosec
return stdout, stderr
except CalledProcessError as exc:
stderr = exc.stderr.decode() if capture_output else "see above"
pytest.exit(
f"Command failed: {command}.\n"
f"Error message: {stderr}.\n"
"Add `-s` option to see more details"
)
def _kube_get_server_pod_name():
output, _ = _run("kubectl get pods -l component=server -o jsonpath={.items[0].metadata.name}")
return output
def _kube_get_db_pod_name():
output, _ = _run(
"kubectl get pods -l app.kubernetes.io/name=postgresql -o jsonpath={.items[0].metadata.name}"
)
return output
def docker_cp(source, target):
_run(f"docker container cp {source} {target}")
def kube_cp(source, target):
_run(f"kubectl cp {source} {target}")
def docker_exec_cvat(command):
_run(f"docker exec {PREFIX}_cvat_server_1 {command}")
def kube_exec_cvat(command):
pod_name = _kube_get_server_pod_name()
_run(f"kubectl exec {pod_name} -- {command}")
def docker_exec_cvat_db(command):
_run(f"docker exec {PREFIX}_cvat_db_1 {command}")
def kube_exec_cvat_db(command):
pod_name = _kube_get_db_pod_name()
_run(["kubectl", "exec", pod_name, "--"] + command)
def docker_restore_db():
docker_exec_cvat_db("psql -U root -d postgres -v from=test_db -v to=cvat -f /tmp/restore.sql")
def kube_restore_db():
kube_exec_cvat_db(
[
"/bin/sh",
"-c",
"PGPASSWORD=cvat_postgresql_postgres psql -U postgres -d postgres -v from=test_db -v to=cvat -f /tmp/restore.sql",
]
)
def running_containers():
return [cn for cn in _run("docker ps --format {{.Names}}")[0].split("\n") if cn]
def dump_db():
if "test_cvat_server_1" not in running_containers():
pytest.exit("CVAT is not running")
with open(CVAT_DB_DIR / "data.json", "w") as f:
try:
run( # nosec
"docker exec test_cvat_server_1 \
python manage.py dumpdata \
--indent 2 --natural-foreign \
--exclude=auth.permission --exclude=contenttypes".split(),
stdout=f,
check=True,
)
except CalledProcessError:
pytest.exit("Database dump failed.\n")
def create_compose_files():
for filename in CONTAINER_NAME_FILES:
with open(filename.with_name(filename.name.replace(".tests", "")), "r") as dcf, open(
filename, "w"
) as ndcf:
ndcf.writelines(
[line for line in dcf.readlines() if not re.match("^.+container_name.+$", line)]
)
def delete_compose_files():
for filename in CONTAINER_NAME_FILES:
filename.unlink(missing_ok=True)
def wait_for_server():
for _ in range(30):
response = requests.get(get_api_url("users/self"))
if response.status_code == HTTPStatus.UNAUTHORIZED:
break
sleep(5)
def docker_restore_data_volumes():
docker_cp(
CVAT_DB_DIR / "cvat_data.tar.bz2",
f"{PREFIX}_cvat_server_1:/tmp/cvat_data.tar.bz2",
)
docker_exec_cvat("tar --strip 3 -xjf /tmp/cvat_data.tar.bz2 -C /home/django/data/")
def kube_restore_data_volumes():
pod_name = _kube_get_server_pod_name()
kube_cp(
CVAT_DB_DIR / "cvat_data.tar.bz2",
f"{pod_name}:/tmp/cvat_data.tar.bz2",
)
kube_exec_cvat("tar --strip 3 -xjf /tmp/cvat_data.tar.bz2 -C /home/django/data/")
def start_services(rebuild=False):
if any([cn in ["cvat_server", "cvat_db"] for cn in running_containers()]):
pytest.exit(
"It's looks like you already have running cvat containers. Stop them and try again. "
f"List of running containers: {', '.join(running_containers())}"
)
_run(
[
"docker-compose",
f"--project-name={PREFIX}",
# use compatibility mode to have fixed names for containers (with underscores)
# https://github.com/docker/compose#about-update-and-backward-compatibility
"--compatibility",
f"--env-file={CVAT_ROOT_DIR / 'tests/python/webhook_receiver/.env'}",
*(f"--file={f}" for f in DC_FILES),
"up",
"-d",
*["--build"] * rebuild,
],
capture_output=False,
)
docker_restore_data_volumes()
docker_cp(CVAT_DB_DIR / "restore.sql", f"{PREFIX}_cvat_db_1:/tmp/restore.sql")
docker_cp(CVAT_DB_DIR / "data.json", f"{PREFIX}_cvat_server_1:/tmp/data.json")
@pytest.fixture(autouse=True, scope="session")
def services(request):
stop = request.config.getoption("--stop-services")
start = request.config.getoption("--start-services")
rebuild = request.config.getoption("--rebuild")
cleanup = request.config.getoption("--cleanup")
dumpdb = request.config.getoption("--dumpdb")
platform = request.config.getoption("--platform")
if platform == "kube" and any((stop, start, rebuild, cleanup, dumpdb)):
raise Exception(
"""--platform=kube is not compatible with any of the other options
--stop-services --start-services --rebuild --cleanup --dumpdb"""
)
if platform == "local":
if start and stop:
raise Exception("--start-services and --stop-services are incompatible")
if dumpdb:
dump_db()
pytest.exit("data.json has been updated", returncode=0)
if cleanup:
delete_compose_files()
pytest.exit("All generated test files have been deleted", returncode=0)
if not all([f.exists() for f in CONTAINER_NAME_FILES]) or rebuild:
delete_compose_files()
create_compose_files()
if stop:
_run(
[
"docker-compose",
f"--project-name={PREFIX}",
# use compatibility mode to have fixed names for containers (with underscores)
# https://github.com/docker/compose#about-update-and-backward-compatibility
"--compatibility",
f"--env-file={CVAT_ROOT_DIR / 'tests/python/webhook_receiver/.env'}",
*(f"--file={f}" for f in DC_FILES),
"down",
"-v",
],
capture_output=False,
)
pytest.exit("All testing containers are stopped", returncode=0)
start_services(rebuild)
wait_for_server()
docker_exec_cvat("python manage.py loaddata /tmp/data.json")
docker_exec_cvat_db(
"psql -U root -d postgres -v from=cvat -v to=test_db -f /tmp/restore.sql"
)
if start:
pytest.exit("All necessary containers have been created and started.", returncode=0)
yield
docker_restore_db()
docker_exec_cvat_db("dropdb test_db")
elif platform == "kube":
kube_restore_data_volumes()
server_pod_name = _kube_get_server_pod_name()
db_pod_name = _kube_get_db_pod_name()
kube_cp(CVAT_DB_DIR / "restore.sql", f"{db_pod_name}:/tmp/restore.sql")
kube_cp(CVAT_DB_DIR / "data.json", f"{server_pod_name}:/tmp/data.json")
wait_for_server()
kube_exec_cvat("python manage.py loaddata /tmp/data.json")
kube_exec_cvat_db(
[
"/bin/sh",
"-c",
"PGPASSWORD=cvat_postgresql_postgres psql -U postgres -d postgres -v from=cvat -v to=test_db -f /tmp/restore.sql",
]
)
yield
@pytest.fixture(scope="function")
def restore_db_per_function(request):
# Note that autouse fixtures are executed first within their scope, so be aware of the order
# Pre-test DB setups (eg. with class-declared autouse setup() method) may be cleaned.
# https://docs.pytest.org/en/stable/reference/fixtures.html#autouse-fixtures-are-executed-first-within-their-scope
platform = request.config.getoption("--platform")
if platform == "local":
docker_restore_db()
else:
kube_restore_db()
@pytest.fixture(scope="class")
def restore_db_per_class(request):
platform = request.config.getoption("--platform")
if platform == "local":
docker_restore_db()
else:
kube_restore_db()
@pytest.fixture(scope="function")
def restore_cvat_data(request):
platform = request.config.getoption("--platform")
if platform == "local":
docker_restore_data_volumes()
else:
kube_restore_data_volumes()