6db56e5d2e
<!-- Raise an issue to propose your change (https://github.com/cvat-ai/cvat/issues). It helps to avoid duplication of efforts from multiple independent contributors. Discuss your ideas with maintainers to be sure that changes will be approved and merged. Read the [Contribution guide](https://docs.cvat.ai/docs/contributing/). --> <!-- Provide a general summary of your changes in the Title above --> ### Motivation and context <!-- Why is this change required? What problem does it solve? If it fixes an open issue, please link to the issue here. Describe your changes in detail, add screenshots. --> ### How has this been tested? <!-- Please describe in detail how you tested your changes. Include details of your testing environment, and the tests you ran to see how your change affects other areas of the code, etc. --> ### Checklist <!-- Go over all the following points, and put an `x` in all the boxes that apply. If an item isn't applicable for some reason, then ~~explicitly strikethrough~~ the whole line. If you don't do that, GitHub will show incorrect progress for the pull request. If you're unsure about any of these, don't hesitate to ask. We're here to help! --> - [x] I submit my changes into the `develop` branch - [x] I have created a changelog fragment <!-- see top comment in CHANGELOG.md --> - [ ] I have updated the documentation accordingly - [x] I have added tests to cover my changes - [x] I have linked related issues (see [GitHub docs]( https://help.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword)) ### License - [x] I submit _my code changes_ under the same [MIT License]( https://github.com/cvat-ai/cvat/blob/develop/LICENSE) that covers the project. Feel free to contact the maintainers if that's a concern. --------- Co-authored-by: Maxim Zhiltsov <maxim@cvat.ai>
241 lines
8.2 KiB
Python
241 lines
8.2 KiB
Python
# Copyright (C) CVAT.ai Corporation
|
|
#
|
|
# SPDX-License-Identifier: MIT
|
|
|
|
import os
|
|
from datetime import timedelta
|
|
from io import BytesIO
|
|
from pathlib import Path
|
|
from unittest import mock
|
|
|
|
import packaging.version as pv
|
|
import pytest
|
|
from cvat_cli._internal.agent import (
|
|
_Event,
|
|
_NewReconnectionDelay,
|
|
_parse_event_stream,
|
|
_TaskCacheLimiter,
|
|
)
|
|
from cvat_sdk import Client
|
|
from cvat_sdk.api_client import models
|
|
from cvat_sdk.core.proxies.tasks import ResourceType
|
|
|
|
from sdk.util import https_reverse_proxy
|
|
|
|
from .util import TestCliBase, generate_images, run_cli
|
|
|
|
|
|
class TestCliMisc(TestCliBase):
|
|
def test_can_warn_on_mismatching_server_version(self, monkeypatch, caplog):
|
|
def mocked_version(_):
|
|
return pv.Version("0")
|
|
|
|
# We don't actually run a separate process in the tests here, so it works
|
|
monkeypatch.setattr(Client, "get_server_version", mocked_version)
|
|
|
|
self.run_cli("task", "ls")
|
|
|
|
assert "Server version '0' is not compatible with SDK version" in caplog.text
|
|
|
|
@pytest.mark.parametrize("verify", [True, False])
|
|
def test_can_control_ssl_verification_with_arg(self, verify: bool):
|
|
with https_reverse_proxy() as proxy_url:
|
|
if verify:
|
|
insecure_args = []
|
|
else:
|
|
insecure_args = ["--insecure"]
|
|
|
|
run_cli(
|
|
self,
|
|
f"--auth={self.user}:{self.password}",
|
|
f"--server-host={proxy_url}",
|
|
*insecure_args,
|
|
"task",
|
|
"ls",
|
|
expected_code=1 if verify else 0,
|
|
)
|
|
stdout = self.stdout.getvalue()
|
|
|
|
if not verify:
|
|
for line in stdout.splitlines():
|
|
int(line)
|
|
|
|
def test_can_control_organization_context(self):
|
|
org = "cli-test-org"
|
|
self.client.organizations.create(models.OrganizationWriteRequest(org))
|
|
|
|
files = generate_images(self.tmp_path, 1)
|
|
|
|
stdout = self.run_cli(
|
|
"task",
|
|
"create",
|
|
"personal_task",
|
|
ResourceType.LOCAL.name,
|
|
*map(os.fspath, files),
|
|
"--completion_verification_period=0.01",
|
|
organization="",
|
|
)
|
|
|
|
personal_task_id = int(stdout.split()[-1])
|
|
|
|
stdout = self.run_cli(
|
|
"task",
|
|
"create",
|
|
"org_task",
|
|
ResourceType.LOCAL.name,
|
|
*map(os.fspath, files),
|
|
"--completion_verification_period=0.01",
|
|
organization=org,
|
|
)
|
|
|
|
org_task_id = int(stdout.split()[-1])
|
|
|
|
personal_task_ids = list(map(int, self.run_cli("task", "ls", organization="").split()))
|
|
assert personal_task_id in personal_task_ids
|
|
assert org_task_id not in personal_task_ids
|
|
|
|
org_task_ids = list(map(int, self.run_cli("task", "ls", organization=org).split()))
|
|
assert personal_task_id not in org_task_ids
|
|
assert org_task_id in org_task_ids
|
|
|
|
all_task_ids = list(map(int, self.run_cli("task", "ls").split()))
|
|
assert personal_task_id in all_task_ids
|
|
assert org_task_id in all_task_ids
|
|
|
|
def test_can_use_access_token_env_variable(
|
|
self, monkeypatch: pytest.MonkeyPatch, access_tokens
|
|
):
|
|
token = next(t for t in access_tokens)["private_key"]
|
|
|
|
from cvat_sdk.api_client.rest import RESTClientObject
|
|
|
|
original_request = RESTClientObject.request
|
|
|
|
calls = 0
|
|
|
|
def patched_request(self, *args, **kwargs):
|
|
nonlocal calls
|
|
calls += 1
|
|
|
|
assert kwargs["headers"].get("Authorization") == f"Bearer {token}"
|
|
return original_request(self, *args, **kwargs)
|
|
|
|
monkeypatch.setenv("CVAT_ACCESS_TOKEN", token)
|
|
monkeypatch.setattr(RESTClientObject, "request", patched_request)
|
|
self.run_cli("task", "ls", authenticate=False)
|
|
|
|
assert calls
|
|
|
|
def test_can_use_current_user_env_variable(self, monkeypatch: pytest.MonkeyPatch):
|
|
# set all user env vars supported by getuser()
|
|
for env_var in ("LOGNAME", "USER", "LNAME", "USERNAME"):
|
|
monkeypatch.setenv(env_var, self.user)
|
|
|
|
from getpass import getuser as original_getuser
|
|
|
|
from cvat_sdk.core.auth import default_auth_factory
|
|
|
|
with (
|
|
mock.patch(
|
|
"cvat_sdk.core.auth.default_auth_factory", wraps=default_auth_factory
|
|
) as mock_auth_factory,
|
|
mock.patch("getpass.getuser", wraps=original_getuser) as mock_getuser,
|
|
mock.patch("getpass.getpass", return_value=self.password) as mock_getpass,
|
|
):
|
|
self.run_cli("task", "ls", authenticate=False)
|
|
|
|
mock_auth_factory.assert_called_once()
|
|
mock_getuser.assert_called()
|
|
mock_getpass.assert_called_once()
|
|
|
|
def test_can_use_pass_env_variable(self, monkeypatch: pytest.MonkeyPatch):
|
|
monkeypatch.setenv("PASS", self.password)
|
|
|
|
from cvat_sdk.core.auth import get_auth_factory
|
|
|
|
with (
|
|
mock.patch(
|
|
"cvat_sdk.core.auth.get_auth_factory", wraps=get_auth_factory
|
|
) as mock_auth_factory,
|
|
mock.patch("getpass.getpass") as mock_getpass,
|
|
):
|
|
self.run_cli(f"--auth={self.user}", "task", "ls", authenticate=False)
|
|
|
|
mock_auth_factory.assert_called_once()
|
|
mock_getpass.assert_not_called()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
["lines", "messages"],
|
|
[
|
|
# empty
|
|
([], []),
|
|
([""], [_Event("", "")]),
|
|
# event only
|
|
(["event: test", ""], [_Event("test", "")]),
|
|
(["event: foo", "event: bar", ""], [_Event("bar", "")]),
|
|
# data only
|
|
(["data: test", ""], [_Event("", "test")]),
|
|
(["data: foo", "data: bar", ""], [_Event("", "foo\nbar")]),
|
|
# event and data
|
|
(["event: test", "data: foo", "data: bar", ""], [_Event("test", "foo\nbar")]),
|
|
(["data: foo", "event: test", "data: bar", ""], [_Event("test", "foo\nbar")]),
|
|
(["data: foo", "data: bar", "event: test", ""], [_Event("test", "foo\nbar")]),
|
|
# fields without values
|
|
(["event: test", "event", ""], [_Event("", "")]),
|
|
(["data: test", "data", ""], [_Event("", "test\n")]),
|
|
# incomplete event
|
|
(["event: test", "data: foo"], []),
|
|
# multiple events
|
|
(
|
|
["event: test1", "data: foo", "", "event: test2", "data: bar", ""],
|
|
[_Event("test1", "foo"), _Event("test2", "bar")],
|
|
),
|
|
# comments
|
|
([":"], []),
|
|
([":1", "event: test", ":2", "data: foo", ":3", ""], [_Event("test", "foo")]),
|
|
# retry
|
|
(["retry: 1234"], [_NewReconnectionDelay(timedelta(milliseconds=1234))]),
|
|
(["retry", "retry:", "retry: a"], []),
|
|
# no space
|
|
(["event:test", "data:foo", ""], [_Event("test", "foo")]),
|
|
# two spaces
|
|
(["event: test", "data: foo", ""], [_Event(" test", " foo")]),
|
|
# carriage return
|
|
(["event: test\r", "data: foo\r", "\r"], [_Event("test", "foo")]),
|
|
],
|
|
)
|
|
def test_parse_event_stream(lines, messages):
|
|
stream = BytesIO(b"".join(line.encode() + b"\n" for line in lines))
|
|
assert list(_parse_event_stream(stream)) == messages
|
|
|
|
|
|
def test_task_cache_limiter_keeps_last_10_tasks(
|
|
tmp_path: Path,
|
|
fxt_login: tuple[Client, str],
|
|
fxt_logger,
|
|
):
|
|
client = fxt_login[0]
|
|
client.logger = fxt_logger[0]
|
|
client.config.cache_dir = tmp_path / "cache"
|
|
|
|
limiter = _TaskCacheLimiter(client)
|
|
|
|
for task_id in range(1, 13):
|
|
limiter._cache_manager.task_dir(task_id).mkdir(parents=True)
|
|
with limiter.using_cache_for_task(task_id):
|
|
pass
|
|
|
|
if task_id <= 10:
|
|
for cached_task_id in range(1, task_id + 1):
|
|
assert limiter._cache_manager.task_dir(cached_task_id).exists()
|
|
elif task_id == 11:
|
|
assert not limiter._cache_manager.task_dir(1).exists()
|
|
for cached_task_id in range(2, 12):
|
|
assert limiter._cache_manager.task_dir(cached_task_id).exists()
|
|
elif task_id == 12:
|
|
assert not limiter._cache_manager.task_dir(1).exists()
|
|
assert not limiter._cache_manager.task_dir(2).exists()
|
|
for cached_task_id in range(3, 13):
|
|
assert limiter._cache_manager.task_dir(cached_task_id).exists()
|