chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:32:23 +08:00
commit 10df941303
2913 changed files with 455804 additions and 0 deletions
+628
View File
@@ -0,0 +1,628 @@
# Copyright (C) CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT
import json
import operator
from collections import defaultdict
from collections.abc import Generator, Iterable
from copy import deepcopy
from pathlib import Path
import pytest
from shared.utils.config import ASSETS_DIR, SHARE_DIR
class Container:
def __init__(self, data, key="id"):
self.raw_data = data
self.map_data = {obj[key]: obj for obj in data}
@property
def raw(self):
return self.raw_data
@property
def map(self):
return self.map_data
def __iter__(self):
return iter(self.raw_data)
def __len__(self):
return len(self.raw_data)
def __getitem__(self, key):
if isinstance(key, slice):
return self.raw_data[key]
return self.map_data[key]
@pytest.fixture(scope="session")
def users():
with open(ASSETS_DIR / "users.json") as f:
return Container(json.load(f)["results"])
@pytest.fixture(scope="session")
def organizations():
with open(ASSETS_DIR / "organizations.json") as f:
return Container(json.load(f)["results"])
@pytest.fixture(scope="session")
def memberships():
with open(ASSETS_DIR / "memberships.json") as f:
return Container(json.load(f)["results"])
@pytest.fixture(scope="session")
def tasks():
with open(ASSETS_DIR / "tasks.json") as f:
return Container(json.load(f)["results"])
def filter_assets(resources: Iterable, **kwargs):
filtered_resources = []
exclude_prefix = "exclude_"
filter_operator = lambda arg, func: bool(func(arg))
for resource in resources:
is_matched = True
for key, value in kwargs.items():
if not is_matched:
break
op = operator.eq
if key.startswith(exclude_prefix):
key = key[len(exclude_prefix) :]
op = operator.ne
elif callable(value):
op = filter_operator
cur_value, rest = resource, key
while rest:
field_and_rest = rest.split("__", maxsplit=1)
if 2 == len(field_and_rest):
field, rest = field_and_rest
else:
field, rest = field_and_rest[0], None
cur_value = cur_value.get(field, None)
# e.g. task has null target_storage
# or there are mutexed project_id, task_id
if not cur_value:
break
if not (not rest and op(cur_value, value) or rest and op == operator.ne):
is_matched = False
if is_matched:
filtered_resources.append(resource)
return filtered_resources
@pytest.fixture(scope="session")
def filter_projects(projects):
def filter_(**kwargs):
return filter_assets(projects, **kwargs)
return filter_
@pytest.fixture(scope="session")
def filter_tasks(tasks):
def filter_(**kwargs):
return filter_assets(tasks, **kwargs)
return filter_
@pytest.fixture(scope="session")
def tasks_wlc(labels, tasks): # tasks with labels count
tasks = deepcopy(tasks)
tasks_by_project = defaultdict(list)
for task in tasks:
tasks_by_project[task["project_id"]].append(task)
task["labels"]["count"] = 0
for label in labels:
task_id = label.get("task_id")
project_id = label.get("project_id")
if not label["parent_id"]:
if task_id:
tasks[task_id]["labels"]["count"] += 1
elif project_id:
for task in tasks_by_project[project_id]:
task["labels"]["count"] += 1
return tasks
@pytest.fixture(scope="session")
def projects():
with open(ASSETS_DIR / "projects.json") as f:
return Container(json.load(f)["results"])
@pytest.fixture(scope="session")
def projects_wlc(projects, labels): # projects with labels count
projects = deepcopy(projects)
for project in projects:
project["labels"]["count"] = 0
for label in labels:
project_id = label.get("project_id")
if not label["parent_id"] and project_id:
projects[project_id]["labels"]["count"] += 1
return projects
@pytest.fixture(scope="session")
def jobs():
with open(ASSETS_DIR / "jobs.json") as f:
return Container(json.load(f)["results"])
@pytest.fixture(scope="session")
def jobs_wlc(jobs, tasks_wlc): # jobs with labels count
jobs = deepcopy(jobs)
for job in jobs:
tid = job["task_id"]
job["labels"]["count"] = tasks_wlc[tid]["labels"]["count"]
return jobs
@pytest.fixture(scope="session")
def invitations():
with open(ASSETS_DIR / "invitations.json") as f:
return Container(json.load(f)["results"], key="key")
@pytest.fixture(scope="session")
def annotations():
with open(ASSETS_DIR / "annotations.json") as f:
return json.load(f)
CloudStorageAssets = Container
@pytest.fixture(scope="session")
def cloud_storages() -> CloudStorageAssets:
with open(ASSETS_DIR / "cloudstorages.json") as f:
return Container(json.load(f)["results"])
@pytest.fixture(scope="session")
def issues():
with open(ASSETS_DIR / "issues.json") as f:
return Container(json.load(f)["results"])
@pytest.fixture(scope="session")
def comments():
with open(ASSETS_DIR / "comments.json") as f:
return Container(json.load(f)["results"])
@pytest.fixture(scope="session")
def webhooks():
with open(ASSETS_DIR / "webhooks.json") as f:
return Container(json.load(f)["results"])
@pytest.fixture(scope="session")
def labels():
with open(ASSETS_DIR / "labels.json") as f:
return Container(json.load(f)["results"])
@pytest.fixture(scope="session")
def quality_reports():
with open(ASSETS_DIR / "quality_reports.json") as f:
return Container(json.load(f)["results"])
@pytest.fixture(scope="session")
def quality_conflicts():
with open(ASSETS_DIR / "quality_conflicts.json") as f:
return Container(json.load(f)["results"])
@pytest.fixture(scope="session")
def quality_settings():
with open(ASSETS_DIR / "quality_settings.json") as f:
return Container(json.load(f)["results"])
@pytest.fixture(scope="session")
def consensus_settings():
with open(ASSETS_DIR / "consensus_settings.json") as f:
return Container(json.load(f)["results"])
@pytest.fixture(scope="session")
def users_by_name(users):
return {user["username"]: user for user in users}
@pytest.fixture(scope="session")
def jobs_by_org(tasks, jobs):
# FUTURE-FIXME: should be based on organizations to include orgs without jobs too
data = {}
for job in jobs:
data.setdefault(tasks[job["task_id"]]["organization"], []).append(job)
data[""] = data.pop(None, [])
return data
@pytest.fixture(scope="session")
def projects_by_org(projects):
data = {}
for project in projects:
data.setdefault(project["organization"], []).append(project)
data[""] = data.pop(None, [])
return data
@pytest.fixture(scope="session")
def tasks_by_org(tasks):
data = {}
for task in tasks:
data.setdefault(task["organization"], []).append(task)
data[""] = data.pop(None, [])
return data
@pytest.fixture(scope="session")
def issues_by_org(tasks, jobs, issues):
data = {}
for issue in issues:
data.setdefault(tasks[jobs[issue["job"]]["task_id"]]["organization"], []).append(issue)
data[""] = data.pop(None, [])
return data
@pytest.fixture(scope="session")
def assignee_id():
def get_id(data):
if data.get("assignee") is not None:
return data["assignee"]["id"]
return get_id
def ownership(func):
def wrap(user_id, resource_id):
if resource_id is None:
return False
return func(user_id, resource_id)
return wrap
@pytest.fixture(scope="session")
def is_project_staff(projects, assignee_id):
@ownership
def check(user_id, pid):
return user_id == projects[pid]["owner"]["id"] or user_id == assignee_id(projects[pid])
return check
@pytest.fixture(scope="session")
def is_task_staff(tasks, is_project_staff, assignee_id):
@ownership
def check(user_id, tid):
return (
user_id == tasks[tid]["owner"]["id"]
or user_id == assignee_id(tasks[tid])
or is_project_staff(user_id, tasks[tid]["project_id"])
)
return check
@pytest.fixture(scope="session")
def is_job_staff(jobs, is_task_staff, assignee_id):
@ownership
def check(user_id, jid):
return user_id == assignee_id(jobs[jid]) or is_task_staff(user_id, jobs[jid]["task_id"])
return check
@pytest.fixture(scope="session")
def is_issue_staff(issues, jobs, assignee_id):
@ownership
def check(user_id, issue_id):
return (
user_id == issues[issue_id]["owner"]["id"]
or user_id == assignee_id(issues[issue_id])
or user_id == assignee_id(jobs[issues[issue_id]["job"]])
)
return check
@pytest.fixture(scope="session")
def is_issue_admin(issues, jobs, is_task_staff):
@ownership
def check(user_id, issue_id):
return is_task_staff(user_id, jobs[issues[issue_id]["job"]]["task_id"])
return check
@pytest.fixture(scope="session")
def find_users(test_db):
def find(**kwargs):
assert len(kwargs) > 0
data = test_db
for field, value in kwargs.items():
if field.startswith("exclude_"):
field = field.split("_", maxsplit=1)[1]
exclude_rows = set(v["id"] for v in filter(lambda a: a[field] == value, test_db))
data = list(filter(lambda a: a["id"] not in exclude_rows, data))
else:
data = list(filter(lambda a: a[field] == value, data))
return data
return find
@pytest.fixture(scope="session")
def test_db(users, users_by_name, memberships):
data = []
fields = [
"username",
"id",
"privilege",
"role",
"org",
"membership_id",
"is_superuser",
"has_analytics_access",
]
def add_row(**kwargs):
data.append({field: kwargs.get(field) for field in fields})
for user in users:
for group in user["groups"]:
add_row(
username=user["username"],
id=user["id"],
privilege=group,
has_analytics_access=user["has_analytics_access"],
is_superuser=user["is_superuser"],
)
for membership in memberships:
username = membership["user"]["username"]
for group in users_by_name[username]["groups"]:
add_row(
username=username,
role=membership["role"],
privilege=group,
id=membership["user"]["id"],
org=membership["organization"],
membership_id=membership["id"],
has_analytics_access=users_by_name[username]["has_analytics_access"],
is_superuser=users_by_name[username]["is_superuser"],
)
return data
@pytest.fixture(scope="session")
def org_staff(memberships):
def find(org_id):
if org_id in ["", None]:
return set()
else:
return set(
m["user"]["id"]
for m in memberships
if m["role"] in ["maintainer", "owner"]
and m["user"] is not None
and m["organization"] == org_id
)
return find
@pytest.fixture(scope="session")
def is_org_member(memberships):
def check(user_id, org_id, *, role=None):
if org_id in ["", None]:
return True
else:
return user_id in set(
m["user"]["id"]
for m in memberships
if m["user"] is not None
if m["organization"] == org_id
if not role or m["role"] == role
)
return check
@pytest.fixture(scope="session")
def find_job_staff_user(is_job_staff):
def find(jobs, users, is_staff, wo_jobs=None):
for job in jobs:
if wo_jobs is not None and job["id"] in wo_jobs:
continue
for user in users:
if is_staff == is_job_staff(user["id"], job["id"]):
return user["username"], job["id"]
return None, None
return find
@pytest.fixture(scope="session")
def find_task_staff_user(is_task_staff):
def find(tasks, users, is_staff, wo_tasks=None):
for task in tasks:
if wo_tasks is not None and task["id"] in wo_tasks:
continue
for user in users:
if is_staff == is_task_staff(user["id"], task["id"]):
return user["username"], task["id"]
return None, None
return find
@pytest.fixture(scope="session")
def find_issue_staff_user(is_issue_staff, is_issue_admin):
def find(issues, users, is_staff, is_admin):
for issue in issues:
for user in users:
i_admin, i_staff = (
is_issue_admin(user["id"], issue["id"]),
is_issue_staff(user["id"], issue["id"]),
)
if (is_admin is None and (i_staff or i_admin) == is_staff) or (
is_admin == i_admin and is_staff == i_staff
):
return user["username"], issue["id"]
return None, None
return find
@pytest.fixture(scope="session")
def filter_jobs(jobs):
def filter_(**kwargs):
return filter_assets(jobs, **kwargs)
return filter_
@pytest.fixture(scope="session")
def filter_labels(labels):
def filter_(**kwargs):
return filter_assets(labels, **kwargs)
return filter_
@pytest.fixture(scope="session")
def filter_jobs_with_shapes(annotations):
def find(jobs):
return list(filter(lambda j: annotations["job"].get(str(j["id"]), {}).get("shapes"), jobs))
return find
@pytest.fixture(scope="session")
def filter_tasks_with_shapes(annotations):
def find(tasks):
return list(
filter(lambda t: annotations["task"].get(str(t["id"]), {}).get("shapes"), tasks)
)
return find
@pytest.fixture(scope="session")
def jobs_with_shapes(jobs, filter_jobs_with_shapes):
return filter_jobs_with_shapes(jobs)
@pytest.fixture(scope="session")
def tasks_with_shapes(tasks, filter_tasks_with_shapes):
return filter_tasks_with_shapes(tasks)
@pytest.fixture(scope="session")
def admin_user(users):
for user in users:
if user["is_superuser"] and user["is_active"]:
return user["username"]
raise Exception("Can't find any admin user in the test DB")
@pytest.fixture(scope="session")
def regular_user(users):
for user in users:
if not user["is_superuser"] and user["is_active"]:
return user["username"]
raise Exception("Can't find any regular user in the test DB")
@pytest.fixture(scope="session")
def regular_lonely_user(users):
for user in users:
if user["username"] == "lonely_user":
return user["username"]
raise Exception("Can't find the lonely user in the test DB")
@pytest.fixture(scope="session")
def job_has_annotations(annotations) -> bool:
def check_has_annotations(job_id: int) -> bool:
job_annotations = annotations["job"][str(job_id)]
return bool(
job_annotations["tags"] or job_annotations["shapes"] or job_annotations["tracks"]
)
return check_has_annotations
@pytest.fixture(scope="session")
def access_tokens(access_tokens_by_username):
"Private keys are available in the 'private_key' field."
return sorted(
(t for user_tokens in access_tokens_by_username.values() for t in user_tokens),
key=lambda t: t["id"],
)
@pytest.fixture(scope="session")
def raw_access_tokens_by_username():
with open(ASSETS_DIR / "access_tokens.json") as f:
return json.load(f)["user"]
@pytest.fixture(scope="session")
def access_tokens_by_username(raw_access_tokens_by_username):
"Private keys are available in the 'private_key' field."
private_keys = {
3: "XQRwNl8D.N5EYCzdyWdroeVVfJylkquAmBqgt9Kw2", # nosec
4: "waUchCLi.wWxJTdYBt6R8auMse86bwHobMomjQvEB", # nosec
5: "2HVbBoWR.ZJqJtm3TEKEkjqZwyoL7Ig71LVvKRj79", # nosec
7: "gIUANJCa.W4Y101GNS8wOyFcncvxMZjTEnU7dzAUF", # nosec
}
data = {}
for username, user_tokens in raw_access_tokens_by_username.items():
if not user_tokens:
continue
extended_user_tokens = []
for access_token in user_tokens:
access_token = access_token.copy()
access_token["private_key"] = private_keys[access_token["id"]]
extended_user_tokens.append(access_token)
data[username] = extended_user_tokens
return data
@pytest.fixture(scope="session")
def fxt_local_audio_file_path() -> Generator[Path, None, None]:
yield SHARE_DIR / "audio" / "sample1.mp3"
+707
View File
@@ -0,0 +1,707 @@
# Copyright (C) CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT
import logging
import os
import sys
from enum import Enum
from http import HTTPStatus
from pathlib import Path
from subprocess import PIPE, CalledProcessError, run
from time import sleep
import pytest
import requests
import yaml
from shared.utils.config import ASSETS_DIR, get_server_url
logger = logging.getLogger(__name__)
CVAT_ROOT_DIR = next(dir.parent for dir in Path(__file__).parents if dir.name == "tests")
CVAT_DB_DIR = ASSETS_DIR / "cvat_db"
CLICKHOUSE_INIT_SCRIPT = "components/analytics/clickhouse/init.py"
PREFIX = "test"
CONTAINER_NAME_FILES = ["docker-compose.tests.yml"]
DC_FILES = CONTAINER_NAME_FILES + [
"docker-compose.dev.yml",
"tests/docker-compose.file_share.yml",
"tests/docker-compose.minio.yml",
"tests/docker-compose.pat_settings.yml",
"tests/docker-compose.test_servers.yml",
]
class Container(str, Enum):
DB = "cvat_db"
SERVER = "cvat_server"
WORKER_ANNOTATION = "cvat_worker_annotation"
WORKER_IMPORT = "cvat_worker_import"
WORKER_EXPORT = "cvat_worker_export"
WORKER_QUALITY_REPORTS = "cvat_worker_quality_reports"
WORKER_WEBHOOKS = "cvat_worker_webhooks"
WORKER_UTILS = "cvat_worker_utils"
def __str__(self):
return self.value
@classmethod
def covered(cls):
return [item.value for item in cls if item != cls.DB]
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(
"--keep-data",
action="store_true",
help="Do not reset volumes and database. (default: %(default)s)",
)
group._addoption(
"--no-services",
action="store_true",
help=("""Don't start, stop, or seed any containers — assume the user runs
the test stack externally"""),
)
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
logger.debug(f"Executing a command: {_command}")
if capture_output:
proc = run(_command, check=True, stdout=PIPE) # nosec
stdout = proc.stdout.decode()
else:
proc = run(_command, check=True) # nosec
stdout = ""
if stdout:
logger.debug(f"Output (stdout): {stdout}")
return stdout
def _kube_get_pod_name(label_filter):
return _run(f"kubectl get pods -l {label_filter} -o jsonpath={{.items[0].metadata.name}}")
def _kube_get_server_pod_name():
return _kube_get_pod_name("component=server")
def _kube_get_db_pod_name():
return _kube_get_pod_name("app.kubernetes.io/name=postgresql")
def _kube_get_clichouse_pod_name():
return _kube_get_pod_name("app.kubernetes.io/name=clickhouse")
def _kube_get_redis_inmem_pod_name():
return _kube_get_pod_name("app.kubernetes.io/name=redis")
def _kube_get_redis_ondisk_pod_name():
return _kube_get_pod_name("app.kubernetes.io/name=cvat,tier=kvrocks")
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(container, command, capture_output=True):
return _run(f"docker exec -u root {PREFIX}_{container}_1 {command}", capture_output)
def docker_exec_cvat(command: list[str] | str):
base = f"docker exec {PREFIX}_cvat_server_1"
_command = f"{base} {command}" if isinstance(command, str) else base.split() + command
return _run(_command)
def kube_exec_cvat(command: list[str] | str):
pod_name = _kube_get_server_pod_name()
base = f"kubectl exec {pod_name} --"
_command = f"{base} {command}" if isinstance(command, str) else base.split() + command
return _run(_command)
def container_exec_cvat(request: pytest.FixtureRequest, command: list[str] | str):
platform = request.config.getoption("--platform")
if platform == "local":
return docker_exec_cvat(command)
elif platform == "kube":
return kube_exec_cvat(command)
else:
assert False, "unknown platform"
def kube_exec_cvat_db(command):
pod_name = _kube_get_db_pod_name()
_run(["kubectl", "exec", pod_name, "--"] + command)
def docker_exec_clickhouse_db(command):
_run(["docker", "exec", f"{PREFIX}_cvat_clickhouse_1"] + command)
def kube_exec_clickhouse_db(command):
pod_name = _kube_get_clichouse_pod_name()
_run(["kubectl", "exec", pod_name, "--"] + command)
def docker_exec_redis_inmem(command):
return _run(["docker", "exec", f"{PREFIX}_cvat_redis_inmem_1"] + command)
def kube_exec_redis_inmem(command):
pod_name = _kube_get_redis_inmem_pod_name()
return _run(["kubectl", "exec", pod_name, "--"] + command)
def docker_exec_redis_ondisk(command):
_run(["docker", "exec", f"{PREFIX}_cvat_redis_ondisk_1"] + command)
def kube_exec_redis_ondisk(command):
pod_name = _kube_get_redis_ondisk_pod_name()
_run(["kubectl", "exec", pod_name, "--"] + command)
def docker_restore_db():
docker_exec(
Container.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 docker_restore_clickhouse_db():
docker_exec_cvat(
[
"/bin/sh",
"-c",
f'python "{CLICKHOUSE_INIT_SCRIPT}" --clear',
]
)
def kube_restore_clickhouse_db():
kube_exec_cvat(
[
"/bin/sh",
"-c",
f'python "{CLICKHOUSE_INIT_SCRIPT}" --clear',
]
)
def _get_redis_inmem_keys_to_keep():
return (
"rq:worker:",
"rq:workers",
"rq:scheduler_instance:",
"rq:queues:",
"cvat:applied_migrations",
"cvat:applied_migration:",
)
def docker_restore_redis_inmem():
docker_exec_redis_inmem(
[
"sh",
"-c",
'redis-cli -e --scan --pattern "*" |'
'grep -v "' + r"\|".join(_get_redis_inmem_keys_to_keep()) + '" |'
"xargs -r redis-cli -e del",
]
)
def kube_restore_redis_inmem():
kube_exec_redis_inmem(
[
"sh",
"-c",
'export REDISCLI_AUTH="${REDIS_PASSWORD}" && '
'redis-cli -e --scan --pattern "*" | '
'grep -v "' + r"\|".join(_get_redis_inmem_keys_to_keep()) + '" | '
"xargs -r redis-cli -e del",
]
)
def docker_restore_redis_ondisk():
docker_exec_redis_ondisk(["redis-cli", "-e", "-p", "6666", "flushall"])
def kube_restore_redis_ondisk():
kube_exec_redis_ondisk(
["sh", "-c", 'REDISCLI_AUTH="${CVAT_REDIS_ONDISK_PASSWORD}" redis-cli -e -p 6666 flushall']
)
def running_containers():
return [cn for cn in _run("docker ps --format {{.Names}}").split("\n") if cn]
def dump_db():
if "test_cvat_server_1" not in running_containers():
pytest.exit("CVAT is not running")
try:
run( # nosec
[
sys.executable,
str(Path(__file__).resolve().parents[1] / "utils" / "dump_test_db.py"),
"--output",
str(CVAT_DB_DIR / "data.json"),
],
check=True,
)
except CalledProcessError:
pytest.exit("Database dump failed.\n")
def create_compose_files(container_name_files):
for filename in container_name_files:
with (
open(filename.with_name(filename.name.replace(".tests", "")), "r") as dcf,
open(filename, "w") as ndcf,
):
dc_config = yaml.safe_load(dcf)
for service_name, service_config in dc_config["services"].items():
service_config.pop("container_name", None)
if service_name in (Container.SERVER, Container.WORKER_UTILS):
service_env = service_config["environment"]
service_env["DJANGO_SETTINGS_MODULE"] = "cvat.settings.testing_rest"
if service_name in Container.covered():
service_env = service_config["environment"]
service_env["COVERAGE_PROCESS_START"] = ".coveragerc"
service_config["volumes"].append(
"./tests/python/.coveragerc:/home/django/.coveragerc"
)
yaml.dump(dc_config, ndcf)
def delete_compose_files(container_name_files):
for filename in container_name_files:
filename.unlink(missing_ok=True)
def wait_for_services(num_secs: int = 300) -> None:
for i in range(num_secs):
logger.debug(f"waiting for the server to load ... ({i})")
try:
response = requests.get(get_server_url("api/server/health/", format="json"))
statuses = response.json()
logger.debug(f"server status: \n{statuses}")
if response.status_code == HTTPStatus.OK:
logger.debug("the server has finished loading!")
return
except Exception as e:
logger.debug(f"an error occurred during the server status checking: {e}")
sleep(1)
raise Exception(
f"Failed to reach the server during {num_secs} seconds. Please check the configuration."
)
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 get_server_image_tag():
return f"cvat/server:{os.environ.get('CVAT_VERSION', 'dev')}"
def docker_compose(dc_files, cvat_root_dir):
return [
"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),
]
def start_services(dc_files, rebuild=False, cvat_root_dir=CVAT_ROOT_DIR):
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(dc_files, cvat_root_dir) + ["up", "-d", *["--build"] * rebuild],
capture_output=False,
)
def stop_services(dc_files, cvat_root_dir=CVAT_ROOT_DIR):
run(docker_compose(dc_files, cvat_root_dir) + ["down", "-v"], capture_output=False)
def session_start(
session,
cvat_root_dir=CVAT_ROOT_DIR,
cvat_db_dir=CVAT_DB_DIR,
extra_dc_files=None,
waiting_time=300,
):
stop = session.config.getoption("--stop-services")
start = session.config.getoption("--start-services")
rebuild = session.config.getoption("--rebuild")
keep_data = session.config.getoption("--keep-data")
cleanup = session.config.getoption("--cleanup")
dumpdb = session.config.getoption("--dumpdb")
if session.config.getoption("--collect-only"):
if any((stop, start, rebuild, cleanup, dumpdb)):
raise Exception("""--collect-only is not compatible with any of the other options:
--stop-services --start-services --rebuild --cleanup --dumpdb""")
return # don't need to start the services to collect tests
if session.config.getoption("--no-services"):
# User manages the stack and the server externally — pytest does
# nothing on session start. Per-test restore fixtures still docker-exec
# into the user's containers (which must be named test_<service>_1).
return
platform = session.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":
local_start(
start,
stop,
dumpdb,
cleanup,
rebuild,
keep_data,
cvat_root_dir,
cvat_db_dir,
extra_dc_files,
waiting_time,
)
elif platform == "kube":
kube_start(cvat_db_dir, keep_data)
def local_start(
start,
stop,
dumpdb,
cleanup,
rebuild,
keep_data,
cvat_root_dir,
cvat_db_dir,
extra_dc_files,
waiting_time,
):
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)
dc_files = [cvat_root_dir / f for f in DC_FILES]
if extra_dc_files is not None:
dc_files += extra_dc_files
container_name_files = [cvat_root_dir / f for f in CONTAINER_NAME_FILES]
if cleanup:
delete_compose_files(container_name_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(container_name_files)
create_compose_files(container_name_files)
if stop:
stop_services(dc_files, cvat_root_dir)
pytest.exit("All testing containers are stopped", returncode=0)
start_services(dc_files, rebuild, cvat_root_dir)
if not keep_data:
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")
wait_for_services(waiting_time)
docker_exec_cvat(
[
"sh",
"-c",
"./manage.py flush --no-input && ./manage.py loaddata_sorted /tmp/data.json",
]
)
docker_exec(
Container.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)
def kube_start(cvat_db_dir, keep_data):
if keep_data:
wait_for_services()
return
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_services()
kube_exec_cvat(
["sh", "-c", "./manage.py flush --no-input && ./manage.py loaddata_sorted /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",
]
)
def pytest_sessionstart(session: pytest.Session) -> None:
session_start(session)
def pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None:
session_finish(session)
def session_finish(session):
if session.config.getoption("--collect-only"):
return
platform = session.config.getoption("--platform")
if platform == "local":
if os.environ.get("COVERAGE_PROCESS_START"):
collect_code_coverage_from_containers()
def collect_code_coverage_from_containers():
for container in Container.covered():
process_command = "python3"
# find process with code coverage
pid = docker_exec(container, f"pidof {process_command} -o 1")
# stop process with code coverage
docker_exec(container, f"kill -15 {pid}")
sleep(3)
# get code coverage report
docker_exec(container, "coverage combine", capture_output=False)
docker_exec(container, "coverage json", capture_output=False)
docker_cp(
f"{PREFIX}_{container}_1:home/django/coverage.json",
f"coverage_{container}.json",
)
@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_per_function(request):
platform = request.config.getoption("--platform")
if platform == "local":
docker_restore_data_volumes()
else:
kube_restore_data_volumes()
@pytest.fixture(scope="class")
def restore_cvat_data_per_class(request):
platform = request.config.getoption("--platform")
if platform == "local":
docker_restore_data_volumes()
else:
kube_restore_data_volumes()
@pytest.fixture(scope="function")
def restore_clickhouse_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_clickhouse_db()
else:
kube_restore_clickhouse_db()
@pytest.fixture(scope="class")
def restore_clickhouse_db_per_class(request):
platform = request.config.getoption("--platform")
if platform == "local":
docker_restore_clickhouse_db()
else:
kube_restore_clickhouse_db()
@pytest.fixture(scope="function")
def restore_redis_inmem_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_redis_inmem()
else:
kube_restore_redis_inmem()
@pytest.fixture(scope="class")
def restore_redis_inmem_per_class(request):
platform = request.config.getoption("--platform")
if platform == "local":
docker_restore_redis_inmem()
else:
kube_restore_redis_inmem()
@pytest.fixture(scope="function")
def restore_redis_ondisk_per_function(request):
platform = request.config.getoption("--platform")
if platform == "local":
docker_restore_redis_ondisk()
else:
kube_restore_redis_ondisk()
@pytest.fixture(scope="class")
def restore_redis_ondisk_per_class(request):
platform = request.config.getoption("--platform")
if platform == "local":
docker_restore_redis_ondisk()
else:
kube_restore_redis_ondisk()
@pytest.fixture(scope="class")
def restore_redis_ondisk_after_class(request):
yield
platform = request.config.getoption("--platform")
if platform == "local":
docker_restore_redis_ondisk()
else:
kube_restore_redis_ondisk()
+27
View File
@@ -0,0 +1,27 @@
# Copyright (C) CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT
from functools import partial
from pathlib import PurePosixPath
import pytest
import shared.utils.s3 as s3
@pytest.fixture
def fxt_uploaded_s3_file(request: pytest.FixtureRequest):
def upload_file(s3_client: s3.S3Client, *, path: PurePosixPath | str, data: bytes):
s3_client.create_file(
data=data,
filename=str(path),
)
request.addfinalizer(
partial(
s3_client.remove_file,
filename=str(path),
)
)
return upload_file
+39
View File
@@ -0,0 +1,39 @@
# Copyright (C) CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT
import io
import logging
import pytest
@pytest.fixture
def fxt_stdout(capsys):
class IoProxy(io.IOBase):
def __init__(self, capsys):
self.capsys = capsys
def getvalue(self) -> str:
capture = self.capsys.readouterr()
return capture.out
yield IoProxy(capsys)
@pytest.fixture
def fxt_logger():
logger_stream = io.StringIO()
logger = logging.Logger("test", level=logging.INFO)
logger.propagate = False
logger.addHandler(logging.StreamHandler(logger_stream))
yield logger, logger_stream
@pytest.fixture
def fxt_test_name(request: pytest.FixtureRequest):
name = request.node.name
if request.fixturename and request.fixturename != "fxt_test_name":
name += f"[{request.fixturename}]"
yield name